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 件のコメント :
コメントを投稿