上一次將webpack1升級到3,也僅是 半年前,前端工具發展變化太快了,如今webpack4已經灰常穩定,傳說性能提升非常高,值得升級。
一直用着的webpack3越來越慢,一分多鍾的編譯時間簡直不能忍,升級之后在幾個系統和幾台電腦上評測,平均提高了7-9倍,生產模式的最突出
升級之后完整的 webpack4項目配置DEMO 已經放到Github中,歡迎圍觀 star ~
關於如何升級到V4已經有很多優秀的文章,結合官方文檔你也可以升級起來
本文僅說說本次升級主要做的改動優化點,或者坑
webpack4升級完全指南 webpack4 changelog React 16 加載優化性能
1. 移除了commonchunk插件,改用了optimization屬性進行更加靈活的配置 ,不過稍微不注意,就會有問題,如
Uncaught Error: only one instance of babel-polyfill is allowed
如果一個頁面存在多個entry入口文件,即頁面引用了多個模塊時,默認會產生多個獨立的common區
所以記得將common設為公有,如
optimization: { runtimeChunk: { name: 'common' },
2. 默認的生產模式noEmitOnError為true,導致代碼檢查工具報錯之后無法將檢查結果寫入文件中
按需將其設置為false即可
optimization: { noEmitOnErrors: false,
3. 默認的提取公共模塊機制可能會產生意外的結果,盡量取消默認后再自定義
在多頁面應用中,假設某個頁面的css文件重寫了樣式,就有可能使這個重寫流入到公共樣式中,在另一個頁面被引用而導致布局出錯。這時樣式是不需要提取出來的,除非特殊情況
比如可以將default設置為false,或者表現得更強烈一點
optimization: { splitChunks: { chunks(chunk) { // 不需要提取公共代碼的模塊 return !(configs.commonChunkExcludes || []).includes(chunk.name); }, name: 'common', minChunks: 2, cacheGroups: { default: false, styles: { name: 'common', test: /\.scss|css$/, chunks: 'initial', // 不生成公共樣式文件 minChunks: 999999, enforce: true } } }
4. 將css文件提取的 ExtractTextWebpackPlugin 插件 替換成 mini-css-extract-plugin
升級指南里說着這個新插件不兼容web-dev-server,不過目前還沒遇到,碰到的幾個坑開始以為是它提取出的問題,后來發現並不是..
5. 正確地使用 optimization.concatenateModules ,需要關閉babel的module模塊轉換
6. 看起來似乎 loader 的 exclude 和 include 配置失效了,不知道是為何
7. 加入編譯結果消息彈出提示,更友好,引入 webpack-build-notifier
長長的編譯結果,看起來很乏味,開發人員並不能知道什么時候編譯好了
new WebpackBuildNotifierPlugin({ title: processEntity, suppressSuccess: false, suppressCompileStart: false, suppressWarning: false, activateTerminalOnError: true }),
在win10上看比較醒目直觀,但在win7上僅是狀態欄的氣泡彈出
不過在編譯結果的內容提示還不夠完善,可以改進
8. webpack-dev-server的端口自動獲取空閑端口,多webpack項目共存時很方便
因基本所有獲取空閑端口的npm包都是異步的,原理都是以端口開啟服務器,如果開啟成功則表示這個端口空閑。
但項目的webpack配置是直接 module.export一個配置項的,不是使用NodeJS API的方式,嘗試切換為這種方式時發現竟然與HMR不同兼容,就此作罷
嘗試尋找同步直接獲取空閑端口的辦法,想出了一個簡單的,直接執行 netstat -an 命令列出當前進程端口再正則匹配即可,奈思~

1 let execSync = require('child_process').execSync, 2 // 已使用的端口 3 usedPorts = [], 4 // (初始)可使用的端口 5 freePort = 10000, 6 // 可用端口范圍 7 portStart = 10000; 8 portEnd = 30000, 9 // 查詢最大步 10 maxStep = 100000; 11 12 /** 13 * 獲取隨機端口 14 * @return {[type]} [description] 15 */ 16 function getRandomPort() { 17 return Math.floor(Math.random() * (portEnd - portStart) + portStart); 18 } 19 20 function getFreePort() { 21 console.log('Finding free port...'); 22 23 let stepIndex = 0, 24 res = '', 25 portSplitStr = ':'; 26 27 try { 28 res = execSync('netstat -an', { 29 encoding: 'utf-8' 30 }); 31 usedPorts = res.match(/\s(0.0.0.0|127.0.0.1):(\d+)\s/g); 32 33 if (!usedPorts) { 34 usedPorts = res.match(/\s(\*|127.0.0.1)\.(\d+)\s/g); 35 portSplitStr = '.'; 36 } 37 38 usedPorts = usedPorts.map(item => { 39 let port = item.split(portSplitStr); 40 port = port.slice(-1)[0]; 41 return parseInt(port.slice(0, -1), 10); 42 }); 43 44 usedPorts = [...new Set(usedPorts)]; 45 46 let portAvaliable = false; 47 while (!portAvaliable) { 48 freePort = getRandomPort(); 49 50 if (!usedPorts.includes(freePort)) { 51 portAvaliable = true; 52 console.log('Use port ' + freePort + ' for devServer\n'); 53 } 54 55 if (++stepIndex > maxStep) { 56 console.log('Cannot find free port for devServer\n'); 57 break; 58 } 59 } 60 } catch(e) { 61 console.log('Cannot find free port for devServer\n'); 62 console.log(e); 63 } 64 65 return freePort; 66 } 67 68 module.exports = getFreePort;
9. 編譯的dos進程窗添加標題,多個webpack項目執行時,在任務欄小窗區分更方便
也比較簡單,直接設置即可
process.title = `${configs.versionControl || 'branch'}--${configs.name || 'anonymous'}--[${process.env.NODE_ENV}]`,
不過在使用git bash時,這樣設置是無效的
使用 node-bash-title 即可
require('node-bash-title')(`${configs.versionControl || 'branch'}--${configs.name || 'anonymous'}--[${process.env.NODE_ENV}]`);
10. 引入 dllplugin動態鏈接庫方案,將第三方庫單獨打包,再鏈入我們的webpack項目中
可以參考介篇文章
新建一個webpack.dll.config.js配置文件
let path = require('path'); let webpack = require('webpack'); module.exports = { entry: { // 需要預配置動態鏈接的庫 vendor: [ 'babel-polyfill', // 'echarts' 'react', // 'redux', // 'react-redux', 'react-dom', // 'react-router' ] }, // 啟用sourceMap // devtool: 'cheap-module-source-map', output: { path: path.resolve(__dirname, './'), filename: '[name].js', library: '[name]_library_wcr' }, plugins: [ new webpack.DllPlugin({ path: path.join(__dirname, './', '[name].manifest.json'), name: '[name]_library_wcr' }) ] }
執行一次,將第三方包打包出來,如果該配置文件有改動的,也需要再次打包
使用 DllReferencePlugin 插件鏈接這個manifest清單引用
new webpack.DllReferencePlugin({ manifest: require(path.join(__dirname, './dll/', 'vendor.manifest.json')), }),
使用 add-asset-html-webpack-plugin 這個插件將vendor庫插入到頁面中
需要注意的是,默認它會將vendor插入到所有htmlWebpackPlugin設置的頁面中,所有我們需要通過files屬性定義好
如果有父頁面的,則只插入生成的父頁面中即可
// 動態鏈接庫引用配置 if (configs.vendorDllOpen) { let addAssetHtmlPluginOption = { filepath: require.resolve('./dll/vendor.js'), includeSourcemap: false, hash: true }; if (configs.vendorDllInsertFiles !== 'all') { Object.assign(addAssetHtmlPluginOption, { files: configs.vendorDllInsertFiles }); } commonConfig.plugins.push( new webpack.DllReferencePlugin({ manifest: require(path.join(__dirname, './dll/', 'vendor.manifest.json')), }), new AddAssetHtmlPlugin(addAssetHtmlPluginOption) ); }
不過它的hash控制不能設定位數,不夠優雅
注意這里是由 htmlWebpackPlugin調用的ejs-loader 解析源頁面文件的配置生成的
<% for(var key in htmlWebpackPlugin.files.js) { %> <script src="<%= htmlWebpackPlugin.files.js[key] %>"></script> <% } %>
11. 使用 webpack-bundle-analyzer 分析打包結果
// 打包模塊分析 if (process.argv.includes('--analysis')) { commonConfig.plugins.push(new BundleAnalyzerPlugin({ analyzerMode: 'server', analyzerHost: '127.0.0.1', analyzerPort: require('./getFreePortSync')(), reportFilename: 'report.html', defaultSizes: 'parsed', openAnalyzer: true, generateStatsFile: false, statsFilename: 'stats.json', statsOptions: null, logLevel: 'info' })); }
12. 引入代碼檢查工具套件,關於這部分,可移步 這里
13. 將配置文件再抽取,抽出核心部分與和業務相關的多變動的部分
形成如下結構,一般來說只需要變動 webpack.config.js 這個配置即可