AngularJSやBackbone、KnockoutJSといったMV*フレームワークや、ReactといったVに特化したフレームワーク、jQueryといったライブラリがいっぱいある。
AngularJSを勉強し始めたは良いが、2.0になると全く別モノになってしまうなど、どれを勉強すればよいかわからなくなってしまった。
ということで、まずはフレームワークやライブラリを使わない「素のJavaScript」でMVCモデルを勉強してみようと思った。
つくったToDoアプリの概要は、前回の「TypeScript + AngularJSでToDoアプリをつくってみた」と同じだ。
※ 以降TypeScriptで記載しているが、JavaScriptのソースが見たい方はGitHubにコンパイル後のソースを置いてあるので、そちらを参照ください。
Model層
class TodoModel extends EventDispatcher { 'use strict'; constructor() { super(); } /** * タスクの追加 * @param value タスク内容 */ addNew(value: string): void { console.log('Model: addNew value = ' + value); this.dispatchEvent({ type: 'added', task: value }); } /** * 完了済みタスクの一括削除 */ deleteDone(): void { console.log('Model: addNew'); this.dispatchEvent({ type: 'deleteDone' }); } /** * タスクの削除 * @param index 削除する行番号 */ deleteTask(index: number): void { console.log('Model: deleteTask index = ' + index); this.dispatchEvent({ type: 'deletedTask', index: index }) } /** * タスク件数の取得 * @param taskNode タスクのリストアイテム */ getTaskCount(taskNode: NodeList): void { console.log('Model: getDoneCount taskNode = ' + taskNode); var count = 0; for (var i = 0; i < taskNode.length; i++) { count += (<HTMLInputElement>taskNode[i].childNodes[0]).checked ? 1 : 0; } this.dispatchEvent({ type: 'getDoneCount', doneCount: count, taskCount: taskNode.length }); } }
Model層は、DBアクセスやビジネスロジックを担っている。
が、ほとんど処理がないため、こんなに簡素な作りになっている。
今回はデータの受け渡しがほとんどないが、本来ならVO(Value Object)やDTO(Data Transfer Object)に値を格納しやりとりするのだろう。
※ JavaScriptにVOとかDTOとかの概念があるかは不明
そして、Modelクラスでところどころで使われている「this.dispatchEvent」は、Model層とView層を繋ぐ処理をしている。
--------
追記
@bc_rikko テンプレート(ファイル)にJSONをバインドしてHTML作る、くらいのゆるい感じかな。クラスとか厳密に考えない。
— にぅせんせー (@niusounds) 2015, 4月 7
どうやらJSONでやりとりするっぽい。
EventDispatcher
class EventDispatcher { 'use strict'; listeners: any; constructor() { this.listeners = {}; } /** * イベントリスナの追加 * @param type イベントタイプ * @param callback */ addEventListener(type: string, callback: Function): void { if (!this.listeners[type]) { this.listeners[type] = []; } this.listeners[type].push(callback); } /** * イベントリスナの削除 * @param type イベントタイプ * @param callback */ removeEventListener(type: string, callback: Function): void { for (var i = 0; i < this.listeners[type]; i++) { if (this.listeners[type][i] == callback) { this.listeners[type].splice(i, 1); } } } /** * イベントリスナのクリア */ clearEventListener(): void { this.listeners = {}; } /** * ディスパッチイベントの実行 * @param event 引数{type: イベントタイプ, [args]: 任意} */ dispatchEvent(event): void { if (this.listeners[event.type]) { for (var listener in this.listeners[event.type]) { this.listeners[event.type][listener].apply(this.listeners, arguments); } } } }
このEventDispatcherクラスは、Model層を監視し、View層とModel層を繋ぐ(監視する)役割を果たしている。
デザインパターンでいうところのObserverパターン。
処理の流れとしては、
- View層でイベントディスパッチャに、callbackメソッドを登録する
- View層で発生したクリックイベントなどをトリガーとして、Controllerを呼び出す
- 呼び出されたControllerは、紐付くModel層のメソッドを呼び出す
- Model層でDB接続やビジネスロジックを実行する
- Model層の処理が終わったら、1.で登録したcallbackメソッドを呼び出す
こんな感じの処理を実現するために、要となるクラスがEventDispatcher。
やっていることは、listeners配列にcallbackメソッドを登録して、あとでapplyメソッドを使ってcallbackメソッドを実行している。
View層
class TodoView { 'use strict'; private newTaskBody: HTMLInputElement; private addButton: HTMLButtonElement; private delButton: HTMLButtonElement; private tasks: HTMLUListElement; private taskItems: NodeList; private delLink: NodeList; private doneCount: HTMLSpanElement; private taskCount: HTMLSpanElement; constructor(private model: TodoModel, private controller: TodoController) { var self = this; // タスク内容 this.newTaskBody = <HTMLInputElement>document.getElementById('newTaskBody'); // タスクリスト this.tasks = <HTMLUListElement>document.getElementById('tasks'); // タスクのアイテムリスト this.taskItems = document.getElementsByTagName('li'); // タスク追加ボタン this.addButton = <HTMLButtonElement>document.getElementById('add'); this.addButton.onclick = function () { controller.addNew(self.newTaskBody.value); controller.getTaskCount(self.taskItems); }; model.addEventListener('added', function (event) { self.addNew(event.task); }); // タスクの一括削除ボタン this.delButton = <HTMLButtonElement>document.getElementById('deleteDone'); this.delButton.onclick = function () { controller.deleteDone(); controller.getTaskCount(self.taskItems); }; model.addEventListener('deleteDone', function (event) { self.deleteDone(); }); // タスクの削除リンク this.delLink = document.getElementsByTagName('a'); this.model.addEventListener('deletedTask', function (event) { self.deleteTask(event.index); }); // 完了済みタスク件数と、全体のタスク件数 this.doneCount = <HTMLSpanElement>document.getElementById('doneCount'); this.taskCount = <HTMLSpanElement>document.getElementById('taskCount'); this.doneCount.innerHTML = '0'; // 0で初期化 this.taskCount.innerHTML = '0'; // 0で初期化 this.model.addEventListener('getDoneCount', function (event) { self.renderCounter(event.doneCount, event.taskCount); }); } /** * タスクの追加 * @param value タスク内容 */ addNew(value: string): void { console.log('View: addNew value = ' + value); var self = this; var li = document.createElement('li'); var doneCheckbox = document.createElement('input'); doneCheckbox.type = 'checkbox'; //HACK:この変ちょっと気持ち悪い doneCheckbox.onclick = function () { self.controller.getTaskCount(self.taskItems); } var taskBody = document.createElement('span'); taskBody.innerHTML = value; var delLink = document.createElement('a'); delLink.innerHTML = '[x]'; delLink.href = '#'; //HACK:この変ちょっと気持ち悪い delLink.onclick = function () { var index = self.getIndex(this.parentNode); self.controller.deleteTask(index); self.controller.getTaskCount(self.taskItems); } li.appendChild(doneCheckbox); li.appendChild(taskBody); li.appendChild(delLink); this.tasks.appendChild(li); this.newTaskBody.value = ''; } /** * タスクの削除 * @param index 削除する行番号 */ deleteTask(index: number): void { console.log('View: deleteTask index = ' + index); this.tasks.removeChild(this.tasks.children[index]); } /** * 完了済みタスクの一括削除 */ deleteDone(): void { console.log('View: deleteDone'); var taskList = document.getElementsByTagName('li'); for (var i = taskList.length - 1; 0 <= i; i--) { // children[0] = Checkbox if ((<HTMLInputElement>taskList[i].children[0]).checked) { this.tasks.removeChild(this.tasks.children[i]); } } } /** * 削除対象の行番号取得 * @param node uiノード */ getIndex(node: any): number { console.log('View: getIndex node = ' + node); var children = node.parentNode.childNodes; for (var i = 0; i < children.length; i++) { if (node == children[i]) break; } // children[0] に "#text" があるため、そのままindexを返すと+1状態になる return i - 1; } /** * タスクの進捗件数の表示 * @param doneCount 完了済み件数 * @param taskCount 全体のタスク件数 */ renderCounter(doneCount: number, taskCount: number): void { this.doneCount.innerHTML = doneCount.toString(); this.taskCount.innerHTML = taskCount.toString(); } }
View層は、主に画面からの入力データの取得と、処理結果の画面出力を担っている。
constructorでは、HTMLの要素の取得、イベントの登録、イベントディスパッチャの登録を行っている。
そして、それぞれのメソッドでは、データを画面に表示する処理をしている。
jQueryさえ使わなかったので、こんなにデカいクラスになってしまった。
処理内容は、
this.hoge.onclickが、Controller→Model層のメソッドを実行するための処理。
前処理がしたい場合は、Controllerを呼び出す前に処理を実装する。
this.model.addEventListenerが、callbackメソッドを登録するための処理。
Model層の処理の後でView側で処理するためのもの。
ちなみにthis.model.addEventListener(event)の「event」に、Model層のthis.dispatchEventの引数が渡ってくる。
Controller層
class TodoController { 'use strict'; constructor(private model: TodoModel) { } /** * タスクの追加 * @param value タスク内容 */ addNew(value: string): void { console.log('Controller: addNew value = ' + value); this.model.addNew(value); } /** * 完了済みタスクの一括削除 */ deleteDone(): void { console.log('Controller: deleteDone'); this.model.deleteDone(); } /** * タスクの削除 * @param index 削除する行番号 */ deleteTask(index: number): void { console.log('Controller: deleteTask index = ' + index); this.model.deleteTask(index); } /** * タスクの進捗件数取得 * @param taskNode タスクのliノード */ getTaskCount(taskNode: NodeList): void { console.log('Controller: getDoneCount taskNode = ' + taskNode) this.model.getTaskCount(taskNode); } }
Controller層は、View層とModel層の制御を行う。
といっても今回の例では、イベントが発生したときにModel層を呼び出しているだけだが…。
メイン処理
class App { 'use strict'; constructor() { var model = new TodoModel(); var controller = new TodoController(model); var view = new TodoView(model, controller); } } window.onload = () => { 'use strict'; var app = new App(); }
最後に、メイン処理。
Appクラスは、Model・View・Controllerクラスのインスタンスを生成しているだけ。
window.onloadは、ページ読み込み時にAppクラスのインスタンスを生成しているだけ。
さいごに
書いている途中で、ModelとViewの切り分けで混乱してきた。
そのため、TypeScriptの良さ(静的型チェックや自動補完など)をすべて殺したようなソースコードができあがってしまった。
特にデータのやりとりするためのインターフェースや、ModelとViewの切り分けにはもっと工夫が必要だと感じた。
こうやって素のJavaScriptでMVCモデルのアプリを作ったことで、フレームワークの大切さが実感できたし、アプリ全体のデータの流れがある程度わかるようになった。
フレームワークで何を勉強すればよいかわからない人は、ちょっと遠回りになるけどフレームワークを使わないアプリを作ってみるのをオススメする。
今回のソースはGitHubにあげたので、良ければ参考にしてください。
もし、「やはりお前のMVCモデルはまちがっている。」とかアドバイスなどあれば是非教えて下さい。よろしくお願いします。
以上
written by @bc_rikko
↓の本を読んでから実装すればよかったとちょっと後悔してるw
追記:2015/05/27 18:30
MVWフレームワーク(AngularJSとVue.js)を使って同じToDoアプリをつくってみた
0 件のコメント :
コメントを投稿