前端根据文件流渲染 PDF 和 DOCX 文件

这篇笔记咱们记录: 后端返回文件流数据,前端根据对应的文件流类型渲染成PDF和DOCX文件。渲染PDF可以不用安装包(比如pdfjs-dist),利用浏览器内置的PDF阅读器,实现渲染。

  1. 利用iframe 渲染PDF,src给Blob:url, 不用安装第三方包
  2. fetch和axios请求,获取流数据,为什么fetch不用指定返回类型,axios要制定responseType
  3. 将PDF转为文档流对应的后端代码(Express)
  4. 利用docx-preview 渲染docx文件
利用iframe 渲染PDF

利用iframe, 给src一个流URL(blob:url), 然后后端设置返回的数据类型是application/pdf, iframe就可以渲染

我们用来渲染PDF的iframe

vue 复制代码
<template>
  <div>
    <iframe
      :src="pdfViewerUrl"
      width="100%"
      height="800px"
      frameborder="0"
    ></iframe>
  </div>
</template>

获取PDF流的方法, 这里我们用了fetch, 这样就能渲染了

vue 复制代码
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
import { ref } from 'vue'

const baseUrl = import.meta.env.VITE_API_BASE || ''
const pdfViewerUrl = ref('')
const getPdf = async () => {
  try {
    // 使用 fetch 获取 PDF 流
    const response = await fetch(`${baseUrl}/pdf`, { method: 'GET' })
    if (!response.ok) {
      throw new Error(`HTTP 错误: ${response.status}`)
    }
    // 直接获取Blob(无需手动转换ArrayBuffer)
    const blob = await response.blob()
    
    // 创建可访问的 URL
    const pdfUrl = URL.createObjectURL(blob)
    pdfViewerUrl.value = pdfUrl
  } catch (error) {
    console.error('获取 PDF 失败:', error)
  }
}

onMounted(() => {
  getPdf()
})
onUnmounted(() => {
  if (pdfViewerUrl.value.includes('blob:')) {
    const url = new URL(pdfViewerUrl.value)
    const fileParam = new URLSearchParams(url.search).get('file')
    if (fileParam && fileParam.startsWith('blob:')) {
      URL.revokeObjectURL(fileParam)
    }
  }
})

Chrome正常渲染《小王子》的截图

用fetch获取blob可以有2种方式,

js 复制代码
// 方式一:直接使用response.blob()(更简洁)
const blob = await response.blob(); 

// 方式二:先获取ArrayBuffer再创建Blob
const arrayBuffer = await response.arrayBuffer(); 
const blob = new Blob([arrayBuffer], { type: 'application/pdf' });

方式二的优点

  • 兼容性更好:arrayBuffer()是 Fetch API 最基础的二进制数据获取方式
  • 可控性更强:手动创建 Blob 时可以精确指定 MIME 类型
  • 更清晰的数据流:明确展示了从二进制数据到 Blob 的转换过程

方式一更简洁,但是要注意,这样后端返回流的时候一定要设置成application/pdf

fetch 和 axios请求

如果用axios请求接口,会有一点点不一样,我们要指定responseType是arrayBuffer, 因为 Axios 的默认响应类型是 json,若不指定,二进制数据会被错误解析导致文件损坏。

js 复制代码
import axios from 'axios';

const getPdf = async () => {
  try {
    // 关键:必须设置 responseType: 'arraybuffer'
    const response = await axios.get(`${baseUrl}/pdf`, {
      responseType: 'arraybuffer', // 指定响应类型为二进制缓冲区
      headers: {
        'Content-Type': 'application/pdf'
      }
    });

    // 创建 Blob 对象
    const blob = new Blob([response.data], { type: 'application/pdf' });
    
    // 生成 URL 并加载到 PDF 查看器
    const pdfUrl = URL.createObjectURL(blob);
    pdfViewerUrl.value = pdfUrl
  } catch (error) {
    console.error('获取 PDF 失败:', error);
    // 处理错误(如显示错误信息)
  }
};

为什么fetch不需要指定返回类型呢, 因为fetch返回的响应数据是可读流 ,通过不同的方法将其转换为不同格式,response.arrayBuffer()、response.blob()、response.text()、response.json(), 具体可看MDN文档

将PDF转为文档流对应的后端代码(Express)

知道后端的话,方便我们理解整个渲染逻辑

js 复制代码
const express = require('express')
const fs = require('fs')
const path = require('path')
const app = express()
const port = 3000
const cors = require('cors')

app.use(express.static('public'))
// 允许本地和线上发布的地址访问
app.use(
  cors({
    origin: ['https://pdf-min.netlify.app', 'http://localhost:5173'],
  })
)

// 提供 PDF 文件流
app.get('/pdf', (req, res) => {
  // 小王子pdf和public同一级目录
  const filePath = path.join(__dirname, 'prince.pdf')
  const stat = fs.statSync(filePath)

  res.setHeader('Content-Type', 'application/pdf')
  res.setHeader('Content-Length', stat.size)

  const readStream = fs.createReadStream(filePath)
  readStream.pipe(res)
})

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`)
})

前面我们用了const blob = await response.blob(), 没有用arrayBuffer, 然后设置类型,因为后端这里设置了res.setHeader('Content-Type', 'application/pdf'), 如果前后端都不设置的话,返回的是这样

利用docx-preview 渲染docx文件
vue 复制代码
<template>
  <div>
    <div id="docx-container"></div>
  </div>
</template>

<script setup lang="ts">
import { renderAsync } from 'docx-preview'
const getDocx = async () => {
  try {
    // 使用 fetch 获取 PDF 流
    const response = await fetch(`${baseUrl}/docx`, { method: 'GET' })
    if (!response.ok) {
      throw new Error(`请求异常: ${response.status}`)
    }

    // 获取 ArrayBuffer
    const arrayBuffer = await response.arrayBuffer()
    const docxContainer = document.getElementById('docx-container')

    if (docxContainer) {
      // 渲染 DOCX 到指定容器
      await renderAsync(arrayBuffer, docxContainer, undefined, {
        className: 'docx-render', // 自定义样式类
        breakPages: true, // 启用分页
        ignoreWidth: false, // 保留页面宽度
      })
    }
  } catch (error) {
    console.error('获取 PDF 失败:', error)
  }
}
<script>

我们用了readerAsync方法,我们传了文档流,容器ID,样式容器undefined, 以及一些可选配置

  1. 第一个参数是文档流,
  2. 第二个参数是DOM容器,
  3. 第三个参数是样式容器
  4. 可选配置。

看下我们渲染出的效果

具体配置, 参考文档

typescript 复制代码
// renders document into specified element
renderAsync(
    document: Blob | ArrayBuffer | Uint8Array, // could be any type that supported by JSZip.loadAsync
    bodyContainer: HTMLElement, //element to render document content,
    styleContainer: HTMLElement, //element to render document styles, numbeings, fonts. If null, bodyContainer will be used.
    options: {
        className: string = "docx", //class name/prefix for default and document style classes
        inWrapper: boolean = true, //enables rendering of wrapper around document content
        hideWrapperOnPrint: boolean = false, //disable wrapper styles on print
        ignoreWidth: boolean = false, //disables rendering width of page
        ignoreHeight: boolean = false, //disables rendering height of page
        ignoreFonts: boolean = false, //disables fonts rendering
        breakPages: boolean = true, //enables page breaking on page breaks
        ignoreLastRenderedPageBreak: boolean = true, //disables page breaking on lastRenderedPageBreak elements
        experimental: boolean = false, //enables experimental features (tab stops calculation)
        trimXmlDeclaration: boolean = true, //if true, xml declaration will be removed from xml documents before parsing
        useBase64URL: boolean = false, //if true, images, fonts, etc. will be converted to base 64 URL, otherwise URL.createObjectURL is used
        renderChanges: false, //enables experimental rendering of document changes (inserions/deletions)
        renderHeaders: true, //enables headers rendering
        renderFooters: true, //enables footers rendering
        renderFootnotes: true, //enables footnotes rendering
        renderEndnotes: true, //enables endnotes rendering
        renderComments: false, //enables experimental comments rendering
        renderAltChunks: true, //enables altChunks (html parts) rendering
        debug: boolean = false, //enables additional logging
    }): Promise<WordDocument>

对应的后端代码

js 复制代码
app.get('/docx', (req, res) => {
  const filePath = path.join(__dirname, '健身房小程序安装使用手册.docx')
  const stat = fs.statSync(filePath)

  res.setHeader(
    'Content-Type',
    'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
  )
  res.setHeader('Content-Length', stat.size)
  const readStream = fs.createReadStream(filePath)
  readStream.pipe(res)
})

后端返回的流形式

相关推荐
qq_27866728634 分钟前
ros中相机话题在web页面上的显示,尝试js解析sensor_msgs/Image数据
前端·javascript·ros
烛阴41 分钟前
JavaScript并发控制:从Promise到队列系统
前端·javascript
zhangxingchao1 小时前
关于《黑马鸿蒙5.0零基础入门》课程的总结
前端
zhangxingchao1 小时前
Flutter的Widget世界
前端
&活在当下&1 小时前
element plus 的树形控件,如何根据后台返回的节点key数组,获取节点key对应的node节点
javascript·vue.js·element plus
$程2 小时前
Vue3 项目国际化实践
前端·vue.js
nbsaas-boot2 小时前
Vue 项目中的组件职责划分评审与组件设计规范制定
前端·vue.js·设计规范
fanged2 小时前
Angular--Hello(TODO)
前端·javascript·angular.js
易鹤鹤.2 小时前
openLayers切换基于高德、天地图切换矢量、影像、地形图层
前端
可观测性用观测云3 小时前
从“烟囱式监控”到观测云平台:2025 亚马逊云科技峰会专访
前端