本文的解决方案旨在解决大体积PDF在线浏览加载缓慢、影响用户体验的难题。通过利用分片加载技术,前端请求时附带range及读取大小信息,后端据此返回相应的PDF文件流。这种方式有效地减轻了服务器和浏览器的负担,提升了加载速度和用户体验。同时解决了首次加载全部分片导致浏览器内存不足的问题。
技术栈:Spring Boot、Vue和pdf.js。
index.vue
首先确保vue需要的运行环境已经安装(nodejs),我使用的版本:12.18.2,然后使用vscode打开项目,在终端输入命令:
npm install npm run serve
本示例只是一个简单的springboot项目,核心文件PDFController.java用于分片加载接口,CORSFilter.java为跨域配置
这段代码实现了使用 PDF.js 进行分片加载 PDF 文件的功能。下面是代码的主要实现思路:
这样,客户端就可以使用 PDF.js 来分片加载显示 PDF 文件了。
PDFController.java
/** /** * pdf分片加载的后端实现 * * @param response * @param request * @throws FileNotFoundException */ @GetMapping("/load") public void loadPDFByPage(HttpServletResponse response, HttpServletRequest request) throws FileNotFoundException { // 获取pdf文件,建议pdf大小超过20mb以上 File pdf = ResourceUtils.getFile("classpath:需要分片加载的pdf.pdf"); byte[] pdfData = new byte[0]; try { pdfData = FileUtils.readFileToByteArray(pdf); } catch (IOException e) { throw new RuntimeException(e); } // 以下为pdf分片的代码 try (InputStream is = new ByteArrayInputStream(pdfData); BufferedInputStream bis = new BufferedInputStream(is); OutputStream os = response.getOutputStream(); BufferedOutputStream bos = new BufferedOutputStream(os)) { // 下载的字节范围 int startByte, endByte, totalByte; // 获取文件总大小 int fileSize = pdfData.length; int minSize = 1024 * 1024; // 如果文件小于1 MB,直接返回数据,不需要进行分片 if (fileSize < minSize) { // 直接返回整个文件 response.setStatus(HttpServletResponse.SC_OK); response.setContentType("application/octet-stream"); response.setContentLength(fileSize); bos.write(pdfData); return; } // 根据HTTP请求头的Range字段判断是否为断点续传 if (request == null || request.getHeader("range") == null) { // 如果是首次请求,返回全部字节范围 bytes 0-7285040/7285041 totalByte = is.available(); startByte = 0; endByte = totalByte - 1; response.setStatus(HttpServletResponse.SC_OK); // 写入一些数据到输出流中,否则火狐浏览器会报错:ns_error_net_partinal_transfer bos.write(1); } else { // 断点续传逻辑 String[] range = request.getHeader("range").replaceAll("[^0-9\\-]", "").split("-"); // 文件总大小 totalByte = is.available(); // 下载起始位置 startByte = Integer.parseInt(range[0]); // 下载结束位置 endByte = range.length > 1 ? Integer.parseInt(range[1]) : totalByte - 1; // 跳过输入流中指定的起始位置 bis.skip(startByte); // 表示服务器成功处理了部分 GET 请求,返回了客户端请求的部分数据。 response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); int bytesRead, length = endByte - startByte + 1; byte[] buffer = new byte[1024 * 64]; while ((bytesRead = bis.read(buffer, 0, Math.min(buffer.length, length))) != -1 && length > 0) { bos.write(buffer, 0, bytesRead); length -= bytesRead; } } // 表明服务器支持分片加载 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(endByte - startByte + 1); // 需要设置此属性,否则浏览器默认不会读取到响应头中的Accept-Ranges属性, // 因此会认为服务器端不支持分片,所以会直接全文下载 response.setHeader("Access-Control-Expose-Headers", "Accept-Ranges,Content-Range"); // 第一次请求直接刷新输出流,返回响应 response.flushBuffer(); } catch (IOException e) { e.printStackTrace(); } }
CORSFilter.java 通用的跨域配置
package com.example.pdfload.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 { } }
首次访问返回状态码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(endByte - startByte + 1); // 需要设置此属性,否则浏览器默认不会读取到响应头中的Accept-Ranges属性, // 因此会认为服务器端不支持分片,所以会直接全文下载 response.setHeader("Access-Control-Expose-Headers", "Accept-Ranges,Content-Range");
分片加载返回状态码206,返回响应信息如下:
链接: https://pan.baidu.com/s/1KNn2HE_ZudMRyzPK8_wIjA?pwd=zhou
提取码: zhou