- 背景
我無法使用"vue-virtual-scroller": "1.1.2"使pdf虛擬滾動,所以自己開始研究學習虛擬滾動原理,pdf文件的虛擬滾動是在另一篇文章 js原生虛擬滾動原理及示例的基礎上實現。 - 思路
通過pdfjs獲取每頁的數據并創建canvas元素及上下文,先存儲到列表中,將需要展示頁碼范圍的頁面信息從列表中取出,將相關的canvas元素append到dom節點,然后調用pdfjs的page渲染函數 - 依賴版本
"pdfjs-dist": "2.5.207",
"vue": "2.6.11", - 示例
parent.vue
<template>
<div>
<div class="pdf-container">
<VirtualList
:list="pages"
:item-height="pageHeight"
:wrapHeight="600"
:renderDone="renderDone"
@pageShowUpdate="pageShowUpdate"
>
<template #default>
<div id="wrap-canvas" ref="wrap-canvas"></div>
</template>
</VirtualList>
</div>
</div>
</template>
<script>
import VirtualList from "./VirtualScrollPdfItem.vue";
import * as pdfjsLib from "pdfjs-dist";
import pdfjsWorker from "pdfjs-dist/build/pdf.worker.entry";
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker;
export default {
components: {
VirtualList,
},
data() {
return {
url: "http://storage.xuetangx.com/public_assets/xuetangx/PDF/PlayerAPI_v1.0.6.pdf",
// url: "http://www.pdf995.com/samples/pdf.pdf",
totalPages: null,
pdfDoc: null,
pages: [],
pageHeight: 0,
canvasList: [],
renderDone: false,
};
},
mounted() {
this.init();
},
methods: {
async init() {
var loadingTask = pdfjsLib.getDocument(this.url);
console.log(111, loadingTask);
this.pdfDoc = await loadingTask.promise;
this.totalPages = this.pdfDoc.numPages;
console.log("totalpage", this.totalPages);
this.initPages();
this.$nextTick(() => this.renderPages());
},
async renderPages() {
for (let i = 1; i <= this.totalPages; i++) {
const page = await this.pdfDoc.getPage(i);
const viewport = page.getViewport({ scale: 1 });
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
// 適配Retina屏幕
// const dpr = window.devicePixelRatio || 1;
const dpr = 1;
canvas.width = viewport.width * dpr;
canvas.height = viewport.height * dpr;
this.pageHeight = canvas.height;
context.scale(dpr, dpr);
const item = {
page,
canvasContext: context,
viewport,
canvas,
};
this.canvasList.push(item);
this.renderDone = true;
}
console.log("renderdone");
},
pageShowUpdate(showPages) {
console.log("render canvas", this.$refs);
this.$refs["wrap-canvas"].innerHTML = "";
showPages.split(",").forEach((p) => {
this.renderCanvas(+p);
});
},
renderCanvas(i) {
const { page, canvasContext, viewport, canvas } = this.canvasList[i];
this.$refs["wrap-canvas"].appendChild(canvas);
console.log("wrap-canvas", this.$refs["wrap-canvas"]);
this.$nextTick(() => {
console.log("render page");
page.render({
canvasContext,
viewport,
}).promise;
});
},
initPages() {
this.pages = Array.from({ length: this.totalPages }, (_, i) => ({
pageNumber: i + 1,
}));
},
},
};
</script>
<style scoped>
.pdf-container {
max-width: 700px;
margin: 0 auto;
}
</style>
item.vue
<template>
<!-- 顯示容器 -->
<div id="scroll-container" ref="scroll-container" @scroll="onScroll">
<!-- 使顯示容器出現滾動條 -->
<div
id="content"
ref="content"
:style="{ height: itemHeight * list.length + 'px' }"
>
<!-- 列表容器 -->
<div id="wrap" ref="wrap">
<slot name="default" />
</div>
</div>
</div>
</template>
<script>
export default {
name: "VirtualScrollPdfItem",
props: {
list: {
type: Array,
default: () => [],
},
itemHeight: {
type: Number,
default: 200,
},
wrapHeight: {
type: Number,
default: 600,
},
renderDone: {
type: Boolean,
default: false,
},
},
data() {
return {
pageShow: "",
};
},
watch: {
pageShow(n) {
console.log("watch pageShow", n);
this.$emit("pageShowUpdate", n);
},
renderDone(n) {
if (n) {
this.onScroll();
}
},
},
methods: {
onScroll() {
const scrollTop = this.$refs["scroll-container"].scrollTop;
// console.log("scrolltop", scrollTop);
const start = scrollTop / this.itemHeight;
// 開始展示的項目索引
const startIndex = Math.floor(scrollTop / this.itemHeight);
// 結束的項目索引
let endIndex = Math.ceil(
(scrollTop + this.$refs["scroll-container"].clientHeight) /
this.itemHeight
);
if (endIndex > this.list.length) {
endIndex = this.list.length;
}
// 是否剛好展示第一個完整項
const isZero = scrollTop % this.itemHeight === 0;
// 不是完整項則計算偏移量
const offset = isZero
? 0
: this.itemHeight -
(Math.ceil(scrollTop / this.itemHeight) * this.itemHeight -
scrollTop);
this.renderItems(startIndex, endIndex, offset, scrollTop);
},
renderItems(startIndex, endIndex, offset, scrollTop) {
console.log("start end offset", startIndex, endIndex, offset);
this.pageShow = "";
for (let i = startIndex; i < endIndex; i++) {
console.log("pageshow for", this.pageShow);
if (i === endIndex - 1) {
this.pageShow = this.pageShow + i;
} else {
this.pageShow = this.pageShow + i + ",";
}
}
console.log("pageshow for end", this.pageShow);
// 計算最大滾動距離,超過后鼠標滾輪滾動后不再滾動
const maxScrollTop = this.list.length * this.itemHeight - this.wrapHeight;
console.log("maxscrolltop", maxScrollTop, scrollTop, offset);
if (scrollTop >= maxScrollTop) {
// 調整列表容器位置
this.$refs.wrap.style.top = maxScrollTop - offset + "px";
// 讓滾動條移動
this.$refs["scroll-container"].scrollTop = maxScrollTop;
} else {
// 調整列表容器位置和滾動條位置
this.$refs.wrap.style.top = scrollTop - offset + "px";
this.$refs["scroll-container"].scrollTop = scrollTop;
}
},
},
};
</script>
<style scoped>
#scroll-container {
position: relative;
overflow-y: scroll;
height: 600px;
border: 1px solid blue;
}
#content {
width: 100%;
}
#wrap {
position: absolute;
top: 0;
left: 0;
height: 600px;
width: 100%;
}
</style>