V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
kaiduo
V2EX  ›  Node.js

n.e is not a function 问题记录

  •  
  •   kaiduo · 2022-10-06 01:02:10 +08:00 · 3857 次点击
    这是一个创建于 772 天前的主题,其中的信息可能已经有所发展或是发生改变。
    image

    🔗 原文链接: https://github.com/xiaoxiaojx/blog/issues/43

    问题简述

    TypeError: n.e is not a function 
    

    a 同学说我写的 npm 包 @xxfe/pkg 在 x 项目使用时发布到测试环境报了如上的错误, 但是开发环境没有报错。接着我看了一下 node_modules 中这个包的代码, 这个 Promise 都没 await 咋会被 catch 且还真的被捕获打印了错误日志 🤯, 这是什么瞎猫碰见死耗子的操作 ...

    // node_modules/@xxfe/pkg
    
    try {
    	this.aesUtilPromise = import('../aes-util')
    } catch (e) {
    	console.info('123  ========', e);
    }
    

    仅看 node_modules 中的代码并未发现明显的错误, 其实我们应该看的是 @xxfe/pkg 打包后的代码

    // dist/static/js/xxx.js
    
    try {
    	this.aesUtilPromise=n.e(3)
    } catch(r) {
    	console.info("123  ========",r)
    }
    

    打包后的代码就发现了错误的源头 n.e

    熟悉 webpack 的同学就知道动态 import 函数打包后会被 webpack_require.e 函数给替换, 其原理就是通过动态创建一个 script 标签来加载一个 js, 如下即函数的代码

    /******/  __webpack_require__.e = function requireEnsure(chunkId) {
    /******/   var promises = [];
    /******/
    /******/
    /******/   // JSONP chunk loading for javascript
    /******/
    /******/   var installedChunkData = installedChunks[chunkId];
    /******/   if(installedChunkData !== 0) { // 0 means "already installed".
    /******/
    /******/    // a Promise means "currently loading".
    /******/    if(installedChunkData) {
    /******/     promises.push(installedChunkData[2]);
    /******/    } else {
    /******/     // setup Promise in chunk cache
    /******/     var promise = new Promise(function(resolve, reject) {
    /******/      installedChunkData = installedChunks[chunkId] = [resolve, reject];
    /******/     });
    /******/     promises.push(installedChunkData[2] = promise);
    /******/
    /******/     // start chunk loading
    /******/     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";
    /******/     }
    /******/     // create error before stack unwound to get useful stacktrace later
    /******/     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 函数

    // webpack/lib/MainTemplate.js
    
    this.hooks.requireExtensions.tap("MainTemplate", (source, chunk, hash) => {
    			const buf = [];
    			const chunkMaps = chunk.getChunkMaps();
    			// Check if there are non initial chunks which need to be imported using require-ensure
    			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 集合不为空即可

    // webpack/lib/Chunk.js
    
    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 的前置运行。

    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

    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 函数

    • webpack 版本: 4.39.0

    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 函数的实现

    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
      },
    },
    
    2 条回复    2022-10-09 00:01:08 +08:00
    me404
        1
    me404  
       2022-10-06 08:58:51 +08:00 via iPhone
    厉害
    kaiduo
        2
    kaiduo  
    OP
       2022-10-09 00:01:08 +08:00
    @me404 基操,勿 6 😄
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2623 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 10:49 · PVG 18:49 · LAX 02:49 · JFK 05:49
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.