Safari对canvas最大内存限制,完美解决方案
解决方案:采用虚拟dom,只渲染可视化片段中的canvas元素
<template>
<div ref="viewRef" class="pdf-viewer-container">
<div v-if="viewport" ref="viewInRef" class="pdf-viewer">
<div v-for="pageNum in numPages" class="pdf-viewer-page" :style="{ height: viewport.height * scale+'px' }">
<canvas v-if="isIncludePage(pageNum)" :id="'pdf-canvas-'+(pageNum-1)" />
<div class="page-viewer-num"> {{ pageNum }} / {{ numPages }}</div>
</div>
</div>
</div>
</template>
<script>
import * as PDFJS from "pdfjs-dist/build/pdf.min.js";
export default {
name: 'PDFViewer',
props: {
src: {
type: String,
required: true
}
},
data() {
return {
numPages: 0,
bufferSize: 3,
pageRendering: false,
scale: 1,
frages: [],
viewport: null,
pdfDoc: null,
dpr: window.devicePixelRatio || 1,
oldStartIndex: undefined,
oldFrages: [],
marginBottom: 10
}
},
computed: {
viewEl() {
return this.$refs.viewRef;
},
viewElRect() {
return this.viewEl.getBoundingClientRect();
},
isIncludePage() {
return (pageNum) => {
return this.frages.map(frage => frage.pageNum).includes(pageNum);
}
}
},
async mounted() {
await this.initConfig();
this.initVirtualList();
this.viewEl.addEventListener('scroll', this.debounce(() => this.initVirtualList()));
},
methods: {
loadPDF(url) {
PDFJS.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.js';
return PDFJS.getDocument({
url,
disableAutoFetch: true,
disableFontFace: true,
ignoreErrors: true,
cMapUrl: 'https://unpkg.com/pdfjs-dist/cmaps/',
cMapPacked: true,
}).promise.then(pdfDoc => {
this.numPages = pdfDoc.numPages;
return pdfDoc;
})
},
getViewport() {
return this.pdfDoc.getPage(1).then(page => page.getViewport({ scale: 1}) );
},
async initConfig() {
this.pdfDoc = await this.loadPDF(this.src);
if(!this.numPages) {
return;
}
this.viewport = await this.getViewport();
this.scale = this.viewElRect.width / this.viewport.width;
this.itemHeight = this.viewport.height * this.scale;
},
async initVirtualList() {
const scrollTop = this.viewEl.scrollTop;
const itemHeight = this.itemHeight + this.marginBottom;
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(
this.numPages-1,
Math.floor((scrollTop + this.viewElRect.height) / itemHeight)
);
const startBufferIndex = Math.max(0, startIndex - this.bufferSize);
const endBufferIndex = Math.min(this.numPages-1, endIndex + this.bufferSize);
this.pageRendering = true;
this.oldStartIndex = startBufferIndex;
this.frages = Array.from(
{ length: (endBufferIndex - startBufferIndex)+1 },
(item, i) => ({ id: 'pdf-canvas-'+(i + startBufferIndex), pageNum: i+startBufferIndex+1, pageRendering: true })
);
await this.$nextTick();
return this.renderPage(startBufferIndex, endBufferIndex, () => {
this.pageRendering = false;
this.oldFrages = this.frages;
});
},
renderPage(startIndex, endIndex, rendered) {
if(startIndex > endIndex) {
return;
}
const id = 'pdf-canvas-'+startIndex;
const frageIndex = this.frages.findIndex(frage => frage.id === id);
const frage = this.frages[frageIndex];
const oldFrageIndex = this.oldFrages.findIndex(frage => frage.id === id);
const { width: clientWidth } = this.viewElRect;
if(oldFrageIndex > -1) {
this.renderPage(startIndex+1, endIndex, rendered);
return;
}
this.pdfDoc.getPage(startIndex+1).then(async page => {
Object.assign(frage, { id, page, pageRendering: true });
await this.$nextTick();
const canvas = document.getElementById(id);
const ctx = canvas.getContext("2d");
const viewport = this.viewport;
canvas.width = Math.floor(viewport.width * this.dpr);
canvas.height = Math.floor(viewport.height * this.dpr);
canvas.style.width = Math.floor(clientWidth) + "px";
canvas.style.height = Math.floor(viewport.height * (clientWidth / viewport.width)) + "px";
const renderContext = {
canvasContext: ctx,
transform: [this.dpr, 0, 0, this.dpr, 0, 0],
viewport
};
const renderTask = page.render(renderContext);
renderTask.promise.then(() => {
Object.assign(frage, { renderContext, pageRendering: false });
if(startIndex < endIndex) {
this.renderPage(startIndex+1, endIndex, rendered)
}else{
typeof rendered == 'function' && rendered();
}
});
});
},
debounce(handle, wait =300){
let timer;
return function(...args){
timer && clearTimeout(timer)
timer = setTimeout(()=>{
handle.apply(this,args);
}, wait);
}
}
}
}
</script>
<style scoped>
.pdf-viewer-container {
height: 100vh;
width: 100vw;
overflow-x: hidden;
}
.pdf-viewer-page{
box-shadow: rgb(51, 51, 51) 0 1px 4px 0;
margin-bottom: 10px;
position: relative;
}
.page-viewer-num {
position: absolute;
left: 5px;
bottom: 5px;
background: rgba(0, 0, 0, .6);
padding: 3px;
border-radius: 5px;
color: #ffffff;
}
</style>