前陣子 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 組織方式的第三個版本,可能有些錯謬之處,還迎大家討論或給予建議。