javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > 前端监控上报:Script Error

前端监控上报:Script Error问题的解决方法

作者:米花丶

在微前端和多国业务的场景下,我们经常会遇到HTML页面域名和静态资源域名不统一的情况,这种架构虽然带来了部署和CDN优化的便利,但也引入了一个常见的问题,跨域Script Error,本文将详细介绍这个问题的原因、影响,以及一套完整的解决方案,需要的朋友可以参考下

前言

在微前端和多国业务的场景下,我们经常会遇到 HTML 页面域名和静态资源域名不统一的情况。这种架构虽然带来了部署和 CDN 优化的便利,但也引入了一个常见的问题:跨域 Script Error。本文将详细介绍这个问题的原因、影响,以及一套完整的解决方案。

问题背景

业务场景

我们的项目是一个多国业务的前端应用,采用了以下架构:

问题表现

在这种架构下,我们遇到了以下问题:

监控上报的 JS 错误全部显示为 "Script Error"

错误信息丢失

问题原因分析

1. 浏览器的同源策略

当脚本从不同源加载时,浏览器会应用同源策略(Same-Origin Policy)的安全机制:

2. 缺少 CORS 配置

要获取跨域脚本的详细错误信息,需要满足两个条件:

  1. 脚本标签添加 crossorigin 属性 
  2. 服务器返回正确的 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 标签,包括:

服务器端配置

后端需确保 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 问题:

  1. HTML层:手动为静态资源添加 crossorigin 属性
  2. 打包工具层:Webpack 插件自动处理构建时插入的资源
  3. 异步加载层:全局拦截 document.createElement,处理所有动态加载

以上就是前端监控上报:Script Error问题的解决方法的详细内容,更多关于前端监控上报:Script Error的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文