2015年9月20日 星期日

Fluky - 打造 Isomorphic App 的副產品:一個基於事件驅動的 Flux 框架

Standard

要講到 Fluky 這一個 Flux 框架,這要從我的一個新計畫說起。因為最近又重新興起了一波 Isomorphic App 的熱潮,許多人開始打造了自己的 Isomorphic 網站,自己也做了一個。而什麼是 Isomorphic 呢?簡單來說,就是寫一次程式,然後前後端都可以使用的機制,也是一個網站服務工程師的夢想。還記得過去自己曾實作了 frex.js 試圖達成 API 層面的 Isomorphic,現在 React 這樣的前端框架,更提供了一個打造前後端 Rendering 的 Isomorphic,使得原本在前端動態產生的畫面,可以在後端產生,更一舉解決了 SEO 的問題。

話說,搭上了這波熱潮,最近開始土炮自己的 Isomorphic App,Github 上開啟了一個「Lantern 燈籠」專案,希望做一個標準的專案架構,讓自己以後開發新專案時,可以不需要重新再來一遍。痛苦的是,與很多人一樣,踩到了很多地雷,在專案架構設計上,也一直有很多許要調整的地方,這也難怪,畢竟這是一個興新的開發概念。

於是從零到有的過程中,也有許多副產品,其中包括了一個新的 Flux 框架「Fluky」。很多人問我為什麼不採用當今紅遍半天邊的「redux」,原因其實很簡單,我不想脫離傳統 Flux 模式和 React 開發的概念太遠,然後同時想要用試著更精簡的方式描述這些流程。另外一點是,受到過去 X11 這世界最先進的網路圖形化視窗系統的設計所啟發,打算試著全面使用「事件」來管理資料流和程式上任何的溝通。

如果你有興趣,可以直接以 NPM 來安裝這個 Flux 框架:
npm install fluky

Fluky 的設計


基本上, Fluky 本身的概念很簡單,幾乎所有的行為都是透過 Fluky.dispatch() 這個 API 來進行,包括呼叫 Action、Store,然後所有的行為都可以使用 Fluky.on() 所監聽。也就是說,只需要這兩個 API,幾乎就已經足夠。對於 View 的工作而言,永遠就是呼叫 Fluky.dispatch('action.*') 和監聽 Fluky.on('store.*') 。

這樣設計有什麼好處呢?因為所有的訊息和命令傳遞,都有統一的事件機制和命名規則,理論上來說,事件可以很容易被提到前端或是放在遠端被處理,這就有點像 X11 的設計,可遠端也可本地端進行圖形繪製處理。不過對 Fluky 來說,這目前還算太遠了,目前 Fluky 還沒有真正處理太過複雜的狀況,純粹就是以完全的事件化來處理資料流。

也因為一切都事件化,就可以良好支援 Isomorphic 的設計,例如很多的 Action、Store 工作,可以有個前後端統一的命名和呼叫方法,在 Server 預處理,便於 Server Rendering 的使用,甚至是一部份在前端做,一部份在後端做都有可能。最重要的是,在 Isomorphic 上會碰到的前後端 state 不一致的狀況,也可以很容易使用事件、或是在事件分配中的空擋,進行 state 同步而獲得解決。

此外,為了嘗試新技術,Fluky 也在前端引入了 Generator ,所以如果你想要使用 Fluky,要確保前端瀏覽器能使用 ECMAScript 6+ 最新的標準,或是你必須安裝 babel 模組來打包並轉換你的程式碼為 ES5。

講了這麼多,怎麼使用 Fluky 呢?下面將以實作一個簡單的 TODO 清單為例。

建立 Action

import Fluky from 'fluky';

Fluky.on('action.Todo.toggle', function *(todo) {
  // 用不同的 store 方法處理
  if (todo.completed)
    Fluky.dispatch('store.Todo.unmark', todo.id);
  else
    Fluky.dispatch('store.Todo.mark', todo.id);
});

建立 Store

var todoStore = Fluky.getState('Todo', {
  todos: [
    {
      id: 1,
      name: '寫一篇文章',
      completed: false
    }
  ]
});

Fluky.on('store.Todo.unmark', function *(id) {

  // 找到指定的 TODO 項目
  for (var index in todoStore.todos) {
    var todo = todoStore.todos[index];

    if (todo.id == id) {
      // 改為未完成
      todo.completed = false;

      // 發出 store 已改變的事件
      Fluky.dispatch('store.Todo', 'change');
      break;
    }
  }
});

Fluky.on('store.Todo.mark', function *(id) {

  // 找到指定的 TODO 項目
  for (var index in todoStore.todos) {
    var todo = todoStore.todos[index];

    if (todo.id == id) {
      // 改為完成
      todo.completed = true;

      // 發出 store 已改變的事件
      Fluky.dispatch('store.Todo', 'change');
      break;
    }
  }
});

在 React 元件內的使用

import React from 'react';
import Fluky from 'fluky';

class TodoList extends React.Component {

  constructor() {

    // 取得 Todo 的 Store,從 Fluky 的 state 資料暫存區
    this.state = {
        todos: Fluky.getState('Todo').todos;
    };
  }

  componentDidMount() {
    // 監聽 store 的改變事件
    Fluky.on('store.Todo', Fluky.bindListener(this.onChange));
  }

  componentWillUnmount() {
    // 停止監聽 store
    Fluky.off('store.Todo', this.onChange);
  }

  onChange = () => {

    // 當 store 有改變時更新元件的 state
    this.setState({
      todos: Fluky.getState('Todo').todos;
    });
  }

  toggle = (todo) => {
    // 呼叫 Action 去切換工作項目狀態
    Fluky.dispatch('action.Todo.toggle', todo);
  }

  render: function() {
    var todoList = [];

    // 印出所有的工作項目
    this.state.todos.forEach((todo) => {
      todoList.push(
{todo.text}
); }); return (
{todoList}
); } }

State 資料暫存區的設計

傳統的 Flux 做法,不外乎是載入所要的 Store 檔案,來取得 Store 資料,這樣做不但麻煩且囉唆。既然事件分配器(Event Dispatcher)是 Singleton(只存在一個實例,所有人共用),將 Store 的資料共同管理顯然是比較簡單的做法,然後只需要統一使用 Fluky.getState() 就可以取得所需要的 Store 資料。

如果從前述範例來看,Fluky.getState() 可以帶兩個參數,第一個是 State 的名稱,第二個是當 State 不存在時,其預設值。

當然,這個暫存區是可以整個取出來的,也可以使用 Fluky.setInitialState() 或是藉由 window.Fluky.state 在第一時間載入時整個放回去,這可以應用在解決 Isomorphic App 的前後端 Store 不同步的問題。

後記


新專案「Lantern 燈籠」目標就是嘗試開發一個 Isomorphic 的網站服務,並使用最新的技術,此外,也希望開發一些基本功能(如:會員系統、第三方登入、權限管理等),方便日後開發新網站服務時,可以避免早期的苦工和踩地雷。這是一個 Open Source 專案,如果你有興趣,可以一同開發。:-)