先说结论
使用pdfjs-dist的2.7.570版本
# 使用pdfjs-dist的2.7.570版本的es5产物,该版本的es5的build产物没有特殊写法,对vue.config.js、babel.config.js的兼容性最好,不需要额外再下载其他插件,比如可选链插件等
# 引用方式
import * as pdfjsLib from 'pdfjs-dist/es5/build/pdf'
import pdfWorker from 'pdfjs-dist/es5/build/pdf.worker.entry'
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorker
pdfjsLib.GlobalWorkerOptions.isEvalSupported = false // 关闭 eval 支持(防止漏洞)// 漏洞链接: https://www.venustech.com.cn/new_type/aqtg/20240514/27492.html
背景
某天,产品同事发现C端的资质展示页面(H5)的图片渲染有问题,经排查发现有些链接是.pdf结尾的pdf文件,最后导致某些机型无法正常渲染。如下图所示
<img src="https://xxx.xx.com/xxx.pdf" />
这里有个小点: ios机型会把pdf渲染出来1页,安卓机型无法渲染pdf
遇到问题时的思考
- 这种情况在线上是否多,如果量级不多,个人感觉可以尝试后端进行解析PDF转成图片,在更新数据库。但这需要结合业务系统来看,因为这是“资质文件”,所以得保证业务系统在这方面的功能是怎么样的,是只能上传图片还是pdf,还是都能,不过这是后面分析才得到的结果
- 后端是否好解决,这需要和后端沟通
需求
- 能够根据pdf链接展示出他的所有页数内容,不失真,能在各个机型的web-view中运行,如安卓app、ios的app、支付小程序、微信小程序。
- 只做pdf解析预览功能,不需要额外功能,把内容转成图片即可。
- 尽量做到不修改vue.config.js、babel.config.js来解决这个问题,理想情况就是新增一个组件,然后对应页面判断是链接,最后使用下就好了。毕竟这是老项目,这些可不能乱动。
前期开发工作
- 与后端沟通,结果是后端不好解决,只能交给前端来解决
- 先问gpt,具体方案和关键依赖,可能的坑点
- 搜索gpt,根据得到的关键依赖(
pdfjs-dist
),了解他的issue、坑点、demo等 - 顺便搜下相关文章,但发现质量都挺一般的,数量有点少,于是就参考gpt的,其次就是
pdfjs-dist
的文档有点难看懂
具体实施
- 下载
pdfjs-dist
,下载时下的是最新版本,5.x.x - 新建
pdf-viewr.vue
组件 - 复制gpt给我的代码
- 运行项目
# gpt给的案例
import * as pdfjsLib from 'pdfjs-dist/build/pdf'
import pdfWorker from 'pdfjs-dist/build/pdf.worker.entry'
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorker
// 解析过程省略,主要核心是引用方式的问题
踩坑之旅
依照上述实施行为,结果就是直接报错,运行不了,于是我就开始了我的漫长踩坑之旅
我的依赖&版本
- node: 12.22.22
- @vue/cli: ^3.12.0
- babel-core: 7.0.0-bridge.0
- vue: 2.6.x
版本问题
使用pdfjs-dist
遇到了非常多的版本兼容性问题,由于用的是最新版本,有很多写法,和我的对不上,于是我的想法是降版本,但一开始不知道降到多少,毕竟有1549个版本,想法是问ai,但ai回答出来的版本还是不太行,于是我就打算换个方式,找找有没有现成的封装好的组件
这里找到的是vue-pdf
,这个组件是用pdfjs-dist
+vue2
的,我大喜过望,马上下载使用,写起来很快呀,但新的问题已然埋下
vue-pdf问题
- issue数量特别多,只能说慎用,一共235个
- 这个组件在打包上线后,会无法正常加载出来,导致对应页面白屏。根据gpt的说法是可能丢失了
pdfjs-dist
,然后我就去找原因,这种情况先找issue,我在vue-pdf
的issue中看到了有人遇到了同样的问题,然后我在某个issue中,看到了这么一句话:使用2.7.570版本。
于是我顺藤摸瓜,找到pdfjs-dist
的版本记录,找到他的内容是什么,然后我就发现了一个让我很兴奋的点:
这个依赖的有build和es5的build产物!!!,这是最关键的,他的其他版本我看了几个,我发现没有es5的打包产物
用build的产物仍然存在特殊写法,比如可选链,因为我的项目没有可选链的babel插件,所以仍然不能用
但是es5的产物没有特殊写法,所以理论上用这个就能解决了,因为引用报错的问题是语法相关的兼容性问题,我又不想新增babel插件、loader依赖等
最后在我使用了这个版本后,修改了原先AI提供的案例中的引用方式,于是项目正常跑了起来,这里就不使用vue-pdf
了,使用pdfjs-dist
,用它来解析,然后转图片。
对比如下:
# 前
import * as pdfjsLib from 'pdfjs-dist/build/pdf'
import pdfWorker from 'pdfjs-dist/build/pdf.worker.entry'
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorker
# 后
import * as pdfjsLib from 'pdfjs-dist/es5/build/pdf'
import pdfWorker from 'pdfjs-dist/es5/build/pdf.worker.entry'
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorker
病毒问题
病毒问题也是看vue-pdf
的issue才知道的
根据报告链接所提供的方案就是
pdfjsLib.GlobalWorkerOptions.isEvalSupported = false // 关闭 eval 支持(防止漏洞)
链接: 【漏洞通告】Mozilla PDF.js代码执行漏洞(CVE-2024-4367)
小结
为了解决pdfjs-dist在vue2的老项目的兼容性问题,最终方案就是使用pdfjs-dist@2.7.570,这样就不用影响到之前的vue.config.js、babel.config.js相关配置了。
这个过程解决兼容问题,费时费力,还以为无法解决了,不过皇天不负苦心人,这个问题还是被解决了,所以记录下。
后续计划
- 自己打包高版本(5.x.x)的pdfjs-dist来适应内部项目
- 研究vue-pdf打包上线白屏问题
- 接入vue3项目
- 分页加载优化,大文件优化
代码
pdf解析预览组件
<template>
<div class="pdf-viewer">
<van-loading v-if="loading" type="spinner" size="32px" vertical>加载中</van-loading>
<div v-else>
<div v-if="error" class="fallback">
<p>当前信息无法加载,您可以<a :href="pdfUrl" target="_blank">点击查看</a></p>
</div>
<div v-else>
<div v-for="(page, index) in pages" :key="index" class="pdf-page">
<img :src="page" :alt="'pdf ' + (index + 1)" class="pdf-image">
</div>
</div>
</div>
</div>
</template>
<script>
import { Loading } from 'vant'
// ? 使用pdfjs-dist的2.7.570版本的es5产物,该版本的es5的build产物没有特殊写法,对本项目vue.config.js、babel.config.js的兼容性最好
import * as pdfjsLib from 'pdfjs-dist/es5/build/pdf'
import pdfWorker from 'pdfjs-dist/es5/build/pdf.worker.entry'
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorker
pdfjsLib.GlobalWorkerOptions.isEvalSupported = false // 关闭 eval 支持(防止漏洞)// 漏洞链接: https://www.venustech.com.cn/new_type/aqtg/20240514/27492.html
export default {
name: 'pdf-viewer',
props: {
pdfUrl: {
type: String,
required: true,
},
},
components: {
[Loading.name]: Loading,
},
data() {
return {
pages: [],
loading: true,
error: false,
}
},
mounted() {
this.loadPdf()
},
methods: {
async loadPdf() {
try {
const loadingTask = pdfjsLib.getDocument(this.pdfUrl)
const pdf = await loadingTask.promise
const pageImages = []
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
const page = await pdf.getPage(pageNum)
const viewport = page.getViewport({ scale: 2 }) // scale 调大可提高清晰度
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
canvas.width = viewport.width
canvas.height = viewport.height
await page.render({ canvasContext: context, viewport }).promise
pageImages.push(canvas.toDataURL())
}
this.pages = pageImages
this.loading = false
} catch (e) {
console.error('PDF 加载失败:', e)
this.error = true
this.loading = false
}
},
},
}
</script>
<style lang="stylus" scoped>
.pdf-viewer {
width: 100%;
overflow-x: hidden;
}
.pdf-image {
width: 100%;
display: block;
object-fit: contain;
}
.fallback {
text-align: center;
color: #999;
font-size: 14px;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
a {
color: #007aff;
text-decoration: underline;
}
}
</style>
链接类型判断
// 判断文件类型(扩展名优先,fallback 为 HEAD)
async detectFileType(url) {
// 1. 特殊 scheme 优先判断
if (url.startsWith('data:image/')) return 'image'
if (url.startsWith('data:application/pdf')) return 'pdf'
if (url.startsWith('blob:')) return 'unknown'
// 2. 扩展名判断(宽松匹配)
if (/\.(jpe?g|png|gif|bmp|webp|svg)([\?#].*)?$/i.test(url)) return 'image'
if (/\.pdf([\?#].*)?$/i.test(url)) return 'pdf'
// 3. 尝试发 HEAD 请求获取 content-type
try {
const res = await axios.head(url) // ? 支付宝小程序如果遇到404链接会导致页面白屏,微信、安卓不会
const contentType = res.headers['content-type'] || ''
if (contentType.includes('image/')) return 'image'
if (contentType.includes('application/pdf')) return 'pdf'
} catch (e) {
console.warn('链接类型获取失败:', url, e.message)
}
return 'unknown'
}