趁webpack5还没出,先升级成webpack4吧
上一次将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 这个配置即可