做者:刘华css
随着前端构架工具的不断发展,提供了不少提升咱们的开发体验和开发效率的能力,同时构建已经成为前端技术栈中常见的技术。html
webpack 也是众多构建工具中崭露头角一员,早期的 webpack 配置复杂难懂,随着其发展,相关配置也不断简化,性能也不断提升,可是对于深刻使用的开发人员,一般它的默认配置并不适用于业务开发,须要针对本身业务调整适配。前端
你对 webpack 了解多少?如何针对业务集成最佳配置?如何优化开发体验?如何开足马力,实现极速的 webpack 的构建性能 🚀?又会有哪些坑 💣?本文带你解答这些问题 🔭。node
本文涉及到的全部代码片断的完整代码请参考a8k仓库react
对构建有所了解的,可直接略过本节webpack
此处不会深刻介绍相关配置,更多的详细说明与配置参见官方文档,稍做介绍关键配置项铺垫后面内容。git
webpack 查找依赖的入口文件配置,入口文件能够有多个。es6
单页面应用入口配置 一般作法配置:vendor.js 第三方依赖库,polyfill.js 特性填充库,index.js 单页面应用入口文件github
// 导出配置 module.exports = { entry: { vendor: './src/vendor.js', polyfill: './src/polyfill.js', index: './src/index.js', }, }; 复制代码
多页面应用入口配置 和单页面应用相似,但不一样页面会不一样有入口文件,这种状况高效的作法就不是直接写死在 entry 里面了,而是经过生成 webpack.config 时,扫描指定目录肯定每一个页面的入口文件以及全部的页面。web
下面举个例子
假定你的页面都放置在 src/pages 目录下面,而且你的每一个页面单独一个目录,而且其中有 index.html 和 index.jsx
const path = require('path'); const fs = require('fs'); // 处理公共entry const commonEntry = ['./src/vendor.js', './src/polyfill.js']; // 页面目录 const PAGES_DIR = './src/pages/'; const entry = {}; // 遍历页面目录 const getPages = () => { return fs.readdirSync(PAGES_DIR).filter(item => { let filepath = path.join(PAGES_DIR, item, 'index.js'); if (!fs.existsSync(filepath)) { filepath = `${filepath}x`; // jsx } if (!fs.existsSync(filepath)) { return false; } return true; }); }; getPages(options).forEach(file => { const name = path.basename(file); // 加入页面须要的公共入口 entry[name] = [...commonEntry, `${PAGES_DIR}/${file}/index`]; }); // 导出配置 module.exports = { entry, }; 复制代码
入口 boundle 如何插入对应的 html 中?
咱们一般须要这个插件HtmlWebpackPlugin
自动处理,具体代码以下:
const plugins = []; if (mode === 'single') { // 单页面只须要一次HtmlWebpackPlugin plugins.push( new HtmlWebpackPlugin({ minify: false, filename: 'index.html', template: './src/index.html', }) ); } if (mode === 'multi') { // 多页面遍历目录,使用目录下面的html文件 // 不一样页面的配置不一样,每一个页面都单独配置一个html // 全部页面的公共部分能够抽离后,经过模版引擎编译处理 // 具体的方式后面部分loader中提到 const files = getPages(options); files.forEach(file => { const name = path.basename(file); file = `${PAGES_DIR}/${file}/index.html`; // 添加runtime脚本,和页面入口脚本 const chunks = [`runtime~${name}`, name]; plugins.push( new HtmlWebpackPlugin({ minify: false, filename: `${name}.html`, template: file, chunks, }) ); }); } // 导出配置 module.exports = { plugins, }; 复制代码
该项配置输出的 bundle 的相关信息,比较经常使用的配置以下:
{ output:{ // name是你配置的entry中key名称,或者优化后chunk的名称 // hash是表示bundle文件名添加文件内容hash值,以便于实现浏览器持久化缓存支持 filename: '[name].[hash].js', // 在script标签上添加crossOrigin,以便于支持跨域脚本的错误堆栈捕获 crossOriginLoading:'anonymous', //静态资源路径,指的是输出到html中的资源路径前缀 publicPath:'https://7.ur.cn/fudao/pc/', path: './dist/',//文件输出路径 } } 复制代码
该项配置主要用于解析模块依赖的自定义项, 比较常规的配置项以下,modules用于加速绝对路径查找效率,alias能够用户自定义模块查找路径。
resolve: { modules: [ path.resolve(__dirname, 'src'), path.resolve(__dirname,'node_modules'), ], alias: { components: path.resolve(__dirname, '/src/components'), }, } 复制代码
扩展 若是你使用了绝对路径后,可能就发现vscode智能代码导航就失效了,别慌!请在想目录下面配置jsconfig.json
文件解决这个问题,配置和上面对应:
{ "compilerOptions": { "baseUrl": ".", "paths": { "src/*": ["./src/*"], "components/*": ["./src/components/*"], "assets/*": ["./src/assets/*"], "pages/*": ["./src/pages/*"] } }, "include": ["./src/**/*"] } 复制代码
这样,你就能够愉快的使用vscode的智能代码提示和导航了!
该项主要配置就是rules了,rules中配置对于不一样资源的处理器,是其核心之一,这里简单添加一个示例代码
module: { // 这些库都是不依赖其它库的库 不须要解析他们能够加快编译速度 // 一般能够将那些大型的库且已经编译好的库排除,减小webpack对其解析耗时 noParse: /node_modules\/(moment|chart\.js)/, rules: [ { test: /\.jsx?$/, use: resolve('babel-loader'), // 须要被这个loader处理的资源 include: [ path.resolve(projectDir, 'src'), path.resolve(projectDir, 'node_modules/@tencent'), ].filter(Boolean), // 忽略哪些压缩的文件 exclude: [/(.|_)min\.js$/], } ] 复制代码
该顶配项中最重要最经常使用的是:splitChunks
,minimizer
minimizer
能够本身配置输出的文件压缩插件,js压缩咱们可使用webpack集成的uglifyjs,也可使用Terser,Terser支持es6代码的压缩,同时支持多进程压缩;css压缩咱们可使用optimize-css-assets-webpack-plugin
压缩,它使用cssnano做为处理引擎,帮助咱们去除重复样式.
splitChunks
是webpack4.x推出的重磅功能,优化的公共chunk提取策略,更高效的提取公共模块,在后面性能优化中会详细说明其使用方法。
plugin 能够介入整个构建过程任何阶段。例如:报告构建耗时、修改输出代码支持主域重试、添加构建进度报告、代码压缩、资源替换等不少能力都在这里实现。
plugin不展开讨论,由于插件太多了。对于项目须要本身实现插件的,须要注意一点,当你使用插件对输出结果处理时,应当在文件输出到磁盘以前处理,咱们之前的构建中主域重试插件就踩了这个坈,致使最终构建的代码出现错误,缘由是该插件直接修改磁盘上面的文件,两次构建同时启动,结束时两次构建的插件都修改了磁盘上同一个文件,最终致使bug,而且致使咱们须要强行清理发布环境代码才恢复正常发布。
温馨的开发体验,有助于提升咱们的开发效率,优化开发体验也相当重要
自从webpack推出热刷新后,前端开发者在开环境下体验大幅提升。 没有热刷新能力,咱们修改一个组件后
加入热构建后:
主要看一下咱们业务基于React技术栈,如何在构建中接入热刷新。
不管什么技术栈,都须要在dev模式下加上webpack.HotModuleReplacementPlugin
插件
在全部entry中插入require.resolve('../utils/webpackHotDevClient')
,webpackHotDevClient
这份代码是由react官方的create-react-app提供的
在webpack-dev-server
模块的启动参数中添加hot:true
在你须要热加载的js文件中添加如下代码(这段代码在构建生产包会自动删除):
if (process.env.NODE_ENV==='development' && module.hot) { module.hot.accept() } 复制代码
注:也可使用react-hot-loader来实现,具体参考官方文档
辅导的H5/PC项目都有部分页面支持直出,之前直出调试方式是以下流程所示:
这种调试流程太长,每一次修改都须要从新构建静态资源,并重启node服务,很是耗时,其次直出模式下,非直出的页面将没法正常访问,整个流程没法走通。
所以, 提出了新的解决方案, 采用 webpack watch+nodemon
结合的模式实现对SSR热调试的支持。node 服务须要的html/js经过webpack插件动态输出,当nodemon检测到变化后将自动重启,html文件中的静态资源所有替换为dev模式下的资源,并保持socket链接自动更新页面。
实现热调试后,调试流程大幅缩短,和普通非直出模式调试体验保持一致。在a8k
中经过k dev -s
命令便可开启ssr调试模式。下面是SSR热调试的流程图:
问题:
给style-loader
开启sourceMap后, sourceMap是内联在style文件中的,须要经过link导入,这种方式是经过JavaScript生成blob后丢个link标签解析。以后咱们能够在dev工具中直接看到每一个样式所在的源文件位置,方便快速的调试样式。但也一样引发一个问题FOUC(页面加载后闪烁),可参见这个ssue
解决方法:
添加singleton: true
参数可解决这个问题,可是sourceMap就不能定位到源文件了,而是合并后的文件中的位置,两者不可兼得。因此在a8k
工具中提供了可选项,默认开启singleton:true
,经过k dev -c
可开启cssSourceMap映射
辅导大多数项目node_modules依赖数量都很是惊人,辅导PC项目剔除构建相关依赖后,依赖包都1883个,依赖包的安装耗时也就大幅增长,所以减小依赖包安装耗时,对构建总体提高很是重要,方法那就是缓存。
JB系统编译 每次编译都会启动一个新的目录,这致使项目依赖的众多node_modules没法缓存,每次编译从新安装耗时很是长,针对JB的编译,我开发了@tencent/im-build模块自动缓存项目依赖的node_modules,大幅提高了编译性能。
OCI编译系统 OCI中不须要额外的插件支持,该系统自己已经能够经过配置实现部分目录缓存,二次利用的能力,使用方法以下:
在项目根目录添加.orange-cache.cache
文件,并添加你须要缓存的目录
/node_modules
/fudao_qq_com_pc_imt
复制代码
修改.orange-ci.yml
配置,添加缓存配置文件路径
push: - cacheFrom: .orange-ci.cache #其它配置省略 复制代码
优化前
优化后
中间结果缓存优化一样能大幅提高构建性能,对模块的编译自己就是CPU密集型任务。一般来讲每次构建并不是全部模块都须要被从新处理,能够只考虑处理那些文件内容有变化的模块,那么文件内容没有变化的模块就能够从缓存中获取,一般经过文件内容hash值做为缓存文件的名称,这就是“热构建”。
在webpack中,可以被缓存的内容有:loader处理结果、plugin处理结果、输出文件结果。下面详细说明不一样资源不一样阶段的缓存方式。
test: /\.jsx?$/, use: [ { loader: resolve('babel-loader'), options: { babelrc: false, // cacheDirectory 缓存babel编译结果加快从新编译速度 cacheDirectory: path.resolve(options.cache, 'babel-loader'), presets: [[require('babel-preset-imt'), { isSSR }]], }, }, ], 复制代码
test: /\.(js|mjs|jsx)$/, enforce: 'pre', use: [ { options: { cache: path.resolve(options.cache, 'eslint-loader'), }, loader: require.resolve('eslint-loader'), }, ], 复制代码
eslint-loader一般只须要在开发模式下开启,方便及时的提醒开发者,存在eslint错误,及时修复
css-loader/sass-loader/postcss-loader自己并无提供缓存机制,这里须要用到cache-loader辅助咱们实现对css/scss的构建结果缓存,具体使用方式以下:
{ loader: resolve('cache-loader'), options: { cacheDirectory: path.join(cache, 'cache-loader-css') }, }, { loader: resolve('css-loader'), options: { importLoaders: 2, sourceMap, }, }, ...因为篇幅缘由,这里不展现其它更多loader 复制代码
只须要将该loader添加到这个loader的最头部便可,该loader不只能够对于css缓存
JS代码压缩咱们采用了TerserPlugin
插件,具体配置以下:
{ // 设置缓存目录 cache: path.resolve(cache, 'terser-webpack-plugin'), parallel: true,// 开启多进程压缩 sourceMap, terserOptions: { compress: { // 删除全部的 `console` 语句 drop_console: true, }, }, } 复制代码
上面在不一样的plugin和loader上面配置了cache目录,对于CI系统来讲你须要将cache目录路径固定,以便于重复使用缓存内容,使用方式:JB就配置/tmp/xxx
目录,OCI系统可配置在项目目录。
⚠️注意:因为使用了缓存,当你修改你的编译配置后,须要当即清理缓存结果,最好的作法是在构建工具中自动检测相关配置是否有变化,自动清理缓存
resolve: { //加快搜索速度 modules: [ 'node_modules', path.resolve(projectDir, 'src'), path.resolve(projectDir, 'node_modules') ], }, 复制代码
module: { // 这些库都是不依赖其它库的库 不须要解析他们能够加快编译速度 noParse: /node_modules\/(moment|chart\.js)/, } 复制代码
// 指处理指定目录的文件 include: [ path.resolve(projectDir, 'src'), path.resolve(projectDir, 'node_modules/@tencent'), ].filter(Boolean), // 忽略哪些压缩的文件 exclude: [/(.|_)min\.js$/], 复制代码
咱们在使用lodash库是,一般只会用到其中很是少的function,可是像下面这段代码,将会致使lodash所有被打入最终的bundle中。
import _ from 'lodash' _.difference(1, 2) 复制代码
这种状况幸亏有插件能够帮咱们优化,经过lodashPlugin便可自动处理lodash的按需引用
使用方法以下:
const LodashPlugin = require('lodash-webpack-plugin'); plugins:[ // 支持lodash包 按需引用 new LodashPlugin(), ] 复制代码
加入这个plugin后,上面的代码自动处理为以下代码:
import difference from 'lodash/difference'; difference([1, 2], [1, 3]); 复制代码
注意:导入代码方式必须使用import,不能使用require
经过webpack-node-externals
插件实现这一点,具体使用方法以下:
const nodeExternals = require('webpack-node-externals'); module.export={ // 省略其它配置 externals: [ nodeExternals({ // 注意若是存在src下面其余目录的绝对引用,都须要添加到这里 whitelist: [ /^components/, /^assets/, /^pages/, /^@tencent/, /\.(scss|css)$/ ], }), ], // 省略其它配置 } 复制代码
在webpack4以前,咱们处理公共模块的方式都是使用CommonsChunkPlugin
,而后该插件的让开发这配置繁琐,而且公共代码的抽离,不够完全和细致,所以新的splitChunks
改进了这些能力。使用的正确姿式以下:
splitChunks: { chunks: 'all', minSize: 10000, // 提升缓存利用率,这须要在http2/spdy maxSize: 0,//没有限制 minChunks: 3,// 共享最少的chunk数,使用次数超过这个值才会被提取 maxAsyncRequests: 5,//最多的异步chunk数 maxInitialRequests: 5,// 最多的同步chunks数 automaticNameDelimiter: '~',// 多页面共用chunk命名分隔符 name: true, cacheGroups: {// 声明的公共chunk vendor: { // 过滤须要打入的模块 test: module => { if (module.resource) { const include = [/[\\/]node_modules[\\/]/].every(reg => { return reg.test(module.resource); }); const exclude = [/[\\/]node_modules[\\/](react|redux|antd)/].some(reg => { return reg.test(module.resource); }); return include && !exclude; } return false; }, name: 'vendor', priority: 50,// 肯定模块打入的优先级 reuseExistingChunk: true,// 使用复用已经存在的模块 }, react: { test({ resource }) { return /[\\/]node_modules[\\/](react|redux)/.test(resource); }, name: 'react', priority: 20, reuseExistingChunk: true, }, antd: { test: /[\\/]node_modules[\\/]antd/, name: 'antd', priority: 15, reuseExistingChunk: true, }, }, }, 复制代码
简要解释上面这段配置
作了这么多优化,下面是基于模块超过2.5k的辅导h5项目,构建耗时对比,感觉一下效果
优化前:热构建须要40s
优化后:只须要20s
构建的配置和优化的工做并不小,将最佳实践收敛和集成为独立的模块,在不一样项目中复用,能够大幅减小构建维护工做,以及后续升级优化工做难度。
IMWeb团队的项目目前也独立维护一套基于React技术栈的构建最佳实践工具a8k
,在全部的项目中不会在看到复杂多样的webpack配置,以及各类花样的前置、后置脚本。各项目仅须要简单的关键配置便可快速接入该构建工具,享受其带来的开发体验提高,和构建性能提高。
用过node-sass的童鞋应该遇到过,安装node-sass遇到各类编译错误、二进制文件下载错误、甚至文件写入权限错误等等😟。也有各类骚操做解决这个问题,但终归不能一劳永逸。
因而就出现想经过postcss插件去兼容sass语法,虽然经过插件可以兼容部分语法,可是想要在已经有必定量的业务代码中,替换node-sass的风险是很是高的,本人亲自测试各类坑💣
固然也有其余途径解决这个问题,不只让你使用完整的sass语法,同时也免去各类安装node-sass的问题,官方的sass-loader其实已经提供了dart-sass解析模块的支持具体参见文档,可能有人担忧dart-sass的js模块性能不高,本人亲测在咱们项目中2000+的模块中,dart-sass的编译性能并无明显降低的感受,同时咱们使用使用了缓存能力,一般只变异哪些变化的资源。
具体的配置入下:
{ loader: resolve('sass-loader'), options: { // 安装dart-sass模块:npm i -D sass implementation: require('sass'), includePaths: [ // 支持绝对路径查找 path.resolve(projectDir, 'src'), ], sourceMap, }, }, 复制代码
node-sass 变量使用问题 我在H5中发现不少这种语法的代码,可是实际上没有生效,构建后,并无替换为变量的值。
编译后:
解决方法以下:
我的以为postcss是css预处理器的将来,如今的postcss对于css就像babel对于JavaScript。postcss经过插件支持将来的css特性,于此同时你还能够自定义插件实现想要的特性。但其余的less、sass这种预处理器,就难以介入它的处理过程,只能按照它既定的规则处理。所以对于全新的项目建议直接使用postcss+postcss-preset-env
使用最新的css语法特性,同时以便于在将来浏览器全面支持相关特性后,快速接入支持。
💣若是你使用了css-loader
的import能力,同时有使用了post-css-import
插件的import能力,两个插件会存在冲突,不建议同时使用!
若是使用了postcss-custom-properties
,须要注意在8.x版本中存在一个bug,没法解析以下语法:
:root{ --green: var(--customGreen, #08cb6a); // 8.x没法正确处理该语法 --primary: var(--customPrimary, var(--green)); } .test { background: color(var(--primary) shade(5%)); // 上面面这句将会被转换为以下代码,最终致使浏览器没法解析该语法 background: var(--green); background: var(--primary); // 咱们指望转换为 background: #08cb6a; } 复制代码
解决方法:禁用 postcss-preset-env 中的custom-properties,安装6.x版本的custom-properties,单独添加该插件。
若是在开发模式下面启用了eslint-loader
对jsx?
文件校验,而且启动了其缓存能力,当修改eslint校验规则,你须要清理缓存文件而且从新启动构建,不然规则修改不会生效!若是使用a8k
工具构建,可使用k clean
命令自动处理处理。
篇幅太长不详细介绍了,有兴趣的能够在这里看到相关源代码webpack-retry-load-plugin, 后续输入相关文章介绍如何实现CSS/JS同步异步代码重试。