NW.js + React.js + Google Material Design Lite 簡單整合範例

前言

上一篇 “NW.js + React.js + Material UI 簡單整合範例” 我用一個簡單的範例示範如何在 NW.js 裡整合 React.js 以及 Material-UI. 不過這畢竟是別人開發的山寨版. 既然 Google 發表了 Material Design Lite, 我們就不用再繼續用山寨版了. 再者 Material-UI 這個 Project 目前有遭遇到一些跟新版 React.js 相容性的問題, 因為它跟 React.js 內部綁得太深了, 所以我就趁現在還在研究階段就先跳槽了.

在上一篇我忘記提到要看這兩篇之前, 你必須要熟悉 Node.js, npm, 以及 NW.js 的操作. 如果不了解, 網上有很多教學以及大作. 詳情請問 Google 大神.

其實在整合的過程, 我也遇到了一些問題. 問題的點在於 Material Design Lite 只處理靜態的網頁資料. 如果要動態產生網頁內容, 必須要通知 Material Design Lite 去更新顯示. 可是 React.js 是用 Virtual DOM 的概念最小化 DOM 的更新次數以達到高效能的畫面處理. 那要怎麼做呢? 很簡單, 就是 React.js 在 DOM 資料更動之後通知 Material 去更新畫面, 以下用一個簡單的流程表示.

  1. 主程式註冊一個函式去收聽某一個自訂事件, 當收到事件時更新畫面.
  2. React.js 畫面更新 -> 發事件 -> 主程式收到事件 -> 呼叫 Material Design Lite 更新畫面.

2015/7/10 更新:

原本的範例是自己寫 EventDispatcher 去做這件事情, 後來發現 W3C 的 DOM4 Mutation Observers 其實大家都已經先偷跑了, 所以目前可以採用這個方案.

準備工作

請先下載 Node.js 或是 io.js 並安裝. 接下來下載 NW.js 並解壓縮到某一個目錄. 將 NW.js 的路徑設定到 PATH.

我的工作目錄下有下列子目錄:

  • app: 我用來放自己寫的 Javascript 程式的目錄
  • lib: 我用來放外部函式庫的地方
  • css: 我用來放 style sheet 的地方. 在這範例用不到.

底下還有 index.html 以及 package.json.

在 app 底下有下列檔案:

  • main.js
  • main_ui.js

在 lib 底下有下列檔案:

  • material.min.css
  • material.min.js

與 Node.js 一樣, NW.js 需要 package.json 告訴它要從哪裡開始載入, 並採用那些函式庫. 以下是我採用的 packages:

{
  "name": "Tester",
  "main": "index.html",
  "scripts": {
    "start": "nw",
  },
  "dependencies": {
    "node-jsx": "^0.12.4",
    "react": "0.13.1",
    "react-tap-event-plugin": "^0.1.6",
  }
}

範例一: package.js

接下來是 index.html 的內容:

<!DOCTYPE html>
<html>
    <head>
        <title>Tester</title>
        <link rel="stylesheet" type="text/css" href="lib/material.min.css">
        <link rel="stylesheet" href="http://fonts.googleapis.com/icon?family=Material+Icons">
        <script src="lib/material.min.js"></script>
    </head>
    <body>
        <script>
        global.document = window.document
        global.navigator = window.navigator
        MutationObserver = window.MutationObserver || window.WebKitMutationObserver;

        var observer = new MutationObserver(function(mutations, observer) {
            componentHandler.upgradeDom();
        });

        observer.observe(document, {
            subtree: true,
            attributes: true,
            childList: true
        });

        window.onload = function() {
            require('node-jsx').install({extension: '.js'});
            require('./app/main.js');
        }
        </script>
    </body>
</html>

範例二: index.html

其實 Material Design Lite 有提供 npm 的 package 可以安裝, 但是透過 npm 就沒辦法自訂顏色了. 當然你可以自己配色, 不過 Material Design Lite 網站有提供簡單的配色盤. 因為我有自己改顏色, 所以我就直接用下載版了.

為了不跟自己的程式碼混一起, 我把外部 Javascript 跟 CSS 都放在 lib 底下. 至於 Material Deisgn Lite 還是有用到外部的 CSS 以及 Icon, 如果要做完全 Offline 的應用程式, 有興趣的朋友可以自己下載自己改, 在此就不詳述了.

接下來看到這一段程式碼:

....
MutationObserver = window.MutationObserver || window.WebKitMutationObserver;

var observer = new MutationObserver(function(mutations, observer) {
    componentHandler.upgradeDom();
});

observer.observe(document, {
    subtree: true,
    attributes: true,
    childList: true
});
....

2015/7/10 更新:

這一段需要特別注意, 因為 Material Design Lite 只處理靜態 DOM 物件, 所以只要頁面上的 DOM 物件內容改變了, 就必須要通知 Material Design Lite 的 componentHandler 重新 Render 畫面上的特效, 不然可能有些顯示會出問題.

這次我採用不一樣的策略, 不用自己實作 EventDispatcher 讓每個物件去通知更新. 相反的, 我利用”偷跑”的 DOM Mutation Observer 去監看目前 DOM Document 更動情形, 一有更動就重新更新. 在 IE/Mozilla 的瀏覽器裡 Mutation Observer 是叫 window.MutationObserver, 在 webkit based 的瀏覽器 (Chrome/Opera/Safari) 則是用 window.WebKitMutationObserver. 上述的程式碼產生一個新的 Mutation Observer 物件並做了一個 callback function 去執行 Material Design Lite 的更新.

我設定的 observer.observe 的選項代表的意義:

  • subtree: 監聽的 DOM 物件的所有子孫物件(物件樹)的更動
  • attributes: 屬性的更動
  • childList: 監聽的 DOM 物件的子物件

其他的部分在上一篇有說明.

接下來是 main.js 的內容:

(function () {
    var React = require('react'),
        injectTapEventPlugin = require("react-tap-event-plugin"),
        Main = require('./main_ui.js');

    //Needed for React Developer Tools
    window.React = React;

    //Needed for onTouchTap
    //Can go away when react 1.0 release
    //Check this repo:
    //https://github.com/zilverline/react-tap-event-plugin
    injectTapEventPlugin();

    // Render the main app react component into the document body. 
    // For more details see: https://facebook.github.io/react/docs/top-level-api.html#react.render
    React.render(<Main />, document.body);
})();

範例三: app/main.js

內容的解釋在上一篇有提過了不再詳述.

主程式

接下來是主程式的範例, 跟上一篇的範例一樣, 我只想用一個按鈕作範例.

var React = require('react'),
    dispatcher = require('./EventDispatcher.js');

var MainUi = React.createClass({
    render: function() {
        return (
            <button id="btnTest" className="mdl-button mdl-js-button mdl-button--raised mdl-button--accent mdl-js-ripple-effect">
              Button
            </button>
        );
    }
});

module.exports = MainUi;

範例四: app/main_ui.js

跟上次不太一樣的是, 我這次多加了這個部分:

....
<button id="btnTest" className="mdl-button mdl-js-button mdl-button--raised mdl-button--accent mdl-js-ripple-effect">
Button
</button>
....

這裡就是實際上 Material Design Lite 的實作, 詳細用法可以參考 Material Design Lite 網站.

像 Android app 一樣, React.js 有自己的 Life Cycle. 要讓 React.js 效能好就要讓頁面減少 DOM 物件的 Rendering, 這部分可以參考 React.js 的進階.

執行

確認環境設定好之後, 在工作目錄下打 npm install 安裝所需的套件. 接下來執行 nw . 應該就可以看到一個有水波紋效果的按鈕在螢幕上.

後記

新的 DOM4 Mutation Observers 的解決方案看起來簡潔許多. 當然還是要密切注意新的標準會不會又胎死腹中. 除此之外, React.js 跟 Material Design Lite 目前都還算相處融洽.

留言

熱門文章