起因
我们公司有一个部门需要做微前端,其中有一个 JS 链接主应用和子应用都需要,但是在部署的是否发现了子应用的 JS 出现了跨域的情况。
而且子应用单独启动时,是没有任何问题的。
既然是跨域问题,那赶紧去 OSS 看看有没有设置跨域。
发现已经设置了跨域呀。
此时细心的小伙伴已经发现了问题,并想到了解决方案
解密
造成这个问题的原因有三个:
- 微前端加载子应用过程
- 云厂商策略
- 浏览器缓存策略
微前端子应用过程
细心的小伙伴会发现,主应用的 JS 的 type
是 script
,但是子应用的 JS 的 type
却是 fetch
类型,这就涉及到微前端加载子应用的过程。
微前端的入口是一个 html 地址,其为了实现 JS 沙箱和 CSS 隔离,其基本流程是:
- 先获取 HTML 文本内容
- 分析 HTML 文本内容,获取外链的 JS 和 CSS 链接数组
- 遍历数组,通过
fetch
获取每个 JS 和 CSS 文本 - 通过 eval 函数执行 JS 文本,并通过代理 window 对象,实现沙箱;CSS 通过增加前缀增加隔离
我们以 qiankun 为例,看一下其获取 JS 链接的过程:
所以子应用的 type
才是 fetch
而不是正常的 script
。
但如果你是觉得只是因为 fetch
才跨域的,但实际单独 fetch
是没有任何问题滴。
云厂商策略
我测试了两家国内知名云厂商,发现都是如下策略:对于非跨域请求,则不返回跨域头,对于跨域请求才返回跨域头。
当然其目的当然是好的,因为跨域的情况下,在未设置 Access-Control-Max-Age
或者其值过期的情况下,就算文件未改变的情况下,还是会导致发起请求,询问是否跨域,增加了请求量。
但是到这里我们还是不明白为什么两个同时请求时,就会 GG。
浏览器缓存策略
我们仔细观察报错的请求,发现 fetch
请求只有 Response Headers
并没有 Request Headers
,怎么回事呢?
原因也很简单,就是因为浏览器没有发起请求,此请求被认为其和上一个 JS 是同一个资源,所以返回了上一个请求的 Response。
真相大白
通过上述三点分析我们已经彻底明白了:
- 首先是微前端的主应用先通过
script
请求到 JS,(云厂商 Response 里没有跨域头) - 然后微前端框架通过
fetch
方式获取子应用的 JS - 然后浏览器发现这个资源加载过了,于是就返回了 script 的 Response,但是由于没有跨域头,所以浏览器就报了跨域错误。
解决方案
1、子应用区分开发和生产环境
由于是同一份 JS,其实子应用没有必要重新加载一遍的,我们可以通过类似 html-webpack-plugin
区分开发环境和生产环境。
- 当子应用本地开发时,将 JS 添加到 HTML 模板中;
- 当生产构建环境时,不将其打进,如此就可以完美解决上述问题。
2、资源无缓存
我们知道了,本质上其由于浏览器缓存策略引起的,那我们就可以增加 cache-control: no-cache
的方式,不允许浏览器利用缓存,也可以完美解决这个问题。
坏处:会导致流量增加
3、主应用 script 增加 crossorigin
属性
由于fetch
请求复用了第一次的响应结果导致的报错,如果第一次就让他返回带跨域头的结果不就可以了嘛,所以我们可以通过 crossorigin 头,让 script 返回跨域头信息。
4、增加 vary: origin
我们看到阿里云跨域设置中,有一个选择框就是让你选择是否增加 vary: origin
这个 header 头。
Vary 是一个HTTP响应头部信息,它决定了对于未来的一个请求头,应该用一个缓存的回复(response)还是向源服务器请求一个新的回复。它被服务器用来表明在 content negotiation algorithm(内容协商算法)中选择一个资源代表的时候应该使用哪些头部信息(headers).
那为什么 Vary: origin
可以区分 fetch
请求 和 script
呢?
原来 script
是没有 origin
这个请求头的,而 fetch 的方式因为跨域,所以浏览器会强制加上 Origin
。
因为 vary: origin
响应头的原因,所以导致 fetch 并能复用 script
方式请求的 response,所以也避免了跨域。
结束语
虽然问题很小,但是引出来的知识还是挺多的,你学到了吗?