前言

最近社群中關於 Flux 架構 的討論很火紅 ,Flux 架構 是 Facebook 所倡導的一種新型架構方式,期望可以解決在我們常見的 MVC 架構上,資料流混亂的問題。

(Original: Fluxxor)

相關的介紹中英文都相當多,就不再多做介紹,引其中兩篇我覺得相當好的文章供有興趣的朋友參考︰

本篇其實算是我目前在學習 Flux 的中間心得整理,會有本篇筆記的動機,主要因為我在學習 Flux 的過程中,覺得各位大大提供的範例,都太「完整」了,一 clone 下來整個目錄結構、檔案,該放的都放好,該裝的都裝好了,直接就可以開始開發。

對我這種笨蛋來說,這些範例雖然體貼,但有點搔不到我想從頭開始打造玩具的癢處,於是我就試著從頭到尾打造一個簡單的 Flux App,功能簡單到近乎白痴,就是個計數器而已。在過程中體驗 Flux 架構的作法和好處,本篇筆記就是這個過程中的思路記錄,供自己未來可以回頭來看。

相關網址如下︰

這個練習用 Flux App 是以 Node.js 開發,因此會假設大家對 Node.js/NPM 有基本的認知,並且有寫過 Node.js App 的經驗。

Step 1 - 建立 App 基本設定

每個 Node.js 最開頭,當然就是以 npm init 指令,NPM 會要求你填入基本專案資訊,來產生 package.json 檔。這些資訊和我們接下來的動作,關係沒那麼大,所以可以視個人需要填入對應的值。

至此我們已經完成了步驟一,大家可以瀏覽看看 GitHub 上 步驟一 branch 的 package.json 檔案內容︰https://github.com/LittleLin/hello-flux-app/tree/step-01

Step 2 - 建立純 HTML 的 template

第二步,我想先建立未來目標 App 的版面,在這邊我直接取用 Bootstrap 中的 Cover 範例 中的 HTML & CSS,稍微調整之後,新增了以下兩個檔案︰

css/style.css
index.html

因為檔案內容比較長,就不貼上來,一樣可以參考 GitHub 上 步驟 2 branch 和 步驟 1 branch 的差異比較︰https://github.com/LittleLin/hello-flux-app/compare/step-01...step-02

此時我們可以瀏覽 index.html 檔案,看看結果是否與示範網站相同。

Step 3 - 建立 View Component: 基本型

第三步,我們可以開始建立 Flux 架構中的 View Component 的部份,也就是下圖中的系統元件︰

React 介紹

Facebook 在 Flux 架構中的 View Component 角色,建議我們使用也是 Facebook 推出的 React library 做為解決方案。

React 是 Facebook 推出來針對使用者界面的 JS Library,定位上有點類似傳統 MVC 架構上的 View 的部份 (但不完全等價)。而 React 在設計上最大的特點,就是希望將網頁中可重用的部份,使用 React 中的 JSX 語法 (和一般 HTML 語法非常相似),獨立寫成一個個 Component

這些以 JSX 語法寫成的 React Component,會再被編譯成一般的 JavaScript 程式碼,使得所以瀏覽器也可以平順執行,整個結構簡圖可參考如下︰

工程師 <-> (Coding) <-> React Component(JSX) <-> (編譯轉換) <-> 純 JS <-> 瀏覽器

Talk is cheap,讓我們開始來寫點 code 吧!

安裝需要的 package

要開發 React Component,我們需要安裝新的 Node.js package: react

$ npm install --save react

React 程式進入點 - js/app.js 檔

在安裝 react package 後,此時我們開始撰寫我們第一個 React 程式,也是我們後續 React Component 的進入點,我們編輯 js/app.js 檔,打入以下程式碼︰

var React = require('react');

React.render(
  <div>Hello</div>,
  document.getElementById('app-container')
);

這裡的 React.render(),接收兩個參數,第一個參數是預計輸出的內容,這個輸出內容格式,是以 JSX 語法撰寫;第二個參數,則是 template 預計被填入的 DOM 元件。

以上述的程式碼為例,則是說明,我們預計將內容 <div>Hello</div> 的內容,填入網頁中 id 為 app-container 的 DOM 元件中。

這邊我們可以留意我們預計輸出的內容︰<div>Hello</div> ,雖然看起來和我們常見的 HTML 很像,但它其實是不折不扣的 JSX 語法,只是 JSX 語法和 HTML 語法非常相像,我們等一下會再繼續看到這樣的例子。

編譯 React Component

如我們前文所說,React 中的 JSX 語法,需要被 compile 轉換成純 js 檔,我們的瀏覽器才看得懂。我們將使用 Browserify 搭配 reactify 轉換模組,來幫我們進行轉換 JSX 語法的動作。

我們先安裝這兩個相關的 Node.js package︰

npm install --save-dev watchify browserify reactify

在安裝完成後,我們可以執行以下指令,來轉換 js/app.js 檔,我們想將轉換後的檔案,放置在 js/bundle.js 這個檔案路徑下︰

browserify -t reactify -o js/bundle.js js/app.js

上述的 browserify 指令參數,說明如下︰

  • -t: 預計使用的語法轉換模組。上述中指定的 reactify,就是我們先前安裝的 reactify 模組
  • -o: 檔案轉換後,預計儲存的目標檔路徑

在編譯後,我們可以看看產出的 js/bundle.js 檔的內容,可以發現原本才 6 行的 js/app.js 檔,編譯後的 bundle.js 卻有上萬行!這是為什麼呢?

這是因為 browserify 背後的原理,其實是將 react package 中的相關內容,給編譯進 js/bundle.js 檔中,細部原理有時間會再開新文來做說明,在這邊我們只要知道 js/bundle.js 是瀏覽器看得懂的純粹 JavaScript 語法就可以了。

調整 index.html 檔

此時我們開始在網站中,引入以 React 建立的 View Component,首先我們先建立一個 id 為 app-container 供 React 來填入內容︰

<div class="cover-container">
  <div id="app-container">
  </div>
...
</div>

接著,我們在網站中,引入剛剛編譯出來的 js/bundle.js 檔︰

<body>
...
  <script src="js/bundle.js"></script>
</body>

在完成後,我們重新整理 index.html,此時應該可以看到結果如下︰

至此,我們可以參考 GitHub 上 步驟 3 branch 和 步驟 2 branch 的差異比較︰https://github.com/LittleLin/hello-flux-app/compare/step-02...step-03,來更了解在兩個步驟間,我們做了哪些事。

Step 4 - 建立 View Component: 重構頁面結構

在第四步中,我們想將網頁中,重要的部份,移至 React Component,以在未來享受 Flux 帶來的好處。以頁面來看,我們想將整個計數器區,獨立成為一個新的 React Component:

以程式碼中來看,就是將下面這區的程式碼,給獨立成一個 React Component︰

第一個 React Component - js/components/HelloApp.react.js

我們將新增我們第一個 React Component︰HelloApp,按 Flux 的慣例,React Component 的放置目錄,是在 js/components 資料夾中,因此我們也依詢這慣例,新增 js/components/HelloApp.react.js 檔案,並填入以下程式碼︰

var React = require('react');

var HelloApp = React.createClass({  
  render: function() {
    return (
      <div className="inner cover">
        <h1 className="cover-heading">Hello Flux!</h1>
        <p className="lead">
          我只是個簡單的計數器,記錄您和我打了幾次招呼!
        </p>
        <p className="lead">
          打招呼︰<em>0</em> 次 
        </p>
        <p className="lead">
          <a href="#" className="btn btn-lg btn-default">Hi!</a>
        </p>
      </div>
    );
  }
});

module.exports = HelloApp;

可以看到我們只是簡單建立一個 Node.js 的 module。當中我們使用 react package 的 createClass() 來建立一個 React Component,並在 render function 指定我們預計要輸出的內容。

在程式碼中,我們可以看到,我們只是單純 index.html 中的 HTML 區塊,搬到 render function 下而已。唯一的改動,就是將原本 index.html 中的 class 屬性,更改為 className 屬性,因為在 JSX 語法中,class 是保留字,所以我們必須做這樣的調整。

從這件事我們也可以觀察到,雖然 JSX 語法大體上和 HTML 很相似,但在一些細節上還是有所不同的。

js/app.js 檔引入 HelloApp Component

這時候我們希望可以在程式中,引入我們新寫好的 HelloApp Component,我們修改了 js/app.js 檔成以下內容︰

var React = require('react');
var HelloApp = require('./components/HelloApp.react');

React.render(
  <HelloApp />,
  document.getElementById('app-container')
);

上述程式我們可以看到,我們已將 HelloApp Cmponent 給引入程式中,並預計將此 Component 的內容,給填入 id 為 app-container 的 DOM 元件中。

這時候我們可以重新執行︰

$ browserify -t reactify -o js/bundle.js js/app.js

來產生 js/bundle.js 檔,再以瀏覽器重新載入檢視 index.html 檔,就可以看到和原來範例網站一樣的結果了。

簡化手動重新產生 js/bundle.js 檔的痛苦

至此,我們已經兩次手動重新產生了 js/bundle.js 檔,你可能會覺得每次改了什麼東西,要看結果都要重新執行 browserify 指令,非常囉嗦!不用怕,我們可以開始使用 watchify package 來解決這個痛點。

watchify 是一個在背景中幫你監看專案程式碼異動的工具,一旦有任何異動,watchify 就會呼叫 browserify 重新幫你編譯產生 js/bundle.js 檔。

我們已經在步驟三中先安裝好了 watchify package,此時我們只要編輯 package.json 檔,加入以下設定︰

{
// ...

  "scripts": {
    "start": "watchify -o js/bundle.js -v -d js/app.js",
    // ...

  },
// ...

  "browserify": {
    "transform": [
      "reactify"
    ]
  }
// ...

}

在設定完成後,我們可以開啓一個新的 terminal,切換到專案目錄下,執行 npm start 指令,然後我們可以看到以下指令輸出︰

littlelin@hello-flux-app:~/workspace (master) $ npm start

> hello-flux-app@1.0.0 start /home/ubuntu/workspace
> watchify -o js/bundle.js -v -d js/app.js

1654618 bytes written to js/bundle.js (4.19 seconds)
1654632 bytes written to js/bundle.js (0.50 seconds)

這時候只要我們不中斷這個 watchify process,watchify 就會持續幫我們監看專案中的異動,並在檔案異動時,重新產生 js/bundle.js 檔。

把玩 React Component 下的事件處理

在版型建立後,我們可以來試玩看看 React Component 下的事件處理,來觀察在沒有實作完整 Flux 架構下,我們怎麼達到計數器的功能。

我們將 HelloApp Compoenent 調整如下︰

// ...
var HelloApp = React.createClass({
  getInitialState: function() {
    return {
      currentCount: 0
    }
  },
  
  render: function() {
    return (
      <div className="inner cover">
        <h1 className="cover-heading">Hello Flux!</h1>
        <p className="lead">
          我只是個簡單的計數器,記錄您和我打了幾次招呼!
        </p>
        <p className="lead">
          打招呼︰<em>{this.state.currentCount}</em> 次 
        </p>
        <p className="lead">
          <a href="#" className="btn btn-lg btn-default" onClick={this._onClick}>Hi!</a>
        </p>
      </div>
    );
  },
  
  _onClick: function() {
    this.setState({currentCount: this.state.currentCount + 1});
  },
});
//...

(或見︰https://github.com/LittleLin/hello-flux-app/blob/step-04/js/components/HelloApp.react.js)

這邊我們可以看到,我們為 HelloApp Component 引入了一個狀態 (State),名稱為 currentCount,初始狀態值我們在 React Component 中的內建 getInitialState() 可以指定,以我們的例子來說,我們在 Component 載入的一開始,將 currentCount 狀態值設定為 0。

我們另外針對連結的 onClick 事件,設置了一個 _onClick() 事件處理器(event handler),看程式碼我們可以了解,這個處理器只是單純將現有的 currentCount + 1 罷了。

但此時你重新瀏覽 index.html 檔,會發現頁面的計數功能生效了!每次點按 Hi 的連接,數字都會往上加一,這是因為 React Component 中的 State 發生了作用。

State 在 React 中是一個重要的概念,因為不管什麼時候,只要 State 值有變動,則整個頁面區塊會被重繪(rerender),所以我們可以看到,雖然我們只是在事件處理器中對 currentCount 往上加一,但整個區塊會重繪,所以我們會看到數字會也會一直往上加。

講到這裡,已經有點多了,剩下的部份,可能就再開一篇下集,來接著說吧。

還是一樣,我們可以參考 GitHub 上 步驟 4 branch 和 步驟 3 branch 的差異比較︰https://github.com/LittleLin/hello-flux-app/compare/step-03...step-04,來更了解在兩個步驟間,我們做了哪些程式調整。

小結

在本文中,我已經建立了基本的 React component,也示範了 React 編譯的方式,在下集中,我將繼續介紹,如何引入 Flux 中另外三個重要角色︰Dispatcher、Store、Actions,和他們的作用。

(待續...)