Vite 的请求路径种类
- 相对路径,相对于根目录的路径。如:
http://localhost/src/main.ts
- 以
/@fs/
开头 + 绝对路径,例如:http://localhost/@fs/app/vite/packages/vite/dist/client/env.mjs
其中 /app/vite/packages/vite/dist/client/env.mjs
为绝对路径,可以直接访问文件。
这两种不同路径种类的使用场景,其实很简单,就是看要访问的文件,是否在项目根目录中?
如果文件在 Vite root 根目录中,则直接使用相对路径
但如果在 Vite root 根目录外,相对路径就需要使用 ../
这种,这种形式不能马上看出文件的位置,因此直接使用绝对路径更好,但是需要跟相对路径做区分,因此用 /@fs/
开头 + 绝对路径的方式
这里一个两种请求种类都有的项目,在线运行地址
该项目设置了 root 为 /root
文件夹,因此 public
文件夹就在 root 外了,因此访问 /public/vite.svg
就会用 /@fs/
+ 绝对路径的方式访问了。
在开发 monorepo 项目的时候,经过就会遇到模块是在 Vite root 目录外的。
源码解析
Vite 在转换一个文件时,会将它的 import 的模块的路径标准化,例如:
我们访问 http://localhost/src/main.ts
时,Vite 会转换 main.ts 的代码,转换前和转换后的结果如下:
// 转换前的源代码 import { createApp } from 'vue' import './style.css' import App from './App.vue' createApp(App).mount('#app') // 转换后的代码 import { createApp } from "/node_modules/.vite/deps/vue.js?v=3386baa1"; import "/src/style.css"; import App from "/src/App.vue"; createApp(App).mount("#app"); 可以看到 import 的模块路径被改变了,路径被标准化为基于根目录的相对路径(如果在 Vite 根目录外,则用 /@fs/)
我们再来看看路径标准化的相关源码(有节选):
// 标准化 url,例如: ./App.vue -> /src/App.vue const normalizeUrl = async ( url: string, pos: number, forceSkipImportAnalysis: boolean = false, ): Promise<[string, string]> => { // 解析 url,resolved.id 就是当前文件的绝对路径 const resolved = await this.resolve(url, importerFile) // 通过绝对路径判断 // 如果路径在 Vite 根目录内,就用相对路径 if (resolved.id.startsWith(root + '/')) { // 去掉 root 根目录的前缀,就是相对路径了 url = resolved.id.slice(root.length) } else if ( // 如果文件存在 fs.existsSync(cleanUrl(resolved.id)) ) { // 在绝对路径前,拼接 /@fs/ url = path.posix.join('/@fs/', resolved.id) } else { // 文件不存在,这可能是一个 Vite 的虚拟模块 // 例如:plugin-vue:export-helper,不是真实存在的模块,但在 Vue 插件中会被转换成代码 // 这个可以不管,跟本文无关 url = resolved.id } return [url, resolved.id] }
从这里可以看出,相对路径和绝对路径的使用场景,就是根据文件是否在 root 目录中来决定的
到这里,其实已经解决了我们的问题了,但我们可以想得更深:
既然可以绝对路径访问文件,那输入另一个的路径,是不是就能访问到别的文件了? 这样有安全问题了啊
安全问题
支持绝对路径访问文件是有风险的,坏人可以通过输入其他路径,获取到整个机器的所有文件了(只要能知道路径),可能那些文件里面就有敏感信息,因此非常危险。
为了避免产生安全问题,Vite 限制了 Dev Server 的文件访问范围,让其只能访问到部分项目用到的文件,这就是 Vite 的文件安全访问策略。
如果访问了允许范围外的文件,Vite 就会返回以下错误页面。
我们通过 localhost 访问的,别人用 localhost + 绝对路径也是访问它自己的机器,这应该没什么安全问题?
如果是本地开发,使用 localhost 访问,那的确没有什么安全问题。Vite 的 server.host
默认值是 localhost
,因此 Dev Server 也只会绑定到 localhost,别人是没办法访问的。
但其实还有另一种开发模式 —— 远程开发。代码是写在服务器上的,然后 Vite 也是跑在服务器上的,然后通过网络去访问页面。这种情况下,就要远程访问 Dev Server,就会有安全问题,要防止别人通过绝对路径,访问到服务器上的其他数据了。
有关远程开发细节,可以查看我的文章《JetBrains 远程开发的使用和心得》
Vite 文件安全访问策略
我们直接从源码看看,Vite 是如何判断是否有允许访问的:
// 函数返回 true 就是允许访问 function ensureServingAccess( url: string, server: ViteDevServer, res: ServerResponse, next: Connect.NextFunction, ): boolean { // 判断是否允许访问 if (isFileServingAllowed(url, server)) { return true } // 如果不允许访问,但文件又是存在的,就会返回 403 的页面 if (isFileReadable(cleanUrl(url))) { const urlMessage = `The request url "${url}" is outside of Vite serving allow list.` // 当前允许访问的路径 const hintMessage = ` ${server.config.server.fs.allow.map((i) => `- ${i}`).join('\n')} Refer to docs https://vitejs.dev/config/server-options.html#server-fs-allow for configurations and more details.` server.config.logger.error(urlMessage) server.config.logger.warnOnce(hintMessage + '\n') res.statusCode = 403 // 响应请求,响应的是 403 页面。 res.write(renderRestrictedErrorHTML(urlMessage + '\n' + hintMessage)) res.end() } else { // 如果文件不存在,那就不管了,别的 server 中间件会返回 404 HTTP 状态码 next() } return false }
从上述代码中可以知道,我们上一小节看到的 Vite 403 错误页面,就是这里返回的
是否允许访问的核心判断逻辑在 isFileServingAllowed
:
export function isFileServingAllowed( url: string, server: ViteDevServer, ): boolean { // 如果不执行不严格的 fs 策略,就允许访问。 if (!server.config.server.fs.strict) return true // 标准化为绝对路径 const file = fsPathFromUrl(url) if (server._fsDenyGlob(file)) return false if (server.moduleGraph.safeModulesPath.has(file)) return true if (server.config.server.fs.allow.some((dir) => isParentDirectory(dir, file))) return true return false }
主要有几个判断:
- 是否执行了严格的 fs 策略,对应的 Vite 配置是 server.fs.strict,默认是
true
- 是否命中 deny 拒绝名单,对应的配置是 server.fs.deny,默认为
['.env', '.env.*', '*.{pem,crt}']
- 是否为项目中使用到的文件。
server.moduleGraph.safeModulesPath
是一个Set<string>
,它记录了所有项目中被 import 的文件的绝对路径。因此,如果项目中使用到了在 root 根目录外的文件,也是能被正常访问到的。但没有使用的文件就不行了。 - 是否命中 allow 名单。对应的配置是 server.fs.allow,如果不配置,Vite 将当前目录加入到 allow,如果是 monorepo 项目,还会将 workspaces 的目录加入到 allow
如果不被允许,Vite 就会返回 403 页面,从而保证了安全性
为什么不直接用 url 判断,而是要先将 url 标准化为绝对路径再判断?
因为需要确保安全性。假如通过 url 是否是 root 开头,来判断是否允许访问,是有问题的。
假如 Vite 的 root 为 /root
,那坏人可以 /@fs/root/../other/password.txt
,去绕过这个策略,这就会出现安全漏洞了。
总结
本文以一个开发中的一个小问题作为开头,提出疑问:为什么 Vite 的请求有时候是相对路径,有时候是 /@fs/ 开头 + 绝对路径?
然后逐步进行解答,最终得出结论:在 root 外的会用 /@fs/
进行访问。
问题虽然很简单,但还可以再一步深入,提出了潜在安全问题,并探索 Vite 是如何解决的,最终还从源码中了解到了 Vite 文件安全访问策略。
如果这篇文章对您有所帮助,可以点赞加收藏👍,您的鼓励是我创作路上的最大的动力。也可以关注我的公众号订阅后续的文章:Candy 的修仙秘籍(点击可跳转)