添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

最近做项目遇到在线预览和下载pdf文件,试了多种pdf插件,例如 jquery.media.js (ie无法直接浏览),最终决定用 pdf.js ,这篇文章记录一下我做技术选型的过程。

二、pdf预览的各种实现方式

方式一、a标签直接打开

pdf文件理论上可以在浏览器直接打开预览但是需要打开新页面。在仅仅是预览pdf文件且UI要求不高的情况下可以直接通过a标签href属性实现预览

<a href="文档地址"></a>

缺点:google的word excel ppt 预览资源,必须是公共可访问的。而且谷歌浏览器上点击就会下载,IE上可以选择是预览还是下载

方式二、通过iframe、embed、obj标签嵌入内容

1)iframe

直接通过页面内嵌iframe

$("<iframe src='"+ this.previewUrl +"' width='100%' height='362px' frameborder='1'>").appendTo($(".video-handouts-preview"));

此外还可以在iframe标签之间提供一个提示类似这样

<iframe :src="previewUrl" width="100%" height="100%">
This browser does not support PDFs. Please download the PDF to view it:
<a :href="previewUrl">Download PDF</a>
</iframe>

2)embed标签

<embed :src="previewUrl" type="application/pdf" width="100%" height="100%">

此标签h5特性中包含四个属性:高、宽、类型、预览文件src!与< iframe > < / iframe > 不同,这个标签是自闭合的的,也就是说如果浏览器不支持PDF的嵌入,那么这个标签的内容什么都看不到!

3)object标签

还有object标签,这种方式和iframe使用差别较小:

<object :src="previewUrl" width="100%" height="100%">
This browser does not support PDFs. Please download the PDF to view it: <a :href="previewUrl">Download PDF</a>
</object>

方式三、jquery.media.js插件

通过jquery插件jquery.media.js实现这个插件可以实现pdf预览功能(包括其他各种媒体文件)但是对word等类型的文件无能为力。实现方式:js代码:

<script type="text/javascript" src="jquery-1.7.1.min.js"></script>  
<script type="text/javascript" src="jquery.media.js"></script>

html结构:

<div id="handout_wrap_inner"></div> </body>

调用方式:

<script type="text/javascript">  
 $('#handout_wrap_inner').media({
		width: '100%',
		height: '100%',
		autoplay: true,
        src:'http://storage.xuetangx.com/public_assets/xuetangx/PDF/PlayerAPI_v1.0.6.pdf',
</script>

方式四、PDFObject.js插件

PDFObject实际上也是通过上面说的iframe、embed、obj标签实现的

<!DOCTYPE html>
    <title>Show PDF</title>
    <meta charset="utf-8" />
    <script type="text/javascript" src='pdfobject.min.js'></script>
    <style type="text/css">
        html,body,#pdf_viewer{
            width: 100%;
            height: 100%;
            margin: 0;
            padding: 0;
    </style>
</head>
    <div id="pdf_viewer"></div>
</body>
<script type="text/javascript">
    if(PDFObject.supportsPDFs){
        // PDF嵌入到网页
        PDFObject.embed("index.pdf", "#pdf_viewer" );
    } else {
        location.href = "/canvas";
</script>
</html>

还可以通过以下代码进行判断是否支持PDFObject预览

if(PDFObject.supportsPDFs){
   console.log("Yay, this browser supports inline PDFs.");
} else {
   console.log("Boo, inline PDFs are not supported by this browser");

方式五、pdf.js

我最后选择了pdf.js插件(兼容ie10及以上、谷歌、安卓,苹果),强烈推荐该插件,下面介绍用法。

pdf.js在vue中的使用:

1)下载插件

下载路径:github.com/mozilla/pdf… 百度网盘下载 链接:pan.baidu.com/s/1z4_o8ahN… 提取码:7efu

2)将下载构建后的插件放到文件中public(vue/cli 3.0)

3)在vue文件中直接使用,贴上完整代码

<template>
  <div class="wrap">
    <iframe :src="pSrc" width="100%" height="100%"></iframe>
  </div>
</template>
<script>
  export default {
    name: "pdf",
    data() {
      return {pSrc: '',};
    methods: {
      loadPDF() {
        //baseurl :pdf存放的文件路径,可以是本地的,也可以是远程,这个是远程的,亲测可以用
        // ie有缓存加个随机数解决
        let baseurl = 'http://storage.xuetangx.com/public_assets/xuetangx/PDF/PlayerAPI_v1.0.6.pdf';
        let pSrc = baseurl + '?r=' + new Date();
        this.pSrc = '../../plugin/pdf/web/viewer.html?file=' + encodeURIComponent(pSrc) + '.pdf';
    mounted: function () {
      this.loadPDF();
</script>
<style scoped>
  .wrap {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    bottom: 0;
</style>

好啦,现在我们把dom添加到页面中,打开页面就可以看到预览了

这边给个在线链接:storage.xuetangx.com/public_asse… 大家可以试试,最终效果是这样的:

5)如何魔改?

下面我举个例子,来讲讲怎么魔改展示页面。 比如说,如果你想要的禁掉pdf文件的下载、打印等功能,最简单的方法是,找的自己导入文件里的viewer.html,路径为build下的generic文件夹下的web里的viewer.html,如下: 在这个html里找到对应下载的dom直接就可以,切记不可以注掉,注掉会报错。如下,红色框中,一个是下载一个是打印,直接隐藏就可以。 如果有人问这样也不安全,那可以和客户商量不在页面展示,因为只要页面可以看到的东西,截屏也可以截下来,注定是不安全的。

6)踩坑注意事项

踩坑1:如果url中含有中文,则需要转码

关于文件地址的url,如果包含中文,需要转码,我们尽量进行转码,pdf会自动解码,这样靠谱一点

var downloadUrl = baseUrl + 'xxx/接口地址?token=' + this.token + '&filePath='+paymentFiles[i].FILE_PATH+'&fileName='+paymentFiles[i].FILE_NAME;
//由于地址需要传文件名字和路径 文件名字包含中文,所以需要转码
var url = encodeURIComponent(downloadUrl); //获取pdf预览地址
//把文件流地址天津到file参数中
url = "/libs/DPFjs/web/viewer.html?file=" + url;
//把dom添加到页面中,打开页面即可显示
accountingArchivesQuery_ViewHTML+= '';

如果不进行转码就会出现以下错误

踩坑2:把文件地址赋值给file,打开浏览器跨域报错

这个的原因是因为viewer.js中有跨域判断的代码注释掉就好了

但是有一些场景后台给我返回的数据是base64格式,如果是base64数据的pdf又如何处理呢?直接使用是不ok的,pdf不支持base64,继续折腾。

踩坑3:PDF.js默认不支持pdf base64格式数据的预览

解决方法修改源码,让pdf.js支持base64位数据,但是这里有一个大坑(IE不兼容...),不知道各位使用之后有没有同样问题, [RFC2045]中有规定:Base64一行不能超过76字符,超过则添加回车换行符。因此需要把base64字段中的换行符,回车符给去掉。且需要转换成pdf.js能直接解析的Uint8Array类型 在viewer.html中引入viewer.js之前添加以下代码

var DEFAULT_URL = "";
var pdfUrl = document.location.search.substring(1);
if(null == pdfUrl || "" == pdfUrl){
varBASE64_MARKER = ';base64,';//声明文件流编码格式
var preFileId = "";
var pdfAsDataUri = sessionStorage.getItem("_imgUrl");//这里就是pdf文件的base64码,我是通过session传递base64的
var pdfAsArray = convertDataURIToBinary(pdfAsDataUri);
DEFAULT_URL = pdfAsArray;
//编码转换
functionconvertDataURIToBinary(dataURI) {
//[RFC2045]中有规定:Base64一行不能超过76字符,超过则添加回车换行符。因此需要把base64字段中的换行符,回车符给去掉。
var base64Index = dataURI.indexOf(BASE64_MARKER) + BASE64_MARKER.length;
var newUrl = dataURI.substring(base64Index).replace(/[\n\r]/g,'');
var raw = window.atob(newUrl);//这个方法在ie内核下无法正常解析。
var rawLength = raw.length;
//转换成pdf.js能直接解析的Uint8Array类型
var array = newUint8Array(newArrayBuffer(rawLength));
for (i = 0; i < rawLength; i++) {
              array[i] = raw.charCodeAt(i) & 0xff;
return array;

修改viewer.js中 defaultUrl 的 value 为 DEFAULT_URL

在页面中使用 把请求回来的base64数据存储到本地 sessionStorage 打开/viewer.html 自动获取本地数据进行预览

if(fileList[i].source == 'pdf'){
//使用pdf.js 预览base64格式         
 sessionStorage.setItem('_imgUrl',"data:application/pdf;base64,"+fileList[i].fileBase64);
 imageHTML = '</span></span></div></div></li><li style='list-style:none outside none;'><div style='font-size:14px;font-style:normal;font-weight:400;font-family:&quot;Source Code Pro&quot;, &quot;DejaVu Sans Mono&quot;, &quot;Ubuntu Mono&quot;, &quot;Anonymous Pro&quot;, &quot;Droid Sans Mono&quot;, Menlo, Monaco, Consolas, Inconsolata, Courier, monospace, &quot;PingFang SC&quot;, &quot;Microsoft YaHei&quot;, sans-serif;line-height:22px;color:rgb(171, 178, 191);text-align:left;float:left;width:24px;height:22px;border-top:0px none rgb(171, 178, 191);border-bottom:0px none rgb(171, 178, 191);border-right:1px solid rgb(197, 197, 197);border-left:0px none rgb(171, 178, 191);text-decoration:none solid rgb(171, 178, 191);background:rgba(0, 0, 0, 0) none repeat 0% 0%;'></div><div style='font-size:14px;font-style:normal;font-weight:400;font-family:&quot;Source Code Pro&quot;, &quot;DejaVu Sans Mono&quot;, &quot;Ubuntu Mono&quot;, &quot;Anonymous Pro&quot;, &quot;Droid Sans Mono&quot;, Menlo, Monaco, Consolas, Inconsolata, Courier, monospace, &quot;PingFang SC&quot;, &quot;Microsoft YaHei&quot;, sans-serif;line-height:22px;margin:0px 0px 0px 8px;color:rgb(171, 178, 191);text-align:left;float:left;width:494.312px;height:22px;border-top:0px none rgb(171, 178, 191);border-bottom:0px none rgb(171, 178, 191);border-right:0px none rgb(171, 178, 191);border-left:0px none rgb(171, 178, 191);text-decoration:none solid rgb(171, 178, 191);background:rgba(0, 0, 0, 0) none repeat 0% 0%;'><div style='font-size:14px;font-style:normal;font-weight:400;font-family:&quot;Source Code Pro&quot;, &quot;DejaVu Sans Mono&quot;, &quot;Ubuntu Mono&quot;, &quot;Anonymous Pro&quot;, &quot;Droid Sans Mono&quot;, Menlo, Monaco, Consolas, Inconsolata, Courier, monospace, &quot;PingFang SC&quot;, &quot;Microsoft YaHei&quot;, sans-serif;line-height:22px;color:rgb(171, 178, 191);text-align:left;float:none;width:494.312px;height:22px;border-top:0px none rgb(171, 178, 191);border-bottom:0px none rgb(171, 178, 191);border-right:0px none rgb(171, 178, 191);border-left:0px none rgb(171, 178, 191);text-decoration:none solid rgb(171, 178, 191);background:rgba(0, 0, 0, 0) none repeat 0% 0%;'><span style='font-size:14px;font-style:normal;font-weight:400;font-family:&quot;Source Code Pro&quot;, &quot;DejaVu Sans Mono&quot;, &quot;Ubuntu Mono&quot;, &quot;Anonymous Pro&quot;, &quot;Droid Sans Mono&quot;, Menlo, Monaco, Consolas, Inconsolata, Courier, monospace, &quot;PingFang SC&quot;, &quot;Microsoft YaHei&quot;, sans-serif;line-height:22px;color:rgb(152, 195, 121);text-align:left;float:none;border-top:0px none rgb(152, 195, 121);border-bottom:0px none rgb(152, 195, 121);border-right:0px none rgb(152, 195, 121);border-left:0px none rgb(152, 195, 121);text-decoration:none solid rgb(152, 195, 121);background:rgba(0, 0, 0, 0) none repeat 0% 0%;'> frameborder="0" scrolling="no" name="发票图片" >';
 preview_div.append(imageHTML)

谷歌正常预览,ie报错!

打印看一下,分析原因可能是atob(newUrl);//这个方法在ie内核下可能无法正常解析。

贯彻一把唆的原则.直接把base64流,还原成pdf,谷歌预览正常ie预览正常!!! 解决方案:把pdf base64流转pdf 前提是原本就是pdf 且pdf.js只能预览pdf 以下代码添加到后台接口返回回调

var base = fileList[i].fileBase64//要传入的base64数据
var bstr = atob(base)
var n = bstr.length;
var u8arr = newUint8Array(n);
while (n--) {u8arr[n] = bstr.charCodeAt(n);}
//确定解析格式,转换成pdf 可能可以变成img,但没有深入研究
var blob = newBlob([u8arr], {type: 'application/pdf;chartset=UTF-8'});
var url = window.URL.createObjectURL(blob)
//使用pdf.js 预览
var add = "/libs/DPFjs/web/viewer.html?file=" + url;
imageHTML = '';
preview_div.append(imageHTML)

ie也正常预览啦 好了,上面就是我推荐的方案:pdf.js的写法。

基于pdf.js封装的npm库

npm上面有基于pdf.js,开发的各种库:如pdf-vue,pdfjs.dist,vueshowpdf,下面也介绍一下:

1)vue-pdf

vue-pdf,就是对pdf.js做了vue版本的封装。

npm install --save vue-pdf

pdf 页面显示

<template>
        ref="pdf"
        :src="url"
      </pdf>
    </div>
</template>
<script>
import pdf from 'vue-pdf'
export default {
  components:{
  data(){
      return {
          url:"http://image.cache.timepack.cn/nodejs.pdf",
</script>
我装包的时候报错了,好像是vue-pdf这个库对webpack的版本好像有要求,解决半天无果,就直接换方案了,反正也是基于pdf.js的封装,我自己直接用pdf.js算了。

2)vueshowpdf

vueshowpdf和vue-pdf一样,基于pdf.js做了二次封装,但是看了一下npm的包界面,此包已弃用,建议不使用这个方案。

3)pdfjs-dist

你可以理解为pdf.js的npm版本

npm i pdfjs-dist --save-dev
import * as PDFJS from "pdfjs-dist/legacy/build/pdf";
// 设置pdf.worker.js文件的引入地址
PDFJS.GlobalWorkerOptions.workerSrc = require("pdfjs-dist/legacy/build/pdf.worker.entry.js");
// data是一个ArrayBuffer格式,也是一个buffer流的数据
PDFJS.getDocument(data).promise.then(pdfDoc=>{
    const numPages = pdfDoc.numPages; // pdf的总页数
    // 获取第1页的数据
    pdfDoc.getPage(1).then(page =>{
     // 设置canvas相关的属性
     const canvas = document.getElementById("the_canvas");
     const ctx = canvas.getContext("2d");
     const dpr = window.devicePixelRatio || 1;
     const bsr =
       ctx.webkitBackingStorePixelRatio ||
       ctx.mozBackingStorePixelRatio ||
       ctx.msBackingStorePixelRatio ||
       ctx.oBackingStorePixelRatio ||
       ctx.backingStorePixelRatio ||
     const ratio = dpr / bsr;
     const viewport = page.getViewport({ scale: 1 });
     canvas.width = viewport.width * ratio;
     canvas.height = viewport.height * ratio;
     canvas.style.width = viewport.width + "px";
     canvas.style.height = viewport.height + "px";
     ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
     const renderContext = {
       canvasContext: ctx,
       viewport: viewport,
     // 数据渲染到canvas画布上
     page.render(renderContext);

缺点: 这个也报错了,因为他的源码用了很多草案语法比如"?."。我编译直接报错,尝试用babel装编译包也没用,最后放弃了这个方案。 还是那句话,基于pdf.js封装的,我还不如直接用pdf.js。

1.pdf预览的各类方案对比

那么对比完来一个总结,你也可以直接看这里: 最最最简单是,直接a标签弹外链,用浏览器自带的pdf预览。但不能改样式,也不能放在弹窗里展示。

如果只想简单在页面中嵌套 PDF,使用 iframe / object / embed等标签是最好的选择,它不需要你自己去编写翻页组件、不需要去调整样式,用户体验佳。(jquery.media.jsPDFObject.js这些库也只是把pdf转成标签展示而已,原理是一样的)

如果对权限控制、样式定制需求较高,使用pdf.js及是最好的选择,他的原理是将pdf转换成canvas,所以浏览器兼容性好。同时接口和属性较全,扩展能力强,自由度高。 至于是否使用基于pdf.js的npm库,如vue-pdfPdfjs.list,看个人意愿想不想选择二次封装的插件,我个人不太喜欢用别人二次封装的东西。

方案优点缺点原理
a标签一行代码实现功能,最最最简单只能跳外链,且无法改UI利用浏览器自身的pdf处理
iframe / object /embed简单易用包含了打印、翻页、缩放等内嵌功能无法改UI,不支持打印、复制等功能的禁用将pdf 作为插件内嵌在这三个HTML标签内
jquery.media.js使用方便,包含打印等内嵌功能不支持IE,没法改UI将pdf转化为标签
PDFObject.js使用方便,包含打印等内嵌功能,可以判断是否支持预览没法改UI将pdf转化为标签
Pdf.js对浏览器的兼容性好,只需要支持H5,支持改UI,解析功能强大,最优解!跨域报错、不支持预览base64格式pdf文件流解析pdf为Canvas,放到viewer.html页面来展示,所以改viewer.html就能改UI。
vue-pdf样式组件可自定义,包含加载进度、翻页、页内元素可交互等固定宽高的比例,包裹 PDF的父容器如果过小,则无法完全展示基于pdf.js实现
vueshowpdf样式简单清爽包含翻页、缩放功能,可以禁止打印此包已弃用,建议不使用这个方案。基于pdf.js实现
Pdfjs.list对浏览器的兼容性好,解析功能依赖pdf.js,可以加水印源码用了很多草案语法如?.,需要升级babel的解析包才能用基于pdf.js

2.过程问题汇总

1)pdf 文件放在前端项目文件夹下,为何 pdf 出不来?

  是因为路径问题。将 pdf 放在 public > static 下,并用 /static/xxx.pdf 的路径方式进行引用( / 即代表 public)即可。

2)pdf 放在服务器上时,访问 pdf 文件跨域

  跨域问题一般是在后端这边没有配好 Access-Control-Allow-Origin 权限,我这边后端是使用 nginx 来进行代理,因此只需在 nginx 中配置好相关的跨域权限即可。具体参考文档:MDN 跨域相关文档

3)如何控制权限,让外部的人无法直接通过链接来访问 pdf?

  这边有两个思路,第一个是在发起文件请求时,在头部中带 token;第二个是使用防盗链技术。

防盗链(最终解决方案)

什么是防盗链?   浏览器在加载非本站的资源时,会增加一个头域,头域名字固定为:Referer。   而在 直接粘贴 地址到浏览器地址栏访问时,请求的是本站的该 url 的页面,是 不会有这个 referer 这个http头域的。举个例子: 将 http://.../xxx.pdf 复制进网页地址栏中访问,在调试工具的 Network 中的 headers 看不到 Referer 通过项目中的 src 对该资源进行访问,会出现 Referer   这个 referer 标签正是为了告诉请求响应者(被拉取资源的服务端),本次请求的引用页是谁,资源提供端可以分析这个引用者是否“友好”,是否允许其“引用”,对于不允许访问的引用者,可以不提供图片,这样访问者在页面上就只能看到一个图片无法加载的浏览器默认占位的警告图片,甚至服务端可以返回一个默认的提醒勿盗链的提示图片。   总结:防盗链只允许在名单上的人访问,而不在名单上的人禁止访问。 如何使用防盗链?   一般的站点或者静态资源托管站点来提供防盗链的设置,也就是让服务端识别指定的 Referer,在服务端接收到请求时,通过匹配 referer 头域与配置,对于 指定放行,对于其他 referer 视为盗链。   因此,在服务端配置相关防盗链配置,并 放行前端访问的IP 即可(这里因为是后端同学配的,感兴趣的同学自行上网搜索下如何配置) 参考网址:juejin.cn/post/684490… juejin.cn/post/707159… juejin.cn/post/684490… blog.csdn.net/weixin_4267…

  •