动态脚本栈追踪:如何识别抛出异常的目标动态脚本?
如何让动态注入脚本的栈追踪信息明确标识来源脚本
假设我们有两个本地生成的动态脚本:
const scripts = [ "console.log(new Error().stack)", "console.log(new Error().stack)" ];
目前常用的两种运行方式:
for(let i = 0; i < scripts.length; ++i) { (new Function(scripts[i]))(); // 或 let script = document.createElement("script"); script.textContent = scripts[i]; document.body.append(script); }
在Firefox浏览器中,这两种方式生成的栈追踪信息无法区分脚本来源:
new Function():anonymous@....html line ... > Function:3:12body.append():@....html line ... > injectedScript:1:12
这导致当某一动态脚本抛出异常时,无法通过栈追踪信息定位具体是哪个脚本引发的问题。
现有一种粗糙的临时方案:通过构建嵌套Function并添加空行来区分,代码如下:
new Function(` ${"".padEnd(scriptID, "\n")} new Function("${script}"); `)
Firefox中生成的栈追踪信息为:... > Function line ${offset+scriptID} > Function:3:13,但该方案实现不够优雅,以下是几种更优的实现方式:
更优实现方案
1. 给异常附加自定义标识
直接在每个动态脚本的执行逻辑中添加异常捕获,给异常对象附加脚本ID,无需依赖栈追踪的行号hack:
function runScriptWithId(scriptCode, scriptId) { try { const wrappedCode = ` try { ${scriptCode} } catch (e) { e.scriptId = ${JSON.stringify(scriptId)}; throw e; } `; (new Function(wrappedCode))(); } catch (e) { console.error(`脚本[${e.scriptId}]抛出异常`, e); } } // 使用示例 scripts.forEach((code, idx) => runScriptWithId(code, `script-${idx}`));
这种方式定位异常更直接,异常对象会明确携带脚本标识。
2. 为动态脚本添加源映射(Source Map)
这是最规范的方案,通过生成简易的源映射,让浏览器栈追踪直接显示自定义的脚本名称:
function runScriptWithSourceMap(scriptCode, scriptName) { // 构建带源映射注释的脚本内容 const sourceMap = btoa(JSON.stringify({ version: 3, file: scriptName, sources: [scriptName], names: [], mappings: ';' // 仅做名称映射,无需精确行号对应 })); const scriptWithMap = ` ${scriptCode} //# sourceMappingURL=data:application/json;base64,${sourceMap} `; const scriptEl = document.createElement('script'); scriptEl.textContent = scriptWithMap; document.body.appendChild(scriptEl); } // 使用示例 scripts.forEach((code, idx) => runScriptWithSourceMap(code, `dynamic-script-${idx}.js`));
在Firefox中,栈追踪会直接显示自定义的脚本名称(如dynamic-script-0.js),彻底解决来源区分问题。
3. 用命名函数包装动态脚本
如果仅需要简单区分,可将动态代码包裹在命名函数中,让栈追踪显示函数名称:
function runNamedScript(scriptCode, scriptName) { // 处理脚本名称,确保符合标识符规则 const validFuncName = scriptName.replace(/[^a-zA-Z0-9_]/g, '_'); const wrappedCode = ` function ${validFuncName}() { ${scriptCode} } ${validFuncName}(); `; (new Function(wrappedCode))(); } // 使用示例 scripts.forEach((code, idx) => runNamedScript(code, `dynamic_script_${idx}`));
栈追踪信息会显示命名函数的名称(如dynamic_script_0@...),快速区分脚本来源。
内容的提问来源于stack exchange,提问作者Denis Migdal
相关产品推荐
相关产品推荐

