前面大致介紹完初始環境建置,今天就來看看 frontend server 的設計,並實際安裝套件和啟動 server。
雖然 Node.js 在安裝時已自帶一套 package manager npm,我們這次會另外安裝別的 package manager 來協助專案的套件安裝過程。內建的 npm 有什麼不足之處呢?第一個問題在於速度,3.x 版後的 npm 在盡可能攤平 node_modules
巢狀 dependency 結構的計算上犧牲了安裝速度;當然在後續的版本中已大幅改善,且開發者自己也能用其他方式加速 npm install
的速度。
第二個比較大的問題是缺少一般套件管理工具常見的 lock 機制。所謂的 lock 機制是在安裝時期記錄實際安裝於環境內的套件版本,而非套件宣告檔指定的版本。一般而言,套件宣告檔會以較寬鬆的版本指定形式撰寫,再交由安裝時期的管理工具進行 version resolving。有了 lock 機制,就能確保在不同的環境、不同的時間下進行套件安裝時,都能安裝到一致的套件版本。
以 iOS 開發常用的套件管理工具 CocoaPods 為例,套件安裝的過程大致可整理為下列步驟:
Podfile
來指定需要的套件pod install
安裝套件Podfile.lock
檔案,並產生給 Xcode 使用的 xcworkspace
檔案如果專案中已存在 Podfile.lock
,CocoaPods 在第二步會根據檔案內指定的套件版本進行安裝,只有在 Podfile
有版本更動時會更新 Podfile.lock
的內容。
回到 npm,其實 npm 本身有稱為 shrinkwrap 的 lock 機制,但 shrinkwrap 是個 optional 功能,需要手動執行 npm shrinkwrap
產生 lock 檔。因此,Facebook 於今年提出了一個新的套件管理工具 Yarn(註1),除了宣稱安裝速度極快,也提供 yarn.lock
為 lock 機制,讓 Node.js 開發能夠擁有 lock 機制的好處。
使用 npm install -g yarn
安裝 Yarn 後,接下來就能直接使用 yarn
、yarn add <package-name>
等指令做套件安裝,每次均會更新 yarn.lock
。不過,Yarn 當然不會是最佳選擇,目前(12/7 現在)還在 0.17.x 的階段、使用上也容易遇到一堆 bug(尤其是剛推出時非常令人困擾的 issue),所以還是會常搭配 npm 進行安裝。
本次專案使用 single page application (SPA) 架構設計,frontend server 只運行一個簡單的 Express.js app 負責提供 HTML 給瀏覽器,Routing 機制全部交由前端的 JS 處理。(Routing 的細節預計在後續的章節介紹)因此 Express.js side 的 router 就只有單純的把所有 URL 導給單一頁面:
app.get('*', (req, res) => {
res.render('index.server.pug', {
src: {
script: scriptSrc
},
title: 'Online Course'
});
});
在 SPA 架構下 frontend server 做的事情很少,理論上在 production 階段,連上面這個提供 HTML 頁面的 router 都能以靜態形式搬移到 CDN 或 AWS Lambda 上;不過這次的專案中預計會使用 token-based authentication 機制,屆時 frontend server 還會負責一些 authentication 的工作,這部份會等 API Server 完成後回來實作 frontend server 的註冊/登入機制。
React.js 被稱為 universal (isomorphic) 的原因是其支援於 Node.js 執行部分 React.js 程式的特性。借助這個特性,Node.js server 可以在收到頁面的 request 時先在 server 進行一次 ReactDOMServer.renderToString
過程,輸出靜態 HTML code 作為 response body,瀏覽器即可直接檢視頁面內容而不用等待前端的 ReactDOM.render
執行完畢。這個機制稱為 server-side rendering (SSR),能夠強化 search engine optimization (SEO)、程式效能和 code reuse 能力。
要注意的是,SSR 並非 drop-in feature,若是需要使用 SSR 機制,在撰寫 client-side code 時要注意程式碼是否有 universal 性質,例如:
require
呼叫componentDidMount
methodui-router
的 resolve
機制可達到非同步 routing 功能)需要能在 server side 執行,所以需要 isomorphic-fetch
、superagent
或 axios
等 universal AJAX library在實作 React SSR 時,Webpack 的 entry
和 server side 的進入點會分開各自執行 ReactDOM.render
和 ReactDOMServer.renderToString
,比較麻煩的是需要各自準備 initial state 交給 root component。以使用 Redux 為例,通常的作法是在 server side 設定 initial state、進行 @connect
和 ReactDOMServer.renderToString
,然後將 initial state 塞在 HTML (本專案用 Pug 進行 server side HTML rendering)內:
script(type="text/javascript").
window.__INITIAL_STATE__='!{initialState}';
並從 Webpack 的 entry
提取出來:
const initialState = window.__INITIAL_STATE__;
window.__INITIAL_STATE__ = null;
// feed initialState to Redux store...
註1:yarn 原為名叫 kpm 的內部專案,後來 Facebook 公開後改了個和其他領域的現有工具同樣的名稱