webpack4 升级 webpack5 过程踩坑
版权声明:本文为博主原创文章,未经博主允许不得转载。 文章底部留言可联系作者。
一、背景
由于项目越来越庞大复杂,打包时间也非常长,本地开发环境每次重启都要打包好久也和你头疼,正好借此契机对webpack做了一个升级。
升级前使用webpack4,打包耗时如下图:需要 30467ms
升级webpack5之后,打包耗时如下图: 需要 5730ms
二、升级过程
可以查看官方文档 从v4升级到v5
1. 先升级 webpack 和 webpack-cli
npm install --save-dev webpack@latest webpack-cli@latest webpack-dev-server@latest webpack-merge@latest
我之前版本这里是
"webpack": "^4.41.0",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.7.1",
"webpack-merge": "^4.2.1"
升级到的版本是
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.11.1",
"webpack-merge": "^5.8.0"
webpack-merge
升级以后,使用方式改为如下:
修改前:
const webpackMerge = require("webpack-merge");
修改后:
const { merge } = require('webpack-merge');
2. 执行npm start
看看效果。
在 package.json
中 scripts
的 start
命令如下:
"scripts": {
"start": "cross-env NODE_ENV=dev webpack-dev-server --hot --progress --colors --config ./webpack.dev.js",
}
1)--colors 报错
在v4版本中,我们可以使用 --colors或者 --color,但是在v5版本中只能使用 --color
调整命令:
"scripts": {
"start": "cross-env NODE_ENV=dev webpack-dev-server --hot --progress --color --config ./webpack.dev.js",
}
2)OpenBrowserPlugin 报错
打开浏览器的插件 open-browser-webpack-plugin
目前在 webpack5 中不能使用了,所以去掉。
-
webpack5 在开发环境可以通过 devServer.open 的方式去打开浏览器,但是不太建议,因为会导致构建速度明显变慢。
- 我这边针对加这个配置和不加分别进行三次构建,最后一次 配置open(需要60s左右启动),不配置(需要7s左右就可以启动),相差近10倍。所以建议不加。
-
可以利用 react-dev-utils 当中的 openBrowser 来实现,这个不会太影响构建速度(测试第三次构建时大概6-7s),相当于自己写一个plugin。如下:
安装 react-dev-utils
npm install --save-dev react-dev-utils@latest
我安装的版本
"react-dev-utils": "^12.0.1"
在plugins中加一个对象,参考 Plugins 中的 compiler钩子
// 引入
const openBrowser = require('react-dev-utils/openBrowser')
....
// 使用
plugins:[
{
apply(compiler){
let run = false
// 在 compilation 完成时执行
compiler.hooks.done.tap('open-browser', () => {
if(!run){
openBrowser('your url')
run = true
}
})
}
}
]
3)devServer 中 disableHostCheck报错
这里需要参考下 webpack-dev-server v3 to v4 guide
devServer: {
...
disableHostCheck: true,
...
},
修改为:
devServer: {
...
allowedHosts: "all",
...
},
当设置为
'all'
时会跳过 host 检查。并不推荐这样做,因为不检查 host 的应用程序容易受到 DNS 重绑定攻击。
4) devServer 涉及的改动总结:
- The
inline
(iframe
live mode) option was removed without replacement.
v3 中有,但在 v4 中移除
devServer: {
...
inline: true, // v4中直接移除
...
},
progress
/overlay
/clientLogLevel
option were moved to theclient
option
v3 中:
devServer: {
clientLogLevel: "info",
overlay: true,
progress: true,
},
v4 中:
devServer: {
client: {
logging: "info",
// Can be used only for `errors`/`warnings`
//
// overlay: {
// errors: true,
// warnings: true,
// }
overlay: true,
progress: true,
},
},
contentBase
/contentBasePublicPath
/serveIndex
/watchContentBase
/watchOptions
/staticOptions
options were moved tostatic
option:
把 contentBase 选项放到 static 的选项中:
v3 中:
devServer: {
contentBase: path.join(__dirname, "public"),
contentBasePublicPath: "/serve-content-base-at-this-url",
serveIndex: true,
watchContentBase: true,
watchOptions: {
poll: true,
},
},
v4中:
devServer: {
static: {
directory: path.resolve(__dirname, "static"),
staticOptions: {},
// Don't be confused with `devMiddleware.publicPath`, it is `publicPath` for static directory
// Can be:
// publicPath: ['/static-public-path-one/', '/static-public-path-two/'],
publicPath: "/static-public-path/",
// Can be:
// serveIndex: {} (options for the `serveIndex` option you can find https://github.com/expressjs/serve-index)
serveIndex: true,
// Can be:
// watch: {} (options for the `watch` option you can find https://github.com/paulmillr/chokidar)
watch: true,
},
},
3. 执行 npm run build 看看
在 package.json
中 scripts
的 build
命令如下:
"scripts": {
"build": "cross-env NODE_ENV=prod webpack --progress --config ./webpack.prod.js",
}
1) html-webpack-plugin 报错
在webpack文档找到 html-webpack-plugin介绍,打开 html-webpack-plugin github:
安装最新版本的 html-webpack-plugin
"html-webpack-plugin": "^5.5.0"
原本的配置不需要做修改
const path = require('path')
const rootPath = path.resolve(__dirname, "../")
const isPro = process.env.NODE_ENV == 'pro';
...
plugins: [
new HtmlWebpackPlugin({
title: '项目名称',
inject: true,
hash: false,
favicon: path.resolve(path.resolve(rootPath, "./app"), "./logo.png"),
minify: {
removeComments: isPro, // 移除 HTML 中的注释
collapseWhitespace: isPro, // 删除空白符与换行符
minifyCSS: isPro // 压缩内联 css
},
filename: 'index.html',
template: path.resolve(path.resolve(rootPath, "./app"), "./index.html")
})
]
2) optimization.moduleIds 警告
-
下图是webpack 文档中的介绍,主要 Warning 部分提到的:
-
并且项目使用到的
NamedChunksPlugin
要做如下调整:-
NamedChunksPlugin
→optimization.chunkIds: 'named'
-
下图是
webpack
官网对optimization.chunkIds
的说明
-
所以修改前:
optimization: {
moduleIds: "hashed"
...
}
修改后:
optimization: {
moduleIds: "hashed",
chunkIds: 'named',
}
3) 压缩css 使用 css-minimizer-webpack-plugin
之前使用的 optimize-css-assets-webpack-plugin 在github 首页也明确表示,Webpack5 之后优先使用 Webpack 官方出品的 css-minimizer-webpack-plugin。 也可以看webpack文档关于 CssMinimizerWebpackPlugin 的介绍
安装 css-minimizer-webpack-plugin
:
npm install css-minimizer-webpack-plugin --save-dev
webpack中使用:
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
module: {
rules: [
{
test: /.s?css$/,
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
},
],
},
optimization: {
minimizer: [
// For webpack@5 you can use the `...` syntax to extend existing minimizers (i.e. `terser-webpack-plugin`), uncomment the next line
// `...`,
new CssMinimizerPlugin(),
],
},
plugins: [new MiniCssExtractPlugin()],
};
4) @babel/runtime 相关报错
在github上搜到了一个提问# Compile error with Webpack 5 after upgrading but working good with Webpack 4.4.1 和我报错类似吧,建议是把 @babel/runtime
升级到 ^7.12.5
,不过这个比较早了,所以我升级到了最新版本,build编译就通过了。
"@babel/runtime": "^7.19.0"
到此为止没有报错了,但是还没有结束奥,因为有些特性还没有修改,下面再介绍一下
4. 去除dll动态链接库
所谓动态链接,就是把一些经常会共享的代码制作成 DLL 档,当可执行文件调用到 DLL 档内的函数时,Windows 操作系统才会把 DLL 档加载存储器内,DLL 档本身的结构就是可执行档,当程序有需求时函数才进行链接。透过动态链接方式,存储器浪费的情形将可大幅降低。
具体可以查看这个文章:《辛辛苦苦学会的 webpack dll 配置,可能已经过时了》
vue-cli 和 create-react-app 都移除了 dll,具体原因:
在这个 issue 里尤雨溪解释了去除的原因:
dll
option will be removed. Webpack 4 should provide good enough perf and the cost of maintaining DLL mode inside Vue CLI is no longer justified.
create-react-app 在这个 PR 中也做出了说明:
所以如果项目用了webpack4,再使用dll收益不大,所以我们项目里也做了移除
三、新特性
1. 资源模块(asset module)
资源模块(asset module)是一种模块类型,它允许使用资源文件(字体,图标等)而无需配置额外 loader。
在 webpack5 之前,我们一般都会用以下loader
raw-loader
将文件导入为字符串url-loader
将文件作为 data URI 内联到 bundle 中file-loader
将文件发送到输出目录
webpack5 内置了静态资源构建能力,所以直接使用下面4中模块类型,来替换这些loader
asset/resource
发送一个单独的文件并导出 URL。之前通过使用file-loader
实现。asset/inline
导出一个资源的 data URI。之前通过使用url-loader
实现。asset/source
导出资源的源代码。之前通过使用raw-loader
实现。asset
在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用url-loader
,并且配置资源体积限制实现。
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
+ module: {
+ rules: [
+ {
+ test: /.(png|jpg|svg|gif)$/,
+ type: 'asset/resource'
+ }
+ ]
+ },
};
2. 内置 fileSystem Cache能力
cache.type
:缓存类型,支持'memory' | 'filesystem'
,需要设置filesystem
才能开启持久缓存
module.exports = {
...,
cache: {
type: 'filesystem',
// 可选配置
buildDependencies: {
config: [__filename], // 当构建依赖的config文件(通过 require 依赖)内容发生变化时,缓存失效
},
name: '', // 配置以name为隔离,创建不同的缓存文件,如生成PC或mobile不同的配置缓存
...,
},
}
3.不再为 Node.js 模块 自动引用 Polyfills,Polyfill 交由开发者自由控制
移除了 Node.js Polyfills,会导致一些包变得不可用(会在控制台输出 'XXX' is not defined),如果前端包里使用了 process、path 这些依赖,需要手动添加 Polyfill 支持。
4. Tree Shaking 改进
tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的 静态结构 特性,例如
import
和export
。这个术语和概念实际上是由 ES2015 模块打包工具 rollup 普及起来的。
Webpack5 能够支持深层嵌套的 export 的 Tree Shaking.
5. 模块联邦(Module Federation)
具体可以查看这篇文章了解 精读《Webpack5 新特性 - 模块联邦》。
简单来讲模块联邦可以让跨应用间真正做到模块共享。
点击这里看 webpack文档 # Module Federation
模块联邦的使用方式如下:
引入 ModuleFederationPlugin
模块,有如下几个重要参数:
name
: 当前应用的名称,需要唯一性;library
: 其中这里的 name 为作为 umd 的 name;exposes
: 需要导出的模块,用于提供给外部其他项目进行使用;remotes
: 需要依赖的远程模块,用于引入外部其他模块;filename
: 入口文件名称,用于对外提供模块时候的入口文件名;shared
: 配置共享的组件,一般是对第三方库做共享使用;
我们以 app_one
项目是消费方(消费其他remote
模块),app_two
是提供方(暴露模块供消费方使用) 为例:
// 引入模块
const { ModuleFederationPlugin } = require("webpack").container
// app_one 配置
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "app_one",
remotes: {
app_two:"app_two@http://localhost:3000/remoteEntry.js",
},
exposes: {
AppContainer: "./src/App"
},
shared: ["react", "react-dom", "react-router-dom"]
}),
],
};
设置了 remotes: { app_two: "app_two_remote" }
,在代码中就可以直接利用以下方式直接从对方应用调用模块:
import { Search } from "app_two/Search";
我们也可以结合React 组件懒加载使用
const Search = React.lazy(() => import('app_two/Search'));
我们引入的 app_two
配置如下:
// app_two 配置
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "app_two",
library: { type: "var", name: "app_two" },
filename: "remoteEntry.js",
exposes: {
Search: "./src/Search" // Search 在 exposes 被导出
},
shared: ["react", "react-dom"]
}),
],
};
6. 顶层await(Top Level Await)
在顶层使用 await,在 async 函数外部使用 await 字段。它就像巨大的 async 函数,原因是 import 它们的模块会等待它们开始执行它的代码,因此,这种省略 async 的方式只有在顶层才能使用。
通过以下配置开启:
module.exports = {
...,
experiments: {
topLevelAwait: true,
},
}
参考文章
转载自:https://juejin.cn/post/7152796924850995214