
使用 vite-html-plugin 后无法访问 public/ 文件夹下的文件
Vite
问题描述
最近有一个需求是要把一个pdf文件放在Vite
的public/
文件夹下,前端直接通过URL
来访问(iframe
内嵌访问)
但是通过URL,比如 localhost:5173/demo.pdf
访问会自动的跳到跟目录下 e.g. /
-> /home
(vue-router配置)
🖼️ 项目目录结构:
🖼️ 读取pdf失败,还是跳转到跟目录下:
给人一种好像重定向了的感觉,查看一下发现这个这个资源的Content-Type
居然是text/html
,我想不会是Vite
的问题吧。
于是我马上pnpm create vite
新建了一个项目,使用同样的Vite
版本,是正常的,这锅Vite
不背 🙂↔️
🖼️ 有问题的PDF文件的Content-Type
:
🖼️ 正常PDF文件的Content-Type
:
大概知道什么问题之后,自然的把关注点放在vite.config.ts
上了,应该是某个Vite
插件改变了原有的工作模式,最后通过排查,发现了是vite-html-plugin
这个问题。
不出所料,在查看仓库issues
的时候,也发现有人遇到了同样的问题。
解决方案
目前作者还没有修改这个问题(这个仓库貌似不太active了,上次release
在2023年的10月29)。总的来说,解决方案如下:
// 源码位置:packages/core/src/htmlPlugin.ts
function createRewire(
reg: string,
page: any,
baseUrl: string,
proxyUrlKeys: string[],
) {
return {
from: new RegExp(`^/${reg}*`),
to({ parsedUrl }: any) {
const pathname: string = parsedUrl.path
const excludeBaseUrl = pathname.replace(baseUrl, '/')
const template = path.resolve(baseUrl, page.template)
// 在这里添加判断比如添加一个startsWith("/public")
if (excludeBaseUrl.startsWith("/public")) {
return excludeBaseUrl;
}
if (excludeBaseUrl === '/') {
return template
}
const isApiUrl = proxyUrlKeys.some((item) =>
pathname.startsWith(path.resolve(baseUrl, item)),
)
return isApiUrl ? parsedUrl.path : template
},
}
}
然后发npm,再升级下自己的依赖。
不过这个问题只会在开发下有,因为会走上面的逻辑,在生产上不会有。
源码分析
仓库地址 (https://github.com/vbenjs/vite-plugin-html)
# 安装依赖
pnpm i
# 报错
Scope: all 5 workspace projects
Lockfile is up to date, resolution step is skipped
Already up to date
. postinstall$ pnpm run stub
│ > [email protected] stub /Users/xxx/vite-plugin-html
│ > pnpm run prepack --filter ./packages -- --stub
│ ERR_PNPM_NO_SCRIPT Missing script: prepack
│ Command "prepack" not found. Did you mean "pnpm run prepare"?
│ ELIFECYCLE Command failed with exit code 1.
└─ Failed in 499ms at /Users/xxx/vite-plugin-html
ELIFECYCLE Command failed with exit code 1.
问题出在package.json
中的
{
"stub": "pnpm run prepack --filter ./packages -- --stub"
}
pnpm 没有找到子项目,修改为
{
"stub": "pnpm --filter ./packages/core prepack --stub"
}
即可
源码入口 packages/core/src/index.ts
-> packages/core/src/htmlPlugin.ts
-> createPlugin()
实现了以下几个钩子:
在解析 Vite 配置后调用。可以读取 vite 的配置,进行一些操作
configResolved(resolvedConfig) {
viteConfig = resolvedConfig
// 获取环境变量
env = loadEnv(viteConfig.mode, viteConfig.root, '')
},
在解析 Vite 配置前调用。可以自定义配置,会与 vite 基础配置进行合并
config(conf) {
const input = createInput(userOptions, conf as unknown as ResolvedConfig)
if (input) {
return {
build: {
rollupOptions: {
input,
},
},
}
}
},
用于配置开发服务器的钩子。
configureServer(server) {
let _pages: { filename: string; template: string }[] = []
const rewrites: { from: RegExp; to: any }[] = []
if (!isMpa(viteConfig)) {
// 如果是单页面系统
const template = userOptions.template || DEFAULT_TEMPLATE
const filename = DEFAULT_TEMPLATE
_pages.push({
filename,
template,
})
// _pages: { filename: 'index.html', template: 'index.html' }
} else {
_pages = pages.map((page) => {
return {
filename: page.filename || DEFAULT_TEMPLATE,
template: page.template || DEFAULT_TEMPLATE,
}
})
}
const proxy = viteConfig.server?.proxy ?? {}
const baseUrl = viteConfig.base ?? '/'
const keys = Object.keys(proxy)
let indexPage: any = null
for (const page of _pages) {
if (page.filename !== 'index.html') {
rewrites.push(createRewire(page.template, page, baseUrl, keys))
} else {
indexPage = page
}
}
// ensure order
if (indexPage) {
rewrites.push(createRewire('', indexPage, baseUrl, keys))
}
// 使用connect中间件
server.middlewares.use(
// 通过 connect-history-api-fallback 这个依赖 rewrite 了路由的跳转
// 这里的 rewrites 从 createRewire() 方法获取
history({
disableDotRule: undefined,
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
rewrites: rewrites,
}),
)
},
这里涉及到一个依赖connect-history-api-fallback
,这个依赖解决的事情是在spa
页面上刷新或者直接访问系统URL的时候,不会因为服务器没有对应的文件而回到index.html
。
当访问localhost:5173/demo.pdf
的时候
function createRewire(
reg: string,
page: any,
baseUrl: string,
proxyUrlKeys: string[],
) {
return {
from: new RegExp(`^/${reg}*`),
to({ parsedUrl }: any) {
const pathname: string = parsedUrl.path
const excludeBaseUrl = pathname.replace(baseUrl, '/') // -> /demo.pdf
const template = path.resolve(baseUrl, page.template) // -> index.html
console.log('excludeBaseUrl', excludeBaseUrl)
if (excludeBaseUrl.startsWith("/public")) return excludeBaseUrl;
if (excludeBaseUrl === '/') return template
const isApiUrl = proxyUrlKeys.some((item) =>
pathname.startsWith(path.resolve(baseUrl, item)),
)
return isApiUrl ? parsedUrl.path : template // -> 返回的是 index.html
// 所以访问 localhost:5173/demo.pdf 会跳转至 localhost:5173/index.html
},
}
}
转换 index.html 的专用钩子
transformIndexHtml:
getViteMajorVersion() >= 5
? {
// @ts-ignore
order: 'pre',
handler: transformIndexHtmlHandler,
}
: {
enforce: 'pre',
transform: transformIndexHtmlHandler,
},
这里就用到了ejs
模版来构造html文件
在 Vite
本地服务关闭前,Rollup
输出文件到目录前调用