CORS 以及如何节省一次 OPTIONS 请求

为了解决跨域资源共享问题,浏览器厂商和标准组织在 HTTP 协议的基础上,提出了 CORS 标准协议。CORS 协议由一组 HTTP Header 构成,用于标识某个资源是否可以被跨域访问。

这里只是简单介绍一下 CORS 标准,更详细的内容可以直接看规范文档:Fetch Standard

当前端使用 XHR 或者 fetch 等其他方法请求一个跨域资源时,如果是非简单请求(后面会解释),浏览器会自动帮你先发出一个叫做预检(cors-preflight-request)的请求, 对应的 HTTP Request Method 为 OPTIONS。这个请求对服务器是安全的,也就是说不会对服务器的资源做任何改变,仅仅用于确认 header 响应。

该请求 header 中会包含以下两个字段:

Access-Control-Request-Method: 该字段的值对应当前请求类型,例如 GET、POST、PUT等等。浏览器会自动处理。
Access-Control-Request-Headers: 该字段的值对应当前请求可能会携带的额外的自定义 header 字段名,多个字段用逗号分割。浏览器会自动处理,将请求中非简单的 header 字段全部列出来,例如标识请求流水的 x-request-id,用于 Auth 鉴权的 Authorization 字段。
对于 OPTIONS 请求,按照规范实现的服务端会响应一组HTTP header,但不会返回任何实体内容。如果服务端支持该跨域请求,建议返回 204 状态码(返回 200 也可以)。如果不支持,建议返回 403 状态码(返回 404 或其他错误状态码也可以)。

响应的 header 可以包含以下字段:

Access-Control-Allow-Origin: 允许哪些域被允许跨域,例如 http://qq.com 或 https://qq.com,或者设置为 * ,即允许所有域访问(通常见于 CDN )
Access-Control-Allow-Credentials: 是否携带票据访问(对应 fetch 方法中 credentials),当该值为 true 时,Access-Control-Allow-Origin 不允许设置为 *
Access-Control-Allow-Methods: 标识该资源支持哪些方法,例如:POST, GET, PUT, DELETE
Access-Control-Allow-Headers: 标识允许哪些额外的自定义 header 字段和非简单值的字段(这个后面会解释)
Access-Control-Max-Age: 表示可以缓存 Access-Control-Allow-Methods 和 Access-Control-Allow-Headers 提供的信息多长时间,单位秒,一般为10分钟。
Access-Control-Expose-Headers: 通过该字段指出哪些额外的 header 可以被支持。
对于 CORS 的服务端实现,前端同学可以随便看一下 koa/cors 的源码,一目了然,地址在这里:cors/index.js at master · koajs/cors · GitHub

OK,原理部分就这么多,梳理一下跨域的请求流程:

1. 当我们发起跨域请求时,如果是非简单请求,浏览器会帮我们自动触发预检请求,也就是 OPTIONS 请求,用于确认目标资源是否支持跨域。如果是简单请求,则不会触发预检,直接发出正常请求。

2. 浏览器会根据服务端响应的 header 自动处理剩余的请求,如果响应支持跨域,则继续发出正常请求,如果不支持,则在控制台显示错误。

由此可见,当触发预检时,一次 AJAX 请求会消耗掉两个 TTL,严重影响性能。

那么如何节省掉 OPTIONS 请求来提升性能呢?从上文可以看出,有两个方案:

发出简单请求。
服务器端设置 Access-Control-Max-Age 字段,那么当第一次请求该URL时会发出 OPTIONS 请求,浏览器会根据返回的 Access-Control-Max-Age 字段缓存该请求的OPTIONS预检请求的响应结果(具体缓存时间还取决于浏览器的支持的默认最大值,取两者最小值,一般为 10分钟)。在缓存有效期内,该资源的请求(URL和header字段都相同的情况下)不会再触发预检。(chrome 打开控制台可以看到,当服务器响应 Access-Control-Max-Age 时只有第一次请求会有预检,后面不会了。注意要开启缓存,去掉 disable cache 勾选。)
但是要注意的是,该缓存只针对这一个请求 URL 和相同的 header,无法针对整个域或者模糊匹配 URL 做缓存。

可以看到方案2 虽然可以设置缓存,但很局限,只限于缓存一个 URL 地址,并不适用于频繁跨域调用后台的各个接口(当然也可以考虑封装一下,固定一个接口地址,传不同的body内容)。

那方案一中,什么是简单请求呢?规范规定了,当请求同时满足以下所有情况时,才会被浏览器认为是一个简单请求:

请求方法必须是以下之一:GET、HEAD、POST,也就是说 PUT、PATCH 等方法必然会触发预检。
只有以下 header 字段允许被修改或被设置,否则必然触发预检。
Accept、Accept-Language、Content-language、Content-Type(但有限定值)、DPR、Downlink、Save-Data、Viewport-Width、Width
Content-Type 的值只被允许设置为以下三个之一:application_x-www-form-urlencoded、multipart_form-data、text/plain。也就是说,如果请求的 Content-Type 被设置为 application/json;charset=utf-8 时也必然会触发预检。
添加任何额外的自定义的 header 都会触发预检,例如 x-request-id,但服务端可以设置缓存这一个请求的OPTIONS 响应。
XMLHttpRequestUpload 在请求中使用的任何对象上都没有注册事件侦听器。这个比较少见。详细可以参考:XMLHttpRequest.upload - Web APIs | MDN
ReadableStream 请求中未使用任何对象。这个比较少见,应该是指 Fetch API 中的 Request 中的 Body,本人没有去验证。
当满足以上条件时,就不会触发预检了。例如使用script标签加载跨域的 CDN 的资源就是很常见的普通 GET 请求,不会触发预检,有兴趣的同学可以打开 chrome 控制台,看一下 CDN 资源返回的 header。

顺便说一句题外话,当 CDN 设置了 Access-Control-Allow-Origin响应头允许跨域时,我们可以给script标签添加crossOrigin属性,从而可以使用 window.onerror 捕获 CDN 上的 js 运行时导致的详细错误信息,包括堆栈等。

如果不设置crossOrigin属性,则可能只会捕获到script error,无法获取额外的堆栈信息。

crossOrigin属性的值默认为anonymous,即不携带 cookie,如果设置为use-credentials,则会携带 cookie 和客户端证书等票据。

示例:

<script src="https://qq.com/a.js" crossOrigin="anonymous"></script>
全文到这里就结束了,如果你的生产环境存在这个性能问题,那么首先把本地的AJAX请求中的自定义 header 去掉,同时可以理直气壮的拿这篇文章发给后端同学,让其在支持CORS的同时,针对性优化,避免触发 OPTIONS 请求,提升性能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值