全网首发: webpack懒编译流程解析

左边是浏览器效果, 右边是打包进程

  • 懒编译

webpack5.17.0开始引入了 lazyCompilation, 为入口和异步模块提供按需编译服务。即使项目无限增大, 也可以将项目的调试编译时间维持在一个固定的区间内。只有当我们访问对应的模块, 比如异步路由组件后webpack才会开始编译, 而不是在启动项目时全量编译。

  • 进度需求

一般异步路由异步组件在系统设计时都会提供进度条, 但是并不会和构建过程耦合, 所以往往只会提供一个简单的loading。如果组件依赖过多, 这等待过程往往会让人非常焦虑。

  • 源码解析

需要实现懒编译进度条, 我们就需要知道何时开始何时结束以及编译进度。 由于 lazyCompilationdev-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.在LazyCompilationProxyModulecodeGeneration中生成懒编译客户端的调用代码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, 已经编译完成的模块被载入activetrue。所以我们只需要在keepAlive中通过active状态就可以在页面内知道懒模块开始编译, 通过返回的dispose回调中知道模块结束编译。并且webpacklazyCompilation 配置项中提供了自定义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.clientprogress配置开关, devServerclient会通过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...&nbsp;{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.webpacklazyCompilation自带的clientbackend实现都比较野蛮粗暴, 包括webpack-dev-serverclient等等。做一些进阶开发的个人或团队, 可以通过重新组织代码逻辑重新实现他们, 有必要时可以通过PR方式反哺给开源项目。

3.文章比较注重过程, 结果大家都有各自的UI实现, 简单一笔带过。

  • 谢谢, 欢迎大家留言讨论, 不足的地方欢迎指出, 有需要的可以点赞收藏关注
相关推荐
天宇&嘘月2 小时前
web第三次作业
前端·javascript·css
小王不会写code2 小时前
axios
前端·javascript·axios
发呆的薇薇°3 小时前
vue3 配置@根路径
前端·vue.js
luckyext4 小时前
HBuilderX中,VUE生成随机数字,vue调用随机数函数
前端·javascript·vue.js·微信小程序·小程序
小小码农(找工作版)4 小时前
JavaScript 前端面试 4(作用域链、this)
前端·javascript·面试
前端没钱4 小时前
前端需要学习 Docker 吗?
前端·学习·docker
前端郭德纲4 小时前
前端自动化部署的极简方案
运维·前端·自动化
海绵宝宝_4 小时前
【HarmonyOS NEXT】获取正式应用签名证书的签名信息
android·前端·华为·harmonyos·鸿蒙·鸿蒙应用开发
码农土豆5 小时前
chrome V3插件开发,调用 chrome.action.setIcon,提示路径找不到
前端·chrome
鱼樱前端5 小时前
深入JavaScript引擎与模块加载机制:从V8原理到模块化实战
前端·javascript