
左边是浏览器效果, 右边是打包进程
- 懒编译
webpack
从5.17.0
开始引入了 lazyCompilation, 为入口和异步模块提供按需编译服务。即使项目无限增大, 也可以将项目的调试编译时间维持在一个固定的区间内。只有当我们访问对应的模块, 比如异步路由组件后webpack
才会开始编译, 而不是在启动项目时全量编译。
- 进度需求
一般异步路由异步组件在系统设计时都会提供进度条, 但是并不会和构建过程耦合, 所以往往只会提供一个简单的loading
。如果组件依赖过多, 这等待过程往往会让人非常焦虑。
- 源码解析
需要实现懒编译进度条, 我们就需要知道何时开始何时结束以及编译进度。 由于 lazyCompilation 和 dev-server 配置文档均未对细节有任何描述, 我们只能阅读他们的实现源码确定懒编译的过程。
相关源码列表
lazyCompilationPlugin.js 懒编译插件
lazyCompilationBackend.js 懒编译后端实现
lazy-compilation-web.js 懒编译客户端
web
实现
1.在normalModuleFactory.hooks.module
中根据配置和模块信息判断是否需要懒编译的模块, 如果需要懒编译模块转为LazyCompilationProxyModule
, 并将原始模块挂载为originalModule
。
js
// LazyCompilationPlugin.js
compiler.hooks.thisCompilation.tap("LazyCompilationPlugin",
(compilation, { normalModuleFactory }) => {
normalModuleFactory.hooks.module.tap(
"LazyCompilationPlugin", (originalModule, createData, resolveData) => {
...
const { client, data, active } = moduleInfo
return new LazyCompilationProxyModule(
compiler.context,
originalModule,
resolveData.request,
client,
data,
active
)
})
})
2.在LazyCompilationProxyModule
的codeGeneration
中生成懒编译客户端的调用代码client.keepAlive
以及当前模块导出结果module.exports
。
js
// LazyCompilationPlugin.js
class LazyCompilationProxyModule {
codeGeneration({ runtimeTemplate, chunkGraph, moduleGraph }) {
...
const keepActive = Template.asString([
`var dispose = client.keepAlive({ data: data, active: ${JSON.stringify(
!!block
)}, module: module, onError: onError });`
]);
...
// 已编译模块返回
`module.exports = ${runtimeTemplate.moduleNamespacePromise({
chunkGraph,
block,
module,
request: this.request,
strict: false, // TODO this should be inherited from the original module
message: "import()",
runtimeRequirements
})};`
...
// 未编译模块返回
`module.exports = new Promise(function(resolve, reject) { resolveSelf = resolve; onError = reject; });`
}
}
3.当页面打开需要懒编译的模块时, 将调用client.keepAlive
并使用EventSource
将需要懒编译的模块信息通过参数的形式发送给backend
。
js
// lazy-compilation-web.js
exports.keepAlive = function (options) {
var data = options.data
var onError = options.onError
var active = options.active
var module = options.module
errorHandlers.add(onError)
var value = activeKeys.get(data) || 0
activeKeys.set(data, value + 1)
if (value === 0) {
updateEventSource()
}
...
}
var updateEventSource = function updateEventSource() {
...
activeEventSource = new EventSource(
urlBase + Array.from(activeKeys.keys()).join("@")
);
}
4.backend
接收到模块状态信息后, 记录并对比之前的信息判断是否为第一次载入, 如果为第一次载入调用compiler.watching.invalidate
重新运行编译过程。
js
// lazyCompilationBackend.js
const requestListener = (req, res) => {
const keys = req.url.slice(prefix.length).split("@");
...
let moduleActivated = false
for (const key of keys) {
const oldValue = activeModules.get(key) || 0
activeModules.set(key, oldValue + 1)
if (oldValue === 0) {
logger.log(`${key} is now in use and will be compiled.`)
moduleActivated = true
}
}
if (moduleActivated && compiler.watching) compiler.watching.invalidate();
}
5.当编译过程重新运行时, 需要进行编译的懒编译模块通过backend.module
回调获取到active
状态, 并将原始模块通过异步依赖的方式加入懒编译模块并编译。
js
// LazyCompilationPlugin.js
class LazyCompilationProxyModule {
build(options, compilation, resolver, fs, callback) {
...
if (this.active) {
const dep = new LazyCompilationDependency(this)
const block = new AsyncDependenciesBlock({})
block.addDependency(dep)
this.addBlock(block)
}
callback()
}
}
...
class LazyCompilationDependency {
constructor (proxyModule) {
super()
this.proxyModule = proxyModule
}
}
...
class LazyCompilationDependencyFactory {
create(data, callback) {
const dependency = /** @type {LazyCompilationDependency} */ (
data.dependencies[0]
)
callback(null, {
module: dependency.proxyModule.originalModule
})
}
}
...
compilation.dependencyFactories.set(
LazyCompilationDependency,
new LazyCompilationDependencyFactory()
)
6.待编译完毕, webpack
客户端收到更新通知, 旧的模块dispose
运行并调用client.keepAlive()
的回调。载入新模块包含已经编译好的原始模块替换旧模块完成懒编译过程。
javascript
// LazyCompilationPlugin.js
codeGeneration({ runtimeTemplate, chunkGraph, moduleGraph }) {
...
`var dispose = client.keepAlive({ data: data, active: ${JSON.stringify(
!!block
)}, module: module, onError: onError });`
...
"module.hot.dispose(function(data) { data.resolveSelf = resolveSelf; dispose(data); });"
...
}
- 进度实现
通过上述代码我们可以发现, 懒编译模块第一次被载入运行client.keepAlive()
的active
状态为false
, 已经编译完成的模块被载入active
为true
。所以我们只需要在keepAlive
中通过active
状态就可以在页面内知道懒模块开始编译, 通过返回的dispose
回调中知道模块结束编译。并且webpack
的 lazyCompilation 配置项中提供了自定义client
的功能, 只需要复制原始的client
实现, 并在里面加入postMessage
发送编译开始/结束消息。
js
// my-lazy-compilation-client.js
exports.keepAlive = function (options) {
...
var bingding = options.active === false
if(bingding){
window.postMessage({
type: "webpack.lazy.start",
module: options.module.id
})
}
return function () {
...
if(bingding){
window.postMessage({
type: "webpack.lazy.end",
module: options.module.id
})
}
}
}
// webpack config
lazyCompilation: {
imports: true,
backend: {
client: require.resolve('./my-lazy-compilation-client.js')
}
}
打开devServer.client
的progress
配置开关, devServer
的client
会通过postMessage
发送webpackProgress
编译进度信息(这部分的源码解析后面可能会补上 )。只需要创建一个进度页面, 并在内加入message
事件就可以对进度窗口进行进度更新和显示控制。
ts
// index.tsx
export default class LazyProgress {
service: iLazyProgress
...
protected render() {
return <div v-show={this.service.visible} class={style.model}>
<div class={style.bar}>
<div class={style.value} style={{ width: this.service.percent + '%' }}></div>
</div>
<div class={style.msg}>
<div class={style.title}>Building... {this.service.percent}%</div>
{this.service.msg.map(item => <div class={style.text}>{item}</div>)}
</div>
</div>
}
}
// service.ts
export default class ILazyProgress implements iLazyProgress {
...
private onMessage: (event: MessageEvent) => void
private init() {
this.onMessage = event => {
const data = event.data
if (data) {
if (data.type === 'webpackProgress') {
this.percent = data.data.percent
const msg = data.data.msg
if (msg && this.msg[0] !== msg) {
this.msg.unshift(msg)
if (this.msg.length > 10) {
this.msg.pop()
}
}
} else if (data.type === 'webpack.lazy.start') {
this.enabled = true
} else if (data.type === 'webpack.lazy.end') {
this.enabled = false
this.msg = []
this.percent = 0
}
}
}
window.addEventListener('message', this.onMessage, false)
}
destroy() {
window.removeEventListener('message', this.onMessage, false)
}
}
- 总结
1.包括webpack
在内, 很多开源项目由于某些原因对一些进阶功能或者配置并没有特别的文档说明, 对于开发者特别是框架开发者或下层开发者来说, 养成阅读源码了解原理和实现的习惯就变得极其重要了。
2.webpack
的lazyCompilation
自带的client
和backend
实现都比较野蛮粗暴, 包括webpack-dev-server
的client
等等。做一些进阶开发的个人或团队, 可以通过重新组织代码逻辑重新实现他们, 有必要时可以通过PR
方式反哺给开源项目。
3.文章比较注重过程, 结果大家都有各自的UI
实现, 简单一笔带过。
- 谢谢, 欢迎大家留言讨论, 不足的地方欢迎指出, 有需要的可以
点赞
、收藏
、关注
。