Skip to main content

3 posts tagged with "webapp"

View All Tags

· 2 min read

幾週前在 Modern Web 2015 分享了以「Javascript,征服世界是可能的嗎?」為主題的演講。

演講主題靈感來自於 The JavaScript World Domination 一文。

原本想用編年史的形式表現,一直發展到演講的前幾天,才演變成最終使用的形式。

公布講題後,一些人先跑來問我 JS 是不是真的可能征服世界。
我能理解大家想要知道最終的答案迫切。但其實大多數時候,聽別人的預測,還是不準確的比率更高。

就我而言,了解別人推論的過程,與他所引用的資料,會影響我對他預測結果的信賴度。哪些資訊是我所不知道的(秘密),哪些是我知道但沒有意識到與推斷目標關聯性的。從推敲的過程中,我可以學到一些新東西,也可以產生一些新想法。這樣的過程比偷看答案有趣地多。

這份投影片裡面分享了四個 JS 征服世界的秘密,你是否已經知道?我的觀察跟你的觀察一致嗎?有沒有什麼其他你觀察到的秘密想分享哩?

· 3 min read

This time, its different. The transpilers are build-time polyfills that fill the gap of current browser/server implementation and the newest JS/CSS specs.

Transpilers trans-compile Javascript and CSS to current workable version, so developers could be more productive with JS/CSS latest features and transpilers would translate them into current supported version of code.

From my opinion the most useful es6 feature is arrow functions (=>) which comes from coffeescript. This syntax sugar bind the this value automatically, so developer wont forgot the binding anymore.

The original code is

(function() {
‘use strict’;
document.addEventListener(‘DocumentLocalized’, function() {
// App.init();
}.bind(this));
}());

We can use arrow function to replace function() {}.bind(this) to () => {}

(function() {
‘use strict’;
document.addEventListener(‘DocumentLocalized’, () => {
// App.init();
});
}());

Currently the arrow functions only default enabled on Firefox. So developer could not use this directly on their project. With babel javascript transpiler the js could be translated to current workable version automatically.

(function() {
‘use strict’;
document.addEventListener(‘DocumentLocalized’, function() {
// App.init();
});
}());

The transpiler will know the content does not need 'this' reference and skip the binding. Note the Javascript transpiler still stick into vanilla javascript. It does not invent new syntax, but it make new specs could be used or experiment in current environment.

From CSS perspective, CSS variables brings variables into CSS. Pre-define some color and use everywhere on project is possible.

The origin style is

a {
color: #847AD1;
}

pre {
padding: 10px;
}

It's frustrated when the stylesheets expand larger and we need to change the new color or size everywhere.

We can use CSS variables to predefine the changeable stuff into pseudo ':root' element.

:root {
–maincolor: #847AD1;
–size: 10px;
}

a {
color: var(–maincolor);
}

pre {
padding: var(–size);
}

Looks good. But the same situation occurred. Currently the CSS variables only support on Firefox. So developer could not use this directly on their project. With myth CSS transpiler the CSS could be translated to current workable version automatically.

a {
color: #847AD1;
}

pre {
padding: 10px;
}

Note the CSS transpiler still stick into CSS specs. It does not invent new syntaxes like LESS or SASS does. It just make new CSS specs could be used or experiment in current environment.

Besides the feature polyfill, running transpiler before deploy also help developer catch out the error since transpiler will traverse the source and parsing syntaxes. Transpiler will bark you when any error in your source.

You may wonder setup an environment with above JS/CSS transpiler seems take some time. webapplate 2.3.0 already integrated with babel  and myth grunt tasks to auto transpile your JS/CSS code to current workable version. webapplate jshint/githook also support es6 syntax validation (yes the .jshintrc is exactly borrowed from gaia project), so your project is future proved and keep maintainable.

· 13 min read

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