Keep

分析 Webpack import() 过程

12/13/2017, 10:30:00 AM 4 min read

在项目中遇到的问题是,使用 webpack 动态加载的模块代码没有执行,导致页面中出现空白。 在开发者工具里能够看到 news.js 文件确实是加载成功的,但在 .init() 代码执行失败,在 console.error 打印的日志中看到 “component 未定义”。代码如下:

import(/* webpackChunkName: 'news' */ `component/news`)
  .then(component => component.init())
  // 加载失败或者 init 失败 (ps: 一个 catch 接受两种情况, 会让代码变复杂)
  .catch(console.error)

为了搞清楚 webpack 中 import 的工作过程,尝试去读 development 环境下打包后的代码,分析过程:

import() 做了什么

webpack 在打包时,会对 import() 做代码转换,根据下面转换后代码可以看到,__webpack_require__.e(chunkId) 返回 promise 对象,而业务中真实拿到的数据是 __webpack_require.bind(null, moduleId) 返回的结果。

var promise = __webpack_require_.e(0).then(__webpack_require.bind(null, 75))
promise.then(component => component.init())

__webpack_require__.e 函数辅助做什么事情

  • 创建 jsonp 请求,并且返回 promise 对象。文件只需要加载一次, 对发送请求会以 chunkId 为 key 缓存到 installedChunks 对象中,数据结构如下:
// 已加载过
installedChunks[chunkId] === 0
// 正在加载中
installedChunks[chunkId] = [resolve, reject, promise]
  • 那么在发送请求之前先确认是否存在 cache,存在就直接返回 promise 对象:
var installedChunkData = installedChunks[chunkid]
if (installedChunkDate === 0) return Promise.resolve()
  • 如果请求已经发送,但正在请求过程中, 返回同一个 promise:
    if (installedChunkData) {
      return installedChunks[chunkId][2]
    }
  • 如果是首次发送, 添加标记:
  var promise = new Promise((resolve, reject) => {
      installedChunkData = installedChunks[chunkId] = [resolve, reject]
  })
installedChunkData[2] = promise

下面到 webpackJsonp 函数

当脚本加载完成后,函数 webpackJsonp(chunkIds, moreModules, executeModules) 会自动执行,执行过程中会把所有 moreModules(它可能是 array 或 object 考虑到数组稀疏) 挂到全局 modules 上, 以备 require/import(会被 webpack 转为 __webpack_require__ 函数) 时查找。

由于 chunkId 对应脚本已经加载并且执行完,此时针对 chunkId 更新标记 installedChunks[chunkId] === 0,同时把 installedChunks[chunkId] 中存储的 promise 状态置为 fullfilled,也就是说 import() 函数返回的 promise 对象状态更新。

看__webpack_require.bind(null, moduleId)函数

__webpack_require 是相对重要的函数,负责模块本身引入其它公共模块,引入过程是:

  • 到 installedModules 缓存里找 moduleId 是否执行过, 执行过直接返回 module.exports - 毕竟一个模块仅执行一次
  • 否则到 modules 里面找 moduleId 对应的函数(ps: jsonp 函数里已挂到 modules 上), 并且准备好一份 module 对象,同时放到缓存里面:
  var module = installedModules[moduleId] = {
      i: moduleId,
      l: false, // loadedloaded
      exports: { }
  }
  • 同步执行函数 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)。module 和 module.exports 作为参数是引用传递, 此时模块内部已经向 exports 上写好方法等。
  • 把 module.l 设置为 true.
  • 返回 module.exports 对象, 即模块返回的对象。

chunkId 和 moduleId 的关系?

webpack 约定在使用 import 语法时, 需要指定 chunkName(chunkId) 和源文件模块(moduleId)对应关系如下,那么在 build 过程就很容易得到 chunkId 和 moduleId 对应关系。

import(/* webpackChunkName: "news" */ `component/news`)
  .then(component => component.init())

了解 import() 整个过程后, 很容易找到问题所在了。component 未定义 说明 chunk 文件加载完后,在执行过程中 module.exports 对象异常。

Tag:
JavaScript

@read2025, 生活在北京(北漂),程序员,宅,喜欢动漫。"年轻骑士骑马出城,不曾见过绝望堡下森森骸骨,就以为自己可以快意屠龙拯救公主。"