你们必定看过不少电子设备开箱测评,今天咱们也来跑一个软件新版的上手测评 —— Webpack 5!javascript
从 2017 年发出关于 v5 的投票开始,到 2019 年 10 月发布第一个 beta 版本,目前是 5.0.0-beta.16。如今在收集使用反馈、生态升级的过程当中,相信不久后就能够正式发布了。此次升级重点:性能改进、Tree Shacking、Code Generation、Module Federation。html
下面咱们跟着 Changelog 来动手,测测重点内容~前端
优化持久缓存
首先简单说 Webpack 中 graph 的概念:java
Webpack 在执行的时候,以配置的 entry 为入口,递归解析文件依赖,构建一个 graph,记录代码中各个 module 之间的关系。每当有文件更新的时候, 递归过程会重来,graph 发生改变。node
若是简单粗暴地重建 graph 再编译,会有很大的性能开销。Webpack 利用缓存实现增量编译,从而提高构建性能。react
缓存(内存 / 磁盘两种形式)中的主要内容是 module objects,在编译的时候会将 graph 以二进制或者 json 文件存储在硬盘上。每当代码变化、模块之间依赖关系改变致使 graph 改变时, Webpack 会读取记录作增量编译。webpack
以前可使用 loader 设置缓存:git
-
使用 cache-loader
能够将编译结果写入硬盘缓存,Webpack 再次构建时若是文件没有发生变化则会直接拉取缓存 -
还有一部分 loader 自带缓存配置,好比 babel-loader
,能够配置参数cacheDirectory
使用缓存,将每次的编译结果写进磁盘(默认在 node_modules/.cache/babel-loader 目录)
v5 中缓存默认是 memory
,你能够修改设置写入硬盘:github
module.exports = { cache: { type: 'filesystem', // cacheDirectory 默认路径是 node_modules/.cache/webpack cacheDirectory: path.resolve(__dirname, '.temp_cache') }};
注:对大部分 node_modules
哈希处理以构建依赖项,代价昂贵,还下降 Webpack 执行速度。为避免这种状况出现,Webpack 加入了一些优化,默认会跳过 node_modules
,并使用 package.json
中的 version
和 name
做为数据源。web
优化长期缓存
Webpack 5 针对 moduleId
和 chunkId
的计算方式进行了优化,增长肯定性的 moduleId 和 chunkId 的生成策略。moduleId 根据上下文模块路径,chunkId 根据 chunk 内容计算,最后为 moduleId 和 chunkId 生成 3 - 4 位的数字 id,实现长期缓存,生产环境下默认开启。
-
对比原来的 moduleId
原来的 moduleId 默认值是自增 id,容易致使文件缓存失效。在 v4 以前,能够安装 HashedModuleIdsPlugin
插件覆盖默认的 moduleId 规则, 它会使用模块路径生成的 hash 做为 moduleId。在 v4 中,能够配置 optimization.moduleIds = 'hashed'
-
对比原来的 chunkId
原来的 chunkId 默认值自增 id。好比这样的配置下,若是有新的 entry 增长,chunk 数量也会跟着增长,chunkId 也会递增。以前能够安装 NamedChunksPlugin
插件来稳定 chunkId;或者配置 optimization.chunkIds = 'named'
NodeJS 的 polyfill 脚本被移除
最开始,Webpack 目标是容许在浏览器中运行 Node 模块。可是如今在 Webpack 看来,大多模块就是专门为前端开发的。在 v4 及之前的版本中,对于大多数的 Node 模块会自动添加 polyfill 脚本,polyfill 会加到最终的 bundle 中,其实一般状况下是没有必要的。在 v5 中将中止这一行为。
好比如下一段代码:
// index.jsimport sha256 from 'crypto-js/sha256'; const hashDigest = sha256('hello world');console.log(hashDigest);
在 v4 中,会主动添加 crypto
的 polyfill,也就是 crypto-browserify
。咱们运行的代码是不须要的,反而最后的包变大,编译结果 「417 kb」:
在 v5 中,若是遇到了这样的状况,会提示你进行确认。若是确认不须要 node polyfill,按照提示 alias 设置为 false 便可。最后的编译结果仅有 「5.69 kb」:
配置 resolve.alias: { crypto: false }
:
浏览器执行结果:
更好的 TreeShaking
如今有这样一段代码:
// inner.jsexport const a = 'aaaaaaaaaa';export const b = 'bbbbbbbbbb';
// module.jsimport * as inner from "./inner";export { inner };
// index.jsimport * as module from "./module";console.log(module.inner.a);
在 v4 中毫无疑问,以上代码 a、b 变量是被所有打包的:
但咱们只调用了 a
变量,理想状况应该是 b
被识别为 unused
,不被打包。这一优化在 v5 中实现了。在 v5 中会分析模块 export
与 import
之间的依赖关系,最终的代码生成很是简洁:
重大的变革
若是说以上的变动优化都是常规路数,那么下面的功能有点出乎意料。
Module Federation
让 Webpack 达到了线上 runtime 的效果,让代码直接在独立应用间利用 CDN 直接共享,再也不须要本地安装 NPM 包、构建再发布了!
设计初衷
Webpack 认同多个单独的构建应可以构成一个应用。这些独立的构建不相互依赖,所以能够单独开发和部署。这一般称为微型前端,但还不只仅是如此。
在以前咱们但愿共享代码是如何作的?
「NPM」
维护一个 CommonComponents 的 NPM 包,在不一样项目中安装、使用。若是 NPM 包升级,对应项目都须要安装新版本,本地编译,打包到 bundle 中。
「UMD」
UMD 优势在 runtime。缺点也明显,体积优化不方便,容易有版本冲突。
「微前端」
独立应用间的共享也是问题。通常有两种打包方式:
-
子应用独立打包,模块解耦了,但公共的依赖不易维护处理 -
总体应用一块儿打包,能解决公共依赖;但庞大的多个项目又使打包变慢,后续也很差扩展
Webpack 5 实现了全新的解决方案

从图中能够看到,这个方案是直接将一个应用的 bundle,应用于另外一个应用。
应用能够模块化输出,就是说它自己能够自我消费,也能够动态分发 runtime 子模块给其余应用。
理论比较抽象,咱们动手试一下。
实践测试
如今有两个应用 app1
(localhost:3001)、app2
(localhost:3002):

入口文件:
// app1 & app2: index.jsimport App from "./App";import React from "react";import ReactDOM from "react-dom";
ReactDOM.render(<App />, document.getElementById("root"));
app2
生产了 Button
组件:
// app2: Button.jsimport React from "react";
const Button = () => <button>App 2 Button</button>;
export default Button;
app2
自身消费 Button
组件:
// app2: App.jsimport LocalButton from "./Button";import React from "react";
const App = () => ( <div> <h1>Basic Host-Remote</h1> <h2>App 2</h2> <LocalButton /> </div>);
export default App;
app1
引用 app2
的 Button
组件:
// app1: App.jsimport 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;
先看生产了 Button
组件的 app2
,其配置文件:
// app2:webpack.config.jsconst HtmlWebpackPlugin = require("html-webpack-plugin");const { ModuleFederationPlugin } = require("webpack").container;const path = require("path");
module.exports = { entry: "./src/index", mode: "development", devServer: { contentBase: path.join(__dirname, "dist"), port: 3002, }, output: { publicPath: "http://localhost:3002/", }, module: { rules: [ // ... ], }, plugins: [ new ModuleFederationPlugin({ name: "app2Lib", library: { type: "var", name: "app2Lib" }, filename: "app2-remote-entry.js", exposes: { Button: "./src/Button", }, shared: ["react", "react-dom"], }), new HtmlWebpackPlugin({ template: "./index.html", }), ],};
这段配置描述了,须要暴露出 Button
组件、须要依赖 react
、react-dom
。管理 exposes
和 shared
的模块为 app2Lib
,生成入口文件名为 app-remote-entry.js
。
app1
的配置文件:
const HtmlWebpackPlugin = require("html-webpack-plugin");const { ModuleFederationPlugin } = require("webpack").container;const path = require("path");
module.exports = { entry: "./src/index", mode: "development", devServer: { contentBase: path.join(__dirname, "dist"), port: 3001, }, output: { publicPath: "http://localhost:3001/", }, module: { rules: [ // ... ], }, plugins: [ new ModuleFederationPlugin({ name: "app1", library: { type: "var", name: "app1" }, remotes: { app2: "app2Lib", }, shared: ["react", "react-dom"], }), new HtmlWebpackPlugin({ template: "./index.html", }), ],};
这段配置描述了,使用远端模块 app2Lib
,依赖 react
、react-dom
。
最后一步:在 app1
html 中加载 app2-remote-entry.js
:
// app1: index.html<html> <head> <script src="http://localhost:3002/app2-remote-entry.js"></script> </head> <body> <div id="root"></div> </body></html>
运行结果:


「引用的 app2/Button
是如何找到的呢?」
经过 app1
的配置文件,知道了 app2
是远端加载。在生成的 app1 main.js
描述为:

看这里的 data
数组:
data[1]
即 webpack/container/reference/app2
,这里是返回 app2Lib
对象:
module.exports = app2Lib;
data[0]
即 webpack/container/remote-overrides/a46c3e
,这里提供了 app2
须要的 react
、react-dom
依赖,并返回 app2Lib
:
module.exports = (external) => { if (external.override) { external.override(Object.assign({ "react": () => { return Promise.resolve().then(() => { return () => __webpack_require__(/*! react */ "./node_modules/react/index.js") }) }, "react-dom": () => { return Promise.resolve().then(() => { return () => __webpack_require__(/*! react-dom */ "./node_modules/react-dom/index.js") }) } }, __webpack_require__.O)) } return external;};
因此最后 promise
的赋值变成了:
var promise = app2Lib.get('Button');
这么一看,app2Lib
是全局变量呀。
继续看 app1
加载的 app2-remote-entry.js
内容。果真,生成了一个全局变量 app2Lib
:

app2Lib
对象拥有两个方法,具体为:

var get = (module) => { return ( __webpack_require__.o(moduleMap, module) ? moduleMap[module]() : Promise.resolve().then(() => { throw new Error('Module \"' + module + '\" does not exist in container.'); }) );};
var override = (override) => { Object.assign(__webpack_require__.O, override);};
因此,app2/Button
实际就是 app2Lib.get('Button')
,而后根据映射找到模块,随后__webpack_require__
:
var moduleMap = { "Button": () => { return __webpack_require__.e("src_Button_js").then(() => () => __webpack_require__(/*! ./src/Button */ "./src/Button.js") ); }};
最后再说 shared: ['react', 'react-dom']
:
app2
中指明了须要依赖 react
、react-dom
,并指望消费的应用提供。若是 app1
没有提供,或没有提供指定版本,以下把代码注释:
plugins: [ new ModuleFederationPlugin({ name: "app1", library: { type: "var", name: "app1" }, remotes: { 'app2': "app2Lib", }, // shared: ["react", "react-dom"], // 版本不一致同理 // shared: { // "react-15": "react", // "react-dom": "react-dom", // }, }), new HtmlWebpackPlugin({ template: "./index.html", }),]
那么,刚才 app1 main.js
中的 data[0]
即 webpack/container/remote-overrides/a46c3e
会变为:
module.exports = (external) => { if (external.override) { external.override(__webpack_require__.O); // external.override(Object.assign({ // "react": () => { // return Promise.resolve().then(() => { // return () => __webpack_require__(/*! react */ "./node_modules/react/index.js") // }) // }, // "react-dom": () => { // return Promise.resolve().then(() => { // return () => __webpack_require__(/*! react-dom */ "./node_modules/react-dom/index.js") // }) // } // }, __webpack_require__.O)) } return external;};
app1
则从 app2
加载 react
依赖:


总结,根据 app2
配置的 exposes
& shared
内容,产生对应的模块文件,以及模块映射关系,经过全局变量 app2Lib
进行访问;app1
经过全局变量 get
能知道应该去如何加载 button.js
,override
能知道共享依赖的模块。
以上,Federation 初看很像 DLL + External,但好处是你无需手动维护、打包依赖,代码运行时加载。这种模式下,调试也变得容易,再也不须要复制粘贴代码或者 npm link
,只须要启动应用便可。这里仅以 Button
组件为例,Button
能够一个组件,也能够是一个页面、一个应用。Module Federation 的落地,结合自动化流程等系列工做,还须要你们在各自场景中实践。
社区探索实践
-
各类复杂场景样例:https://github.com/module-federation/module-federation-examples/ -
腾讯:探索 webpack5 新特性 Module federation 在腾讯文档的应用 -
蚂蚁: 调研 Federated Modules,应用秒开,应用集方案,微前端加载方案改进等 -
百度:reskript webpack5 升级实验
其余特性
-
Top Level Await -
SplitChunks 支持更灵活的资源拆分 -
不包含 JS 代码的 Chunk 将再也不生成 JS 文件 -
Output 默认生成 ES6 规范代码,也支持配置为 5 - 11 -
......
详细请阅读 Changlog
以上 Demo 官方也有给出,供你们参考。咱们也将本身内部项目作了升级尝试,过程当中会出现一些 plugins 不兼容的状况。根据官方 Changelog 说明,均可以找到答案,临时修改下相关 plugin 代码。若是你的升级尝试中也遇到了,能够自行处理下,同时也反馈回社区,共同推动新版发布进程。
总的来讲,Webpack 5 初步上手体验后,打包体积、速度都有不错的提高,多数功能的使用配置也更便捷灵巧,Module Federation 让人眼前一亮。抛砖引玉,你们感兴趣能够来交流各自的解读和研究。
若是你对新鲜事物充满好奇,喜欢专研技术、乐于分享,对 IM 产品、桌面客户端基础引擎、基础平台建设感兴趣,欢迎你的加入!
❝文章做者:王欣瑜(Suite Commercialization Engineering 团队)
❞
字节跳动飞书业务,海量 hc,极速响应,快来和我成为同事吧~职位介绍
最后
若是你以为这篇内容对你挺有启发,我想邀请你帮我三个小忙:
点个「在看」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-)
欢迎加我微信「qianyu443033099」拉你进技术群,长期交流学习...
关注公众号「前端下午茶」,持续为你推送精选好文,也能够加我为好友,随时聊骚。

本文分享自微信公众号 - 前端下午茶(qianduanxiawucha)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。