问题简述 1 TypeError : n.e is not a function
a 同学说我写的 npm 包 @xxfe/pkg 在 x 项目使用时发布到测试环境报了如上的错误, 但是开发环境没有报错。接着我看了一下 node_modules 中这个包的代码, 这个 Promise 都没 await 咋会被 catch 且还真的被捕获打印了错误日志 🤯, 这是什么瞎猫碰见死耗子的操作 … 1 2 3 4 5 6 7 try { this .aesUtilPromise = import ('../aes-util' ) } catch (e) { console .info ('123 ========' , e); }
仅看 node_modules 中的代码并未发现明显的错误, 其实我们应该看的是 @xxfe/pkg 打包后的代码 1 2 3 4 5 6 7 try { this .aesUtilPromise =n.e (3 ) } catch (r) { console .info ("123 ========" ,r) }
打包后的代码就发现了错误的源头 n.e
熟悉 webpack 的同学就知道动态 import 函数打包后会被 webpack_require .e 函数给替换, 其原理就是通过动态创建一个 script 标签来加载一个 js, 如下即函数的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 __webpack_require__.e = function requireEnsure (chunkId ) { var promises = []; var installedChunkData = installedChunks[chunkId]; if (installedChunkData !== 0 ) { if (installedChunkData) { promises.push (installedChunkData[2 ]); } else { var promise = new Promise (function (resolve, reject ) { installedChunkData = installedChunks[chunkId] = [resolve, reject]; }); promises.push (installedChunkData[2 ] = promise); var script = document .createElement ('script' ); var onScriptComplete; script.charset = 'utf-8' ; script.timeout = 120 ; if (__webpack_require__.nc ) { script.setAttribute ("nonce" , __webpack_require__.nc ); } script.src = jsonpScriptSrc (chunkId); if (script.src .indexOf (window .location .origin + '/' ) !== 0 ) { script.crossOrigin = "anonymous" ; } var error = new Error (); onScriptComplete = function (event ) { }; var timeout = setTimeout (function ( ){ onScriptComplete ({ type : 'timeout' , target : script }); }, 120000 ); script.onerror = script.onload = onScriptComplete; document .head .appendChild (script); } } return Promise .all (promises); };
问题排查 那么为什么代码中用到了 import 函数, webpack 却没有注入 webpack_require .e 函数的实现了 ?
此时我们只能看 webpack 的代码实现, 可以发现当 Object.keys(chunkMaps.hash).length 条件为 true 时, 才会注入 ${this.requireFn}.e 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 this .hooks .requireExtensions .tap ("MainTemplate" , (source, chunk, hash ) => { const buf = []; const chunkMaps = chunk.getChunkMaps (); if (Object .keys (chunkMaps.hash ).length ) { buf.push ("// This file contains only the entry chunk." ); buf.push ("// The chunk loading function for additional chunks" ); buf.push (`${this .requireFn} .e = function requireEnsure(chunkId) {` ); buf.push (Template .indent ("var promises = [];" )); buf.push ( Template .indent ( this .hooks .requireEnsure .call ("" , chunk, hash, "chunkId" ) ) ); buf.push (Template .indent ("return Promise.all(promises);" )); buf.push ("};" ); } }
顺着函数调用顺序发现关键是 getAllAsyncChunks 函数返回值 chunks 集合不为空即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 getAllAsyncChunks ( ) { const queue = new Set (); const chunks = new Set (); const initialChunks = intersect ( Array .from (this .groupsIterable , g => new Set (g.chunks )) ); for (const chunkGroup of this .groupsIterable ) { for (const child of chunkGroup.childrenIterable ) { queue.add (child); } } for (const chunkGroup of queue) { for (const chunk of chunkGroup.chunks ) { if (!initialChunks.has (chunk)) { chunks.add (chunk); } } for (const child of chunkGroup.childrenIterable ) { queue.add (child); } } return chunks; }
chunks 集合只有一处往集合增加数据的逻辑。initialChunks 可以理解为首屏 html 中 script 标签的 js, 通常是 main.js 及其运行前依赖的 js, 比如 main.js 需要依赖 react, react-dom 等 js 的前置运行。
1 2 3 if (!initialChunks.has (chunk)) { chunks.add (chunk); }
因为业务项目 webpackConfig.optimizationa.splitChunks 的配置把 a 同学写的 @xxfe/pkg 包都打入到了 xxfe_vendor 文件中, 而 @xxfe/ scope 下的依赖被业务项目大量使用, 所以无疑是业务项目 main.js 前置依赖的一个 js, 故 xxfe_vendor 在首屏 html 中 script 标签的 js 中
所以 xxfe_vendor 是 initialChunks 中的其中一个, 故此处 if 为 false
1 2 3 4 5 6 7 8 9 10 11 12 13 xxfe_vendor : { name : 'xxfe_vendor' , chunks : 'all' , priority : 8 , enforce : true , test : (module ) => { const resource = getModulePath (module ) if (/@xxfe(\\|\/)/ .test (resource)) { return true } return false }, },
至此我们理清了导致问题的原因, @xxfe/pkg 中的 import 函数引用的 js 及其自身代码由于分包的 splitChunks 设置都被打入到了 xxfe_vendor 中, 而 xxfe_vendor 又是业务项目 main.js 前置运行依赖的 js, 故 webpack 错误的认为你不需要 webpack_require .e 函数
Q: 这个算谁的 bug ?
A: webpack 的 bug。因为不能说用户需要加载的 js 如果在首屏其中之一, 就不注入 webpack_require .e 函数的实现。业务项目通过 splitChunks 进行分包不是 npm 包的作者所能决定, 是否注入 webpack_require .e 函数的实现应该由是否有 import 函数语法来决定。
Q: 为什么开发环境没有报错 ?
A: 业务项目的 splitChunks 设置了只在生产构建生效
问题解决 将如下的 chunks 字段由 all 改为了 initial, 表示该分包设置将不要影响到动态 import 函数异步加载的 js (该类型为 async chunks), 使得 @xxfe/pkg 将不会被合入 xxfe_vendor 文件中, 那么如上的 initialChunks 也将不包含 @xxfe/pkg, 从而 webpack 也会如约注入 webpack_require .e 函数的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 xxfe_vendor : { name : 'xxfe_vendor' , chunks : 'initial' , priority : 8 , enforce : true , test : (module ) => { const resource = getModulePath (module ) if (/@xxfe(\\|\/)/ .test (resource)) { return true } return false }, },