为什么 Hook Function.prototype.call 不好用
一种常见思路是拦截 webpack 模块执行时的 call:
const oldCall = Function.prototype.call;
Function.prototype.call = function (...args) {
if (this.name == '84686') {
webpackRequire = args[3];
}
return oldCall.apply(this, args);
};
这个方法有两个核心问题:
-
webpack 模块函数通常没有名字,
this.name是''或anonymous,而不是模块 ID,所以this.name == '84686'几乎永远不成立。 -
性能灾难:现代前端框架(React、Vue、Webpack Runtime)每秒调用
call数以万次,全部经过你的 hook,页面直接卡死。
另外,如果 webpack 已经执行完才开始 hook,Function.prototype.call 根本拿不到任何东西——模块早就加载完了。
正确方案:向 webpackChunk 注入入口模块
webpack 5 会在 window 上挂一个全局数组,名字通常是 webpackChunk 加项目名前缀。向这个数组 push 一个特殊的 chunk,可以在 runtime 函数里拿到 __webpack_require__:
// 找到 webpackChunk 数组
const chunkKey = Object.keys(window).find(k => k.startsWith('webpackChunk'));
if (chunkKey) {
let webpackRequire;
window[chunkKey].push([
[Symbol()], // chunk ID(用 Symbol 避免冲突)
{}, // 模块定义(空)
function (require) {
webpackRequire = require; // 在这里拿到 __webpack_require__
}
]);
console.log(webpackRequire);
// 现在可以用 webpackRequire(模块ID) 访问任意模块
}
这个方法有效的原因:即使 webpack 已经执行完,也可以通过这种方式从现有的 chunk 数组里拿到 __webpack_require__,因为 webpack 会立即处理新 push 进来的 chunk。
在 webpack 执行前 Hook(如果需要监听所有模块)
如果你的目标是在每个模块加载时做点什么(比如修改特定模块的导出),需要在 webpack 执行之前 hook push 方法:
const chunkKey = Object.keys(window).find(k => k.startsWith('webpackChunk'));
const oldPush = window[chunkKey].push;
window[chunkKey].push = function (...args) {
const runtime = args[0][2];
// 拦截 runtime 函数
if (typeof runtime === 'function') {
args[0][2] = function (__webpack_require__) {
// 保存引用
window._webpackRequire = __webpack_require__;
return runtime.apply(this, arguments);
};
}
return oldPush.apply(this, args);
};
用 webpack_require 访问模块
拿到 __webpack_require__ 后,可以直接访问任意 webpack 模块:
// 按模块 ID 获取模块导出
const lodash = webpackRequire(96486);
const mtopModule = webpackRequire(82122);
console.log(lodash);
console.log(mtopModule.default);
模块 ID 可以在浏览器 Sources 面板里搜索特征字符串定位,也可以遍历 webpackRequire.m(模块注册表)查找:
Object.keys(webpackRequire.m).forEach(id => {
const src = webpackRequire.m[id].toString();
if (src.includes('targetFunction')) {
console.log('found at module id:', id);
}
});
油猴脚本注意:@run-at 时机
在 Tampermonkey 里使用时,需要根据目的选择合适的 @run-at:
| 时机 | 说明 |
|---|---|
document-start | 最早,适合在 webpack 执行前 hook push |
document-end | 适合页面加载完后注入 chunk 拿引用 |
如果 webpack 在 DOMContentLoaded 之前执行完,用 document-start + push hook;如果只是要访问模块,用 document-end + push 注入即可。
