標籤:

Isomorphic 構建基礎

最近整理了一下項目上的 Isomorphic 構建流程和基礎代碼,在這裡總結一下。

目標

Isomorphic JavaScript 的目標其實很簡單,就是為了解決 SPA 的兩個問題:

  • SEO 不友好

  • 首屏載入太慢

要解決這兩個問題,就必須所有需要 SEO 和可能作為首屏的頁面都可以在頁面請求 response 的時候就顯示出來,這就要求這些頁面需要 server render。同時為了保持 SPA 的優點,後續頁面都要 client render。為了最大化代碼的復用,server render 和 client render 都應該儘可能基於同一份代碼,這就是 Isomorphic 的由來。

實現

由於我們公司是基於 React 和 Webpack 的項目,所以我們的實現也是基於這兩個的。下面我們來講講現在比較流行的實現方案。

代碼劃分

將代碼分為 Server、Client、Common、Router 四個部分。

  • Router 是所有路由相關的代碼,也是所有 component 的入口,對外只暴露一個 `createRouter` 方法。

  • Common 是 Server 和 Client 都會共用到的代碼,不包括 component

  • Server 是 Server Render 相關的代碼

  • Client 是 Client Render 相關的代碼

按照項目和團隊習慣,可能還有很多其他的部分,但是其他部分一般都不涉及到特殊的構建方式,一般跟 client bundle 到一起就行了。

universal-webpack

原本我們項目使用的是 universal-webpack 這個工具,它主要做了下面幾件事情:

  • 根據一個基礎配置生成 client、server 端的配置

  • 在 client bundle 的過程中將 client code, common code, router code, node_modules 都 bundle 到一起,並且生成 `webpack-chunks.json`,指導 server 端如何引入靜態文件。

  • 在 server bundle 的過程中將 server code, common code, router code 都 bundle 到一起

  • 提供一個輔助 server 啟動的方法,這個方法會等待 server code 和 `webpack-chunks.json` 都編譯好,才去運行 server

我們在使用它的過程中就發現了兩個問題:

  • 我們的 webpack 配置被 `universal-webpack` 隱式的修改了,當我們想要自定義的時候,很容易被搞暈

  • `universal-webpack` 的 server 端實現方式是把 server code, common code, router code 都 bundle 到一起,而實際上我們只需要用 babel 來轉換 server code, common code,然後 bundle router code,在 server 端 import router 就好了

這兩個問題導致我們決定不依賴 `universal-webpack`,自己去完成構建的配置。

我們的實現

首先來看一下我們最終的結構:

  • client 端通過 webpack,將 client code, common code, router, node_modules 都 bundle 到一起,並且在過程中生成 `webpack-chunks.json`

  • 通過 express / koa / nginx 等伺服器 host 這些靜態文件

  • router 通過 webpack 單獨 bundle,以便 server 端使用

  • server 端通過 babel 轉換 server code, common code,生成最終的 server 端代碼

  • 最終的 server 端代碼將 router、`webpack-chunks.json` import 進來,run 起來以後就大功告成了

這是我們的最終結構,過程中會涉及到很多實現:server 端的配置和啟動代碼、client 端 webpack 配置、如何保證 server render 和 client 第一次 render 輸出的結果一致、如何在開發環境做 hot reload。這篇文章主要介紹前兩個。

Server 端的配置和啟動

Server 端的 server code 和 common code 只需要 babel 處理,所以配置可以很簡單的放在 .babelrc 中:

{n "presets": [n ["env", {n "targets": {n "node": "current"n }n }],n "stage-3"n ]n}n

Server 端的 router 需要用到 webpack 來處理,這是因為其中有很多靜態資源的處理,如果用 babel 雖然可以做,但是沒有辦法復用一些跟 client 端相同的 webpack 配置。下面會列出來一些主要的 webpack 配置:

  • `style-loader`, `css-loader` 要替換為 `css-loader/locals`,這樣就只會替換 className

  • 所有使用了 `file-loader` 的,要加上 `emitFile: false` 這個 option,這樣就不會真的生成文件

  • `target: node`,這樣如果我們生成了多個 chunk,webpack 會使用 import 來引入他們;或者直接用 `LimitChunkCountPlugin`,限制 `maxChunks: 1` 也可以

  • `output.libraryTarget: commonjs2`,這樣我們在外部才可以用 import 來引入生成的代碼

  • `externals`,為了避免 webpack 將依賴也 bundle 到代碼中(因為 server 端我們是可以直接 import 這些第三方的),我們還需要用到一個插件 webpack-node-externals,來將所有的依賴都放到 `externals` 中。

這樣我們 Server 端的配置就完成了。而 server 端的啟動就很簡單了,只需要等待 server code bundle 完成,`webpack-chunks.json` 生成好,就能啟動了。啟動的時候在 production 就對著生成好的代碼 node run 一下就可以了,在開發環境可以不用 babel 編譯,直接用 babel-node 來啟動。

Client 端的 webpack 配置

Client 端的 webpack 配置,除了兩個需要注意的地方,其他都與一般的 SPA 程序一樣。

  • 需要生成 `webpack-chunks.json`,可以使用 assets-webpack-plugin 這個插件。如果要自定義格式,可以配置 `processOutput`

  • `css-loader` 的 `localIdentName` 需要跟 server 端配置保持一致,這樣才能讓樣式正確的顯示

總結

前端的構建越來越複雜,很多前端開發可能傾向於直接使用一些 start kit,或者工具來直接跳過自己配置,但是這樣會讓自己對於構建這一塊不是很了解,在未來業務擴展需要自定義的時候就蒙逼了。所以作為前端開發還是需要有工程的思維,了解並且熟悉這些工程化的方式。其實前端工程化整體來講還是比較弱的,現在並沒有一個非常成熟的方案來實現 Isomorphic,比如:如何保證 server render、client render 時的數據一致性;對於非同步載入的 component,如何做 hot reload(Asynchronous React Router v3 routes fail to hot-reload · Issue #288 · gaearon/react-hot-loader);如何做 css 的 code split 和非同步載入……有很多的問題還沒有被很好的解決,說明這方面還有很大的提升空間,github.com/zeit/next.js 正在做這方面的嘗試,雖然現階段還不完善,不過也是一個很好的參考。我們也可以做出自己的解決方案,讓前端構建和架構越來越好。

推薦閱讀:

React Redux 中間件思想遇見 Web Worker 的靈感(附demo)
有誰能詳細講一下css如何畫出一個三角形?怎麼想都想不懂?
每個頁面都新建了一個css,這樣會不會帶來麻煩?後端的也總說這樣不利於修改,一改就要一路改過去好麻煩的說。
請問學習前端最有效的辦法是什麼?
react中出現的"hydrate"這個單詞到底是什麼意思?

TAG:前端开发 |