Webpack實戰-構建同構應用
同構應用是指寫一份代碼但可同時在瀏覽器和伺服器中運行的應用。
認識同構應用
現在大多數單頁應用的視圖都是通過 JavaScript 代碼在瀏覽器端渲染出來的,但在瀏覽器端渲染的壞處有:
- 搜索引擎無法收錄你的網頁,因為展示出的數據都是在瀏覽器端非同步渲染出來的,大部分爬蟲無法獲取到這些數據。
- 對於複雜的單頁應用,渲染過程計算量大,對低端移動設備來說可能會有性能問題,用戶能明顯感知到首屏的渲染延遲。
為了解決以上問題,有人提出能否將原本只運行在瀏覽器中的 JavaScript 渲染代碼也在伺服器端運行,在伺服器端渲染出帶內容的 HTML 後再返回。
這樣就能讓搜索引擎爬蟲直接抓取到帶數據的 HTML,同時也能降低首屏渲染時間。由於 Node.js 的流行和成熟,以及虛擬 DOM 提出與實現,使這個假設成為可能。實際上現在主流的前端框架都支持同構,包括 React、Vue2、Angular2,其中最先支持也是最成熟的同構方案是 React。
由於 React 使用者更多,它們之間又很相似,本節只介紹如何用 Webpack 構建 React 同構應用。
同構應用運行原理的核心在於虛擬 DOM,虛擬 DOM 的意思是不直接操作 DOM 而是通過 JavaScript Object 去描述原本的 DOM 結構。
在需要更新 DOM 時不直接操作 DOM 樹,而是通過更新 JavaScript Object 後再映射成 DOM 操作。虛擬 DOM 的優點在於:
- 因為操作 DOM 樹是高耗時的操作,盡量減少 DOM 樹操作能優化網頁性能。而 DOM Diff 演算法能找出2個不同 Object 的最小差異,得出最小 DOM 操作;
- 虛擬 DOM 的在渲染的時候不僅僅可以通過操作 DOM 樹來表示出結果,也能有其它的表示方式,例如把虛擬 DOM 渲染成字元串(伺服器端渲染),或者渲染成手機 App 原生的 UI 組件( React Native)。
以 React 為例,核心模塊 react
負責管理 React 組件的生命周期,而具體的渲染工作可以交給 react-dom
模塊來負責。
react-dom
在渲染虛擬 DOM 樹時有2中方式可選:
- 通過
render()
函數去操作瀏覽器 DOM 樹來展示出結果。 - 通過
renderToString()
計算出表示虛擬 DOM 的 HTML 形式的字元串。
構建同構應用的最終目的是從一份項目源碼中構建出2份 JavaScript 代碼,一份用於在瀏覽器端運行,一份用於在 Node.js 環境中運行渲染出 HTML。
其中用於在 Node.js 環境中運行的 JavaScript 代碼需要注意以下幾點:- 不能包含瀏覽器環境提供的 API,例如使用
document
進行 DOM 操作, 因為 Node.js 不支持這些 API; - 不能包含 CSS 代碼,因為服務端渲染的目的是渲染出 HTML 內容,渲染出 CSS 代碼會增加額外的計算量,影響服務端渲染性能;
- 不能像用於瀏覽器環境的輸出代碼那樣把
node_modules
里的第三方模塊和 Node.js 原生模塊(例如fs
模塊)打包進去,而是需要通過 CommonJS 規範去引入這些模塊。 - 需要通過 CommonJS 規範導出一個渲染函數,以用於在 HTTP 伺服器中去執行這個渲染函數,渲染出 HTML 內容返回。
解決方案
接下來改造在3-6使用 React 框架中介紹的 React 項目,為它增加構建同構應用的功能。
由於要從一份源碼構建出2份不同的代碼,需要有2份 Webpack 配置文件分別與之對應。
構建用於瀏覽器環境的配置和前面講的沒有差別,本節側重於講如何構建用於服務端渲染的代碼。
用於構建瀏覽器環境代碼的 webpack.config.js
配置文件保留不變,新建一個專門用於構建服務端渲染代碼的配置文件 webpack_server.config.js
,內容如下:
const path = require(path);const nodeExternals = require(webpack-node-externals);module.exports = { // JS 執行入口文件 entry: ./main_server.js, // 為了不把 Node.js 內置的模塊打包進輸出文件中,例如 fs net 模塊等 target: node, // 為了不把 node_modules 目錄下的第三方模塊打包進輸出文件中 externals: [nodeExternals()], output: { // 為了以 CommonJS2 規範導出渲染函數,以給採用 Node.js 編寫的 HTTP 服務調用 libraryTarget: commonjs2, // 把最終可在 Node.js 中運行的代碼輸出到一個 bundle_server.js 文件 filename: bundle_server.js, // 輸出文件都放到 dist 目錄下 path: path.resolve(__dirname, ./dist), }, module: { rules: [ { test: /.js$/, use: [babel-loader], exclude: path.resolve(__dirname, node_modules), }, { // CSS 代碼不能被打包進用於服務端的代碼中去,忽略掉 CSS 文件 test: /.css/, use: [ignore-loader], }, ] }, devtool: source-map // 輸出 source-map 方便直接調試 ES6 源碼};
以上代碼有幾個關鍵的地方,分別是:
target: node
由於輸出代碼的運行環境是 Node.js,源碼中依賴的 Node.js 原生模塊沒必要打包進去;externals: [nodeExternals()]
webpack-node-externals 的目的是為了防止 node_modules 目錄下的第三方模塊被打包進去,因為 Node.js 默認會去 node_modules 目錄下尋找和使用第三方模塊;{test: /.css/, use: [ignore-loader]}
忽略掉依賴的 CSS 文件,CSS 會影響服務端渲染性能,又是做服務端渲不重要的部分;libraryTarget: commonjs2
以 CommonJS2 規範導出渲染函數,以供給採用 Node.js 編寫的 HTTP 伺服器代碼調用。
為了最大限度的復用代碼,需要調整下目錄結構:
把頁面的根組件放到一個單獨的文件 AppComponent.js
,該文件只能包含根組件的代碼,不能包含渲染入口的代碼,而且需要導出根組件以供給渲染入口調用,AppComponent.js
內容如下:
import React, { Component } from react;import ./main.css;export class AppComponent extends Component { render() { return <h1>Hello,Webpack</h1> }}
分別為不同環境的渲染入口寫兩份不同的文件,分別是用於瀏覽器端渲染 DOM 的 main_browser.js
文件,和用於服務端渲染 HTML 字元串的 main_server.js
文件。
main_browser.js
文件內容如下:
import React from react;import { render } from react-dom;import { AppComponent } from ./AppComponent;// 把根組件渲染到 DOM 樹上render(<AppComponent/>, window.document.getElementById(app));
main_server.js
文件內容如下:
import React from react;import { renderToString } from react-dom/server;import { AppComponent } from ./AppComponent;// 導出渲染函數,以給採用 Node.js 編寫的 HTTP 伺服器代碼調用export function render() { // 把根組件渲染成 HTML 字元串 return renderToString(<AppComponent/>)}
為了能把渲染的完整 HTML 文件通過 HTTP 服務返回給請求端,還需要通過用 Node.js 編寫一個 HTTP 伺服器。
由於本節不專註於將 HTTP 伺服器的實現,就採用了 ExpressJS 來實現,http_server.js
文件內容如下:const express = require(express);const { render } = require(./dist/bundle_server);const app = express();// 調用構建出的 bundle_server.js 中暴露出的渲染函數,再拼接下 HTML 模版,形成完整的 HTML 文件app.get(/, function (req, res) { res.send(`<html><head> <meta charset="UTF-8"></head><body><div id="app">${render()}</div><!--導入 Webpack 輸出的用於瀏覽器端渲染的 JS 文件--><script src="./dist/bundle_browser.js"></script></body></html> `);});// 其它請求路徑返回對應的本地文件app.use(express.static(.));app.listen(3000, function () { console.log(app listening on port 3000!)});
再安裝新引入的第三方依賴:
# 安裝 Webpack 構建依賴npm i -D css-loader style-loader ignore-loader webpack-node-externals# 安裝 HTTP 伺服器依賴npm i -S express
以上所有準備工作已經完成,接下來執行構建,編譯出目標文件:
- 執行命令
webpack --config webpack_server.config.js
構建出用於服務端渲染的./dist/bundle_server.js
文件。 - 執行命令
webpack
構建出用於瀏覽器環境運行的./dist/bundle_browser.js
文件,默認的配置文件為webpack.config.js
。
構建執行完成後,執行 node ./http_server.js
啟動 HTTP 伺服器後,再用瀏覽器去訪問 http://localhost:3000
就能看到 Hello,Webpack
了。
可以看到伺服器返回的是渲染出內容後的 HTML 而不是 HTML 模版,這說明同構應用的改造完成。
本實例提供項目完整代碼
閱讀原文
推薦閱讀:
※徹底解決Webpack打包性能問題
※Webpack實戰-構建離線應用
※webpack到底怎麼用?
※webpack3之Scope Hoisting
TAG:webpack |