| 导语 2020 年 10 月 10 日,webpack5 正式发布,并带来了诸多重大的变动,将会使前端的构建效率与质量大为提高。其实如今各大博客网站已经有不少关于 webpack5 的文章,但真正经过业务实践并得到第一手数据的并很少,因此今天就给你们介绍一下 webpack5 在企鹅辅导业务中的升级与实践ad下面是企鹅辅导h5项目分别在 webpack4 和 webpack5 版本下的构建实测数据,测试环境为个人 MacBook Pro 15 寸高配。css
在上表打包的结果基础之上,修改项目中的代码后,从新进行打包获得以下结果:html
打包后文件的大小:前端
从上表的测试结果能够看出,webpack5 构建性能相对于 webpack4 提高不少,但在打包完成的 bundle 大小上,与 v4 差距不大。由此能够看出 webpack5 的新特性带来了一些优化,下面结合这些新的特性来分析为何可以作到这些优化。node
webpack5 新特性react
webpack5 的发布带来了不少新的特性,例如优化持久缓存、优化长期缓存、Node Polyfill 脚本的移除、更优的 tree-shaking 以及 Module Federation 等。下面针对这些新的特性做出分析。webpack
一、编译缓存git
顾名思义,编译缓存就是在首次编译后把结果缓存起来,在后续编译时复用缓存,从而达到加速编译的效果。github
1.一、webpack4 缓存方案web
webpack4 及以前的版本自己是没有持久化缓存的能力的,只能借助其余的插件或 loader 来实现,例如:npm
使用 cache-loader 来缓存编译结果到硬盘,再次构建时在缓存的基础上增量编译长期缓存。
使用自带缓存的 loader,如:babel-loader,能够配置 cacheDirectory
来将 babel 编译的结果缓存下来。
使用 hard-source-webpack-plugin 来为模块提供中间缓存。
以下图所示,使用以上缓存方案的结果,默认存储在 node_modules/.cache 目录下:
1.二、webpack5 缓存方案
webpack5 统一了持久化缓存的方案,有效下降了配置的复杂性。另外因为 webpack 提供了构建的 runtime,全部被 webpack 处理的模块都能获得有效的缓存,大大提升了缓存的覆盖率,所以 webpack5 的持久化缓存方案将会比其余第三方插件缓存性能要好不少。
webpack5 缓存的开启能够经过如下配置来实现:
module.exports = {
cache: {
// 将缓存类型设置为文件系统
type: "filesystem",
buildDependencies: {
/* 将你的 config 添加为 buildDependency,
以便在改变 config 时得到缓存无效*/
config: [__filename],
/* 若是有其余的东西被构建依赖,
你能够在这里添加它们*/
/* 注意,webpack.config,
加载器和全部从你的配置中引用的模块都会被自动添加*/
},
// 指定缓存的版本
version: '1.0'
}
}
复制代码
以下图所示,webpack5 默认将构建的缓存结果放在 node_modules/.cache 目录下,能够经过配置更改目录:
注意事项:
cache 的属性 type 会在开发模式下被默认设置成 memory,并且在生产模式中被禁用,因此若是想要在生产打包时使用缓存须要显式的设置。
为了防止缓存过于固定,致使更改构建配置无感知,依然使用旧的缓存,默认状况下,每次修改构建配置文件都会致使从新开始缓存。固然也能够本身主动设置 version 来控制缓存的更新。
更多缓存的配置能够参考官方文档:
二、长效缓存
长效缓存指的是能充分利用浏览器缓存,尽可能减小因为模块变动致使的构建文件 hash
值的改变,从而致使文件缓存失效。
2.一、webpack4 长效缓存方案
webpack4 及以前的版本 moduleId
和 chunkId
默认是自增的,更改模块的数量,容易致使缓存的失效。
使用脚手架建立一个简单的项目,构建结果以下:
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(
<React.StrictMode>
<div />
</React.StrictMode>,
document.getElementById('root'));
复制代码
注释掉入口文件 test.js 里引用的 css 文件,如上代码,构建结果以下:
由上图可知,仅仅改了其中一个文件,结果构建出来的全部 js 文件的 hash
值都变了,不利于浏览器进行长效缓存。v4 以前的解决办法是使用 HashedModuleIdsPlugin
固定 moduleId
,它会使用模块路径生成的 hash
做为 moduleId
;使用 NamedChunksPlugin
来固定 chunkId
。
其中 webpack4 中能够根据以下配置来解决此问题:
optimization.moduleIds = 'hashed'
optimization.chunkIds = 'named'
复制代码
2.二、webpack5 长效缓存方案
webpack5 增长了肯定的 moduleId
,chunkId
的支持,以下配置:
optimization.moduleIds = 'deterministic'
optimization.chunkIds = 'deterministic'
复制代码
此配置在生产模式下是默认开启的,它的做用是以肯定的方式为 module
和 chunk
分配 3-5 位数字 id
,相比于 v4 版本的选项 hashed
,它会致使更小的文件 bundles。
因为 moduleId
和 chunkId
肯定了,构建的文件的 hash 值也会肯定,有利于浏览器长效缓存。同时此配置有利于减小文件打包大小。
在开发模式下,建议使用:
optimization.moduleIds = 'named'
optimization.chunkIds = 'named'
复制代码
此选项生产对调试更友好的可读的 id
。
三、Node Polyfill 脚本被移除
webpack4 版本中附带了大多数 Node.js 核心模块的 polyfill,一旦前端使用了任何核心模块,这些模块就会自动应用,可是其实有些是没必要要的。
webpack5 将不会自动为 Node.js 模块添加 polyfill,而是更专一的投入到前端模块的兼容中。所以须要开发者手动添加合适的 polyfill。
import sha256 from 'crypto-js/sha256';
const hashDigest = sha256('hello world1');
console.log(hashDigest);
复制代码
上面代码在v4中打包结果以下:
使用 wepack4 打包,主动添加了crypto
的 polyfill,即 crypto-browserify
,打包大小为 441k。在 wepack5 中打包这样的代码,构建会提示开发者进行确认是否须要 node polyfill,以下图:
若是确认不须要 polyfill,可根据提示设置 fallback
,以下:
resolve: {
fallback: {
"crypto": false
}
}
复制代码
打包结果为:
打包后 js 文件小了 305k,去除掉项目不须要的 node polyfill,对于减少打包大小收益很可观。
四、更优的 tree-shaking
// const.js
export const a = 'hello';
export const b = 'world';
// module.js
export * as module from './const';
// index.js
import * as main from './module';
console.log(main.module.a)
复制代码
有如上的一段代码,在 v4 构建中打包后的结果以下:
从上图能够看出,const.js 导出的 a,b 变量都被打包了,但实际上咱们只用到了 a,期待的是b 应该不被打包进去。
webpack5 对 tree-shaking 进行了优化,分析模块的 export
和 import
的依赖关系,去掉未被使用的模块,打包结果以下:
!function(){"use strict"; console.log("hello")}();
复制代码
能够看出代码很是简洁。
Module Federation 使得使 JavaScript 应用得以从另外一个 JavaScript 应用中动态地加载代码 —— 同时共享依赖。至关于 webpack 提供了线上 runtime 的环境,多个应用利用 CDN 共享组件或应用,不须要本地安装 npm 包再构建了,这就有点云组件的概念了。
以 github 上的例子为例,basic-host-remote
上图是项目的目录结构,能够看出存在 2 个应用 app一、app2。其中 app1 使用了 app2 的代码,那么 app1 是如何引用 app2 的代码呢?看下面的代码:
// app1
import React from "react";
const RemoteButton = React.lazy(() => import("app2/Button"));
const App = () => (
<div>
<h1>Basic Host-Remote</h1>
<h2>App 1</h2>
<React.Suspense fallback="Loading Button">
<RemoteButton />
</React.Suspense>
</div>);
export default App;
复制代码
其中最重要的就是
const RemoteButton = React.lazy(() => import("app2/Button"));
复制代码
直接在 app1 的项目中引用了 app2 项目的代码。是如何作到的?咱们看下构建配置:
先看提供组件 Button 的 app2 的配置:
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { ModuleFederationPlugin } = require("webpack").container;
const path = require("path");
module.exports = {
// 有删减
plugins: [
new ModuleFederationPlugin({
name: "app2",
library: {
type: "var",
name: "app2"
},
filename: "remoteEntry.js",
exposes: {
"./Button": "./src/Button",
},
shared: {
react: {
singleton: true
},
"react-dom": {
singleton: true
}
},
}),
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
],
};
复制代码
依赖共享主要是由插件 ModuleFederationPlugin
来提供的,由上面的配置能够看出 app2 暴露出了 Button 组件,依赖 react、react-dom,生成入口文件为 remoteEntru.js。下面再来看下 app1的配置:
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { ModuleFederationPlugin } = require("webpack").container;
const path = require("path");
module.exports = {
//http://localhost:3002/remoteEntry.js
plugins: [
new ModuleFederationPlugin({
name: "app1",
remotes: {
app2: "app2@http://localhost:3002/remoteEntry.js",
},
shared: {
react: {
singleton: true
},
"react-dom": {
singleton: true
}
},
}),
],
};
复制代码
结合以前 app2 的配置来看,app1 加载远程的 app2 模块,依赖 react、react-dom。
浏览器里运行效果如图:
Module Federation 还有不少的潜力能够挖掘,例如能够将咱们项目中经常使用的依赖包 react 全家桶等打成一个包,作成一个 runtime,开发环境和生产环境依赖一个 runtime,这样能够大大减小项目的大小,提升编译速度。
一些更实用的用法须要咱们在实际使用中继续探索,发挥 webpack5 更大的价值。
一、在 webpack4 中标记过时的功能都已经在 webpack5 移除了。
二、开发环境下默认使用可读的名称为 module
命名,不须要使用以下语法:
import(/* webpackChunkName: "name" */ "module")
复制代码
三、原生 worker 支持
......
本文针对 webpack5 的比较重要的特性进行了说明,具体的一些变动能够去参考官方文档。
升级踩坑
升级的过程比较枯燥,基本上就是调试、修改、继续调试的过程,下面列出几个比较典型的问题。
一、升级 webpack 及相关包的版本
这个过程是比较耗时的,须要将 webpack 的版本及相关 loader 和 plugin 的版本进行升级,现在 webpack5 已正式发布,相关插件基本上都兼容了 webpack5,因此大部分问题都能经过升级包版本解决。
二、配置 webpack5 编译缓存不生效
这个问题就比较坑了,脚手架建立一个简单项目后,根据官网文档配置 cache,启动构建:
webpack --config webpack-dist.config.js
cache: {
type: 'filesystem'
}
复制代码
结果构建是成功,可是相应的缓存却一直没有生成,其中构建提示以下:
提示说 webpack-dist.config.js 找不到,当时就很懵了,这个文件明明是存在的,并且配置缓存策略时,并无这个文件。查阅大量文档以后开始翻看源码,其中部分以下:
// webpack/lib/cache/PackFileCacheStrategy.js
if (newBuildDependencies.size > 0 || !this.buildSnapshot) {
if (reportProgress)
reportProgress(0.5, "resolve build dependencies");
this.logger.debug(`Capturing build dependencies... (${Array.from(newBuildDependencies).join(", ")})`);
promise = new Promise((resolve, reject) => {
this.logger.time("resolve build dependencies");
this.fileSystemInfo.resolveBuildDependencies(this.context,newBuildDependencies,)
...
复制代码
打印 newBuildDependencies 获得结果:
发现还真有这个文件,并且相比于其余绝对路径,这个相对路径可能没法找到。
继续断点调试,追溯这里的 newBuildDependencies 的值,发现webpack-dist.config.js 这个文件是在 webpack-cli 里写入的,
const cacheDefaults = (finalConfig, parsedArgs) => {
// eslint-disable-next-line no-prototype-builtins
const hasCache = finalConfig.hasOwnProperty('cache');
let cacheConfig = {};
if (hasCache && parsedArgs.config) {
if (finalConfig.cache && finalConfig.cache.type === 'filesystem') {
cacheConfig.buildDependencies = {
config: parsedArgs.config,
};
}
console.log(3333, cacheConfig)
return {
cache: cacheConfig
};
}
return cacheConfig;
};
复制代码
从这里看出当配置持久缓存时,使用命令行自动的给 cache 加上 config 后面的参数。因为找不到这个相对路径,从而致使缓存逻辑执行报错,缓存失败。
个人解决办法:
const path = require('path');
const exec = require('child_process').exec;
const config = path.resolve(__dirname, 'webpack-dist.config.js');
const cmdStr = `webpack --config ${config}`;
exec(cmdStr, function(err,stdout,stderr){
if(err) {
console.log('get weather api error:'+stderr);
} else {
console.log(stdout);
}
});
复制代码
获取 webpack-dist.config.js 的绝对路径,传给命令行,就能够解决。可能还有更优雅的解决方法,后面继续探索。
三、loader 配置参数修改
出现以下报错时,表示 webpack5 不兼容之前的 webpack 的写法了,须要按最新版的规则来修改:
{
test: /\.css$/,
loaders: ['css-loader'],
// 提取出css
}
loaders改成use
{
test: /\.css$/,
use: ['css-loader'],
// 提取出css
}
复制代码
四、去掉 node polyfill
因为 webpack5 会自动去掉 polyfill,所以会出现以下提示
解决办法是按照提示修改,确认是否须要添加 polyfill
resolve: {
fallback: {
"domain": false
}
}
复制代码
webpack5 正式发布已经有一段时间了,总的来讲:
构建性能大幅度提高,依赖核心代码层面的持久缓存,覆盖率更高,配置更简单。
打包后的代码体积减小。
默认支持浏览器长期缓存,下降配置门槛。
使人激动的新特性 Module Federation,蕴含极大的可能性。