Lantern 專案:快速打造屬於自己的 Isomorphic 網站服務
話說,Isomorphic 一直是 Node.js 開發者的夢想,期望同一套程式碼前後端都可以使用,大幅簡化程式碼和加速開發。此外,動態網頁的 SEO 問題也可以同時獲得解決,許多效能問題也可以得到改善。但是,要實現 Isomorphic 的架構,有很多的問題得先解決,會花大量時間在前期工作上,往往讓許多開發者頭痛。
儘管頭痛,仍然阻止不了大家往 Isomorphic 的世界前進,我也因此建立了一個專案「Lantern」,希望能讓更多人能以 Isomorphic 架構,快速建構出自己的網站服務,省去許多前期工作的時間。該專案是一個網站服務的樣板,實作了會員系統、權限管理、第三方登入、多國語系和送信機制等功能,在使用者介面上也做了一個還算美觀的介面。基本上,開發者只要 clone 下來,然後修改設定檔或改改介面、增加點功能,就可以快速完成一個屬於自己的全新網站服務。
最特別的是,「Lantern」整合了現今所有最新的技術和概念,包括了 Koa、React、FLUX、ES6/7+、Webpack 以及 Semantic UI,大量運用了 Generator、class 及 decorator 等最新 JavaScript 語言特性來簡化設計。所以,如果你想要接觸最新的技術,完全可以透過修改「Lantern」專案來學習和熟悉。
目前「Lantern」支援 Facebook 剛發佈的最新 React v0.14+ 和 react-router 1.0.0+,也避免使用像 redux 這類反 FLUX 原始設計的框架,讓原本熟悉 React 和 FLUX 架構的開發者,可以快速上手。也提供一些常見的 Extension,方便開發者寫出前後端通用的程式碼,大多數情況下,開發者不需思考程式碼運行在前端還是後端。
快速安裝使用
若想要使用「Lantern」,方式很簡單,先從 Github 取得程式碼:git clone git@github.com:cfsghost/lantern.git
安裝必要之 NPM 模組:
npm install
使用 webpack 編譯專案(若要正式上線,可加上 -p 選項來編譯):
webpack
運行網站服務:
node app.js
最後可以使用瀏覽器開啟網址,確認是否成功:
http://localhost:3001/
修改設定檔
一般情況,你無需做任何設定就可以把服務跑起來,但如果你需要修改網站名稱、使用自己的第三方登入設定以及電子郵件伺服器,可以修改 Lantern 的設定檔。設定檔是 JSON 的格式,相當容易修改。- 只要進入到「configs」目錄
- 把「general.json.default」複製一份並更名為「general.json」
- 修改「general.json」內的設定
- 重啟服務
目錄架構
如果你想要開始客製化網站服務,需要先簡單理解「Lantern」的目錄架構。- src - 主要程式
- js - 頁面部分的程式
- img - 存放圖片
- less - CSS 原始碼
- translations - 存放多國語言的對應表
- routes - 主要為 Restful API
- lib - 後端的相關函式庫(資料庫、第三方認證、發送電子郵件等功能)
- models - 資料庫 Schema
快速上手開發
首先記得,只要你修改了「src」底下的任何檔案,你必須重新執行「webpack」來進行編譯。或是可以跑一個「webpack -w」在背景,讓 webpack 在檔案有變更的時候自動重新編譯程式碼:webpack -w
一般來說,我們會從頁面修改和增減開始進行客製化工作。由於「Lantern」是採用 React 來繪製頁面,所有的頁面程式都將放在「src/js/components」底下,只要看到副檔名為「.jsx」的檔案,就分別是各種畫面上的元件。
建立新的頁面
建立頁面需要修改「src/js/routes.js」,加入一個網址及對應的頁面元件(以 Chatroom.jsx 為例):module.exports = [ // 省略 ... { path: '/chatroom', handler: require('./components/Chatroom.jsx') } ];
接著可以建立「src/js/components/Chatroom.jsx」檔案,開始設計你的頁面。如果需要使用 FLUX 的機制,可以載入並引入「Lantern」所提供之 decorator 到你的 React 元件上:
import React from 'react'; import { flux } from 'Decorator'; @flux class MyComponent extends React.Component { constructor() { super(); this.state = { messages: [] }; } componentWillMount() { this.flux.on('state.Chatroom', this.flux.bindListener(this.onChange)); } componentWillUnmount() { this.flux.off('state.Chatroom', this.onChange); } onChange = () => { var store = this.flux.getState('Chatroom'); this.setState({ messages: store.messages }); } render() { return <div>{this.state.messages}</div>; } } export default MyComponent;
開發自己的 Actions 和 Stores
假設你已經很了解 FLUX 的開發模式,你可以直接開始設計 Action 和 Store。對「Lantern」而言,無論是 Action 和 Store 都是一樣的東西,只不過執行的順序不一樣。建立 Action(放在 src/js/actions/chatroom.js):
export default function *() { this.on('action.Chatroom.say', function *(name, message) { this.dispatch('store.Chatroom.addMessage', name + ':' + message); }); };
建立 Store(放在 src/js/stores/chatroom.js):
export default function *() { // 初始化一個 state 用來存放 store 的資料 var store = this.getState('Chatroom', { messages: [] }); this.on('store.Chatroom.say', function *(msg) { // 加入新訊息到 store store.messages.push(msg); // State(Store) 已經更新,React 元件會被觸發更新 this.dispatch('state.Chatroom'); }); };
最後在「actions/index.js」和「stores/index.js」分別載入新建立的 Action 和 Store:
export default { // ...省略 chatroom: require('./chatroom') };
存取 Restful API
「Lantern」提供了統一的方法呼叫 Restful API,無論前端還是後端都可以使用(在 Store 或 Action 中),此外,如果在後端使用呼叫,該方法會自動接續使用者的 Session (登入)狀態,進行 Restful API 存取。使某些使用者登入後才可存取的 API,更為容易被存取。export default function *() { this.on('store.Chatroom.getMessages', function *() { var store = this.getState('Chatroom'); try { var res = yield this.request .get('/apis/messages') .query(); // 取得聊天室訊息,並更新到 store store.messages = res.body; // State(Store) 已經更新,React 元件會被觸發更新 this.dispatch('state.Chatroom'); } catch(e) { switch(e.status) { case 500: case 400: console.log('Something\' wrong'); break; } } }); };
在畫 React 元件前先預載資料
後端要把畫面送到瀏覽器前,有時需要先資料庫的資料載入,預先植入畫面之中,前端有時也需要預先載入一些資料,以便畫面宣染時有實質內容。我們可以透過載入「@preAction」這個 decorator 來達成這個需求。「@preAction」會在元件初始化前,先去執行一些工作。底下範例是利用「@preAction」去跑 FLUX 裡的 Action - 「Chatroom.fetchMessages」:
import { preAction } from 'Decorator'; // 相當於 this.flux.dispatch('action.Chatroom.fetchMessages') @preAction('Chatroom.fetchMessages') class MyComponent extends React.Component { // ... }
當然可能要預先做的工作不只一項,而且可能要帶入 React 元件的 props 或更多資訊到 Action 中。「@preAction」可以被帶入函數,作更複雜的設計:
@preAction((handle) => { handle.doAction('Chatroom.fetchMessages'); handle.doAction('Chatroom.doSomething', handle.props.blah, 123); })
因為 Store 會因為「@preAction」而被更新、有資料,這時就可以理所當然地在元件初始化時直接取用 State(Store)的內容。
class MyComponent extends React.Component { constructor(props, context) { super(); this.state = { messages: context.flux.getState('Chatroom').messages; }; } // 省略 ... }
動態載入 JavaScript 或 CSS 檔案
很多 JavaScript 或 CSS 檔案是隨著 React Component 的載入,才會被動態載入,有時甚至需要照順序載入。此外,通常這樣的機制比較多會被使用在前端瀏覽器的頁面上,同樣的載入程式碼工作,在後端 Rendering 時往往會壞掉而無法通用,這在 Isomorphic 的架構中往往需要特別處理,像是判斷執行期是在前端還是後端,相當麻煩。
為此,「Lantern」提供了「@loader」這個 Decorator,使開發者可以容易引入動態載入的機制,而且不用思考前後端的問題,也可以控制載入順序,或是等待檔案載入完成。
以下範例就是一個載入地圖 API 的範例,載入工作只會在前端執行,不會在後端執行:
import { loader } from 'Decorator'; @loader class MyMap extends React.Component { componentWillMount() { // Loader 在後端不會有任何作用 this.loader.css('https://example.com/css/test.css'); } // componentDidMount 只會在前端觸發 componentDidMount() { this.loader.css('https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.5/leaflet.css'); this.loader.css('https://api.tiles.mapbox.com/mapbox.js/plugins/leaflet-minimap/v1.0.0/Control.MiniMap.css'); this.loader.script([ 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.5/leaflet.js', 'https://api.tiles.mapbox.com/mapbox.js/plugins/leaflet-minimap/v1.0.0/Control.MiniMap.js' ], function() { // 初始化地圖 ... }); } render() { return ; } }
取得和監聽視窗資訊
為了更方便前端排版,尤其是需要滿版的設計時,我們往往需要得知或監控瀏覽器視窗的大小,通常做法是存取瀏覽器中的「window」物件,並監聽事件來達成。但「window」物件只在瀏覽器上存在,在後端如果存取該物件,會失敗而且有錯誤發生,在以往 Isomorphic 架構中,每次都要特別處理,相當麻煩。因此「Lantern」預設提供了一個名為「Window」的 Store,將這類資訊包裝起來,使 React Component 能輕易存取又不會因在後端或前端而出現問題。
下面範例就是存取 Window 的例子,以及監聽視窗大小改變的事件。
@flux class MyPage extends React.Component { constructor(props, context) { super(); var win = context.flux.getState('Window'); this.state = { winWidth: win.width, winHeight: win.height }; } componentWillMount() { this.flux.on('state.Window', this.flux.bindListener(this.updateDimensions)); } componentWillUnmount() { this.flux.off('state.Window', this.updateDimensions); } updateDimensions = () => { var win = this.flux.getState('Window'); this.setState({ winWidth: win.width, winHeight: win.height }); } render() { return{this.state.winWidth}x{this.state.winHeight}; } }
看不懂很多 ES6 和 ES7 的東西?
這邊已經整理了一些常用的對應表「ES6 and ES7」,方便開發者理解其中的語法。更多文件和說明
更多資訊可以參考 Github 上的 Wiki:
後記
其實「Lantern」已經改版了幾次,因為之前在好幾個要上線的專案上,每次都發現有些許不足之處,所以就不斷翻新架構和改進,甚至是優化效能。到目前為止,大致已經算是穩定的狀態,未來的開發方向不外乎是繼續寫 Isomorphic 的 Extension,以及效能優化。如果你有興趣,歡迎加入並共同改善這個專案。:-)
留言
張貼留言