执子念

Vue 3 文件预览上线后 404:从 URL、Blob 到 Worker/WASM 的完整交付

开发环境里,附件预览往往半天就能跑起来:选中一个 DOCX,组件出现;换成 PDF,也能翻页。可一旦部署到 /oa/ 这样的子路径,页面却开始白屏,Network 里跟着出现一串 Worker、WASM 或字体 404。

这不是一个“改路径就结束”的偶发问题。浏览器端文件预览至少包含两类交付物:业务组件的 JavaScript,以及解析器按需加载的运行时资源。前者进入了前端产物,不等于后者也已经上线。

本文用 Vue 3 + Vite 把这条链路从头走一遍,重点解决四件事:

  1. 什么文件应该传 URL,什么文件应该先下载成 File

  2. Worker、WASM、字体和 vendor 资源怎样随应用发布;

  3. 子路径部署为什么会改变资源地址;

  4. 上线前怎样用真实文件验收,而不是只看组件有没有渲染。

文件预览工作台

一、先把“文件从哪里来”说清楚

预览组件最终接收的输入通常可以归为两类:URL,或者已经在浏览器内存中的 File/Blob

可以直接使用 URL 的典型场景包括:

  • 同源地址,登录态由 Cookie 自动携带;

  • 公开文件;

  • 有短期签名、无需额外 Header 的对象存储地址;

  • 服务端支持 Range,希望 PDF 能渐进读取。

更适合由业务层先下载的场景包括:

  • 请求必须携带自定义 Token;

  • 下载前要做解密或二次鉴权;

  • 401、重试、审计必须经过统一请求层;

  • 返回地址不能被预览器直接访问。

关键不是 URL 和 Blob 谁“更先进”,而是权限边界在哪里。URL 保留了浏览器按需请求的可能;fetch + Blob 让业务层拿回控制权,但通常意味着完整下载和额外内存占用。

URL 与 File 两种输入路径

二、先用完整入口跑通,再谈拆包

文件类型较多、尚未做过体积测量时,可以先用完整入口把行为和部署链路跑通:

pnpm add @file-viewer/vue3-full
pnpm add -D @file-viewer/vite-plugin

一个最小的本地文件预览组件如下:

<script setup lang="ts">
import { ref } from 'vue'
import { FileViewer, type ViewerOptions } from '@file-viewer/vue3-full'

const selected = ref<File>()
const options: ViewerOptions = {
  theme: 'light',
  styleIsolation: 'auto',
  toolbar: { position: 'bottom-right' },
}

function selectFile(event: Event) {
  const input = event.target as HTMLInputElement
  selected.value = input.files?.[0]
}
</script>

<template>
  <input
    type="file"
    accept=".docx,.xlsx,.pptx,.pdf"
    @change="selectFile"
  >
  <FileViewer
    v-if="selected"
    :file="selected"
    :options="options"
    class="viewer"
  />
</template>

<style scoped>
.viewer {
  display: block;
  height: min(760px, 78vh);
}
</style>

Full 包已经带完整 preset,不需要再重复安装和注入同一批 renderer。等真实业务的文件分布、首屏体积和缓存命中率都有数据后,再决定是否按能力拆包。

三、组件能渲染,不代表运行时已经交付

DOCX、XLSX、PPTX 和 PDF 看起来进入同一个 Vue 组件,底层却是不同的解析链路。它们可能按需请求 Worker、WASM、字体、字典或 vendor 文件。

Vite 配置需要同时声明站点基址和资源复制:

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileViewerRenderers } from '@file-viewer/vite-plugin'

export default defineConfig({
  base: '/oa/',
  plugins: [
    vue(),
    fileViewerRenderers({ copyAssets: true }),
  ],
})
组件、构建和运行时资源的关系

这段配置完成两件互相独立的事:

  • base: '/oa/' 告诉 Vite,应用不是部署在域名根目录;

  • copyAssets: true 把与当前包版本匹配的运行时资源复制到构建结果。

生产环境应当命中 /oa/file-viewer/...。如果请求变成 /file-viewer/...,通常是基址丢失;如果目录存在但某些文件 404,则要检查应用包和静态资产是不是来自不同版本。

静态资源由统一服务托管时,可以显式设置地址:

import { setDefaultFullAssetBaseUrl } from '@file-viewer/vue3-full'

setDefaultFullAssetBaseUrl('/static/file-viewer/')

非 Vite 项目则可以在构建阶段复制同版本资产:

npx --no-install file-viewer-copy-assets ./public/file-viewer

不要从旧服务器手工拷一份 WASM 继续用。JavaScript、Worker 和 vendor 之间可能存在内部协议与导出差异,版本错配经常只表现为空白页或一句很模糊的初始化失败。

四、鉴权下载后要保留真实文件名

需要自定义 Header 的附件,可以让业务请求层完成下载,再构造一个带扩展名的 File

<script setup lang="ts">
import { ref } from 'vue'
import { FileViewer } from '@file-viewer/vue3-full'

const previewFile = ref<File>()
const errorMessage = ref('')

async function openAttachment(id: string, filename: string) {
  errorMessage.value = ''

  const response = await fetch(`/api/attachments/${id}`, {
    credentials: 'include',
    headers: { 'X-Preview-Request': '1' },
  })

  if (!response.ok) {
    errorMessage.value = `附件读取失败:${response.status}`
    return
  }

  const blob = await response.blob()
  previewFile.value = new File([blob], filename, {
    type: blob.type,
    lastModified: Date.now(),
  })
}
</script>

<template>
  <button @click="openAttachment('42', '合同.docx')">
    预览合同
  </button>
  <p v-if="errorMessage">{{ errorMessage }}</p>
  <FileViewer v-if="previewFile" :file="previewFile" />
</template>

这里不能只传一个没有后缀的 Blob。Office 容器的 MIME 在不同下载服务中并不总是可靠,正确扩展名仍是选择解析链路的重要依据。

同时要接受这条方案的成本。一个 600 MB 文件完整下载为 Blob 后,浏览器需要持有这份二进制,解析期间还可能产生额外对象。大 PDF 如果可以走同源 URL 和 Range,请优先保留渐进读取;必须使用自定义鉴权头时,再单独评估文件上限、取消请求和内存告警。

五、同一个组件背后不是同一个解析器

不同文件的验收点不能合并成一句“能打开”:

  • DOCX 要看关系文件、图片、字体、页眉页脚和复杂表格;

  • XLSX 要看工作表、合并单元格、样式和大表格滚动;

  • PPTX 要看页面顺序、主题、图片与独立 Worker;

  • PDF 要看目录、旋转页、中文字体,以及服务端是否真正支持 Range。

多类文件的实际预览过程

当前实现中,208 个已注册扩展名映射到 25 条预览链路。这个数字描述的是扩展名路由矩阵,不是 208 套独立渲染器,也不意味着所有文档都能像原生 Office 一样像素级还原。

本文只讨论 .pptx,不把结构完全不同的旧版 .ppt 混进同一条接入路径。

六、生产验收清单

我现在会固定执行下面六步:

  1. 用真实 base 执行 build 和 preview,在 /oa/ 下访问,不用开发服务器代替生产验证;

  2. 准备 DOCX、XLSX、PPTX、PDF 各一个脱敏样本,逐个查看 Network;

  3. 确认 Worker、WASM、字体和 vendor 都来自自己的域名,且没有 404;

  4. 同时覆盖 URL 与 File 两种输入,测试 401、CORS、Range、取消下载和快速切换;

  5. 检查文档内容,而不只检查首屏:表格、页序、目录、旋转页都要进入回归;

  6. 断开公网再测一次,确认所谓内网部署没有暗中依赖公共 CDN。

性能记录至少要包含浏览器版本、设备内存、文件大小、页数和峰值内存。没有这些条件,“大文件可用”只是无法复现的印象。

七、什么时候不应该选浏览器端预览

浏览器端解析适合文件已经处在业务权限体系内、团队希望减少额外副本,并愿意把运行时和兼容性测试纳入前端交付的系统。

如果目标是像素级统一输出、服务器全文检索、病毒扫描、集中水印或归档 PDF,服务端转换通常更合适。公开低敏文件且团队不想维护运行时时,可以评估成熟在线服务。需要编辑、批注协同或复杂公式重算时,则应该使用专业编辑器,而不是让只读预览器承担不属于它的职责。

组件标签只是入口。文件输入、静态资产、版本、鉴权、内存和生产回归一起闭环,预览功能才算真正交付。

文中的接口以 File Viewer v2.2.5 为验证基线,完整参数可查阅 Vue 3 接入与部署文档

posted on 2026-08-05 15:48  执子念  阅读(12)  评论(0)    收藏  举报

导航