油猴脚本获取 webpack_require:webpackChunk.push 注入比 Hook call 更稳

为什么 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);
};

这个方法有两个核心问题:

  1. webpack 模块函数通常没有名字this.name''anonymous,而不是模块 ID,所以 this.name == '84686' 几乎永远不成立。

  2. 性能灾难:现代前端框架(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 注入即可。