PDF.js实现按需加载pdf文件
- 说明
- 前言
- 前端项目
- 分片加载的效果
- 前端项目结构
- 前端核心代码
- 项目运行与访问
- 后端项目
- 项目结构
- 核心代码实现
- 注意事项
- 项目源码
- 关于没有实现按需加载的可能
说明
本文主要是介绍pdf.js的前后端项目的实现,包含可直接运行的源码。由于本人偏向于后端开发,因此前端的vue方面的demo介绍可能略有不足之处,敬请谅解。可运行源码放在文章末尾处,如果项目运行问题可私信
前言
本文主要是解决大体积pdf在线浏览加载缓慢,影响用户体验的问题。以及实现了分片加载后的,首次加载时自动加载了全部的pdf分片,导致浏览器报出内存不足的问题
技术栈为:SpringBoot、Vue、pdfjs
主要核心思路:前端请求时请求头附带请求范围range及读取大小,后端根据请求头返回相应的pdf文件流
前端项目
分片加载的效果
前端项目结构
前端核心代码
Home index.vue
<el-button type="primary" @click="toLoad">预览文件</el-button>
按钮触发跳转到Pdf文件夹index.vue页面
<template> <div class="container"> <el-button type="primary" @click="toLoad">预览文件</el-button> </div> </template> <script> export default { data() { return {}; }, mounted() {}, methods: { toLoad() { this.$router.push("/pdf"); }, }, }; </script> <style lang="scss" scoped> .container { height: 100vh; display: flex; align-items: center; justify-content: center; } </style>
复制
Pdf index.vue
获取后端返回pdf文件流的接口,this.baseUrl为本地的后端项目接口:http://localhost:8181
this.src =${this.baseUrl}/v1/pdf/load
;
<template> <div class="pdf"> <iframe :src="`/static/pdf/web/viewer.html?file=${encodeURIComponent(src)}`" frameborder="0" style="width: 100%; height: calc(100vh)" ></iframe> </div> </template> <script> import baseUrl from "@/api/baseurl.js"; export default { data() { return { baseUrl: baseUrl.baseUrl, src: "", loading: false, }; }, created() {}, methods: { getPdfCode: function () { this.loading = true; // 数据文件流 转成 pdf this.src = `${this.baseUrl}/v1/pdf/load`; }, // 禁用鼠标右击、F12 来禁止打印和打开调试工具 prohibit() { document.oncontextmenu = function (ev) { return false; //屏蔽右键菜单 }; document.onkeydown = function (e) { if ( e.ctrlKey && (e.keyCode === 65 || e.keyCode === 67 || e.keyCode === 73 || e.keyCode === 74 || e.keyCode === 80 || e.keyCode === 83 || e.keyCode === 85 || e.keyCode === 86 || e.keyCode === 117) ) { return false; } if (e.keyCode === 18 || e.keyCode === 123) { return false; } }; }, }, mounted() { this.$nextTick(() => { this.getPdfCode(); }); }, }; </script> <style lang="scss" scoped></style>
复制
项目运行与访问
首先确保vue需要的运行环境已经安装,然后使用vscode打开项目,在终端输入命令:
npm install
复制
执行完成后,输入运行命令
npm run serve
复制
后端项目
项目结构
后端项目是我在学习别的项目时创建,因此在上面临时写了一个分片加载的接口,直接运行即可
核心代码实现
pdf分片加载的后端实现类 PDFController.java
package com.zhouquan.controller; import org.springframework.util.ResourceUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.*; /** * @author zhouquan * @description todo * @date 2022-07-29 10:29 **/ @RestController @RequestMapping("/v1/pdf") public class PDFController { /** * pdf分片加载的后端实现 * * @param response * @param request * @throws FileNotFoundException */ @GetMapping("/load") public void loadPDFByPage(HttpServletResponse response, HttpServletRequest request) throws FileNotFoundException { File pdf = ResourceUtils.getFile("classpath:泛函分析教程(第2版).pdf"); try ( InputStream is = new FileInputStream(pdf); BufferedInputStream bis = new BufferedInputStream(is); OutputStream os = response.getOutputStream(); BufferedOutputStream bos = new BufferedOutputStream(os)) { // 下载的字节范围 int startByte, endByte, totalByte; if (request != null && request.getHeader("range") != null) { // 断点续传 String[] range = request.getHeader("range").replaceAll("[^0-9\\-]", "").split("-"); // 文件总大小 totalByte = is.available(); // 下载起始位置 startByte = Integer.parseInt(range[0]); // 下载结束位置 if (range.length > 1) { endByte = Integer.parseInt(range[1]); } else { endByte = totalByte - 1; } // 返回http状态 response.setStatus(206); } else { // 正常下载 // 文件总大小 totalByte = is.available(); // 下载起始位置 startByte = 0; // 下载结束位置 endByte = totalByte - 1; // 返回http状态 response.setHeader("Accept-Ranges", "bytes"); response.setStatus(200); } // 需要下载字节数 int length = endByte - startByte + 1; //表明服务器支持分片加载 response.setHeader("Accept-Ranges", "bytes"); //Content-Range: bytes 0-65535/408244,表明此次返回的文件范围 response.setHeader("Content-Range", "bytes " + startByte + "-" + endByte + "/" + totalByte); //告知浏览器这是一个字节流,浏览器处理字节流的默认方式就是下载 response.setContentType("application/octet-stream"); //表明该文件的所有字节大小 response.setContentLength(length); //需要设置此属性,否则浏览器默认不会读取到响应头中的Accept-Ranges属性,因此会认为服务器端不支持分片,所以会直接全文下载 response.setHeader("Access-Control-Expose-Headers", "Accept-Ranges,Content-Range"); // 响应内容 bis.skip(startByte); int len = 0; byte[] buff = new byte[1024 * 64]; while ((len = bis.read(buff, 0, buff.length)) != -1) { if (length <= len) { bos.write(buff, 0, length); break; } else { length -= len; bos.write(buff, 0, len); } } } catch (IOException e) { e.printStackTrace(); } } }
复制
跨域配置类 CORSFilter.java
package com.zhouquan.filter; import org.springframework.stereotype.Component; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class CORSFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletResponse response1 = (HttpServletResponse) response; response1.addHeader("Access-Control-Allow-Credentials", "true"); response1.addHeader("Access-Control-Allow-Origin", "*"); response1.addHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT"); response1.addHeader("Access-Control-Allow-Headers", "range,Accept-Ranges,Content-Range,Content-Type," + "X-CAF-Authorization-Token,sessionToken,X-TOKEN,Cache-Control,If-Modified-Since"); if (((HttpServletRequest) request).getMethod().equals("OPTIONS")) { response.getWriter().println("ok"); return; } chain.doFilter(request, response); } @Override public void destroy() { } @Override public void init(FilterConfig filterConfig) throws ServletException { } }
复制
注意事项
1.首次加载返回状态码200,注意以下属性服务器端在响应头中务必要加上
//表明服务器支持分片加载 response.setHeader("Accept-Ranges", "bytes"); //Content-Range: bytes 0-65535/408244,表明此次返回的文件范围 response.setHeader("Content-Range", "bytes " + startByte + "-" + endByte + "/" + totalByte); //告知浏览器这是一个字节流,浏览器处理字节流的默认方式就是下载 response.setContentType("application/octet-stream"); //表明该文件的所有字节大小 response.setContentLength(length); //需要设置此属性,否则浏览器默认不会读取到响应头中的Accept-Ranges属性,因此会认为服务器端不支持分片,所以会直接全文下载 response.setHeader("Access-Control-Expose-Headers", "Accept-Ranges,Content-Range");
复制
2.之后每次请求都会返回206,即已经实现分片加载。Content-Range: bytes 0-65535/408244,表明此次返回的文件范围
项目源码
前端:
前端基于Vue实现pdf.js对pdf的分片加载和按需加载功能
后端: 可私信找我要
Springboot实现pdf的分片加载功能
或者
https://zhouquanquan.lanzouw.com/b04e69lsd
密码:cj41
复制
关于没有实现按需加载的可能
描述
pdf.js实现了分片加载,但是在首次加载时自动加载了全部的pdf分片,导致打开了几个超过1g的pdf时,浏览器报出内存不足的提示
解决方案
实现pdf.js的按需加载,只需要修改属性disableAutoFetch为true,表示关闭自动获取,只会请求需要的分片,从而实现了按需加载,disableAutoFetch的默认值是false,表示会自动获取所有分片