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



















