介绍webpck5
距离webpack5推出也快有一年多的时间,现在还处在测试版本的阶段,目前已经可以安装使用了
安装方式
```
npm install webpack@next webpack-cli -D
yarn add webpack@next webpack-cli -D
```
复制代码
HTML插件兼容HTML
需要将html-webpack-plugin升级到4.3.0版本
环境要求
最新的官方文档要求node至少在10.13.0版本以上
新特性介绍
持久化缓存
moduleIds & chunkIds的优化
更智能的tree shaking
nodeJs的polyfill脚本被移除
支持生成e6/es2015的代码
SplitChunk和模块大小
Module Federation
使用体验
经过上手使用,webpack5打包体积大小,持续编译速度都有很不错的提升,对webpack4兼容也很平缓,Module Federation也对项目中如何使用微型前端应用提供一种解决方案。如果升级过程中有遇不兼容的情况,可以去webpack5变更日志上查阅。
持久化缓存
在webpack5之前,可以使用cache-loader将编译结构写入硬盘缓存,还可以使用babel-loader,设置option.cacheDirectory将babel-loader编译的结果写进磁盘。
在webpack5中,默认开启缓存,缓存默认是在内存里。可以对cache进行设置
module.export={
cache{
type:'filesystem', // 'memory' | 'filesystem'
cacheDirectory: 'node_modules/.cache/webpack', // 默认将缓存存储在 node_modules/.cache/webpack
// 缓存依赖,当缓存依赖修改时,缓存失效
buildDependencies:{
// 将你的配置添加依赖,更改配置时,使得缓存失效
config: [__filename]
}
}
}
复制代码默认情况下webpack 会假定其所处的 node_modules 目录仅由包管理器修改,将会跳过hash和时间戳处理,出于性能考虑,仅使用package的名称和版本。
当设置 cache.type: "filesystem" 时,webpack 会在内部以分层方式启用文件系统缓存和内存缓存。从缓存读取时,会先查看内存缓存,如果内存缓存未找到,则降级到文件系统缓存。写入缓存将同时写入内存缓存和文件系统缓存。
文件系统缓存不会直接将对磁盘写入的请求进行序列化。它将等到编译过程完成且编译器处于空闲状态才会执行。如此处理的原因是序列化和磁盘写入会占用资源,并且我们不想额外延迟编译过程。
缓存淘汰策略设计:文件缓存存储在 node_modules/.cache/webpack,对于一个缓存集合,最大限度应该不超过 5 个缓存内容,最大累积资源占用不超过 500 MB,当逼近或超过 500MB 的阈值时,优先删除最老的缓存内容。同时,也设计了缓存的有效时长为 2 个星期。
moduleIds & chunkIds的优化
chunk 和 module
chunk:webpack打包最终生成的单独文件块,最终生成的单独文件,一个文件对应一个chunk。
module:每一个源码 js 文件其实都可以看成一个 module。
chunkId的缺点
webpack5改进了moduleIds 和 chunkIds的确定。在webpack5之前,没有从entry打包的chunk文件,都会以1,2,3...的文件命名方式输出。(文件名称后的hash值是用chunkhash生成的)
这样会造成一个后果是,当删除或者暂时不使用1.js这个文件后,那么2.js->1.js,3.js->2.js,这样就会造成原本线上的2.js请求时会造成缓存失效.
在webpack5之前也是可以通过webpackChunkName来解决命名问题
...
Loadable({
loader: () => import(/* webpackChunkName: "home" */ './home'),
loading: (
})
}/>
Loadable({
loader: () => import(/* webpackChunkName: "page1" */'./page1'),
loading: () => (
})
} />
Loadable({
loader: () => import(/* webpackChunkName: "page2" */'./page2'),
loading: () => (
})
} />
....
复制代码这样似乎解决了缓存失效的问题,但我们打开编译后的home.js 会发现还是存有chunkId。如果删除掉home这个菜单,page1,page2打包后的chunkId还是会发生变化,
page1.js打包的文件
删除完home.js后page1.js打包的文件
即使page1.js没有做任何修改,但是由于home.js删除导致的chunkId的变化,所以page1.js的chunkhashi还是会发生变化,缓存失效的问题依旧存在。
webapck5 的改进
采用新的算法,在生产模式下,默认启用这些功能chunkIds: "deterministic", moduleIds: "deterministic"。
此算法采用确定性的方式将短数字 ID(3 或 4 个字符)分配给 modules 和 chunks。这是基于 bundle 大小和长效缓存间的折中方案。
optimization.moduleIds:
可选值:
1. false 告诉webpack不应使用任何内置算法,通过插件提供自定义算法
2.natural 按使用顺序的数字ID。
3.named 方便调试的高可读性id
4.deterministic 根据模块名称生成简短的hash值
5.size 根据模块大小生成的数字id
optimization.chunkIds:
可选值:
1. false 告诉webpack不应使用任何内置算法,通过插件提供自定义算法
2.natural 按使用顺序的数字ID。
3.named 方便调试的高可读性id
4.deterministic 根据模块名称生成简短的hash值
5.size 根据请求到的初始资源size计算的id
6..total-size:根据请求到的解析资源size计算的id
复制代码
更智能的tree shaking
开启tree shaking的条件跟webpack4一样,必须使用ES6模块化,开启production环境
例子
// a.js
export const a =22;
export const b = 33;
// b.js
import * as a from './a.js'
export {a};
// index.js
import * as b from './b.js'
console.log(b.a.a) // 输出22
复制代码webpack4:打包时,会把const b =33也打包进去
webpack5:打包的出来的代码则更精简
nodeJs的polyfill脚本被移除
webpack <= 4 附带了许多 Node.js 核心模块的 polyfil,一旦模块中使用了任何核心模块(即 ”crypto“ 模块),这些模块就会被自动启用。
虽然这使得为 Node.js 编写模块变得简单,但它会将超大的 polyfill 添加到 package 中。在许多情况下,这些 polyfill 并非必要。
import CryptoJS from 'crypto-js';
console.log(CryptoJS.MD5('123123'));
复制代码webpack4 :
webpack5:
支持生成e6/es2015的代码
webpack5 在output中新添加了ecmaVersion,设置output: { ecmaVersion: 6 }就可以使用,在webpack中默认编译生成的代码对应的是es5版本。
两种设置方式:5 =< ecmaVersion <= 11 2009 =< ecmaVersion <= 2020
SplitChunk和模块大小
模块现在能够以更好的方式表示大小,而不是显示单个数字和不同类型的大小。
默认情况下,只能处理 javascript 的大小,但是你现在可以传递多个值来管理它们:
optimization{
splitChunks{
minSize: {
javascript: 30000,
style: 50000,
}
}
}
复制代码
Module Federation
Module Federation 使 JavaScript 应用得以从另一个 JavaScript 应用中动态地加载代码 —— 同时共享依赖。让代码直接在项目间利用 CDN 直接共享,不再需要本地安装 Npm 包、构建再发布了!
动态加载 可以允许代码在运行时按需加载另一个应用的代码。
例子:在app1中引用app2中暴露出来的一个按钮组件
// app1 - webpack.config.js
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
//....
plugins: [
new ModuleFederationPlugin({
name: "app1", // 应用名称 唯一
library: { type: "var", name: "app1" },
remotes: { // 需要引用远程应用,与app2中library的name字段保持一致
app2: "app2",
},
shared: ["react", "react-dom"], // 先判断存不再存这个包,如果不存在就使用app2里的依赖
}),
],
};
复制代码// app2 - webpack.config.js
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
//....
plugins: [
new ModuleFederationPlugin({
name: "app2", // 应用名称 唯一
library: { type: "var", name: "app2" }, // UMD标准导出,和name保持一致即可。
filename: "remoteEntry.js", 暴露出去的chunkname
exposes: { 要暴露出去的模块
"./Button": "./src/Button",
},
shared: ["react", "react-dom"], 与app1共享的依赖,如果app1中有,则会优先使用app1中的依赖。(注:被app1引用)时候会按照app1的/package.json中的版本要求来加载
}),
],
};
复制代码// app1/index.html
// 引入app2暴露的文件
...
...
app1/app.js
import AppTwoButton from 'app/Button';
const App = ()=>{
return (
)
}
app1/bootstrap.js
ReactDOM.render(, document.getElementById("root"));
app1/bootstrap.js
import("./bootstrap");
复制代码从加载流程看懂Module Federation
首先加载的是 app2/remoteEntry.js ,因为是在html中加载的,通过观remoteEntry.js 可以看出,webpakc是将app2定义在了全局变量里面了,这样app1就能够加载app2里面的模块了。
加载app1的打包后的main.js,进入到bootstrap.js寻找依赖前置,分析
然后在加载main.js里的react.js以及react-dom,react依赖的prop-types.js
接着经过remote的时候,发现依赖app2的Button组件
bootstrap.js所有依赖都加载完之后,异步加载完成,执行then逻辑,启动应用
总结
Module Federation给微前端,代码细分,共享依赖,按需加载提供新的思路,而且能做到运行时加载。但缺点公用全局变量和全局style,以及将模块暴露在全局变量下不够优雅。
webpack5还在不断合并代码的过程中,目前版本bate22,webpack5正式版出来后,相信Module Federation功能还会继续优化。总之升级webpack5之后,构建速度和打包体积都有不错的提升。
参考资料
1.三大应用场景调研,Webpack 新功能 Module Federation 深入解析:developer.aliyun.com/article/755…