標籤:

Webpack實戰-為單頁應用生成 HTML

引入問題

在簡單的項目里因為只輸出了一個 bundle.js 文件,所以手寫了一個 index.html 文件去引入這個 bundle.js,才能讓應用在瀏覽器中運行起來。

在實際項目中遠比這複雜,一個頁面常常有很多資源要載入。接下來舉一個實戰中的例子,要求如下:

  1. 項目採用 ES6 語言加 React 框架。
  2. 給頁面加入 Google Analytics,這部分代碼需要內嵌進 HEAD 標籤里去。
  3. 給頁面加入 Disqus 用戶評論,這部分代碼需要非同步載入以提升首屏載入速度。
  4. 壓縮和分離 JavaScript 和 CSS 代碼,提升載入速度。

在開始前先來看看該應用最終發布到線上的代碼:

<html>n<head>n <meta charset="UTF-8">n <!--注入 Chunk app 依賴的 CSS-->n <style rel="stylesheet">h1{color:red}</style>n <!--內嵌 google_analytics 中的 JavaScript 代碼-->n <script>n(function(i,s,o,g,r,a,m){i[GoogleAnalyticsObject]=r;i[r]=i[r]||function(){n(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),nm=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)n})(window,document,script,https://www.google-analytics.com/analytics.js,ga);nga(create, UA-XXXXX-Y, auto);nga(send, pageview);n </script>n <!--非同步載入 Disqus 評論-->n <script async="" src="https://dive-into-webpack.disqus.com/embed.js"></script>n</head>n<body>n<div id="app"></div>n<!--導入 app 依賴的 JS-->n<script src="app_746f32b2.js"></script>n<!--Disqus 評論容器-->n<div id="disqus_thread"></div>n</body>n</html>n

HTML 應該是被壓縮過的,這裡為了方便大家閱讀而格式化了 HTML,並且加入了注釋。

構建出的目錄結構為:

distn├── app_792b446e.jsn└── index.htmln

可以看到部分代碼被內嵌進了 HTML 的 HEAD 標籤中,部分文件的文件名稱被打上根據文件內容算出的 Hash 值,並且載入這些文件的 URL 地址也被正常的注入到了 HTML 中。

如果你還採用手寫 index.html 文件去完成以上要求,這就會使工作變得複雜、易錯,項目難以維護。

本節教你如何自動化的生成這個符合要求的 index.html

解決方案

推薦一個用於方便的解決以上問題的 Webpack 插件 web-webpack-plugin。

該插件已經被社區上許多人使用和驗證,解決了大家的痛點獲得了很多好評,下面具體介紹如何用它來解決上面的問題。

首先,修改 Webpack 配置為如下:

const path = require(path);nconst UglifyJsPlugin = require(webpack/lib/optimize/UglifyJsPlugin);nconst ExtractTextPlugin = require(extract-text-webpack-plugin);nconst DefinePlugin = require(webpack/lib/DefinePlugin);nconst { WebPlugin } = require(web-webpack-plugin);nnmodule.exports = {n entry: {n app: ./main.js// app 的 JavaScript 執行入口文件n },n output: {n filename: [name]_[chunkhash:8].js,// 給輸出的文件名稱加上 Hash 值n path: path.resolve(__dirname, ./dist),n },n module: {n rules: [n {n test: /.js$/,n use: [babel-loader],n // 排除 node_modules 目錄下的文件,n // 該目錄下的文件都是採用的 ES5 語法,沒必要再通過 Babel 去轉換n exclude: path.resolve(__dirname, node_modules),n },n {n test: /.css/,// 增加對 CSS 文件的支持n // 提取出 Chunk 中的 CSS 代碼到單獨的文件中n use: ExtractTextPlugin.extract({n use: [css-loader?minimize] // 壓縮 CSS 代碼n }),n },n ]n },n plugins: [n // 使用本文的主角 WebPlugin,一個 WebPlugin 對應一個 HTML 文件n new WebPlugin({n template: ./template.html, // HTML 模版文件所在的文件路徑n filename: index.html // 輸出的 HTML 的文件名稱n }),n new ExtractTextPlugin({n filename: `[name]_[contenthash:8].css`,// 給輸出的 CSS 文件名稱加上 Hash 值n }),n new DefinePlugin({n // 定義 NODE_ENV 環境變數為 production,以去除源碼中只有開發時才需要的部分n process.env: {n NODE_ENV: JSON.stringify(production)n }n }),n // 壓縮輸出的 JavaScript 代碼n new UglifyJsPlugin({n // 最緊湊的輸出n beautify: false,n // 刪除所有的注釋n comments: false,n compress: {n // 在UglifyJs刪除沒有用到的代碼時不輸出警告n warnings: false,n // 刪除所有的 `console` 語句,可以兼容ie瀏覽器n drop_console: true,n // 內嵌定義了但是只用到一次的變數n collapse_vars: true,n // 提取出出現多次但是沒有定義成變數去引用的靜態值n reduce_vars: true,n }n }),n ],n};n

以上配置中,大多數都是按照前面已經講過的內容增加的配置,例如:

  • 增加對 CSS 文件的支持,提取出 Chunk 中的 CSS 代碼到單獨的文件中,壓縮 CSS 文件;
  • 定義 NODE_ENV 環境變數為 production,以去除源碼中只有開發時才需要的部分;
  • 給輸出的文件名稱加上 Hash 值;
  • 壓縮輸出的 JavaScript 代碼。

但最核心的部分在於 plugins 里的:

new WebPlugin({n template: ./template.html, // HTML 模版文件所在的文件路徑n filename: index.html // 輸出的 HTML 的文件名稱n})n

其中 template: ./template.html 所指的模版文件 template.html 的內容是:

<html>n<head>n <meta charset="UTF-8">n <!--注入 Chunk app 中的 CSS-->n <link rel="stylesheet" href="app?_inline">n <!--注入 google_analytics 中的 JavaScript 代碼-->n <script src="./google_analytics.js?_inline"></script>n <!--非同步載入 Disqus 評論-->n <script src="https://dive-into-webpack.disqus.com/embed.js" async></script>n</head>n<body>n<div id="app"></div>n<!--導入 Chunk app 中的 JS-->n<script src="app"></script>n<!--Disqus 評論容器-->n<div id="disqus_thread"></div>n</body>n</html>n

該文件描述了哪些資源需要被以何種方式加入到輸出的 HTML 文件中。

<link rel="stylesheet" href="app?_inline"> 為例,按照正常引入 CSS 文件一樣的語法來引入 Webpack 生產的代碼。

href 屬性中的 app?_inline 可以分為兩部分,前面的 app 表示 CSS 代碼來自名叫 app 的 Chunk 中,後面的 _inline 表示這些代碼需要被內嵌到這個標籤所在的位置。

同樣的 <script src="./google_analytics.js?_inline"></script> 表示 JavaScript 代碼來自相對於當前模版文件 template.html 的本地文件 ./google_analytics.js

而且文件中的 JavaScript 代碼也需要被內嵌到這個標籤所在的位置。

也就是說資源鏈接 URL 字元串里問號前面的部分表示資源內容來自哪裡,後面的 querystring 表示這些資源注入的方式。

除了 _inline 表示內嵌外,還支持以下屬性:

  • _dist 只有在生產環境下才引入該資源
  • _dev 只有在開發環境下才引入該資源
  • _ie 只有IE瀏覽器才需要引入的資源,通過 [if IE]>resource<![endif] 注釋實現

這些屬性之間可以搭配使用,互不衝突。例如 app?_inline&_dist 表示只在生產環境下才引入該資源,並且需要內嵌到 HTML 里去。

WebPlugin 插件還支持一些其它更高級的用法,詳情可以訪問該項目主頁閱讀文檔。

本實例提供項目完整代碼

《深入淺出Webpack》全書在線閱讀鏈接

閱讀原文


推薦閱讀:

React系列:React架構
React 16 中的異常處理
參加第12屆D2前端技術論壇,你有什麼收穫?
前端發展太快,有些小伙只會用react(了解api),招個jquery熟練的外包較難,如何看?

TAG:webpack | React |