使用 FLUX 架構的概念,漸進改善一般 Javascript 程式碼的組織方式
前陣子 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);