前端監(jiān)控上報:Script Error問題的解決方法
前言
在微前端和多國業(yè)務的場景下,我們經(jīng)常會遇到 HTML 頁面域名和靜態(tài)資源域名不統(tǒng)一的情況。這種架構雖然帶來了部署和 CDN 優(yōu)化的便利,但也引入了一個常見的問題:跨域 Script Error。本文將詳細介紹這個問題的原因、影響,以及一套完整的解決方案。
問題背景
業(yè)務場景
我們的項目是一個多國業(yè)務的前端應用,采用了以下架構:
- HTML 頁面域名:每個地區(qū)/業(yè)務線使用不同主域名
- 例如:
countryA-web.example.com,countryB-web.example.com, ...
- 例如:
- 靜態(tài)資源 CDN 域名:統(tǒng)一使用一個公共 CDN 地址
- 例如:
cdn.example.com
問題表現(xiàn)
在這種架構下,我們遇到了以下問題:
監(jiān)控上報的 JS 錯誤全部顯示為 "Script Error"
- 無法獲取詳細的錯誤堆棧信息
- 無法定位具體的錯誤位置
- SourceMap 無法正確映射
錯誤信息丟失
- 錯誤消息被瀏覽器隱藏
- 無法獲取錯誤發(fā)生的文件、行號、列號
- 調(diào)試和問題排查變得困難
問題原因分析
1. 瀏覽器的同源策略
當腳本從不同源加載時,瀏覽器會應用同源策略(Same-Origin Policy)的安全機制:
- 如果腳本發(fā)生錯誤,且腳本的源與頁面不同,瀏覽器會隱藏錯誤的詳細信息
- 只返回通用的 "Script Error" 消息
- 這是為了防止惡意網(wǎng)站通過錯誤信息獲取敏感數(shù)據(jù)
2. 缺少 CORS 配置
要獲取跨域腳本的詳細錯誤信息,需要滿足兩個條件:
- 腳本標簽添加
crossorigin屬性 - 服務器返回正確的 CORS 響應頭
當你在 <script> 或 <link> 標簽上添加 crossorigin="anonymous",實際上是在告訴瀏覽器:“允許以跨源模式拉取這個資源,并且無須附帶任何憑證(cookie、鑒權頭等) 。”
如果該標簽沒有 crossorigin 屬性,瀏覽器以默認的“no-cors”模式加載資源,只要腳本跨域,一旦腳本發(fā)生錯誤,錯誤信息就會被隱藏,只能看到 Script Error,同時 sourceMap 也無法正確還原堆棧。
只有當 crossorigin 屬性為 anonymous 并且服務器響應了正確的 CORS 頭,瀏覽器才會呈現(xiàn)完整的報錯信息和堆棧文件行號,對異常監(jiān)控和調(diào)試至關重要。
與此同時,服務器/CDN 響應也必須包含正確的 CORS 相關頭部,如 Access-Control-Allow-Origin。
這是因為瀏覽器在跨域加載資源時,會先根據(jù)標簽屬性判斷是否允許訪問細節(jié),再檢查服務器是不是“回應放行”了該跨域請求。如果服務器未設置這些頭部,哪怕你加了 crossorigin 屬性,瀏覽器也會隱藏資源細節(jié)和所有報錯內(nèi)容,僅顯示 Script Error。
如果缺少其中任何一個,瀏覽器都會隱藏錯誤詳情。
3. Webpack/Rspack 動態(tài)加載機制
現(xiàn)代前端構建工具(Webpack、Rspack)在實現(xiàn)代碼分割和懶加載時,會通過 document.createElement('script') 動態(tài)創(chuàng)建 script 標簽來加載 chunk。如果這些動態(tài)創(chuàng)建的標簽沒有 crossorigin 屬性,同樣會導致 Script Error。
解決方案
我們采用了一套三層防護的解決方案:
方案架構圖
解決方案架構
1. HTML 模板層
手動添加 crossorigin 屬性
2. 構建時處理層
Webpack 插件自動添加
3. 運行時攔截層
攔截 createElement 全局處理
HTML層:模板引用資源手動修改
對于 HTML 模板中直接引用的靜態(tài)資源,我們手動添加 crossorigin="anonymous" 屬性:
<!-- 所有跨域的 script 標簽 --> <script src="https://cdn.example.com/static/polyfill.min.js" defer crossorigin="anonymous" ></script> <!-- 所有跨域的 link 標簽(CSS) --> <link rel="stylesheet" rel="external nofollow" crossorigin="anonymous" />
打包工具層:Webpack/Rspack 插件自動處理(構建時)
對于構建工具自動插入的腳本和樣式,我們創(chuàng)建了一個自定義插件:
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);
}
異步加載層:運行時全局攔截(動態(tài)加載)
在業(yè)務開發(fā)中我們會遇到許多異步動態(tài)加載的腳本文件,通過攔截 document.createElement,為所有動態(tài) script/link 節(jié)點設置跨域屬性,無死角覆蓋 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;
};
}
在應用啟動處調(diào)用初始化:
為什么這個方案可以覆蓋所有場景?
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,我們可以捕獲所有通過此方式創(chuàng)建的 script 標簽,包括:
- React.lazy 懶加載的組件
- 動態(tài) import() 加載的模塊
- Webpack chunk 動態(tài)加載
- 第三方庫的動態(tài)加載
- 手動通過
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層:手動為靜態(tài)資源添加
crossorigin屬性 - 打包工具層:Webpack 插件自動處理構建時插入的資源
- 異步加載層:全局攔截
document.createElement,處理所有動態(tài)加載
以上就是前端監(jiān)控上報:Script Error問題的解決方法的詳細內(nèi)容,更多關于前端監(jiān)控上報:Script Error的資料請關注腳本之家其它相關文章!
相關文章
JavaWeb表單及時驗證功能在輸入后立即驗證(含用戶類型,性別,愛好...的驗證)
最近做項目遇到表單在輸入后立即驗證,而不是提交后再驗證,在網(wǎng)上找了下資料,沒有合適的,于是我自己動手寫了一個,分享到腳本之家平臺,供大家參考2017-06-06
JavaScript實現(xiàn)列出數(shù)組中最長的連續(xù)數(shù)
這篇文章主要介紹了JavaScript實現(xiàn)列出數(shù)組中最長的連續(xù)數(shù)的方法及使用,需要的朋友可以參考下2014-12-12
詳解JavaScript中的數(shù)組合并方法和對象合并方法
這篇文章主要介紹了JavaScript中的數(shù)組合并方法和對象合并方法,通過代碼的形式給大家介紹的非常詳細,需要的朋友可以參考下2018-05-05
微信小程序實現(xiàn)自定義picker選擇器彈窗內(nèi)容
這篇文章主要為大家詳細介紹了微信小程序實現(xiàn)自定義picker選擇器彈窗內(nèi)容,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-07-07
BootStrap Validator 版本差異問題導致的submitHandler失效問題的解決方法
這篇文章主要介紹了BootStrap Validator 版本差異問題導致的submitHandler失效問題的解決方法,下面通過本文給大家詳細說明一下,需要的朋友可以參考下2016-12-12

