问题复现
油猴脚本 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(不记录)。
