往期文章:
- 重学webpack4之原理分析
- 重学webpack4之基础篇
- 重学webpack4之loader开发
- 重学webpack4之plugin开发
- 重学webpack4之打包库和组件
- 重学webpack4之构建速度和体积优化
前言
webpack5与webpack4相比有较大的改动,甚至有些颠覆性,目前新版本已经发布,但是配套的loader、plugin还未完全就绪,不建议此时升级项目,但是咱们可以一起学习,了解和掌握其精髓,甚至成为webpack开发者,为之后项目升级做好提前亮~
主要改变
- 通过持久缓存提高构建性能
- 使用更好的算法和默认值来改善长期缓存
- 通过更好的Tree Shaking和代码生成来改善捆绑包大小
- 改善与Web平台的兼容性
- 在v4中实施功能时,请清理处于怪异状态的内部结构,而不引入任何重大更改
- 现在就引入重大更改,为将来的功能做准备,使我们能够尽可能长时间地使用v5
产出物
- 代码量明显的减少
console.log('Hello World');
- 生成代码更清爽
-
- 如果不引入其他文件,没了 webpack4 那种乱糟的辅助代码(模块管理代码),清爽多了
- 如果不引入其他文件,没了 webpack4 那种乱糟的辅助代码(模块管理代码),清爽多了
编译器优化
Compiler大家应该不陌生,在Webpack中充斥着大量的钩子和触发事件
在新的版本中,编译器在使用完毕后应该被关闭,因为它们在进入或退出空闲状态时,拥有这些状态的 hook。 插件可以用这些 hook 来执行不太重要的工作(比如:持久性缓存把缓存慢慢地存储到磁盘上)。同时插件的作者应该预见到某些用户可能会忘记关闭编译器,所以 当编译器关闭所有剩下的工作时应尽快完成。 然后回调将会通知已彻底完成。 当升级到 v5 时,请确保在完成工作后使用 Node.js API 调用 Compiler.close
minSize&maxSize 更好的方式表达
- webpack4版本
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
commons: {
chunks: 'all',
name: 'commons',
minChunks: 1,
minSize: '数值',
maxSize: '数值'
}
}
}
}
}
- webpack5
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
commons: {
chunks: 'all',
name: 'commons',
}
},
//最小的文件大小 超过之后将不予打包
minSize: {
javascript: 0,
style: 0,
},
//最大的文件 超过之后继续拆分
maxSize: {
javascript: 1, //故意写小的效果更明显
style: 3000,
}
}
}
}
Node.js polyfills 自动被移除
过去,Webpack4版本附带了大多数 Node.js 核心模块的 polyfills,一旦前端使用了任何核心模块,这些模块就会自动应用,但是其实有些是不必要的。 V5中的尝试是自动停止 polyfilling 这些核心模块,并侧重于前端兼容的模块。当迁移到 v5时,最好尽可能使用前端兼容的模块,并尽可能手动添加核心模块的polyfills。
持久化缓存
- 添加了用于长期缓存的新算法。在生产模式下默认启用这些功能
chunkIds: "deterministic"
moduleIds: "deterministic"
mangleExports: "deterministic"
该算法以确定性方式为模块和块分配短(3或5位数字)数字ID,并为导出分配短(2个字符)名称。这是包大小和长期缓存之间的折衷方案
- moduleIds/chunkIds/mangleExports: false,禁用默认行为,并且可以通过插件提供自定义算法
注意,在moduleIds/chunkIds: false在webpack4生成的版本有效,而在webpack5中,必须提供自定义插件
迁移:最好使用默认值chunkIds,moduleIds和mangleExports。还可以选择使用旧的默认设置chunkIds: "size", moduleIds: "size", mangleExports: "size",这将生成较小的包,但需要使它们频繁地失效以进行缓存。 注意:在webpack 4中,散列的模块ID导致gzip性能降低。这与更改的模块顺序有关,并且已得到修复。
显示地增加缓存选项 cache
- 开启cache选项,提升webpack打包性能
- 文件系统缓存。它是可选的,可以通过以下配置启用:
module.exports = {
// cache:true | false
cache: {
type: 'filesystem',
// 自定义缓存目录,
// cacheDirectory: path.resolve(__dirname, '.temp_cache')
buildDependencies: {
// 添加项目配置作为构建依赖项,以便配置更改时缓存失效
config: [__filename],
// 如果还有构建依赖其他内容,在此处添加
//注意,配置中引用的webpack、加载器和所有模块都会被自动添加
}
}
};
缓存将存储到node_modules/.cache/webpack,当使用Yarn PnP时缓存目录为.yarn/.cache/webpack
许多内部插件也将使用持久性缓存。如 ourceMapDevToolPlugin缓存SourceMap生成、ProgressPlugin缓存模块数量
持久性缓存将根据使用情况自动创建多个缓存文件,以优化对缓存的读写访问。
默认情况下,时间戳将在开发模式下用于快照,而在生产模式下用于文件哈希。
建议:开发开启缓存,生产关闭缓存
真实内容哈希 contenthash
- webpack4:仅使用文件内部结构的哈希
- webpack5:文件内容真实哈希,仅修改注释或重命名变量时,webpack5能监测到文件变化,而webpack4监听不到变化(这可能对长期缓存产生积极影响)
友好的命名块 ID
- webpack4
-
- 打包产出物会生成 0.xxx.js (chunk)
-
- 简短的数字 ID,在编译之间不会更改。适合长期缓存
- webpack5
-
- development: 默认启用的新命名块 ID 算法,为块(和文件名)提供了易于理解的名称。模块 ID 由其相对于的路径确定 context。块 ID 由块的内容确定,例如:src_demo1_data_js.js
-
- production: 算法实现短(3或5位数字)数字ID,例如:125.js
webpack5这么做的好处:防止之前的 0.xxx.js这种文件名串号
开发调试chunks更加友好
- webpack4: import(/_ webpackChunkName: "name" _/ "module")
- webpack5: import("module") ,开发环境默认开发 optimization.chunkIds:'named'
模块联合
Webpack5 添加了一个称为“模块联合”的新功能,可以让跨应用间真正做到模块共享。
- 该功能允许多个 Webpack 构建协同工作.该功能使 Webpack 可以实现线上 Runtime 效果,代码直接在项目间利用 CDN 直接共享,其结果就是:不再需要每次都得本地安装 Npm 包,再构建再发布了
- Webpack4 可以通过 DLL 或者 Externals 做代码共享时 Common Chunk,但实际上不同同应用和项目间共享就困难了,很难做到项目之间做到按需热插拔。 模块联邦是 Webpack5 新内置的一个重要功能,
- 从运行时角度: 多个构建的模块的行为将类似于巨大的连接模块图
- 从开发角度:从指定的远程版本中导入模块,并以最小的限制使用它们
共享模式的发展
NPM 方式共享模块
如下图所示,正常的代码共享需要将依赖作为 Libaray 安装到项目,进行 Webpack 打包构建再上线
对于项目 Home 与 Search,需要共享一个模块时,最常见的办法就是将其抽成通用依赖并分别安装在各自项目中,虽然可以一定程度解决重复安装和修改困难的问题,但依然依赖本地编译
UMD 方式共享模块
模块以 Webpack UMD 模式打包,并输出到其他项目中。目前比较普遍的模块共享方式:
对于项目 Home 与 Search,直接利用 UMD 包复用一个模块。但问题是包体积无法达到本地编译时的优化效果,且库之间很容易发生冲突。
微前端 方式共享模块
- 微前端一般有两种打包方式:
-
- 子应用独立打包,模块解耦,但无法抽取公共依赖等
-
- 整体应用一起打包,但打包过程较慢,管理较繁琐
模块联合方式
-
将应用Home中的一个包应用于Search中,同时具备整体应用一起打包的公共依赖抽取能力
-
为了让应用具备模块化输出能力,webpack5新增 “中心应用”概念,这个中心应用用于在线动态分发 Runtime 子模块,并不直接提供给用户使用:
-
所有子应用都可以利用 Runtime 方式复用主应用的 Npm 包和模块,这样,可以更好的集成到主应用中
实现方式
配置
// app_a
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "app_a",
remotes: {
app_b: "app_b",
app_c: "app_c"
},
exposes: {
AppContainer: "./src/App"
},
shared: ["react", "react-dom", "react-router-dom"]
}),
]
};
---------------------------------------------------
// app_b
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
export default {
plugins: [
new ModuleFederationPlugin({
name: "app_b",
library: { type: "var", name: "app_b" },
filename: "remoteEntry.js",
exposes: {
Menu: "./src/Menu"
},
shared: ["react", "react-dom"]
})
]
};\
- name 当前应用名称,需要全局唯一,使用的时通过 {expose} 的方式使用;
- filename: 打包出来的文件名
- library 这里的 name 为作为 umd 的 name;
- remotes 可以将其他项目的 name 映射到当前项目中。
- exposes 表示导出的模块,只有在此声明的模块才可以作为远程依赖被使用。
- shared 可以让远程加载的模块对应依赖改为使用本地项目的,优先用 Host 的依赖,如果 Host 没有,本地的
引用
app_a: remotes: { app_b: "app_b", app_c: "app_c" },业务代码就可以直接利用以下方式直接从对方应用调用模块:
- 方式一:不推荐
<head>
<script src="http://localhost:3002/app_b.js"></script>
<script src="http://localhost:3003/app_c.js"></script>
</head>
方式二:
// app_a
// import { Menu } from "app_b/Menu";
// const Menu = await import("app_b/Menu");
const Menu = React.lazy(() => import("app_b/Menu"));
const MenuContainer = () => {
return (
<div>
<React.Suspense fallback="Loading Menu...">
<Menu />
</React.Suspense>
</div>
);
}
export default MenuContainer;
---------------------------------------------------------
// app_b
const AppContainer = React.lazy(() => import("app__remote/AppContainer"));
const App = () => {
return (
<div>
<React.Suspense fallback="Loading App Container...">
<AppContainer />
</React.Suspense>
</div>
);
}
开启实验功能
再见file/url/raw-loader-loader
- 设置 experiments.asset = true,给图片文件设置名字 assetModuleFilename
- js 名字还是原来的 filename
module.exports = {
output: {
assetModuleFilename: 'images/[name].[hash:5][ext]',
},
module: {
rules: [
{
test: /\.(png|jpg|svg)$/,
type: 'asset',
},
],
},
experiments: {
asset: true,
},
};
异步引入·真香
- wepback4环境这样异步引入代码
import('./data').then(_ => {
console.log(_);
});
webpack4 无法处理这种场景
// async.js
let output;
async function main() {
const dynamic = await import('./data');
output = dynamic + '';
}
main();
export default output;
// index.js
import output from './async';
console.log(output); // undefined
webpack5 开启topLevelAwait 可以实现,并且无需外面包裹 async
- webpack配置:experiments.topLevelAwait = true
// async.js
const dynamic = await import('./data');
const output = dynamic + '';
export default output;// hello world
- 更激进的写法
// async.js
const dynamic = import('./data');
const output = (await dynamic).default + '🍊';
export default output; // 输出 hello world🍊]
webpack5 是如何实现 topLevelAwait,保证输出的?
- 我们先来看一个题目
let a = 0;
async function test() {
a = a + (await 10);
console.log(a);
}
test();
console.log(++a);
答案:1 10
分析:当执行到 await 时,会将 10 放到 promise 中,同时将 await 前面的 a 锁死,await本身是es6 Generator 的语法,本质是将一个函数执行暂停,并保存上下文,再次调用时恢复当时的状态,实际管理是协程
-
- 进程、线程和协程的比较 进程:变量隔离,自动切换运行上下文 线程:变量不隔离,自动切换运行上下文切换 协程:变量不隔离,不自动切换运行上下文切换
将协程视为轻量级线程是协程的最直观,最高级的隐喻。与实际线程相比,最大的概念差异是缺少调度程序(所有上下文切换必须由程序完成)
- 如果将a放在 await 后面
let a = 0;
async function test() {
a = (await 10) + a;
console.log(a);
}
test();
console.log(++a);
答案:1 11
实际应用场景
//demo02/index.js
const connectToDB = async () => {
const data = await new Promise(r => {
r('xfz');
});
return data;
};
const result = await connectToDB();
let output = `${result} 🍊`;
export { output };
//执行如下代码
import { output } from './demo02';
console.log(output); // xfz 🍊
WebAssembly 使用就像吃了德芙...
- 整个 wasm 文件
int add (int x, int y) {
return x + y;
}
//然后我们把它编译后生成 util.wasm
// 这里使用 emscripten 来编译 c
- webpack4
-
- 不能同步地加载,否则会报错,不能把wasm当成主chunk,只能如下面方式引入
import('./util.wasm').then(_ => {
console.log(_.add(4, 6));
});
- webpack5,开启 syncWebAssembly和syncWebAssembly后,就可以happy coding 了
// webpack.config.js
module.exports = {
experiments: {
asyncWebAssembly: true,
syncWebAssembly: true,
},
};
// index.js
import { add } from './util'
console.log(add(4, 6))
分析模式升级
- 新添加的 percentBy-option 提示 ProgressPlugin 如何计算进度百分比
plugins: [
new webpack.ProgressPlugin((percentage, message, ...args) => {
console.info(percentage, message, ...args);
}),
],
-
- percentage:0到1之间的数字,指示编译的完成百分比
-
- message:当前正在执行的钩子的简短描述
-
- ...args:零个或多个其他字符串描述当前进度
new webpack.ProgressPlugin({
activeModules: false,// 显示活动模块计数和一个活动模块进行中消息
entries: true, // 显示正在计数的条目消息
handler(percentage, message, ...args) {
// custom logic
},
modules: true, // 显示模块计数进行中消息
modulesCount: 5000, // 最小模块数开始。modules启用属性后生效
profile: false, // 告诉ProgressPlugin收集配置文件数据以进行进度
dependencies: true, // 示正在进行的依赖项计数消息
dependenciesCount: 10000, // 最小依赖项计数开始。dependencies启用属性后生效
percentBy: 'entries' | 'dependencies' | 'modules' | null, // 说明如何计算进度百分比
});
集成了 prepack
- prepack 预包装 ,对于繁重的初始化代码,Prepack 在有效解析 JavaScript 后,直接给结果了
(function () {
function hello() {
return 'hello';
}
function world() {
return 'world';
}
global.s = hello() + ' ' + world();
})();
prepack 觉得你这段代码是废话,直接帮你求值了,经处理后,代码是这样的
s = 'hello world';
webpack-dev-server 变更
- webpack内置devServer
// package.json
"scripts": {
"start": "webpack serve --open",
},
// webpack.config.js
devServer: {
contentBase: './dist',
},
- 其他选项在会后续文章中详细阐述
html-webpack-plugin
- webpack会默认生成一个或多个html文件(也就是代码中可以没有template.html)
- 可以自主选择是否使用webpack默认生成的html
-
- 否,指定 template: './index.html'
-
- 是,通过 options 取代 template.html 文件中的内容,自动根据options插入生成的html中
plugins: [
new HtmlWebpackPlugin({
title: 'Development',
meta: {
keywords: 'webpack5的使用',
},
}),
],
- 无论使用那种方式,生成的html都能自动获取到htmlWebpackPlugin的传参
-
- 自己指定模版中的内容高于options中的内容 例如:
// config
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'src/index.html',
title: 'Development',
meta: {
keywords: 'webpack5的使用',
},
}),
],
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>test</title>
<style>
body {
background-color: pink;
}
</style>
</head>
<body>
</body>
</html>
打包后,生成的html
- 个人更偏向使用默认生成html的方式,因为可以通过webpack轻松控制html里面的内容,而无需像webpack4一样获取html-webpack-plugin的传参数,对于区分开发和生产环境的html更加方便(也可以指定一个基础模版,通过不同环境设置不同的options)
"scripts": {
"start": "webpack serve --config webpack.dev.js",
"build": "webpack --config webpack.prod.js"
},
webpack.dev.js
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
devtool: 'inline-source-map',
devServer: {
contentBase: './dist',
},
plugins: [
new CleanWebpackPlugin({ cleanStaleWebpackAssets: false }),
new HtmlWebpackPlugin({
title: 'Development',
}),
],
};
webpack.prod.js
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
devtool: false,
plugins: [
new CleanWebpackPlugin({ cleanStaleWebpackAssets: false }),
new HtmlWebpackPlugin({
templateContent: `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-dns-prefetch-control" contenxit="on">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, minimal-ui">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="screen-orientation" content="portrait" />
<meta content="yes" name="apple-mobile-web-app-capable">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="format-detection" content="telephone=no">
<meta name="full-screen" content="yes">
<meta name="x5-fullscreen" content="true">
<title>生产环境</title>
<link rel="dns-prefetch" href="//xxx.com">
<link rel="dns-prefetch" href="//cdn.bootcss.com">
<link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.bootcss.com/font-awesome/4.7.0/css/font-awesome.min.css">
</head>
<body>
<div id="app"></div>
<script src="https://polyfill.io/v3/polyfill.js"></script>
<script src="https://cdn.bootcss.com/react/16.7.0/umd/react.production.min.js"></script>
<script src="https://cdn.bootcss.com/react-dom/16.7.0/umd/react-dom.production.min.js"></script>
<script src="https://cdn.bootcss.com/react-router-dom/4.2.2/react-router-dom.min.js"></script>
<script src="https://cdn.bootcss.com/mobx/5.1.0/mobx.umd.min.js"></script>
<script src="https://cdn.bootcss.com/mobx-react/5.2.5/index.min.js"></script>
</body>
</html>
`,
}),
],
};
后续文章
- webpack5之原理分析: writing
- webpack5之基础篇: todo
- webpack5之loader开发: todo
- webpack5之plugin开发: todo
- webpack5之打包库和组件: tpdp
- webpack5之构建速度和体积优化: todo
❤️ 加入我们
字节跳动 · 幸福里团队
Nice Leader:高级技术专家、掘金知名专栏作者、Flutter中文网社区创办者、Flutter中文社区开源项目发起人、Github社区知名开发者,是dio、fly、dsBridge等多个知名开源项目作者
期待您的加入,一起用技术改变生活!!!