使用 vite-html-plugin 后无法访问 public/ 文件夹下的文件

使用 vite-html-plugin 后无法访问 public/ 文件夹下的文件


Vite

问题描述

最近有一个需求是要把一个pdf文件放在Vitepublic/文件夹下,前端直接通过URL来访问(iframe 内嵌访问)

但是通过URL,比如 localhost:5173/demo.pdf 访问会自动的跳到跟目录下 e.g. / -> /home(vue-router配置)

🖼️ 项目目录结构: Project Structure

🖼️ 读取pdf失败,还是跳转到跟目录下: Fail To Access PDF file

给人一种好像重定向了的感觉,查看一下发现这个这个资源的Content-Type居然是text/html,我想不会是Vite的问题吧。

于是我马上pnpm create vite新建了一个项目,使用同样的Vite版本,是正常的,这锅Vite不背 🙂‍↔️

🖼️ 有问题的PDF文件的Content-Type PDF That return text/html

🖼️ 正常PDF文件的Content-Type PDF That return application/pdf

大概知道什么问题之后,自然的把关注点放在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()

实现了以下几个钩子:

  1. configResolved

在解析 Vite 配置后调用。可以读取 vite 的配置,进行一些操作

configResolved(resolvedConfig) {
  viteConfig = resolvedConfig
  // 获取环境变量
  env = loadEnv(viteConfig.mode, viteConfig.root, '')
},
  1. config

在解析 Vite 配置前调用。可以自定义配置,会与 vite 基础配置进行合并

config(conf) {
  const input = createInput(userOptions, conf as unknown as ResolvedConfig)
  if (input) {
    return {
      build: {
        rollupOptions: {
          input,
        },
      },
    }
  }
},
  1. configureServer

用于配置开发服务器的钩子。

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
    },
  }
}
  1. transformIndexHtml

转换 index.html 的专用钩子

transformIndexHtml:
  getViteMajorVersion() >= 5
    ? {
      // @ts-ignore
      order: 'pre',
      handler: transformIndexHtmlHandler,
    }
    : {
      enforce: 'pre',
      transform: transformIndexHtmlHandler,
    },

这里就用到了ejs模版来构造html文件

  1. closeBundle

Vite 本地服务关闭前,Rollup 输出文件到目录前调用