vue-cli中的webpack4一步到位填坑記

wqzwh發表於2018-07-16

基礎介紹

Webpack也在不斷的優化迭代;截至目前,已經更新至 v4.16.0;在Webpack4這個版本,它在原有基礎上,做了很多優化,也引入了頗多的新特性。在新的版本中,將獲得更多模組型別及對.mjs的支援,更好的預設值、更為簡潔的模式設定、更加智慧的來分割Chunk,還新增的splitChunks來自定義分割程式碼塊,諸此等等。在升級至新版Webpack的專案中,在包的構建速度程式碼塊體積&數量、以及執行效率,都會有一個質的飛躍。

因此面對Webpack4優越的功能,將本地專案中從原先的2.7.0一步到位升級至4.16.0,並且相關依賴包以及配置檔案需要做相應的修改。

webpack4.0升級流程

node環境

不再支援Node4,建議使用高版本node,以下做升級使用的是node v8.11.1npm v5.6.0

模組型別

webpack 4之前,js 是 webpack 中的唯一模組型別,因而不能有效地打包其它型別的檔案。而 webpack 4 則提供了 5 種模組型別:

  • javascript/auto: (webpack 3中的預設型別)支援所有的JS模組系統:CommonJS、AMD、ESM
  • javascript/esm: EcmaScript 模組,在其他的模組系統中不可用(預設 .mjs 檔案)
  • javascript/dynamic: 僅支援 CommonJS & AMD,EcmaScript 模組不可用
  • json: 可通過 require 和 import 匯入的 JSON 格式的資料(預設為 .json 的檔案)
  • webassembly/experimental: WebAssembly 模組(處於試驗階段,預設為 .wasm 的檔案)

此外,webpack 4 中會預設解析 .wasm, .mjs, .js 和 .json 為字尾的檔案。

webpack-cli

升級完webpack4然後直接執行專案打包命令npm run build,會提示你需要安裝webpack-cli/webpack-command,可以根據自己的需要選擇安裝,本人選擇的是webpack-cli

配置更新

mode新增

webpack4預設是通過mode來設定是生產環境還是開發環境,所以需要在webpack.dev.conf.jswebpack.prod.conf.js增加相應的mode配置項,並且刪除之前設定環境變數的程式碼process.env.NODE_ENV = 'production'以及外掛配置中設定環境變數的方法,片段程式碼如下:

// webpack.dev.conf.js
module.exports = merge(baseWebpackConfig, {
  mode: 'development',
  // 省略
  plugins: [
    new webpack.DefinePlugin({
        'process.env': config.dev.env
    }),
  ]
} 

// webpack.prod.conf.js
var webpackConfig = merge(baseWebpackConfig, {
  mode: 'production',
  // 省略
  plugins: [
    new webpack.DefinePlugin({
      'process.env': env
    }),
  ]  
}  
複製程式碼

注意:new webpack.DefinePlugin是保證瀏覽器指令碼中能夠訪問process.env變數,以便做相應的邏輯操作

development 模式:

  • 1.主要優化了增量構建速度和開發體驗
  • 2.process.env.NODE_ENV 的值不需要再定義,預設是 development
  • 3.開發模式下支援註釋和提示,並且支援 eval 下的 source maps

production 模式:

  • 1.生產環境預設開啟了很多程式碼優化(minify,splite等)
  • 2.開發時開啟注視和驗證,並且自動加上了eval devtool
  • 3.生產環境不支援watching,開發環境優化了重新打包的速度
  • 4.預設開啟了Scope hoisting和Tree-shaking(原ModuleConcatenationPlugin)
  • 5.自動設定process.env.NODE_ENV到不同環境,也就是不需要DefinePlugin來做這個了
  • 6.如果你給mode設定為none,所有預設配置都去掉了
  • 7.如果不加這個配置webpack會出現提醒,所以還是加上吧

mini-css-extract-plugin使用

因為extract-text-webpack-plugin的最新正式版還沒有對webpack4.x進行支援,即使是使用extract-text-webpack-plugin@next版本依然會出現報contenthash錯誤,所以還是建議使用mini-css-extract-plugin,當然這也是官方推薦的。

主要需要修改webpack.prod.conf.js中的外掛配置以及loaders載入的工具函式utils.js,修改片段程式碼如下:

// webpack.dev.conf.js
module.exports = merge(baseWebpackConfig, {
  // 省略
  plugins: [
    new MiniCssExtractPlugin({
      filename: utils.assetsPath('css/[name].[contenthash].css')
    }),
  ]
} 

// utils.js
if (options.extract) {
return [
  {
    loader: MiniCssExtractPlugin.loader,
    options: {
      publicPath: '../../'
    }
  }
  ].concat(loaders)
} else {
  return ['vue-style-loader'].concat(loaders)
}
複製程式碼

注意:其中utils.js中配置publicPath主要解決css中引用圖片出現路徑錯誤問題。

optimization配置項

再次執行相關打包命令你會發現有如下提示錯誤,片段程式碼如下:

Error: webpack.optimize.CommonsChunkPlugin has been removed, please use config.optimization.splitChunks instead.
    at Object.get [as CommonsChunkPlugin] (/data/test/node_modules/webpack/lib/webpack.js:159:10)
複製程式碼

主要是因為webpack4中刪除了webpack.optimize.CommonsChunkPlugin,並且使用optimization中的splitChunk來替代

主要需要修改webpack.prod.conf.js檔案,並且刪除所有webpack.optimize.CommonsChunkPlugin相關程式碼,片段程式碼如下:

var webpackConfig = merge(baseWebpackConfig, {
  mode: 'production',
  entry: {
    charts: ['echarts'],
    vendors: ['vue', 'vuex', 'vue-router', 'moment'],
    iconfonts: ['ga-iconfont']
  },
  // 省略
  optimization: {
    // minimizer: true, // [new UglifyJsPlugin({...})]
    providedExports: true,
    usedExports: true,
    //識別package.json中的sideEffects以剔除無用的模組,用來做tree-shake
    //依賴於optimization.providedExports和optimization.usedExports
    sideEffects: true,
    //取代 new webpack.optimize.ModuleConcatenationPlugin()
    concatenateModules: true,
    //取代 new webpack.NoEmitOnErrorsPlugin(),編譯錯誤時不列印輸出資源。
    noEmitOnErrors: true,
    splitChunks: {
      // maxAsyncRequests: 1,                     // 最大非同步請求數, 預設1
      // maxInitialRequests: 1,                   // 最大初始化請求數,預設1
      cacheGroups: {
        // 抽離第三方外掛
        commons: {
          // test: path.resolve(__dirname, '../node_modules'),
          chunks: 'all',
          minChunks: 2,
          maxInitialRequests: 5, // The default limit is too small to showcase the effect
          minSize: 0, // This is example is too small to create commons chunks
          name: 'common'
        }
      }
    },
}    
複製程式碼

test主要是通過正則來匹配entry中配置第三方庫,當然這裡也可以寫成path.resolve(__dirname, '../node_modules')來匹配專案中node_modules引入的庫檔案。

chunks形式有三種取值(如果配置了entry,那麼預設從入口檔案中抽離,如果沒有配置entry配置了test,預設按照test中的正則去匹配)個人比較推薦使用all或者async

  • 當取值all的時候,效果是不管是非同步還是同步,都會將入口entry配置的包公共部分抽離出來,好處就是其他檔案很小,公共檔案會只載入一次,不優雅的就是如果enrty配置的包過多會導致一個檔案很大。效果基本如下: vue-cli中的webpack4一步到位填坑記 vue-cli中的webpack4一步到位填坑記

  • 當取值async的時候,效果是將入口entry配置的包抽離非同步的公共部分,主要是看entry中包的引入方式是不是非同步的。效果基本如下: vue-cli中的webpack4一步到位填坑記 vue-cli中的webpack4一步到位填坑記

  • 當取值initial的時候,其實效果不如allasync,就是在初始化的時候,將每個頁面涉及到的包從各自頁面中的js中抽離出來,並且會根據頁面載入這些分離出來的js檔案,對於各頁面公共的js會打包多份。效果基本如下: vue-cli中的webpack4一步到位填坑記

sideEffects開啟時可以剔除無用的模組,用來做tree-shake。當模組的package.json中新增該欄位時,表明該模組沒有副作用,也就意味著webpack可以安全地清除被用於重複匯出(re-exports)的程式碼。

concatenateModules取代了webpack.optimize.ModuleConcatenationPlugin()外掛

noEmitOnErrors取代了new webpack.NoEmitOnErrorsPlugin()外掛。

minChunkssplit前,有共享模組的chunks的最小數目 ,預設值是1,但示例裡的程式碼在default裡把它重寫成2了,從常理上講,minChunks = 2應該是一個比較合理的選擇吧

注意:webpack.optimize.UglifyJsPlugin現在也不需要了,只需要使用optimization.minimizetrue就行,production mode下面自動為true,當然如果想使用第三方的壓縮外掛也可以在optimization.minimizer的陣列列表中進行配置

html-webpack-plugin升級

建議升級至最新版本@4.0.0-alpha,這裡需要把預設的chunksSortMode: dependency刪除,主要是因為webpack4已經刪除相關的CommonsChunkPluginAPI了。

vue-loader升級

其實這個可以不需要升級,但是如果升級至15.x版本以上,在使用中需要執行VueLoaderPlugin外掛方法,其他用法跟之前保持一致,片段程式碼如下:

// webpack.prod.conf.js
const { VueLoaderPlugin } = require('vue-loader')
// 省略
plugins: [
    new VueLoaderPlugin(),
]    
複製程式碼

需要開啟sourceMap

webpack4會預設提示需要開啟sourceMap,因此只要在相關loader配置中的options配置sourceMap:true即可。

其他相關包升級

建議把相關loader統一做一次升級,基本升級如下:

  • babel-loader 7.1.5
  • css-loader 1.0.0
  • file-loader 1.1.11
  • less-loader 4.1.0
  • url-loader 1.0.1
  • vue-style-loader 4.1.0
  • vue-template-compiler 2.5.16

完整配置

webpack.dev.conf.js

const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')

// add hot-reload related code to entry chunks
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
  baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
})

baseWebpackConfig.output.chunkFilename = '[name].[chunkhash].js';            // 路由js命名 這個拆分路由 模組依賴指令碼檔案

module.exports = merge(baseWebpackConfig, {
  mode: 'development',
  module: {
    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
  },
  devtool: '#cheap-module-eval-source-map',
  optimization: {
    // minimizer: true,
    providedExports: true,
    usedExports: true,
    //識別package.json中的sideEffects以剔除無用的模組,用來做tree-shake
    //依賴於optimization.providedExports和optimization.usedExports
    sideEffects: true,
    //取代 new webpack.optimize.ModuleConcatenationPlugin()
    concatenateModules: true,
    //取代 new webpack.NoEmitOnErrorsPlugin(),編譯錯誤時不列印輸出資源。
    noEmitOnErrors: true,
    splitChunks: {
      chunks: 'initial', //'all'|'async'|'initial'(全部|按需載入|初始載入)的chunks
    },
    //提取webpack執行時的程式碼
    runtimeChunk: {
      name: 'manifest'
    }
  },
  plugins: [
    new VueLoaderPlugin(),
    new webpack.HotModuleReplacementPlugin(),
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.html',
      inject: true
    }),
    new FriendlyErrorsPlugin()
  ]
})
複製程式碼

webpack.prod.conf.js

const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')

var webpackConfig = merge(baseWebpackConfig, {
  mode: 'production',
  entry: {
    charts: ['echarts'],
    vendors: ['vue', 'vuex', 'vue-router', 'moment'],
    iconfonts: ['ga-iconfont']
  },
  module: {
    rules: utils.styleLoaders({
      sourceMap: config.build.productionSourceMap,
      extract: true
    })
  },
  devtool: config.build.productionSourceMap ? '#source-map' : false,
  output: {
    path: config.build.assetsRoot,
    filename: utils.assetsPath('js/[name].[chunkhash].js'),
    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js'),
    publicPath: './'
  },
  optimization: {
    // minimizer: true,
    providedExports: true,
    usedExports: true,
    //識別package.json中的sideEffects以剔除無用的模組,用來做tree-shake
    //依賴於optimization.providedExports和optimization.usedExports
    sideEffects: true,
    //取代 new webpack.optimize.ModuleConcatenationPlugin()
    concatenateModules: true,
    //取代 new webpack.NoEmitOnErrorsPlugin(),編譯錯誤時不列印輸出資源。
    noEmitOnErrors: true,
    splitChunks: {
      // maxAsyncRequests: 1,                     // 最大非同步請求數, 預設1
      // maxInitialRequests: 1,                   // 最大初始化請求書,預設1
      cacheGroups: {
        // test: path.resolve(__dirname, '../node_modules'),
        commons: {
          chunks: 'all',
          minChunks: 2,
          maxInitialRequests: 5, // The default limit is too small to showcase the effect
          minSize: 0, // This is example is too small to create commons chunks
          name: 'common'
        }
      }
    },
    //提取webpack執行時的程式碼
    runtimeChunk: {
      name: 'manifest'
    }
  },
  plugins: [
    new VueLoaderPlugin(),
    // 解決moment語言包問題
    new webpack.ContextReplacementPlugin(
      /moment[\\\/]locale$/,
      /^\.\/(zh-cn)$/
    ),

    new MiniCssExtractPlugin({
      filename: utils.assetsPath('css/[name].[contenthash].css')
    }),
    new OptimizeCSSPlugin({
      cssProcessorOptions: {
        safe: true,
        discardComments: { removeAll: true }
      }
    }),

    new HtmlWebpackPlugin({
      filename: config.build.index,
      template: 'index.html',
      inject: true,
      hash:true,// 防止快取
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true
      }
    }),
    new webpack.HashedModuleIdsPlugin(),

    new CopyWebpackPlugin([{
      from: path.resolve(__dirname, '../static'),
      to: config.build.assetsSubDirectory,
      ignore: ['.*']
    }])
  ]
})

if (config.build.productionGzip) {
  var CompressionWebpackPlugin = require('compression-webpack-plugin')

  webpackConfig.plugins.push(
    new CompressionWebpackPlugin({
      asset: '[path].gz[query]',
      algorithm: 'gzip',
      test: new RegExp(
        '\\.(' +
        config.build.productionGzipExtensions.join('|') +
        ')$'
      ),
      threshold: 10240,
      minRatio: 0.8
    })
  )
}

if (config.build.bundleAnalyzerReport) {
  var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
  webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}

module.exports = webpackConfig
複製程式碼

注意事項

preload外掛報錯

如果在專案中使用了preload-webpack-plugin外掛,必須升級至3.0.0-beta.1版本,可以執行以下命令:

npm i preload-webpack-plugin@next -D
複製程式碼

同時需要把html-webpack-plugin外掛版本回退到3.2.0才行,然後在配置檔案按照以下先後順序新增,片段程式碼如下:

// 省略
plugins: [
  new HtmlWebpackPlugin({
    filename: config.build.index,
    template: 'index.html',
    inject: true,
    minify: {
      removeComments: true,
      collapseWhitespace: true,
      removeAttributeQuotes: true
    },
  }),
  new PreloadWebpackPlugin({
    rel: 'prefetch',
  }),
  new PreloadWebpackPlugin({
    rel: 'preload'
  }),
  // 省略
]
複製程式碼

html-webpack-plugin-after-emit

升級webpack4之後,在dev環境下,你會發現修改任何程式碼會導致整個網頁重新整理,而且會報cb is not a function,造成這個原因是html-webpack-plugin-after-emit外掛針對高版本的webpack4html-webpack-plugin3.2.0已經被棄用了,暫時沒找到替代的外掛,可以暫時先註釋掉這段程式碼,程式碼在build/dev-server.js中,片段程式碼如下:

compiler.plugin('compilation', function(compilation) {
    compilation.plugin('html-webpack-plugin-after-emit', function(data, cb) {
        hotMiddleware.publish({ action: 'reload' })
        cb()
    })
})
複製程式碼

總結

按照以上修改基本可以完成webpack4的升級,升級完之後,個人感覺配置更加簡單,刪除了以前很多繁瑣的外掛配置,很多功能webpack4預設就是自帶,經過測試打包速度有了50%多的提升,修改之前打包時間為143894ms左右,升級完之後,用時基本在58080ms左右,效果基本如下: vue-cli中的webpack4一步到位填坑記

再次打包的話,用時基本在27534ms左右,效果基本如下: vue-cli中的webpack4一步到位填坑記

2018-07-21補充

關於entry和optimization配置

上文介紹瞭如何在optimization通過定義配合entry進行打包,如果按照以上配置最終打包的確會生成chartsvendorsiconfonts三個js檔案,但是對於js大小會有所懷疑,因為大小基本在199 bytes以下,這似乎有點奇怪,直接開啟這三個js看看,程式碼如下:

// charts.js
(window.webpackJsonp=window.webpackJsonp||[]).push([[24],{21:function(n,o,p){n.exports=p("K8M1")}},[[21,1,0]]]);
//# sourceMappingURL=charts.2e5cbbfa2a894d2bb5aa.js.map

// iconfonts.js
(window.webpackJsonp=window.webpackJsonp||[]).push([[22],{19:function(n,o,p){n.exports=p("t+cQ")}},[[19,1,0]]]);
//# sourceMappingURL=iconfonts.e90fd0507d501ef81b69.js.map

// vendors.js
(window.webpackJsonp=window.webpackJsonp||[]).push([[23],{20:function(n,o,w){w("oCYn"),w("L2JU"),w("jE9Z"),n.exports=w("wd/R")}},[[20,1,0]]]);
//# sourceMappingURL=vendors.5c535f00ba89522ba93b.js.map
複製程式碼

其實這三段js都被一起打成common.js了,所以從專案載入資源的角度來說,以上這三段似乎是多餘js,那麼該如何刪除這沒用的js呢,其實可以把entry配置的程式碼全部註釋掉,基本如下:

entry: {
  // charts: ['echarts'],
  // vendors: ['vue', 'vuex', 'vue-router', 'moment'],
  // iconfonts: ['ga-iconfont']
}
複製程式碼

按照以上修改,就能少打包三個似乎沒用的js,這個問題感謝我們前端組的小夥伴發現的。

那麼問題來了,如何才能按照入口檔案配置的那樣打成三個js包呢,可以遵循以下配置,片段程式碼如下:

// 前提是不註釋entry中的程式碼

// 省略
optimization: {
  // 省略
  splitChunks: {
    cacheGroups: {
      charts: {
        chunks: 'async',
        minChunks: 2,
        maxInitialRequests: 5,
        minSize: 0, 
        name: 'charts'
      },
      vendors: {
        chunks: 'async',
        minChunks: 2,
        maxInitialRequests: 5,
        minSize: 0, 
        name: 'vendors'
      },
      iconfonts: {
        chunks: 'async',
        minChunks: 2,
        maxInitialRequests: 5,
        minSize: 0, 
        name: 'iconfonts'
      }
    }
  },
  // 省略
}
// 省略
複製程式碼

重點看cacheGroups裡面的配置,將原先的commons改成了三個了,同時chunks需要改成async,按照這樣打包結果基本如下: vue-cli中的webpack4一步到位填坑記

打包速度相關

以下測試速度的專案頁面數是26個頁面,十多個業務元件,一整套組內開發的ui元件庫,以及多個第三方庫。實際時間會受各自專案檔案多少影響。

使用HappyPack外掛能夠提高打包編譯速度,可以參照以下修改,基本修改如下:

// webpack.base.conf.js
// 在rules中的babel-loader改用happypack中的loader
// 省略
module: {
  rules: [{
    test: /\.js$/,
    loader: 'happypack/loader', // 增加新的HappyPack構建loader
    include: [resolve('src')],
    exclude: /node_modules/,
    options: {
      sourceMap: true,
    }
  }
}    
// 省略

// webpack.prod.conf.js
// 省略
plugins: [
  new HappyPack({
    loaders: [{
      loader: 'babel-loader',
      options: { 
        babelrc: true, 
        cacheDirectory: true 
      }
    }],
    threadPool: happyThreadPool
  })
]    
// 省略
複製程式碼

以下是未修改之前打包的所需的時間是26831msvue-cli中的webpack4一步到位填坑記

修改之後打包速度是20387ms,相比之前減少了近6.5s左右: vue-cli中的webpack4一步到位填坑記

通過設定babel-loader中的cacheDirectory屬性也能提高編譯速度,網上很多都是如下設定,但是會報錯,片段程式碼如下:

// 這是錯誤用法,我實測發現報錯
{
  test: /\.js$/,
  loader: 'babel-loader?cacheDirectory=true', // 或者loader: 'babel-loader?cacheDirectory'
  include: [resolve('src')],
  exclude: /node_modules/,
  options: {
    sourceMap: true,
  }
}
複製程式碼

其實可以把cacheDirectory當作一個屬性配置在options中,基本程式碼如下:

{
  test: /\.js$/,
  loader: 'babel-loader',
  include: [resolve('src')],
  exclude: /node_modules/,
  options: {
    sourceMap: true,
    cacheDirectory: true 
  }
}
複製程式碼

增加如上修改,打包速度是20593ms,似乎跟之前沒有多大變化: vue-cli中的webpack4一步到位填坑記

最終經過測試,對於本人專案而言,HappyPackcacheDirectory效果並不能疊加,使用任意其一,都可以能達到20s所有的時間。

以上就是全部內容,如果有什麼不對的地方,歡迎提issues

相關文章