Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

n.e is not a function 问题记录 #43

Open
xiaoxiaojx opened this issue Sep 28, 2022 · 0 comments
Open

n.e is not a function 问题记录 #43

xiaoxiaojx opened this issue Sep 28, 2022 · 0 comments
Labels
DEBUG debug webpack A bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting a

Comments

@xiaoxiaojx
Copy link
Owner

xiaoxiaojx commented Sep 28, 2022

image

问题简述

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
  },
},
@xiaoxiaojx xiaoxiaojx added webpack A bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting a DEBUG debug labels Sep 28, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
DEBUG debug webpack A bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting a
Projects
None yet
Development

No branches or pull requests

1 participant