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,表明此次返回的文件范围
项目源码
前端:
链接:https://pan.baidu.com/s/1xUfVaBSkr4T-PPAw2nEzfg?pwd=c3pf
提取码:c3pf
后端:
链接:https://pan.baidu.com/s/1m8IFVfOiDbMXKUdx1D_NoQ?pwd=otzy
提取码:otzy