// webpack/lib/buildChunkGraph.js
const filterFn = (dep) => {
const depChunkGroup = dep.chunkGroup
// TODO is this needed?
if (blocksWithNestedBlocks.has(dep.block)) return true
if (areModulesAvailable(depChunkGroup, resultingAvailableModules)) {
return false // break all modules are already available
}
return true
}
// For all deps, check if chunk groups need to be connected
for (const [chunkGroup, deps] of chunkDependencies) {
if (deps.length === 0) continue
// 1. Get info from chunk group info map
const info = chunkGroupInfoMap.get(chunkGroup)
resultingAvailableModules = info.resultingAvailableModules
// 2. Foreach edge
for (let i = 0; i < deps.length; i++) {
const dep = deps[i]
// Filter inline, rather than creating a new array from `.filter()`
// TODO check if inlining filterFn makes sense here
if (!filterFn(dep)) {
continue
}
const depChunkGroup = dep.chunkGroup
const depBlock = dep.block
// 5. Connect block with chunk
GraphHelpers.connectDependenciesBlockAndChunkGroup(depBlock, depChunkGroup)
// 6. Connect chunk with parent
GraphHelpers.connectChunkGroupParentAndChild(chunkGroup, depChunkGroup)
}
}
// webpack/lib/buildChunkGraph.js
const filterFn = (dep) => {
const depChunkGroup = dep.chunkGroup
// TODO is this needed?
if (blocksWithNestedBlocks.has(dep.block)) return true
if (areModulesAvailable(depChunkGroup, resultingAvailableModules)) {
return false // break all modules are already available
}
return true
}
// For all deps, check if chunk groups need to be connected
for (const [chunkGroup, deps] of chunkDependencies) {
if (deps.length === 0) continue
// 1. Get info from chunk group info map
const info = chunkGroupInfoMap.get(chunkGroup)
resultingAvailableModules = info.resultingAvailableModules
// 2. Foreach edge
for (let i = 0; i < deps.length; i++) {
const dep = deps[i]
// Filter inline, rather than creating a new array from `.filter()`
// TODO check if inlining filterFn makes sense here
if (!filterFn(dep)) {
continue
}
const depChunkGroup = dep.chunkGroup
const depBlock = dep.block
// 5. Connect block with chunk
GraphHelpers.connectDependenciesBlockAndChunkGroup(depBlock, depChunkGroup)
// 6. Connect chunk with parent
GraphHelpers.connectChunkGroupParentAndChild(chunkGroup, depChunkGroup)
}
}
查看 webpackConfig 中配置的文件名的生成规则 chunkFilename 字段, 知道了原本应该是长度为 8 的 chunkhash 字符串变成了 undefined 导致了本次问题
问题排查
可能性1: 生成 chunkhash 的算法出了问题, 返回了 undefined
首先我们找到把 [chunkhash:8] 这个占位符替换为计算后真实值的代码处, 接着拦截一下该函数运算的所有结果, 发现没有一个文件名字符串中包含 undefined, 故排除了该可能性
其他可能性
本地复现后从错误堆栈发现是如下打包后的代码
__webpack_require__.e(10)
调用后产生的错误__webpack_require__.e
函数是 webpack 对于动态 import 函数的实现, 如上对应的打包前的代码是接着我们查看
__webpack_require__.e
函数的实现, 知道了传入的参数 10 与 53 是 chunkId, 但是不存在下面这个对象的 key 中, 所以{ 55: 'aec66236', 57: '833f5543', 59: '6b297fa3', 62: 'd7940dbd' }[chunkId]
取值是 undefined, 这就解释了为什么文件名中出现了 undefined, 原来完整的文件名是在这里拼接而成下面我们看看
{ 55: 'aec66236', 57: '833f5543', 59: '6b297fa3', 62: 'd7940dbd' }
对象的生成逻辑, 为什么漏掉了 10 这个 key?从代码可以看出 55, 57, 59, 62 这些 key 值其实是
this.getAllAsyncChunks()
函数返回的 chunk 的 id, 从函数名getAllAsyncChunks
可以知道返回值是该页面所有动态加载的 AsyncChunk, 而 chunk.id 为 10 对应的'@xxxx/captcha-prompt-mobile'
模块确实也是动态加载的, 那么是什么原因 webpack 漏掉了import('@xxxx/captcha-prompt-mobile')
继续阅读代码后发现
import('@xxxx/captcha-prompt-mobile')
是没有走到第 6 步与父 chunkGroup 连接起来 (文件之间的引用关系可以用数据结构 Graph 来描叙, 没有连接则表示后续父 chunkGroup 通过 Graph 查询不到与之的依赖关系), 所以 getAllAsyncChunks 函数不包含该 chunk。原因是被如下的 filterFn 函数给过滤了, 被过滤的原因是import('@xxxx/captcha-prompt-mobile')
对应的模块出现在了resultingAvailableModules 中, 继续查看依赖关系发现是有一个 npm 包同步import '@xxxx/captcha-prompt-mobile'
, 原来一切是因为还同时存在一个同步的 import 导致 webpack 认为它不是 AsyncChunk按理来说同步 import 与动态 import 引用这两者都引用同一个文件虽然少见, 但也不应该造成 bug 。难道是该项目是多页应用还有其他未知的秘密?于是我们删除了其他页面, 仅留当前页面进行测试
最后发现只留下一个页面是正常的, 与多个页面打包后的代码不同的是
__webpack_require__.e(10)
变成了Promise.resolve(/* import() */)
于是查看这块的 blockPromise 函数实现发现是第一个 if 条件为 true, 所以返回的是
Promise.resolve(/* import() */)
。 此时 block.chunkGroup 为 undefined, 这也对应了上面说的是被 filterFn 函数过滤了, 所以没有走到为 block.chunkGroup 赋值的代码处接下来我们需要排查为啥多页时返回的是
__webpack_require__.e
, 按照上面的分析多页的 block.chunkGroup 应该也为 undefined 才对。再次回到 buildChunkGraph 文件代码, 发现是import('@xxxx/captcha-prompt-mobile')
对应的模块被另一个页面仅动态 import 引用了, 所以如下的 for 循环有一次走到了第 6 步连接上了这个页面对应的 chunkGroup, 而第 5 就给该 block 设置了 chunkGroup, 即多页时 block.chunkGroup 是存在的, 所以上面的 blockPromise 函数返回的是__webpack_require__.e
问题解决
把当前 webpack 版本 v4.39.0 升级到小版本最新的 v4.46.0 发现问题已经不存在, 于是查看 Release 日志找到是 v4.44.2 版本进行的修复