前端监控上报:Script Error问题的解决方法
作者:米花丶
前言
在微前端和多国业务的场景下,我们经常会遇到 HTML 页面域名和静态资源域名不统一的情况。这种架构虽然带来了部署和 CDN 优化的便利,但也引入了一个常见的问题:跨域 Script Error。本文将详细介绍这个问题的原因、影响,以及一套完整的解决方案。
问题背景
业务场景
我们的项目是一个多国业务的前端应用,采用了以下架构:
- HTML 页面域名:每个地区/业务线使用不同主域名
- 例如:
countryA-web.example.com,countryB-web.example.com, ...
- 例如:
- 静态资源 CDN 域名:统一使用一个公共 CDN 地址
- 例如:
cdn.example.com
问题表现
在这种架构下,我们遇到了以下问题:
监控上报的 JS 错误全部显示为 "Script Error"
- 无法获取详细的错误堆栈信息
- 无法定位具体的错误位置
- SourceMap 无法正确映射
错误信息丢失
- 错误消息被浏览器隐藏
- 无法获取错误发生的文件、行号、列号
- 调试和问题排查变得困难
问题原因分析
1. 浏览器的同源策略
当脚本从不同源加载时,浏览器会应用同源策略(Same-Origin Policy)的安全机制:
- 如果脚本发生错误,且脚本的源与页面不同,浏览器会隐藏错误的详细信息
- 只返回通用的 "Script Error" 消息
- 这是为了防止恶意网站通过错误信息获取敏感数据
2. 缺少 CORS 配置
要获取跨域脚本的详细错误信息,需要满足两个条件:
- 脚本标签添加
crossorigin属性 - 服务器返回正确的 CORS 响应头
当你在 <script> 或 <link> 标签上添加 crossorigin="anonymous",实际上是在告诉浏览器:“允许以跨源模式拉取这个资源,并且无须附带任何凭证(cookie、鉴权头等) 。”
如果该标签没有 crossorigin 属性,浏览器以默认的“no-cors”模式加载资源,只要脚本跨域,一旦脚本发生错误,错误信息就会被隐藏,只能看到 Script Error,同时 sourceMap 也无法正确还原堆栈。
只有当 crossorigin 属性为 anonymous 并且服务器响应了正确的 CORS 头,浏览器才会呈现完整的报错信息和堆栈文件行号,对异常监控和调试至关重要。
与此同时,服务器/CDN 响应也必须包含正确的 CORS 相关头部,如 Access-Control-Allow-Origin。
这是因为浏览器在跨域加载资源时,会先根据标签属性判断是否允许访问细节,再检查服务器是不是“回应放行”了该跨域请求。如果服务器未设置这些头部,哪怕你加了 crossorigin 属性,浏览器也会隐藏资源细节和所有报错内容,仅显示 Script Error。
如果缺少其中任何一个,浏览器都会隐藏错误详情。
3. Webpack/Rspack 动态加载机制
现代前端构建工具(Webpack、Rspack)在实现代码分割和懒加载时,会通过 document.createElement('script') 动态创建 script 标签来加载 chunk。如果这些动态创建的标签没有 crossorigin 属性,同样会导致 Script Error。
解决方案
我们采用了一套三层防护的解决方案:
方案架构图
解决方案架构
1. HTML 模板层
手动添加 crossorigin 属性
2. 构建时处理层
Webpack 插件自动添加
3. 运行时拦截层
拦截 createElement 全局处理
HTML层:模板引用资源手动修改
对于 HTML 模板中直接引用的静态资源,我们手动添加 crossorigin="anonymous" 属性:
<!-- 所有跨域的 script 标签 --> <script src="https://cdn.example.com/static/polyfill.min.js" defer crossorigin="anonymous" ></script> <!-- 所有跨域的 link 标签(CSS) --> <link rel="stylesheet" href="https://cdn.example.com/assets/iconfont.css" rel="external nofollow" crossorigin="anonymous" />
打包工具层:Webpack/Rspack 插件自动处理(构建时)
对于构建工具自动插入的脚本和样式,我们创建了一个自定义插件:
class CrossOriginAssetsPlugin {
apply(compiler) {
const pluginName = 'CrossOriginAssetsPlugin';
compiler.hooks.compilation.tap(pluginName, (compilation) => {
const HtmlWebpackPlugin = require('html-webpack-plugin');
if (HtmlWebpackPlugin && HtmlWebpackPlugin.getHooks) {
HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tap(
pluginName,
data => {
// 处理 script
data.assetTags.scripts.forEach(tag => {
if (isCrossOrigin(tag.attributes?.src)) {
tag.attributes.crossorigin = 'anonymous';
}
});
// 处理 link
data.assetTags.styles.forEach(tag => {
if (isCrossOrigin(tag.attributes?.href)) {
tag.attributes.crossorigin = 'anonymous';
}
});
return data;
}
);
}
});
}
}
function isCrossOrigin(url) {
// 补充判断逻辑:绝对路径跨域、相对路径同源
return url && /^https?:///.test(url);
}
异步加载层:运行时全局拦截(动态加载)
在业务开发中我们会遇到许多异步动态加载的脚本文件,通过拦截 document.createElement,为所有动态 script/link 节点设置跨域属性,无死角覆盖 chunk、第三方库等异步加载场景。
function isCrossOriginUrl(url: string | null | undefined): boolean {
return !!url && /^https?:///.test(url);
}
export function setScriptCrossOrigin(script: HTMLScriptElement) {
const src = script.src || script.getAttribute('src');
if (isCrossOriginUrl(src)) script.crossOrigin = 'anonymous';
}
export function setLinkCrossOrigin(link: HTMLLinkElement) {
const href = link.href || link.getAttribute('href');
if (isCrossOriginUrl(href)) link.crossOrigin = 'anonymous';
}
export function initCrossOriginScriptHandler() {
const originCreateElement = document.createElement.bind(document);
document.createElement = function (tagName: string, options?: ElementCreationOptions) {
const el = originCreateElement(tagName, options);
if (tagName.toLowerCase() === 'script') {
// 当 src 插入时才设置
const observer = new MutationObserver(mutations => {
mutations.forEach(m => {
if (m.type === 'attributes' && m.attributeName === 'src') {
setScriptCrossOrigin(el as HTMLScriptElement);
observer.disconnect();
}
});
});
observer.observe(el, { attributes: true, attributeFilter: ['src'] });
}
if (tagName.toLowerCase() === 'link') {
const observer = new MutationObserver(mutations => {
mutations.forEach(m => {
if (m.type === 'attributes' && m.attributeName === 'href') {
setLinkCrossOrigin(el as HTMLLinkElement);
observer.disconnect();
}
});
});
observer.observe(el, { attributes: true, attributeFilter: ['href'] });
}
return el;
};
}
在应用启动处调用初始化:
为什么这个方案可以覆盖所有场景?
Webpack/Rspack 在运行时加载 chunk 时,会生成类似这样的代码:
// Webpack 生成的 chunk 加载代码
function loadChunk(chunkId) {
return new Promise((resolve, reject) => {
const script = document.createElement('script'); // ← 关键
script.src = chunkUrl;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
通过拦截 document.createElement,我们可以捕获所有通过此方式创建的 script 标签,包括:
- React.lazy 懒加载的组件
- 动态 import() 加载的模块
- Webpack chunk 动态加载
- 第三方库的动态加载
- 手动通过
loadScript()加载的脚本
服务器端配置
后端需确保 CDN/stastic 资源的 HTTP 响应头允许跨源:
Nginx 配置样例(脱敏)
location /static/ {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Range' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length, Content-Range' always;
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
}
总结
通过这套三层防护的解决方案,我们成功解决了多域名架构下的跨域 Script Error 问题:
- HTML层:手动为静态资源添加
crossorigin属性 - 打包工具层:Webpack 插件自动处理构建时插入的资源
- 异步加载层:全局拦截
document.createElement,处理所有动态加载
以上就是前端监控上报:Script Error问题的解决方法的详细内容,更多关于前端监控上报:Script Error的资料请关注脚本之家其它相关文章!
