前陣子 Facebook 推出一套名為 FLUX 的前端程式架構,期望能幫過去沒有條理,程式多了結構就亂得像一團麵條的 Javascript 程式寫法找到一個理想的組織方法。
FLUX 簡介
視圖(View)-> 操作(Action)-> 分配器(Dispatcher)-> 資料處理器(Store)-> 繪圖者(Renderer)-> 視圖(View)
FLUX 的基本原理有別於常用的 MVC(Model/View/Controller)或 MVVM(Model/View/ViewModel)是在 M,V,C(VM)三者之間互相傳遞或修改資料,
MVC (image from fluxxer)
FLUX 重新定義整個組織架構為單向的視圖(View)-> 操作(Action)-> 分配器(Dispatcher)-> 資料處理器(Store)-> 繪圖者(Renderer)-> 視圖(View)的運作流程。
FLUX (image from fluxxer)
就我的理解,FLUX 的架構可以拆分為三個重點流程:
- 跟視圖(View)有關的操作(Action),都透過事件註冊到分配器(Dispatcher)去。
- 分配器 (Dispatcher)負責將操作(Action)傳遞給需要的資料處理器(Store)。
- 資料處理器(Store)負責跟資料直接相關的操作。若資料處理器(Store)修改的結果需要反映到視圖(View)上,可以透過發送訊息通知給繪圖者(Renderer)處理。這邊講到了原本 FLUX 概念圖中沒有提到的繪圖者(Renderer)這個角色,在 Facebook 中他們是用 ReactJS 處理。
瞭解其基本架構之後,我發現其實就算不用他們提供的函式庫,用 Javascript 內建的 addEventListener, handleEvent, customEvent 等方法,也可以利用前面所提的三個重點,漸進寫出符合 FLUX 精神的程式。
目前的 JS 組織方式
一個常見的 JS 檔案,一般的架構是
var App = { init: function app_init() {
// get view
this.view1 = document.getElementById('xxx1');
this.view2 = document.getElementById('xxx2');
// do stuff
this.view1.addEventListener('click', function() {
// do something
});
this.view2.addEventListener('keyup', function() {
// do something
});
}
}; 若我們想要將視圖(View)的操作從 init 分離開來,大部分的人會這樣做
var App = {
init: function app_init() {
// get view
this.view1 = document.getElementById('xxx1');
this.view2 = document.getElementById('xxx2');
// do stuff
this.view1.addEventListener('click', this.clickView1);
this.view2.addEventListener('keyup', this.keyupView2);
},
clickView1: function app_clickView1() {
// do something
},
keyupView2: function app_keyupView2() {
// do something
}
};
如果在 clickView1 或 keyupView2 中要呼叫到 App 裡的參數或方法,那麼我們需要在 addEventListener 時使用 bind (this)
var App = {
init: function app_init() {
// get view
this.view1 = document.getElementById('xxx1');
this.view2 = document.getElementById('xxx2');
// do stuff
this.view1.addEventListener('click', this.clickView1.bind(this));
this.view2.addEventListener('keyup', this.keyupView2.bind(this));
},
clickView1: function app_clickView1() {
// do something
},
keyupView2: function app_keyupView2() {
// do something
this.clickView1();
}
};
大多數書籍的範例大概就停在這裡,沒有再進一步探討程式的組織架構了。即使是龐大的 Javascript 專案如 Gaia,不少部分的程式碼組織方式也是如此。在這種組織方式裡,若有很多的視圖(View)需要操作或修改,我們的程式碼就會開始亂起來。
下面來試著將以上程式漸進改為 FLUX 架構。
改進建議一:將 handleEvent 當作 Dispatcher 來使用
跟視圖(View)有關的操作(Action),都透過事件註冊到分配器(Dispatcher)去。 (溫馨提示:IE 9 以上版本才有支援 handleEvent 方法,在之前版本上使用要加 polyfill)
我們先來想想看視圖(View)跟操作(Action)在前端 Javascript 程式中分別代表著什麼。
視圖(View)很明顯,就是透過 getElementById 等方法,從 HTML 中取得代表對應節點(Node)的元素(element)。
若想要套用 FLUX 架構,我們可以將附加在各個元素(element)上的事件行為分離,將事件註冊到一個統一的地方(分配器),在這個地方對不同的事件進行操作。
Javascript 內建的分配器叫做 handleEvent,它可以拿來處理任何事件 Event,寫法如下。
var App = {
init: function app_init() {
// get view
this.view1 = document.getElementById('xxx1');
this.view2 = document.getElementById('xxx2');
// do stuff
this.view1.addEventListener('click', this);
this.view2.addEventListener('keyup', this);
},
handleEvent: app_handleEvent(evt) {
switch (evt.type) {
case 'click':
switch (evt.target) {
case this.view1:
this.clickView1();
break;
}
break;
case 'keyup':
switch (evt.target) {
case this.view2:
this.keyupView2();
break; }
}
},
clickView1: function app_clickView1() {
// do something
},
keyupView2: function app_keyupView2() {
// do something
this.clickView1();
}
}; 這麼做帶來的明顯好處是所有的呼叫都統一在 handleEvent 中,可以更容易地追查到。
這麼寫也可以在 addEventListener/removeEventListener 時不用使用 bind (this),而 bind (this) 經常有些 side effect 需要特別留意。
例如假使我們想要反註冊 view1 上的 click 方法,使用以下寫法
this.view1.removeEventListener ('click', this.clickView1.bind (this)); 其實並沒有將第一個 event 移除。因為使用了 .bind(this) 後,回傳的其實是一個新的 instance...。
正確的寫法是
this.bindClickView1 = this.clickView1.bind (this)
this.view1.addEventListener ('click', this.bindClickView1);
...
this.view1.removeEventListener ('click', this.bindClickView1); 用 handleEvent 可以省點事,要反註冊時也傳入 this 即可。
this.view1.removeEventListener ('click', this);
改進建議二:將資料處理的部分分離,使用自訂事件來改變 Store 狀態
分配器 (Dispatcher)負責將操作(Action)傳遞給需要的資料處理器(Store)
資料處理器(Store)負責跟資料直接相關的操作。在稍大的 Web App 中,我們可以另外定一個 Object 來處理資料相關的事宜。一般我們的寫法會是
// Store.js
function Store() {
this._data: 0;
};
Store.prototype = {
getSomething: function s_getSomething() {
return this._data;
},
doSomething: function s_doSomething() {
this._data += 1;
},
setSomething: function s_setSomething(val) {
this._data = val;
}
};
// App.js
var App = {
this.store = new Store();
this.store.init();
this.store.getSomething();
this.store.doSomething();
this.store.setSomething (2);
}; 若想要套用 FLUX 架構,首先我們要避免從資料處理器(Store)外部直接改變資料處理器(Store)。我們可以透過在呼叫端使用 window.DispatchEvent 發送自訂事件(CustomEvent),並在資料處理器(Store)中接收自訂事件來做到。
如此一來,資料處理器(Store)將只留下 get 方法來讓外部取得 Store 想提供的資料。
另外如果程式碼改善進入到下一個重點,在操作(Action)時應該不需要再呼叫 Store.getSomething 了,我們將資料處理器(Store)的 getSomething 方法留著給繪圖者(Renderer)使用 。
// Store.js
function Store() {
this._data: 0;
};
Store.prototype = {
init: function s_init() {
window.addEventListener('store_do', this);
window.addEventListener('store_set', this);
},
handleEvent: s_handleEvent(evt) {
switch(evt.type) {
case 'store_do':
this.doSomething();
break;
case 'store_set':
this.setSomething(evt.detail.val);
break;
}
},
getSomething: function s_getSomething() {
return this._data;
},
_doSomething: function s_doSomething() {
this._data += 1;
},
_setSomething: function s_setSomething(val) {
this._data = val;
}
};
// App.js
var App = {
this.store = new Store();
this.store.init();
//this.store.getSomething();
window..dispatchEvent(new CustomEvent('store_do'));
**window..dispatchEvent(new CustomEvent('store_set',**
{'detail':{'val':2}}
));
}; 這麼做帶來的明顯好處是測試時可以簡單地將 Store 與 App 分開來測試,這對大型 App 是很重要的。
改進建議三:讓 Renderer 來處理視圖
若資料處理器(Store)修改的結果需要反映到視圖(View)上,可以透過發送訊息通知給繪圖者(Renderer)處理 > // Renderer.js
var ClickRenderer = {
init: function s_init(element, Store) {
this.element = element;
this.store = Store;
window.addEventListener('render_view1', this);
},
handleEvent: s_handleEvent(evt) {
switch(evt.type) {
case 'render_view1':
this.element.textConent = this.store.getSomething();
break;
}
}};
// Store.js
function Store() {
this._data: 0;
};
Store.prototype = {
init: function s_init() {
window.addEventListener('store_do', this);
window.addEventListener('store_set', this);
},
handleEvent: s_handleEvent(evt) {
switch(evt.type) {
case 'store_do':
this.doSomething();
break;
case 'store_set':
this.setSomething(evt.detail.val);
break;
}
},
getSomething: function s_getSomething() {
return this._data;
},
_doSomething: function s_doSomething() {
this._data += 1;
window..dispatchEvent(new CustomEvent('render_view1'));
},
_setSomething: function s_setSomething(val) {
this._data = val;
}
};
// App.js
var App = {
init: function a_init() {
// get view
this.view1 = document.getElementById('xxx1');
this.view2 = document.getElementById('xxx2');
this.store = new Store();
this.store.init();
ClickRenderer.init(this.view1, **this.store);**
},
handleEvent: a_handleEvent(evt) {
window..dispatchEvent(new CustomEvent('store_do'));
//Store.setSomething(2)
window..dispatchEvent(new CustomEvent('store_set',
{'detail':{'val':2}}
));
};
上段程式在 App 中註冊了 ClickRenderer,並傳入 Store 與 所需的 View 元件。所有的介面更新全交由 ClickRenderer 處理。
(另一個方法是讓繪圖者(Renderer)監看資料處理器(Store)的狀態,然後去改變視圖(View))
http://fluxxor.com/documentation/store-watch-mixin.html
整理完後,一般 javascript 套用 flux 架構的運作流程如下:
簡而言之,上面的各種建議是鼓勵大家多使用 Javascript 內建的 addEventListener, handleEvent, customEvent 等方法。透過大量使用 event,我們可以改善 Javascript 程式邏輯,資料,與介面元件之間的關聯程度。
將 FLUX 架構拆分為三個重點流程來理解或實踐的好處,是我們能漸進地遵循其中一些方法來改善我們現有的程式架構。
以上是我關於如何使用 FLUX 架構在一般 Javascript 組織方式的第三個版本,可能有些錯謬之處,還迎大家討論或給予建議。