问题背景
用 vite 新建了一个 react 项目,画了个简单的页面 demo, 需要输出成静态的 html 文件给其他人预览
直接 yarn build
出来的目录结构
index.html 内容
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>xxx</title>
<script type="module" crossorigin src="/assets/index-nGll5NEH.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-PEdz5tmg.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
直接打开这个 html, 预想中应该可以像开发那样看到相关的页面,但是打开却是一片空白。用 f12 查看一下控制台报错,注意到是跨域的问题。
然后产生疑问就是我这个页面全是静态资源,连服务都没调用,为什么会跨域?
初学前端时简单的写点页面和css直接引用,然后打开 html 是可以正常显示的,为什么用 vite 打包的页面就不行。
于是提出两个要解决的问题:
- 怎么让 vite 打包出可以直接本地访问的静态 html 页面
- 是什么触发了跨域报错问题
问题探讨
解决 vite 打包的静态资源无法直接访问问题
其实打包出来的资源直接挂个网络服务器就能访问,最快的方法是在 dist 目录下面运行下面指令
npm install --global serve
serve
这样会默认将 dist 文件当成 web 服务根目录来启动一个服务器,
根据提示的端口访问就能够正常看到页面
但我想要的是直接打开 html 文件就能看到页面,在网上搜寻一番后找到了比较有效的解决方案
先放上原博客的链接
https://www.cnblogs.com/lingern/p/17789995.html
总结来说就是修改 vite 的项目构建配置信息:
- 修改资源相对基址为
./
- 引入
@vitejs/plugin-legacy
插件, 然后用 legacy 函数做相关配置
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import legacy from "@vitejs/plugin-legacy";
// https://vitejs.dev/config/
export default defineConfig({
base: "./",
plugins: [
react(),
legacy({
targets: ["ie>=11"],
additionalLegacyPolyfills: ["regenerator-runtime/runtime"],
}),
],
});
最终打包出来的 index.html 区别
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>xxx</title>
<script type="module" crossorigin src="./assets/index-b43LkIEh.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-tvuqbY_7.css">
<script type="module">import.meta.url;import("_").catch(()=>1);(async function*(){})().next();if(location.protocol!="file:"){window.__vite_is_modern_browser=true}</script>
<script type="module">!function(){if(window.__vite_is_modern_browser)return;console.warn("vite: loading legacy chunks, syntax error above and the same error below should be ignored");var e=document.getElementById("vite-legacy-polyfill"),n=document.createElement("script");n.src=e.src,n.onload=function(){System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))},document.body.appendChild(n)}();</script>
</head>
<body>
<div id="root"></div>
<script nomodule>!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",(function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()}),!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();</script>
<script nomodule crossorigin id="vite-legacy-polyfill" src="./assets/polyfills-legacy-HAG11n64.js"></script>
<script nomodule crossorigin id="vite-legacy-entry" data-src="./assets/index-legacy-4n7y874Q.js">System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))</script>
</body>
</html>
可以看出和修改配置前打包出来的 html 是有区别,其中比较显眼的是和本次讨论主题——跨域相关的crossorigin
关键字。
我们先去查下 MDN 了解一下这个关键字。
https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin
当然这里就不把文档内容复述了,我直接说一下我学习后的总结:
跨域的字段标准定义在报文头部中, 具体的处理逻辑是浏览器来实现的。也就是浏览器在响应头中设置的跨域信息,需要由浏览器来实施。具体行为就是根据不同的跨域信息,判断是否将响应结果返回给客户端。
由此可以得到一个快速处理跨域的方法,服务端代理。因为如果请求是一个网络服务器发出的,那边响应头中的跨域配置就不会被解读,这时携带的资源就能被正常提取。
这也是为什么在开发一些由 webpack 或者 vite 这些工具来构建的前端项目时,如果出现了跨域问题,网上的攻略会说改一下 webpack.config.js 或者 vite.config.js 中的跨域配置或者代理就能解决。因为在开发环境时,我们的页面请求在使用相对路径情况下,是直接发往工具启动的网络服务器中。
而 crossorigin 这个关键字可以作用在 <audio>, <img>, <link>, <script> and <video> 元素中,为这些元素的资源请求添加默认的跨域配置。
当 crossorigin 没有赋值时,其默认时是空字符串,其效果等同于crossorigin=“anonymous”, 将认证标志设为 same-origin。
以上关于跨域内容的总结也是有点长了,跨域还有更多的具体规定,比如跨域可以怎么控制等等的细节,这里不宜展开讨论,因为我们本次讨论的主题是为什么默认 vite 配置打包出来的静态资源文件会触发跨域。对跨域感兴趣的小伙伴可以深入学习以下文档:
https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
绕了一个圈子去了解了一下跨域基础,回到我们刚刚中断的地方。我们知道在修改配置后打包出来的 html 变了,而我们引入的新配置是 regenerator-runtime/runtime
, 让我们看一下这个库做了什么。
搜索 npm 可以看到这个库的介绍是一个无依赖的运行时,用于支持 Regenerator 转编生成的异步 async
和生成器。也就是将 async 关键字这些换成更加普遍的实现,增强兼容性。
我们知道 es6 在 import
一个模块时其实是引入了一个异步生成器,一般可以结合期约Promise
来完成模块的动态引入。虽然现在大多数浏览器都支持了 es6, 但是这些库存在就是用来提供最大兼容性的。
小结
前面直接打包出来的静态文件无法正常渲染,应该是组件内部的导入语句及其相关关键字没有正常解析导致,与跨域其实没有太大关系,因为即使是可以正常渲染的静态页面,也会出现跨域的错误警告。
那么解决无法渲染的问题只需要按照上面的步骤添加一些运行时垫片即可。
火狐浏览器也不能正常显示静态页面的问题
经过实测,即使采用上述的方案进行打包,在火狐浏览器中也不能显示正常的页面。
从控制台中可以看到两种关键的错误信息:
- 已拦截跨源请求:同源策略禁止读取位于 file://xxxxx/dist/assets/index-legacy-4n7y874Q.js 的远程资源。(原因:CORS 请求不是 http)
- Uncaught (in promise) Error: file://xxxxx/dist/assets/index-legacy-4n7y874Q.js
其中的跨域报错可以查看以下文档进行了解:
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS/Errors/CORSRequestNotHttp?utm_source=devtools&utm_medium=firefox-cors-errors&utm_campaign=default
而请求文件引发 Promise 错误,可能也是火狐禁止加载不安全的外部文件的策略导致。在这里就不深入去探究具体原因了,先把我们的主线讲完。
继续探讨为什么静态打包会引发跨域问题
其实从上面的警告已经可以看到部分答案,那就是静态资源访问各种资源时使用的是 file 协议,而浏览器在这个协议的 URL 中解析到的 origin
的值是 null
。跨域错误便由此来。
我们知道了表面的原因,那么接下来就是根据这个引导,找出相关的定义文档,了解一下 URL 的解析规则。
MDN 关于 origin
字段的说明
https://developer.mozilla.org/en-US/docs/Web/API/URL/origin
提到 file
URL 的 origin
取决于浏览器的具体实现
For file: URLs, the value is browser dependent.
URL 定义文档中关于 origin
字段的说明
https://url.spec.whatwg.org/#dom-url-origin
提到 origin
字段返回 URL 的 origin 序列化后的结果
The origin getter steps are to return the serialization of this’s URL’s origin.
URL 定义文档中关于 origin
的具体值的说明
https://url.spec.whatwg.org/#concept-url-origin
上面的信息已经讲得很清楚了,常规的 http
、https
URL 返回的 origin
信息是一个元组,当然怎么解析这个元组是浏览器内部的事。而对于 file
URL 则明确表示依旧是取决于浏览器的具体实现,且说明了一般情况下的默认值是一个 opaque origin
实例。
HTML 定义文档中关于 origin
怎么取值的说明
https://html.spec.whatwg.org/multipage/browsers.html#concept-origin-opaque
这篇文档中详细介绍了解析 HTML 过程中关于 origin
的取值规范和步骤。然后对于 opaque origin
实例,其序列化后的结果,也就是最终的 origin
的值定义为 null
。
那么其实到这里就已经完成了本文的问题探讨,当然文档中关于 origin
的取值还有很多值得学习的细节,感兴趣的小伙伴可以自行研读。
总结
在浏览器直接打开打包完成的静态 html 页面时,会因为是通过 file
类型的 URL 来访问各类资源而引发跨域问题。其原因是 HTML 和 URL 规范中对 file
URL 的 origin
内容的提取有着相关规定,一般情况下会返回 null
。