Webpack + Vue 多頁面專案升級 Webpack 4 以及打包優化

cnu4發表於2019-02-16

0. 前言

早在 2016 年我就釋出過一篇關於在多頁面下使用 Webpack + Vue 的配置的文章,當時也是我在做自己一個個人專案時遇到的配置問題,想到別人也可能遇到跟我同樣的問題,就把配置的思路分享出來了,傳送門在這裡。

因為那份配置直到現在還有人在關注,同時最近公司幫助專案升級了 Webpack 4,趁機也把之前的配置也升級了一下,而且部落格荒廢了這麼久,都快 9102 年了,不能連年均一篇博文都不到,所以有了下面的分享。

下面的配置主要是給在多頁面下使用 Webpack 的同學在升級 Webpack 時提供一點思路,多頁面的配置思路請點選上面的傳送門。

下面程式碼的地址 https://github.com/cnu4/Webpack-Vue-MultiplePage

1. Webpack 升級 4.x

1.1. 升級和安裝相關依賴

  • webpack 升級
  • webpack-cli webapck4.x 需要新加的依賴
  • mini-css-extract-plugin 取代 extract-text-webpack-plugin
  • 其他相關 loader 和 plugin

    • css-loader
    • file-loader
    • url-loader
    • vue-style-loader
    • vue-template-compiler(注意要保持與 vue 版本一直)
    • html-webpack-plugin@next

1.2 修改配置

mode 構建模式

設定 mode 構建模式,比如 development 會將 process.env.NODE_ENV 的值設為 development

mini-css-extract-plugin

刪除原 extract-text-webpack-plugin 配置,增加 mini-css-extract-plugin 配置

module.exports = {
  plugins: [
    new  MiniCssExtractPlugin({
      filename:  `css/[name].css`
    }),
  ],
}

module.exports = {
  module: {
    rules: [
      {
        test:/.vue$/,
        loader: `vue-loader`,
      },
      { test: /.css$/,
        use: [
          // 開發模式下使用 vue-style-loader,以便使用熱過載
          process.env.NODE_ENV !== `production` ?
            `vue-style-loader` : MiniCssExtractPlugin.loader,
          `css-loader` ] },
    ]
  }
}

optimization

這是 webpack 4 一個比較大的變動點,webpack 4 中刪除了 webpack.optimize.CommonsChunkPlugin,並且使用 optimization 中的splitChunk來替代,下面的配置代替了之前的 CommonsChunkPlugin 配置,同意能提取 JS 和 CSS 檔案

module.exports = {
  optimization: {
    splitChunks: {
      vendors: {
        name:  `venders`,
        chunks:  `all`,
        minChunks: chunks.length
    }
  }
}

vue-loader 升級

vue-loader 15 注意要配合一個 webpack 外掛才能正確使用

const { VueLoaderPlugin } = require(`vue-loader`) 

module.exports = {
  plugins: [ new VueLoaderPlugin() ]
}

html-webpack-plugin 升級

升級到 next,否則開發下無法正常注入資原始檔

檔案壓縮

  • optimize-css-assets-webpack-plugin
  • terser-webpack-plugin

壓縮的配置也移動到了 optimization 選項下,值得注意的是壓縮工具換成了 terser-webpack-plugin,這是 webpack 官方也推薦使用的,估計在 webpack 5 中會變成預設的配置,實測打包速度確實變快了很多。

配置

module.exports = {
    minimizer: [
      new TerserPlugin({ // 壓縮js
          cache:  true,
          parallel:  true
        }
      }),
      new OptimizeCSSAssetsPlugin({ // 壓縮css
        cssProcessorOptions: {
          safe: true
        }
      })
    ]
  }
}

2. 打包速度優化

可以使用下面的外掛看看打包時間主要耗時在哪

speed-measure-webpack-plugin

2.1 相關 plugin 開啟 parallel 選項

TerserPlugin 壓縮外掛可以開啟多執行緒,見上面配置

2.2 HappyPack 和 thread-loader 開啟 Loader 多程式轉換

github 的 Demo 中沒有引入,有興趣的同學可以嘗試,在一些耗時的 Loader 確實可以提高速度

vue-loader 不支援 HappyPack,官方建議用 thread-loader

const HappyPack = require(`happypack`);

exports.module = {
  rules: [
    {
      test: /.js$/,
      // 1) replace your original list of loaders with "happypack/loader":
      // loaders: [ `babel-loader?presets[]=es2015` ],
      use: `happypack/loader`,
      include: [ /* ... */ ],
      exclude: [ /* ... */ ]
    }
  ]
};

exports.plugins = [
  // 2) create the plugin:
  new HappyPack({
    // 3) re-add the loaders you replaced above in #1:
    loaders: [ `babel-loader?presets[]=es2015` ]
  })
];

2.3 提前打包公共程式碼

DllPlugin

使用 DllPlugn 將 node_modules 或者自己編寫的不常變的依賴包打一個 dll 包,提高速度和充分利用快取。相當於 splitChunks 提取了公共程式碼,但 DllPlugn 是手動指定了公共程式碼,提前打包好,免去了後續 webpack 構建時的重新打包。

首先需要增加一個 webpack 配置檔案 webpack.dll.config.js 專門針對 dll 打包配置,其中用到 webpack.DllPlugin

執行 webpack --config build/webpack.dll.config.js 後,webpack會自動生成 2 個檔案,其中vendor.dll.js 即合併打包後第三方模組。另外一個 vendor-mainifest.json 儲存各個模組和所需公用模組的對應關係。

接著修改我們的 webpack 配置檔案,在 plugin 配置中增加 webpack.DllReferencePlugin,配置中指定上一步生成的 json 檔案,然後手動在 html 檔案中引用上一步的 vendor.dll.js 檔案。

後面如果增刪 dll 中的依賴包時都需要手動執行上面打包命令來更新 dll 包。下面外掛可以自動完成這些操作。

AutoDllPlugin

安裝依賴 autodll-webpack-plugin

AutoDllPlugin 自動同時相當於完成了 DllReferencePlugin 和 DllPlugin 的工作,只需要在我們的 webpack 中新增配置。AutoDllPlugin 會在執行 npm install / remove / update package-name 或改變這個外掛配件時重新打包 dll。需要注意的是改變 dll 中指定的依賴包不會觸發自動重新打包 dll。

實際打包中生成環境是沒問題的,但開發模式下在有快取的情況下,autodll 外掛不會生成新的檔案,導致 404,所以在 Demo 中暫時關了這個外掛。不過 dll 提前打包了公共檔案,確實可以提高打包速度,有興趣的同學可以研究下開發模式下的快取問題,歡迎在評論中分享。

module.exports.plugins.push(new AutoDllPlugin({
  inject: true, // will inject the DLL bundles to html
  context: path.join(__dirname, `.`),
  filename: `[name].dll.js`,
  debug: true,
  inherit: true,
  // path: `./`,
  plugins: [
    new TerserPlugin({
      cacheL true,
      parallel: true
    }),
    new MiniCssExtractPlugin({
      filename: `[name].css`
    })
  ],
  entry: {
    vendor: [`vue/dist/vue.esm.js`, `axios`, `normalize.css`]
  }
}));

3. 增加 ES6+ 支援

3.1 安裝依賴

  • @babel/core
  • @babel/plugin-proposal-class-properties
  • @babel/plugin-proposal-decorators
  • @babel/plugin-syntax-dynamic-import
  • @babel/plugin-transform-runtime
  • @babel/preset-env
  • @babel/runtime
  • babel-loader
  • @babel/polyfill

由於專案中是第一次配置 babel,一步到位直接使用新版 7,新版 babel 使用新的名稱空間 @babel,如果是老專案升級 babel 7,可以使用工具 babel-upgrade,讀一下 升級文件

這裡說下上面依賴的作用和升級 babel 7 的改動。

@babel/runtime, @babel/plugin-transform-runtime

新版中 @babel/runtime 只包含了一些 helpers,如果需要 core-js polyfill 瀏覽器不支援的 API,可以用 transform 提供的選項 {"corejs": 2} 並安裝依賴 @babel/runtime-corejs2。即使預設的 polyfill 沒了,但 @babel/plugin-transform-runtime 依然可以為我們分離輔助函式,減少程式碼體積

@babel/polyfill

使用 @babel/runtime 的 polyfill 不會汙染全域性 API,因為不會改動原生物件的原型,它只是建立一個輔助函式在當前作用於生效,所以諸如 [1, 2].includes(1) 這樣的語法也無法被 polyfill。如果不是開發第三方庫,可以使用 @babel/polyfill,相反他的 polyfill 會影響到瀏覽器全域性的物件原型

@babel/preset-env 提供了一個 useBuiltIns 選項來按需引入 polyfill,而不需要引入全部。另一種方法是直接引用 core-js 包下的特定 polyfill。

stage presets

現在需要手動安裝 @babel/plugin-proposal 開頭的依賴是因為 babel 在新版中移除了 stage presets,為的是後續更好維護處於 proposal 階段的語法。想要使用 proposal 階段的語法需要單獨引用對應的 plugin, 上面的配置只加了幾個處於 stage 3 階段的 plugin,老專案建議使用 babel-upgrade 升級,自動新增依賴

3.2 新增配置檔案 .babelrc

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "modules": false,
        "targets": {
          "browsers": [
            "> 1%",
            "last 2 versions",
            "ie >= 11"
          ]
        },
        "useBuiltIns": "usage" // 按需引入 polyfill
      }
    ]
  ],
  "plugins": [
    "@babel/plugin-transform-runtime",
    "@babel/plugin-syntax-dynamic-import",
    ["@babel/plugin-proposal-class-properties", { "loose": false }],
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
  ]
}

3.3 增加 webpack 配置

module.exports = {
  modules: {
    rules: [
      {
        test:  /.js$/,
        loader:  `babel-loader`,
        exclude:  /node_modules/
      }
    ]
  }
}

4. 其他問題

下面是我公司專案中遇到的問題,各位升級過程中如果遇到同樣的問題可以參考一下解決思路。

4.1 json-loader

webpack4 內建的json-loader 有點相容性問題,安裝 json-loader 依賴和更改配置

解決:

{
  test: /.json$/,  //用於匹配loaders所處理檔案擴充名的正規表示式
  use: `json-loader`, //具體loader的名稱
  type: `javascript/auto`,
  exclude: /node_modules/
}

4.2 vue-loader

vue-loader 升級到 15.x 後,會導致舊的 commonjs 寫法載入有問題,需要使用 require(`com.vue`).default 的方式引用元件

13的版本還可以設定 esModule,14 以後的版本不能設定了,vue 檔案匯出的模組一定是 esModule

解決:使用 require(`com.vue`).default 或者 import 的方式引用元件

esModule option stopped working in version 14 · Issue #1172 · vuejs/vue-loader · GitHub

尤大大建議可以自己寫一個 babel 外掛,遇到 require vue 檔案的時候自動加上 default 屬性,這樣就不用改動所有程式碼,我們在專案中也是這樣處理的。

4.3 提取公共 css 程式碼

scss 中 import 的程式碼不能被提取到公共 css 中。scss 中的 @import 是使用 sass-loader 處理的,處理後已經變成 css 檔案,webpack 已經不能判斷是否是同一個模組,所以不能提取到公共的 css 中,但多頁面中我們還是希望一些公共的 css 能被提取到公共的檔案中。

解決:將需要提取到公共檔案的 css 改到 js 中引入就可以,詳見下面 issue

mini-css-extract-plugin + sass-loader + splitChunks · Issue #49

4.4 mini-css-extract-plugin filename 不支援函式

mini-css-extract-plugin 的 filename 選項不支援函式,但我們有時候還是希望能單獨控制公共 css 檔案的位置,而不是和其他入口檔案的 css 使用一樣的目錄格式

解決:使用外掛 FileManagerPlugin 在構建後移動檔案,等 filename 支援函式後再優化

feat: allow the option filename to be a function · Issue #143 · webpack-contrib/mini-css-extract-plugin · GitHub

相關文章