这半周做了一件事,将手上的前端项目从使用过去dva脚手架自带的roadhog2.x打包工具迁移至使用webpack4.x打包,成功让本人掉了不少头发。
背景
先说背景,目前主要做的项目其实都是兄弟姐妹系统(是的没错,就是前端圈位于鄙视链底部的TO B系统),基于早期的JSP多页应用使用React进行拆分重构;技术选型采用的是react
+ antd
+ dva
。我从学校回来接入的时候,项目已经开始一段时间了。当时dva
脚手架还是带的roadhog2.x
构建包工具,它是在webpack
之上的封装,大体上就是提供一个开箱即用的傻瓜式构建方案,技术本身是没有问题的,但是难受就难受在相关文档不是那么全,而且扩展性不足(当然如果你是随便改底层的带哥,当我没说…);比如roadhog2.x
移除了过去支持的dll
配置项,同时sorrycc老哥重心也转移到umi
的开发维护上了…这边随着公司项目版本不断迭代,代码量的日渐增长以及一些工具、第三方库的引入导致项目构建越来越慢,拖了一万年的我终于开始了将roadhog2.x
对应构建方式迁移至webpack4.x
的工作。
webpack4.x
老生常谈
源文件&Chunk&Bundle三者的联系
一语蔽之,它们三个就是同一份代码在不同阶段的产物或者说别名,源文件是我们本地coding的代码,chunk则是源代码在webpack编译过程中的中间产物,最终源代码打包出来的就是bundle文件。
约定大于配置
webpack 4.x
要再装一个webpack-cli
依赖配合,可以通过npm i webpack webpack-cli -D
一起安装。
撸过webpack 4.x
的兄弟姐妹肯定有见过一个WARNING
:The 'mode' option has not been set, webpack will fallback to 'production' for this value.
。现在我们再进行webpack
命令行操作的时候需要指定模式--mode production/development
,如果没有指定会使用默认的production
。两个模式下webpack
会自动地进行相应的优化操作,比如指定production
会自动进行代码压缩等等。
默认情况下entry就是src/index.js
过去我们还需要指定入口文件比如下面这样的:
1 | entry: { |
现在不需要配置,默认就是这个模块了。
默认情况下output被指定为dist/main.js
emm,这个一般就不能不设置了,如果每次打包后的资源文件(html,js,css)名相同,由于强缓存的原因,我们部署在服务器(比如Nginx)上的项目并不会更新,虽然这也可以通过Nginx配置,但其实没啥必要,我们只要使每次打出来的文件名不同(设置hash),浏览器访问的时候就会重新去请求最新的资源。比如:
1 | output: { |
development模式下自动会开启source-map
作为开发者,我们在开发环境下debug往往需要根据控制台的报错信息定位具体文件,如果没有source-map
,我们得到的将是一段处理过的压缩代码,无法定位到具体文件具体代码行,这样非常不利于调试,在webpack4.x前,我们需要手动配置:
1 | module.exports = { |
而现在在webpack4.x中通过指定模式--mode development
将会自动开启该功能。
基本格调
在开始讲迁移的踩坑记录前,我先简要讲讲一般webpack的配置文件由哪些部分组成:
1. entry
,即我们的总入口文件,我们要打包总得把从哪里开始告诉webpack吧?通常这个文件都在src/index.js
。举个例子,你配置完所有的组件以后,肯定有一个顶层爹,中间嵌套的用来提供Provider的也好,配置路由的也好,最终都是将这个爹通过选择器挂载到你的根节点上,类似下面这样:
1 | ReactDOM.render(<Father />, document.getElementById('root')); |
当然我这边项目看了下之前貌似直接拿的ant-design-pro
v1版本的改的(裂开,现在都到v4了)…入口文件dva有自己的封装,v1版本的大概长下面这样:
1 | const app = dva({ |
2. webpack现在有文件解析了,但是咋解析,这个方案需要你告诉webpack。我们需要在module
配置项下的rules
内通过正则判定文件类型然后根据该类型选择不同的loader
来进行不同编译,下面以解析js
和jsx
文件为例子:
1 | { |
3. 指定了不同类型文件的处理方式以后,我们可能还想要做一些额外的扩展,比如代码压缩、生成link
、script
标签、图片拷贝到存放静态资源的目录、编译过程根据库依赖关系自动引入依赖等等。这时候就需要配置plugins
配置项了,拿生成script
标签引入我们的bundle
为例:
1 | new HtmlWebpackPlugin({ |
4. 最终我们得到的编译结果需要一个输出,可以通过配置项中的output
来控制:
1 | output: { |
实战踩坑
mini-css-extract-plugin
webpack4.x
中推荐使用的CSS压缩提取插件,最终会在我们提供的模板HTML中插入一个link标签引入编译后的样式文件;过去版本中的webpack
使用的是extract-text-webpack-plugin
,但是本人最初尝试使用的时候,报了Tapable.plugin is deprecated. Use new API on .hooks instead
问题,去github对应项目下可以发现如下提示:
loader的支持写法以及加载顺序
loader
支持很多种写法,具体看实际场景,简单配置的可以直接写在一个字符串内比如loader: 'style-loader!css-loader'
,匹配顺序从右向左。复杂配置的推荐还是用数组,虽然字符串也可以通过类似GET请求那种拼接方案来设置配置项,但是可阅读性太差了。在数组中,具体loader
我们可以通过对象写法来配置,看上去就清晰明了,例子如下:
1 | module.exports = { |
less处理除了less-loader还需要装less的开发环境依赖
emm…这其实是我当时睿智了,想想都知道没有装less
咋处理呢,通过npm i -D less
解决。
style-loader与mini-css-extract-plugin存在冲突
在我自己鼓捣小DEMO的时候,用style-loader
都是没啥问题的,不过在迁移的项目里,加上就会报错。这里就要理清一个问题,style-loader
到底负责的内容是什么,根据webpack
官方的文档说明,它最终会将处理后的CSS以<style></style>
的DOM结构写入HTML。然后思考一下前面的mini-css-extract-plugin
功能,它俩最终想要的效果是一致的,会有冲突,所以我们移除style-loader
即可。关联issue可以看下这个issue。
css和less文件分开解析
最开始的时候,我对样式的处理都是通过正则test: /\.(css|less)$/
写在一块的,但是一直编译报错,估计是具体配置项不能共享或者有冲突,分开单独做处理问题解决。
antd的样式未加载
之前roadhog
中在webpackrc.js
中的处理是:
1 | ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": true }], |
改用webpack4.x
后,在.babelrc
文件中同样写入以上配置,但是要把style
的值设置为css
,修改后,antd
样式成功载入。
@connect装饰器报错
HOC的装饰器写法,需要配置babel支持。现在webpack一般都不直接在自身配置文件里面设置babel了,而是将babel的配置信息抽出来放到.babelrc
内以JSON格式维护,在plugins
内加入下面这段即可:
1 | ["@babel/plugin-proposal-decorators", { "legacy": true }], |
babel版本
在转webpack4.x
的过程中发现有babel报错的问题,后查发现是兼容性的坑,所以将有问题的怼到了babel7.x
版本配合webpack,7.x版本的babel都带上了@
前缀。
CSS-IN-JS
因为项目内的样式是按照css-modules的规范来写的,所以编译的时候也需要开启支持,在css-loader
的options
内设置modules: true
即可。
根据文件目录以及样式类名生成class
如此生成class
名可以方便我们定位调试一些样式,比如你想在控制台Element
的DOM树结构里ctrl + F
检索对应样式类,然后直接进行调试。这里就需要接着上面的css-modules配置调整了:
1 | { |
当时改的时候有一个坑,即不能像下面这样设置class:
改进后前后对比:
React is not defined
这是我迁移得差不多的时候突然发现的,即部分场景出现了React is not defined
的报错,然后定位了代码发现的确会缺少依赖,比如我在一个组件中引入了antd
的UI组件,即便只是对引入的UI组件进行纯函数的操作,但antd
本身也有对React
的依赖,那为什么之前roadhog
处理就没有问题呢?肯定是有额外的插件做了骚操作!最后在stackoverflow上看到一个老哥的回答,又去webpack官方文档对比了下,靠谱!加入对应插件后解决该问题。
1 | new webpack.ProvidePlugin({ // 根据上下文,在需要依赖React处,自动引入 |
路由跳转组件未挂载
不吹不黑,这东西是我迁移过程中遇到最坑的问题…最早的时候我曾经在webpack输出的内容里看到Router
的warning,但是后面就消失了,造成当时走了弯路,其实罪魁祸首是这个项目在.webpackrc.js
内禁用了import()
这种按需动态引入的方式,就直接导致了我编译出来的文件其实除了根路由的内容,别的内容缺失。找到根源,再定位解决,就容易了,看下roadhog
内对应配置项是用什么处理的即可,最后引入babel-plugin-dynamic-import-node-sync
解决:
CommonsChunkPlugin
webpack4.x中,该用于抽离不同入口文件公共部分的插件已被移除,改用optimization
配置项下的splitChunks
选项使用。
What’s more?
progress-bar-webpack-plugin
用来在命令行可视化webpack编译进度的插件:
1 | new ProgressBar({ |
chalk
用来设置输出颜色的“粉笔”,通过const chalk = require('chalk');
引入。
friendly-errors-webpack-plugin
自定义输出提示工具:
1 | new FriendlyErrorsWebpackPlugin({ |
webpack-merge
这个库主要是用来进行webpack分包的,针对不同环境和功能,我们完全可以将webpack配置文件拆成多个,比如base
文件里就是分包的webpack会共用的配置信息,dev
里就是webpack-dev-server
和development
模式下的配置信息,prod
放生产部署的压缩优化配置,dll
进行代码预编译,提升首次编译后的代码编译效率,一般结构如下:
DllPlugin&DllReferencePlugin
webpack携带的dll预编译插件,它会将几乎不改动的库进行编译(由你指定),然后生成一个编译后的js
以及负责告知webpack之后编译过程哪些内容不需要再处理的json
。
portfinder
查找可用端口。
Result
开发环境编译时长从之前的半分到一分钟不等到现在的10s左右:
TODO
进行生产打包部署的替换。毕竟迁移后的打包结果还需要评估依赖缺失的风险,这中间需要经过大量测试及灰度验证…