为什么 Hook eval 会让 webpack 报 webpack_require is not defined

问题复现

油猴脚本 hook eval 后报错:

ReferenceError: __webpack_require__ is not defined
    at eval (service-mark.js:1:1)
    at eval (<anonymous>)
    at unsafeWindow.eval (my-script.user.js:28:24)
    at ./javascript/common/service-mark.js (task-common.js?61:240:1)

hook 写法:

const oldEval = window.eval;

window.eval = function (code) {
    console.log(code);
    return oldEval.apply(this, args);  // ← 问题在这里
};

根本原因:直接调用 vs 间接调用

JS 中只有一种 direct eval(直接调用)

eval(code)

字面上写 eval(...) 才是直接调用,执行时能访问当前词法作用域。

其他所有形式都是 indirect eval(间接调用),在全局作用域执行:

const e = eval;
e(code);               // indirect

eval.call(null, code); // indirect
eval.apply(this, [code]); // indirect
(0, eval)(code);       // indirect(常见技巧)

webpack 模块代码是在模块工厂函数的作用域里 eval 执行的:

function(module, exports, __webpack_require__) {
    eval("代码内容...");   // direct eval,能访问 __webpack_require__
}

当你 hook 了 window.eval 并用 oldEval.apply(...) 转发时:

window.eval = function(code) {
    return oldEval.apply(this, args);  // indirect eval
    //          ↑ 变成了间接调用,在全局作用域执行
};

__webpack_require__ 是工厂函数的局部变量,全局作用域里不存在,所以报错。

为什么 return eval(code) 也不行

即使改成字面调用:

unsafeWindow.eval = function(code) {
    return eval(code);  // direct eval
};

这里的 eval(code) 是在你的 hook 函数作用域里执行的,不是在 webpack 工厂函数里。作用域仍然不对。

结论:无法通过 hook eval 的方式同时拦截代码和保持 webpack 的模块作用域。 这是 JS 引擎层面的限制,不是 hook 写法问题。

正确方案:hook webpackChunk.push

如果你需要修改 webpack 模块的源码(比如删掉 debugger),正确方式是在模块执行前 hook 模块定义,而不是 hook eval:

const chunkKey = Object.keys(window).find(k => k.startsWith('webpackChunk'));

const oldPush = window[chunkKey].push;

window[chunkKey].push = function(chunk) {
    const modules = chunk[1];

    for (const id in modules) {
        const fn = modules[id];

        modules[id] = function(module, exports, __webpack_require__) {
            // 在这里可以拿到模块源码字符串
            let code = fn.toString();

            // 修改源码(例如移除 debugger)
            code = code.replace(/\bdebugger\b/g, '');

            // 重建函数并在正确的参数上下文里执行
            const newFn = eval('(' + code + ')');

            return newFn(module, exports, __webpack_require__);
        };
    }

    return oldPush.call(this, chunk);
};

这个方法的优势:

  • 在模块函数还没执行之前修改,不破坏作用域
  • __webpack_require__ 仍然由 webpack 正常传入
  • 可以针对特定模块 ID 修改,不影响其他模块

只是想看 eval 执行了什么代码

如果目的只是监听而不是转发执行,可以用 Object.defineProperty 劫持赋值时机:

let _eval = window.eval;

Object.defineProperty(window, 'eval', {
    get() { return _eval; },
    set(v) {
        console.log('eval 被替换', v);
        _eval = v;
    },
    configurable: true
});

或者用 Proxy 记录参数但不干预执行:

const rawEval = window.eval;

// 只记录,不修改执行上下文
const logFn = function(code) {
    console.log('[eval]', typeof code === 'string' ? code.slice(0, 200) : code);
};

// 在外层包一层,直接调用原始 eval
window.__logEval = function(code) {
    logFn(code);
    return rawEval.call(this, code);
};

但真正的”无损 hook eval”做不到,只能选择:记录参数(丢失 webpack 作用域)或不 hook(不记录)。