整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

基于HTML5 构建的 Web端现代化PDF在线预览插件-PDF.js

PDF文件现在在许多企业中常用 - 无论您是要生成销售报告,交付合同还是发送发票,PDF都是首选的文件类型。PDF.js是由Mozilla编写的JavaScript库。由于它使用vanilla JavaScript实现PDF渲染,因此它具有跨浏览器兼容性,并且不需要安装其他插件。在使用PDFJS之前你也可以先了解下原生的PDF<object>对象,本文仅介绍PDFJS。

官网地址

https://mozilla.github.io/pdf.js/

下载和安装

官网提供了下载入口,有稳定版和Beta版,我们要在生产环境下使用建议使用稳定版,官网给我们提供了三种获取PDF.js的方式

使用说明

我们可以直接使用cdn服务,也可以将下载的文件引入,我们看一下示例代码,这里我提供了两种写法,在项目运行之前,请确保你的同级目录下有一个test.pdf文件

//index.html
<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <meta http-equiv="X-UA-Compatible" content="ie=edge">
 <script src="https://cdn.jsdelivr.net/npm/pdfjs-dist@2.0.943/build/pdf.min.js"></script>
 <script src='./index.js'></script>
 <title>PDF</title>
</head>
<body>
 <canvas id="pdf"></canvas>
</body>
</html>
//index.js
// var loadingTask = pdfjsLib.getDocument("test.pdf");
// loadingTask.promise.then(
// function(pdf) {
// // 加载第一页
// pdf.getPage(1).then(function(page) {
// var scale = 1;
// var viewport = page.getViewport(scale);
// //应用到页面的canvas上.
// var canvas = document.getElementById("pdf");
// var context = canvas.getContext("2d");
// canvas.height = viewport.height;
// canvas.width = viewport.width;
// // 渲染canvas.
// var renderContext = {
// canvasContext: context,
// viewport: viewport
// };
// page.render(renderContext).then(function() {
// console.log("Page rendered!");
// });
// });
// },
// function(reason) {
// console.error(reason);
// }
// );
// index.js
(async () => {
 const loadingTask = pdfjsLib.getDocument("test.pdf");
 const pdf = await loadingTask.promise;
 // 加载第一页.
 const page = await pdf.getPage(1);
 const scale = 1;
 const viewport = page.getViewport(scale);
 // 应用到页面的canvas上.
 const canvas = document.getElementById("pdf");
 const context = canvas.getContext("2d");
 canvas.height = viewport.height;
 canvas.width = viewport.width;
 // 渲染canvas.
 const renderContext = {
 canvasContext: context,
 viewport: viewport
 };
 await page.render(renderContext);
})();

当我们运行项目之后,打开浏览器查看,它已经将pdf的内容渲染到了浏览器中,且显示了第一页,如下图所示:

如果就这样的话远远是无法满足我们使用的,因此我们来看一下它比较高级的用法,或者说简单的用法,高级的功能。

使用iframe

首先我们将我们下载的js包加压,复制里面的web文件夹,粘贴到你的项目目录

然后修改你的index.html代码,首先注释掉之前引入的js代码,然后修改body,如下

<body>
 <iframe src="test.pdf" style="border: none;" width="100%" height='1000px'></iframe>
</body>

随后打开我们的浏览器,你会发现一个预览的窗口

它继承了我们常用的功能,比如旋转、下载、打印、自适应缩放、放大、缩小等,我们只需要使用iframe引入我们的pdf文件即可,其余的全部交给pdf来完成,即可获得一个实现一个完整的pdf预览功能。

PDF.js三个不同层

  • 核心 - PDF的二进制格式在此层中进行解释。直接使用该层被认为是高级用法。
  • 显示 - 该层构建在核心层之上,为大多数日常工作提供易于使用的界面。
  • Viewer - 除了提供编程API之外,PDF.js还附带一个现成的用户界面,其中包括对搜索,旋转,缩略图侧边栏和许多其他内容的支持。

PDFJS的这三层分开,让我们很好的来根据业务需求来实现我们想要的部分,其简单的api让我们得心应手,总而言之,PDFJS是一个绝佳的PDF预览解决方案。

总结

PDFJS不仅仅支持pdf的二进制文件,同样还支持base64编码的pdf,如果在你的项目中需要用到pdf的预览等功能,无疑它是一种良好的解决方案,当然想要实现相同的功能有许多办法,我们可以选择最适合我们需求的,官方还提供了一个完整的演示Demo,如下截图,如果你觉得本文对你有帮助,请麻烦转发、点赞加关注吧,后续会分享更多实用有趣的技术!

端实现文件预览功能

需求:实现一个在线预览pdf、excel、word、图片等文件的功能。
介绍:支持pdf、xlsx、docx、jpg、png、jpeg。
以下使用Vue3代码实现所有功能,建议以下的预览文件标签可以在外层包裹一层弹窗。

图片预览

iframe标签能够将另一个HTML页面嵌入到当前页面中,我们的图片也能够使用iframe标签来进行展示。

<iframe  :src="图片地址"
         style="z-index: 1000; height:650px; width: 100%; margin: 0 auto"
         sandbox="allow-scripts allow-top-navigation allow-same-origin allow-popups"
>

「sandbox」这个属性如果是单纯预览图片可以不使用,该属性对呈现在 iframe 框架中的内容启用一些额外的限制条件。属性值可以为空字符串(这种情况下会启用所有限制),也可以是用空格分隔的一系列指定的字符串。

  • allow-scripts: 允许嵌入的浏览上下文运行脚本(但不能创建弹窗)。如果没有使用该关键字,就无法运行脚本。
  • allow-top-navigation: 允许将框架内所加载页面中的超链接导航到父级窗口
  • allow-same-popups: 允许弹窗 (例如 window.open, target="_blank")。如果没有使用该关键字,相应的功能将自动被禁用。
  • allow-same-origin: 如果没有使用该关键字,嵌入的浏览上下文将被视为来自一个独立的源,这将使 same-origin policy 同源检查失败。使用了这个属性,那么当前页面和iframe打开的页面视为同源。

word文档预览(docx)

先下载npm包
npm i docx-preview --save
<div class="docxRef"></div>

<script>
import { renderAsync } from 'docx-preview';

function fn() {
// 这里的res.data是 blob文件流,如果自己的不是blob文件流
// 可以通过URL.createObjectURL(参数) 参数为File格式,转换为blob文件流
    let blob = res.data
    let childRef = document.getElementsByClassName('docxRef');
    renderAsync(blob, childRef[0]) //渲染
}
fn()

</script>

「blob文件流」

预览excel文件(xlsx)

下载包
npm install xlsx@0.16.0
<div class="xlsxClass"></div>
const reader = new FileReader();
//通过readAsArrayBuffer将blob转换为ArrayBuffer对
reader.readAsArrayBuffer(res.data) // 这里的res.data是blob文件流
reader.onload = (event) => {
  // 读取ArrayBuffer数据变成Uint8Array
  var data = new Uint8Array(event.target.result);
  // 这里的data里面的类型和后面的type类型要对应
  var workbook = XLSX.read(data, { type: "array" });
  var sheetNames = workbook.SheetNames; // 工作表名称
  var worksheet = workbook.Sheets[sheetNames[0]];
  // var excelData = XLSX.utils.sheet_to_json(worksheet); //JSON
  let html = XLSX.utils.sheet_to_html(worksheet);
  document.getElementsByClassName('xlsxClass')[0].innerHTML = html
};

pdf预览

下载包 npm install pdfjs-dist
我使用的是npm install pdfjs-dist@2.0.943版本,以下例子使用的是vue3+vite创建的项目
以下例子通过canvas来渲染pdf
<template>
  <div class="box">
    <div class="tool-bar">
      <div>{{ pdfParams.pageNumber }} / {{ pdfParams.total }}</div>
      <button type="primary" :disabled="pdfParams.pageNumber == pdfParams.total" @click="nextPage">下一页
      </button>
      <button type="primary" :disabled="pdfParams.pageNumber == 1" @click="prevPage">上一页</button>
    </div>
    <canvas id="pdf-render"></canvas>
  </div>
</template>

<script setup>
import { onMounted, ref, reactive } from 'vue'
const pdfParams = reactive({
  pageNumber: 1, // 当前页
  total: 0, // 总页数
});

// 不要定义为ref或reactive格式,就定义为普通的变量
let pdfDoc = null;
// 这里必须使用异步去引用pdf文件,直接去import会报错,也不知道为什么
onMounted(async ()=> {
  let pdfjs = await import('pdfjs-dist/build/pdf')
  let pdfjsWorker = await import('pdfjs-dist/build/pdf.worker.entry')
  pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorker
  // 此文件位于public/test2.pdf
  let url = ref('/test2.pdf')
  pdfjs.getDocument(url.value).promise.then(doc => {
    pdfDoc = doc
    pdfParams.total = doc.numPages
    getPdfPage(1)
  })
})

// 加载pdf的某一页
const getPdfPage = (number) => {
  pdfDoc.getPage(number).then(page => {
    const viewport = page.getViewport()
    const canvas = document.getElementById('pdf-render')
    const context = canvas.getContext('2d')
    canvas.width = viewport.viewBox[2]
    canvas.height = viewport.viewBox[3]
    viewport.width = viewport.viewBox[2]
    viewport.height = viewport.viewBox[3]
    canvas.style.width = Math.floor(viewport.width) + 'px'
    canvas.style.height = Math.floor(viewport.height) + 'px'

    let renderContext = {
      canvasContext: context,
      viewport: viewport,
      // 这里transform的六个参数,使用的是transform中的Matrix(矩阵)
      transform: [1, 0, 0, -1, 0, viewport.height]
    }
    // 进行渲染
    page.render(renderContext)
  })
}
// 下一页功能
const prevPage = () => {
  if(pdfParams.pageNumber > 1) {
    pdfParams.pageNumber -= 1
  } else {
    pdfParams.pageNumber  = 1
  }
  getPdfPage(pdfParams.pageNumber)
}
// 上一页功能
const nextPage = () => {
  if(pdfParams.pageNumber < pdfParams.total) {
    pdfParams.pageNumber += 1
  } else {
    pdfParams.pageNumber = pdfParams.total
  }
  getPdfPage(pdfParams.pageNumber)
}
</script>

以上pdf代码引用文章:(54条消息) 前端pdf预览、pdfjs的使用_pdf.js_无知的小菜鸡的博客-CSDN博客
pdfjs官方代码:例子 (mozilla.github.io)
以上代码看不懂的地方可以查阅官方代码,大部分都是固定的写法。

「以上注意点:」

  • 必须异步引用pdf的文件!!!
  • pdf演示文件位于public/test2.pdf
  • transform: [1, 0, 0, -1, 0, viewport.height],使用了transform中的Matrix(矩阵)
  • 下一页和上一页功能都需要重新渲染

近在实现共享 PDF 文档的需求,存在主讲人这样一个角色,上传 PDF 文档后,通知其它连接中的终端,进行实时同步展示的功能。对于这样的需求,pdf.js 成功的让我想起了它。

PDF 文档的预览,总的就是要加载速度快,尽最快的速度完成渲染,呈现给用户看,不要出现长时间的白屏或 Loading 状态的现象,另外 PDF 文档需要支持翻页等操作。具体看看一步步的实现。

文档分片下载速度

PDF 文档上传

分片上传文档,支持秒传,VUE 支持分片上传的插件一搜一大把,可以采用 vue-simple-uploader 等,具体如何实现,这里不详细论述,简单贴一下秒传校验的实现。

import SparkMD5 from 'spark-md5';
/**
 * 文件秒传 MD5 校验
 * @param file 上传的文件信息
 */
md5File(file) {
    const fileReader = new FileReader(),
        blobSlice = File.prototype.slice,
        chunkSize = 1024 * 1000,						// 分片大小
        chunks = Math.floor(file.size / chunkSize),		// 总的分片数量
        spark = new SparkMD5.ArrayBuffer();				// 三方库 SparkMD5
    let currentChunk = 0;
    // 加载分片
    const loadNext = () => {
        const start = currentChunk * chunkSize;
        let end = file.size;
        if (currentChunk < chunks - 1) end = start + chunkSize;
        fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
    }
    // 暂停文件上传
    file.pause();
  	// 开始校验文件MD5
    loadNext();
    fileReader.onload = (e) => {
        spark.append(e.target.result);
      	// 小于总分片, 继续加载
        if (currentChunk < chunks - 1) {
            currentChunk++;
            loadNext();
        } else {
          	// 分片全部加载完成, 生成 MD5
            const md5 = spark.end();
          	// 开始服务端校验 MD5 ( 秒传 )
            this.md5Success(md5, file);
        }
    };
    fileReader.onerror = () => {
      	// 文件读取出错, 取消上传
        console.log(`文件${file.name}读取出错,请检查该文件`);
        file.cancel();
    };
}

文件秒传MD5校验

PDF 文档分片

一个 PDF 文档,无法一次就预览所有内容,在有限的可视区域内,只能显示有限的内容,那我们就获取能在有限区域内所能展示的那部分内容,以加快 Content Download 的速度,减少用户第一次打开时的 Loading 时间。

假设一个 PDF 文档有 1000 页,以 5 页为一片,将该文档切分成 200 个分片,首次打开默认请求第一个分片,其后根据翻页来确定是否继续加载后续的分片信息(如需刷新后仍然展示刚刚所在页,则需记录当前页,根据该值与分片的页数来确定当前属于第几个分片,进而再请求相应分片即可)。

服务端如何进行分片,则交给服务端就好了,这里就不详细说了(得注意下中文乱码的情况)。假设文件信息格式及单个分片的请求地址如下所示:

/**
 * 文件信息.
 * 在文件上传后即可拿到.
 */
const file = {
  	id: 1,
    md5: 'e10adc3949ba59abbe56e057f20f883e',
    total: 1000,
    name: 'VUE 如何实现高性能的 PDF 在线预览',
    // ...
}

/**
 * 请求分片.
 * $http 是我针对 axios 的一些常用方法,拦截器等重新封装后工具类库
 */

文件信息格式及分片请求地址格式

请求 PDF 分片

pdf.js 接口中,getDocument 可用于获取远程文档,返回 PDFDocumentLoadingTask 对象,该对象是一个下载远程 PDF 文档的任务,提供了一些监听方法,可通过 promise 拿到下载完成的 PDF 对象,最终会生成并返回 PDFDocumentProxy 对象,我们接下来所有的操作都是基于该代理类进行的。

注意在 PDF 文档中存在有中文时,会出现不显示的情况,控制台也会报如下的错误提示

Warning: The CMap "baseUrl" parameter must be specified, ensure that the "cMapUrl" and "cMapPacked" API parameters are provided.

主要是 PDF 文档内容存在不支持的字体,暂且引入三方字体来解决该问题

const url = `https://www.makeit.vip/${md5}-${page}.pdf?id=${fid}&token=${token}`,
			cMapUrl = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.4.456/cmaps/';
const 
promise = PDFJS.getDocument({
			url,
			cMapUrl,
			cMapPacked: true
		}).promise;

渲染 PDF 文档

因为我实现的是 PDF 一页的内容,按屏幕尺寸 100% 宽度来显示的,这样很容易高度就超出可视范围了,所以单页采用滚动形式,多页则采用按钮触发翻页的形式来展示(为了更好的适配不同尺寸的屏幕,显示的效果与主讲人完全同步),并非采用一字往下无限排,滚动翻页的形式。

/**
 * 获取分片
 * @param fid 文件ID
 * @param md5 文件唯一标识
 * @param token 授权码
 * @param num 第N个分片
 */
public getFragmentation(
	fid: number,
	md5: string,
	token: string,
	num = 0
) {
	// 请求分片, 得到 promise...
	// ...
	promise.then((pdf: any) => {
	  	for (let i = 1; i <= pdf.numPages; i++) {
	  		pdf.getPage(i).then((page) => {
	  			const pagination = num * 5 + i;
	  			this.renderPage(pagination, page);
	  		});
	  	}
	});
}


/**
 * 渲染分页内容.
 * @param pagination 第N页
 * @param page 分页属性
 */
protected renderPage(
	pagination: number,
	page: any
) {
	// 根据缩放比例, 获取文档的可视属性
	const viewport = page.getViewport({scale: 1});
	// 创建用于渲染的Canvas元素
	const canvas = document.createElement('canvas'),
		context = canvas.getContext('2d');
		canvas.width = viewport.width;
		canvas.height = viewport.height;
	// 渲染文档
	const renderContext = {
		canvasContext: context,
		viewport
	};
	return page.render(renderContext).promise;
}

PDF 文档翻页

在上一页/下一页的不断操作中,1000页的内容,不断的进行渲染,难不成要渲染1000个DOM出来?显然不合理,非得把浏览器给搞崩了才肯罢休吗?几十个 Canvas 就让你卡的不要不要的了。具体实现也简单,保证只显示 5 个的前提下,根据上一页或下一页的操作,增加或删除相应的 DOM即可。最后贴一下稍微完整一些的代码(稍微加了一些注释)。

  1. 获取分片
/**
 * 获取分片
 * @param fid 文件ID
 * @param md5 文件唯一标识
 * @param token 授权码
 * @param num 第N个分片
 * @param showPage 显示第N个分片中的第X页
 * @param speaker 是否为主讲人
 * @param render 是否直接渲染
 * @param clear 是否清除原有内容
 */
public getFragmentation(
	fid: number,
	md5: string,
	token: string,
	num: number = 0,
	showPage = 1,
	speaker?: boolean,
	render?: boolean,
	clear?: boolean
): Promise<any> {
	const url = `${process.env.VUE_APP_PROXY_SERVER}/${md5}-${num}.pdf?id=${fid}&token=${token}`,
		cMapUrl = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.4.456/cmaps/';
	let promise = PDFJS.getDocument({
		url,
		cMapUrl,
		cMapPacked: true
	}).promise;
	promise.then((pdf: any) => {
		/** 记录 PDFDocumentProxy 对象, 可避免重复请求已经请求过的分片 */
		this.files.page = showPage;
		if (!this.files.pdfs) this.files.pdfs = {} as any;
		this.files.pdfs[num] = pdf;
	});
	/** 是否执行渲染操作 */
	if (render) {
		/** 清除 */
		if (clear) {
			const documents = this.getContainer() as HTMLDivElement;
			if (documents) documents.innerHTML = '';
		}
		/** 渲染 - 重新赋值 promise, 保证加载完成后的操作时序 */
		promise = new Promise((resolve) => {
			promise.then((pdf: any) => {
				/** 开始遍历循环 */
				for (let i = 1; i <= pdf.numPages; i++) {
					pdf.getPage(i).then((page) => {
						const pagination = num * 5 + i;
						if (!this.files.paginations[pagination]) {
							/** 存储分页信息(宽/高/ID等 - ID可用于判断是否已经渲染及清除DOM操作) */
							this.files.paginations[pagination] = {} as any;
						}
						this.files.paginations[pagination].id = Utils.uid();
						const renderFinish = this.renderPage(pagination, page, speaker);
						if (renderFinish) {
							renderFinish.then(() => {
								if (i === pdf.numPages) {
									/**
									 * 1. 当前为最后一页或倒数第2页, 请求下一分片
									 * 2. 当前为第一页或第2页, 请求上一分片
									 */
									const left = showPage % 5;
									if (
										left === 0 ||
										left === 4
									) {
										/** 回调 - 请求下一个分片 */
										if (num + 1 <= this.files.total) {
											this.getFragmentation(
												fid,
												md5,
												token,
												num + 1,
												showPage,
												speaker
											).then(() => {
												/**
												 * 下一个分片请求成功后的处理
												 * 根据剩余个数, 决定继续渲染下一分片的1页还是2页
												 */
												this.getFragmentationSuccess(
													showPage,
													left ? 1 : 0
												);
											});
										}
									} else if (
										left === 1 ||
										left === 2
									) {
										/** 回调 - 请求上一个分片 */
										if (num - 1 >= 0) {
											this.getFragmentation(
												fid,
												md5,
												token,
												num - 1,
												showPage,
												speaker
											).then(() => {
												this.getFragmentationSuccess(
													showPage,
													left === 2 ? 1 : 0,
													'prev'
												);
											});
										}
									}
									/**
									 * 渲染完成后返回.
									 * 因为我需要5页全部渲染完成后,初始化每一页上面的涂鸦功能,
									 * 所以我在最后才返回 Promise, 以保证时序的正确性.
									 * 若仅仅是展示, 没有其它功能的话, 无需返回 Promise.
									 */
									resolve();
								}
							});
						}
					});
				}
			});
		});
	}
	return promise;
}
  1. 渲染页面
/**
 * 渲染分页内容.
 * @param pagination 第N页
 * @param page 分页属性
 * @param speaker 是否为主讲人
 * @param type 类型(上下页区分)
 */
protected renderPage(
	pagination: number,
	page: any,
	speaker = false,
	type = 'next'
): Promise<any> | void {
	const documents = this.getContainer() as HTMLDivElement;
	if (documents) {
		const item = this.createPage(pagination),
			pageView = page.view,
			scale = this.getScale(pageView, speaker, item),
			viewport = page.getViewport({scale});
		if (this.files.page !== pagination) item.style.display = 'none';
		/** 这个就是保存一些用得到的属性, 具体实现代码就不贴出来了. */
		this.setPaginationAttrs(
			pagination,
			viewport,
			pageView,
			scale
		);
		/** 创建元素 */
		const canvas = document.createElement('canvas'),
			context = canvas.getContext('2d');
		canvas.width = viewport.width;
		canvas.height = viewport.height;
		item.appendChild(canvas);
		/** 判断是要插入还是追加元素 */
		if (type === 'next') documents.appendChild(item);
		else if (documents.firstChild) documents.insertBefore(item, documents.firstChild);
		/** 渲染文档 */
		const renderContext = {
			canvasContext: context,
			viewport
		};
		return page.render(renderContext).promise;
	}
}
  1. 获取缩放比
/**
 * 获取缩放比.
 * @param origin 文档原始尺寸
 * @param speaker 是否为主讲人(主讲人默认以宽为基准)
 * @param wrapper 画布容器(超出宽度的话, 需要手动设置高度)
 * @param width 待变更元素的宽度
 */
protected getScale(
	origin: any,
	speaker: boolean,
	wrapper?: HTMLDivElement,
	width?: number
): number {
	width = width ?? 0;
	const documents = this.getContainer() as HTMLDivElement;
	if (documents && !width) width = documents.offsetWidth;
	if (speaker) {
		/**
		 * 左右两边增加了一些偏移量(主讲人与普通用户大小不一样, 这个函数的代码也没啥好贴)
		 * 主讲人默认是以屏幕宽度为基准进行文档缩放的.
		 */
		const offsetWidth = this.getOffsetWidth(width);
		return Math.round(offsetWidth / origin[2] * 100) / 100;
	} else {
		/**
		 * 非主讲人为了与主讲人显示内容一致, 默认采用高度为基准, 但有特殊情况,
		 * 就是高度保证一致的情况下, 宽度却超出了屏幕的可视区域, 这时候就要将
		 * 文档显示区域所在的容器高度进一步缩短, 保证宽度是在可视区域内, 具体
		 * 实现就看 getHeightAndScale 这个方法了.
		 */
		const heightAndScale = this.getHeightAndScale(documents, origin),
			scale = heightAndScale.scale;
		if (wrapper && heightAndScale.height) wrapper.style.height = `${heightAndScale.height}px`;
		return scale;
	}
}
  1. 判定宽度是否超出可视区域
/**
 * 获取文档显示高度与缩放比例.
 * @param documents
 * @param origin
 */
protected getHeightAndScale(
	documents?: HTMLDivElement,
	origin?: any
): {
	height: number;
	scale: number;
} {
	documents = documents ?? this.getContainer() as HTMLDivElement;
	let wrapperHeight = 0, lastScale = 0;
	if (documents) {
		const size = this.files.speaker,		// 所记录的主讲人的屏幕尺寸
			width = documents.offsetWidth,		// 当前用户显示容器的可视宽度
			height = documents.offsetHeight;	// 当前用户显示容器的可视高度
		let originWidth;
		/** 获取原始文档宽度 */
		if (!origin) {
			const pagination = this.getActivePagination();
			originWidth = pagination.originWidth;
		} else {
			originWidth = origin[2];
		}
		/**
		 * 计算主讲人的缩放比.
		 * 往下出现的 200 / 150 之类的常数, 为设定好的显示偏移量.
		 */
		const speakerRatio = Math.round((size.width - 200) / originWidth * 100) / 100;
		/** 非主讲人默认以高度为基准来计算文档显示的缩放比例 */
		lastScale = Math.round(speakerRatio / (size.height - 150) * (height - 100) * 100) / 100;
		/** 如果以高度为基准的情况下, 判定宽度是否超出可视区域 */
		const destWidth = Math.round(originWidth * lastScale * 100) / 100,
			offsetWidth = width - (this.isSpeaker() ? 200 : 120),
			diffWidth = destWidth - offsetWidth;
		if (diffWidth > 0) {
			/**
			 * 如果超出可视区域, 重新设定缩放比,
			 * 文档内容显示所在的DIV容器, 将进一步缩小,
			 * 以保证宽度在正常的可视区域内
			 */
			wrapperHeight = (Math.round((offsetWidth * (size.height - 150)) / (size.width - 200) * 100) / 100);
			lastScale = Math.round(offsetWidth / originWidth * 100) / 100;
		}
	}
	return {
		height: wrapperHeight,
		scale: lastScale
	};
}

总结

翻页控制的代码我就不贴出来了,与请求分片中的判定类似。总的实现,没有太大的难点,理清思路之后就很好实现了,上传速度快慢先不说,秒传校验通过的情况下,基本在 16ms 内完成 Content Download,直至页面渲染出来,整个过程大概 1s 左右,有个前提是我的实现是等 5 个分页都渲染完成后又进行了一系列的涂鸦初始化操作后的时间,不做其它处理,只做展示,速度将会更快。