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

浩麟發表於2019-02-27

引入問題

在簡單的專案裡因為只輸出了一個 bundle.js 檔案,所以手寫了一個 index.html 檔案去引入這個 bundle.js,才能讓應用在瀏覽器中執行起來。

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

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

在開始前先來看看該應用最終釋出到線上的程式碼:

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

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

構建出的目錄結構為:

dist
├── app_792b446e.js
└── index.html
複製程式碼

可以看到部分程式碼被內嵌進了 HTML 的 HEAD 標籤中,部分檔案的檔名稱被打上根據檔案內容算出的 Hash 值,並且載入這些檔案的 URL 地址也被正常的注入到了 HTML 中。 如果你還採用手寫 index.html 檔案去完成以上要求,這就會使工作變得複雜、易錯,專案難以維護。 本節教你如何自動化的生成這個符合要求的 index.html

解決方案

推薦一個用於方便的解決以上問題的 Webpack 外掛 web-webpack-plugin。 該外掛已經被社群上許多人使用和驗證,解決了大家的痛點獲得了很多好評,下面具體介紹如何用它來解決上面的問題。

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

const path = require('path');
const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const DefinePlugin = require('webpack/lib/DefinePlugin');
const { WebPlugin } = require('web-webpack-plugin');

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

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

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

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

new WebPlugin({
  template: './template.html', // HTML 模版檔案所在的檔案路徑
  filename: 'index.html' // 輸出的 HTML 的檔名稱
})
複製程式碼

其中 template: './template.html' 所指的模版檔案 template.html 的內容是:

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

該檔案描述了哪些資源需要被以何種方式加入到輸出的 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實戰-為單頁應用生成HTML

《深入淺出Webpack》全書線上閱讀連結

閱讀原文

相關文章