1. 背景知识:script 标签有哪些属性
-
src
: 脚本来源地址 -
type
: 表示所代表的脚本类型,有如下取值:- 未设置(默认)/一个空字符串/一个 JavaScript MIME 类型:普通脚本/传统脚本
module
:模块脚本- importmap:代表元素体内包含导入映射(importmap)表
- 任何其他值:所嵌入的内容被视为一个数据块,不会被浏览器处理
-
integrity
:包含用户代理可用于验证所获取到资源的完整性的内联元数据 -
crossorigin
:见下文详解 -
其他属性,比如
async
defer
等,与本文关系不大,详见MDN
2. crossorigin 属性的作用
1. 【html 标准】文档中的解释
对于普通脚本,它控制是否公开错误信息。对于模块脚本,它控制用于跨域请求的凭据模式。(原文见此)
2. 个人总结
默认情况下,可以从任意位置加载 js 。
如果配置了该属性,会让浏览器启用CORS检查(判断响应头中Access-Control-Allow-Origin
是否与当前文档同源),如果检查不通过,则浏览器拒绝执行代码。
什么情况下要启用检查?
- 需要捕获跨域脚本中的错误信息
type="module"
并且需要发送凭据
只验证了 cookie,其他认证信息不知道怎么测试
- 需要校验跨域脚本的完整性(配置了
integrity
的时候) - 文档中的脚本(并不单单指跨域脚本)需要使用 跨域隔离的 API(比如SharedArrayBuffer,原因见此)
3. 不设置 crossorigin
属性时的表现
不跨站就会发 cookie;跨站的话,只有满足
SameSite=None; Secure=true
的 cookie 才会发送
-
如果设置了
type="module"
,则一定不发送 cookie -
收到响应后,浏览器会正常执行脚本代码
-
全局错误捕获方面
-
页面同源的脚本出错:
- promise 异常:可以捕获✅
- 其他普通异常:可以捕获✅
-
跨域脚本出错:
- promise 异常:无法捕获❌
- 其他普通异常:只能捕获到
Script error.
的错误,没有详细信息
-
4. 设置了 crossorigin
属性时的表现
crossorigin 有 2 个值:use-credentials
和anonymous
。
只有指定use-credentials
时才表现为use-credentials
,其他任何不合法的值都视为 anonymous
1. anonymous
-
不管有没有设置
type="module"
,获取资源时,都不会发送 cookie -
收到响应后,只有满足 响应头中
Access-Control-Allow-Origin
=== 请求头中的origin
字段,浏览器才会执行脚本代码,否则会抛出 CORS 异常
-
如果设置了
integrity
,就还会再校验一次资源完整性,不满足也会报错
-
全局错误捕获方面
-
页面同源的脚本出错:
- promise 异常:可以捕获✅
- 其他普通异常:可以捕获✅
-
跨域脚本出错:
- promise 异常:可以捕获✅
- 其他普通异常:可以捕获✅
-
2. use-credentials
-
不管有没有设置
type="module"
,发送请求时是否带上 cookie,都取决于 cookie 的同站策略 -
收到响应后,响应头中需要同时满足以下条件,浏览器才会执行脚本代码,否则会抛出 CORS 异常:
Access-Control-Allow-Origin
=== 请求头中的origin
字段Access-Control-Allow-Credentials
为 true
-
如果设置了
integrity
,就还会再校验一次资源完整性,不满足也会报错 -
错误捕获方面,与
anonymous
相同
5. 深入错误捕获
1. 网络解释
2.【html 标准】文档中的说明
1. 普通异常为什么会抛出Script error.
?
html 标准文档 中有这样一句话:
如果该脚本属于classic script
且muted errors
为 true,则普通异常就会抛出Script error.
- 什么是
classic script
?
就是指普通脚本。
省略
type
属性,或将其设置为空字符串,或将其设置为JavaScript 的MIME类型,意味着该脚本是classic script
( 原文见此)
muted errors
什么情况下会为 true?
跨域脚本就为 true,后面附带了一句极为简单的解释:“会泄漏私有信息”。(原文见此)
- 为什么设置了
crossorigin
就可以暴露错误详情?
以下为个人理解,没有找到原文。
因为设置了之后,浏览器会校验响应头(Access-Control-Allow-Origin
),发现服务器是允许页面内访问该脚本资源的,于是允许暴露错误信息。
2. 全局 Promise 异常为什么无法捕获?
- 全局 Promise 异常是
HostPromiseRejectionTracker
进行处理的(见下图标注序号 1 处,原文见此)
HostPromiseRejectionTracker
内部,判断如果是classic script
,则直接 return (原文见此)
(个人理解同源的情况下应该走到第 5 小点中,就可以正常捕获)
- 回到 1 中截图标识 2 的位置,提到:
If a rejection is still not handled after this, then the rejection may be reported to a developer console.
由于HostPromiseRejectionTracker
方法未进行任何处理,没有被全局监听所捕获,于是就在控制台抛出了异常。
3. 源码分析:跨域脚本抛出的普通异常为什么没有详细堆栈
1. v8 源码
首先,在 v8 源码中,script脚本默认就是跨域的。注释提到会在“主线程合并期间修复”(未研究对应代码)
上图中的注释下面调用了ScriptOriginOptions
方法进行初始化script
,其第一个参数就是表示是否为跨域脚本,传入的值是 false
:
而异常信息的IsSharedCrossOrigin()
方法就是直接调用script
的IsSharedCrossOrigin()
方法
因此,可以知道, 异常信息的IsSharedCrossOrigin()
方法会默认返回 false
.
2. chromium 源码
在chromium源码中,如果异常信息的IsSharedCrossOrigin()
方法返回 false
,会将sanitize_script_errors
设为SanitizeScriptErrors::kSanitize
之后,判断到满足sanitize_script_errors == SanitizeScriptErrors::kSanitize
,则调用CreateSanitizedError
方法
就是这个方法里,会抛出Script error
,并且没有给出堆栈信息。
以上可以解释普通异常为什么没有堆栈,至于 Promise 异常为什么不能捕获,还没有看懂。并且限于水平,未能调试该源码,不太好说这些没有错漏之处,颇为遗憾……