在Vue2或UniApp项目中直接处理PDF文件是个常见需求,但浏览器原生对PDF的支持有限。你可能遇到过这些问题:无法自定义UI、无法获取阅读进度、难以实现复杂交互。这时候iframe就派上用场了。
我去年做过一个电子合同签署系统,客户要求能高亮批注PDF且实时同步签署位置。试过直接用pdf.js,发现UI定制成本太高,最终选择将整个PDF查看器用iframe嵌入,主应用只负责业务逻辑,完美解耦。
iframe方案有三大优势:
无论是Vue2还是UniApp,核心思路都是将PDF查看器作为静态资源部署。这是我的推荐结构:
code复制/public
/static
/pdfjs
viewer.html # PDF查看器入口
build/ # pdf.js官方库
web/ # 查看器UI资源
/src
/components
PDFViewer.vue # 我们的封装组件
在vue-cli项目中,public目录下的文件会原样复制到dist。UniApp稍微特殊,需要放在src/static(HBuilderX创建的项目)或static目录(cli创建的项目)。
直接使用pdf.js官方demo的viewer.html时,要注意三个关键修改:
javascript复制// 在viewer.html的初始化脚本中修改
pdfjsLib.GlobalWorkerOptions.workerSrc =
window.location.pathname.includes('uniapp')
? '../../static/pdfjs/build/pdf.worker.mjs'
: '../build/pdf.worker.mjs';
html复制<!-- 在head标签内提前声明消息监听 -->
<script>
window.MESSAGE_HANDLERS = {};
window.addEventListener("message", (event) => {
const { cmd, handler } = event.data;
if(handler && window.MESSAGE_HANDLERS[handler]) {
window.MESSAGE_HANDLERS[handler](event.data);
}
});
</script>
css复制/* 在viewer.css末尾添加 */
@media screen and (max-width: 768px) {
#viewerContainer {
padding: 0;
}
.toolbar {
flex-wrap: wrap;
}
}
主应用与iframe之间推荐使用postMessage通信。这是我封装的消息管理器:
javascript复制// 在Vue组件中
export default {
methods: {
sendToPDF(cmd, payload) {
const iframe = this.$refs.iframe;
if (!iframe || !iframe.contentWindow) return;
iframe.contentWindow.postMessage({
cmd,
payload,
timestamp: Date.now()
}, '*');
},
setupMessageListener() {
window.addEventListener('message', (event) => {
// 安全校验
if (event.origin !== window.location.origin) return;
const { cmd, payload } = event.data;
switch(cmd) {
case 'PAGE_CHANGED':
this.currentPage = payload.page;
break;
case 'LOAD_COMPLETE':
this.totalPages = payload.total;
break;
}
});
}
}
}
iframe内对应的接收逻辑:
javascript复制// 在viewer.html的PDF加载完成后
window.parent.postMessage({
cmd: 'LOAD_COMPLETE',
payload: {
total: pdfDoc.numPages
}
}, '*');
// 页码变化时
function onPageChange(num) {
window.parent.postMessage({
cmd: 'PAGE_CHANGED',
payload: {
page: num
}
}, '*');
}
实际项目中我们往往需要更复杂的交互,比如:
javascript复制// 主应用侧
async function getPDFMetadata() {
return new Promise((resolve) => {
const handler = `handler_${Date.now()}`;
window.MESSAGE_HANDLERS[handler] = (data) => {
resolve(data.payload);
delete window.MESSAGE_HANDLERS[handler];
};
this.sendToPDF('GET_METADATA', { handler });
});
}
javascript复制// iframe侧
const COMMAND_MAP = {
'ZOOM_IN': (payload) => {
PDFViewerApplication.pdfViewer.currentScale += 0.1;
},
'HIGHLIGHT': (payload) => {
// 实现高亮逻辑
}
};
window.addEventListener('message', (event) => {
const { cmd, payload } = event.data;
if (COMMAND_MAP[cmd]) {
COMMAND_MAP[cmd](payload);
}
});
跨域限制:虽然本地开发时同源,但部署后可能出现问题。解决方案是在iframe的src中使用相对路径时显式指定origin:
javascript复制// 替代简单的'/static/pdfjs/viewer.html'
const origin = window.location.origin;
this.viewerUrl = `${origin}/static/pdfjs/viewer.html`;
内存泄漏:频繁创建/销毁iframe会导致内存累积。实测推荐两种方案:
javascript复制beforeDestroy() {
window.removeEventListener('message', this.messageHandler);
this.$refs.iframe.src = '';
}
滚动同步问题:当需要保持主应用和PDF查看器滚动位置同步时,推荐使用IntersectionObserver:
javascript复制// 在主应用中
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.sendToPDF('SCROLL_TO', { y: entry.boundingClientRect.top });
}
});
});
observer.observe(this.$refs.iframe);
html复制<iframe
v-show="isVisible"
:src="isVisible ? viewerUrl : ''"
@load="handleIframeLoad"
/>
javascript复制let pageChangeTimer;
function onPageChange(num) {
clearTimeout(pageChangeTimer);
pageChangeTimer = setTimeout(() => {
window.parent.postMessage(/*...*/);
}, 300);
}
javascript复制// 在应用初始化时提前创建隐藏iframe
const preloadIframe = document.createElement('iframe');
preloadIframe.style.display = 'none';
preloadIframe.src = '/static/pdfjs/viewer.html';
document.body.appendChild(preloadIframe);
在需要根据用户权限禁用某些PDF功能时,可以通过初始化参数控制:
javascript复制// 主应用传递权限配置
this.sendToPDF('INIT_CONFIG', {
permissions: {
allowPrint: user.role === 'admin',
allowDownload: false,
allowAnnotations: true
}
});
// iframe内接收处理
window.addEventListener('message', (event) => {
if (event.data.cmd === 'INIT_CONFIG') {
document.getElementById('print').disabled = !event.data.permissions.allowPrint;
}
});
当需要同时显示多个PDF时,推荐使用动态命名:
javascript复制// 生成唯一ID
function generateViewerId() {
return 'viewer_' + Math.random().toString(36).substr(2, 9);
}
// 在iframe通信中加入实例ID
this.sendToPDF('SWITCH_PAGE', {
viewerId: this.viewerId,
page: 5
});
UniApp中需要特殊处理的问题:
json复制{
"path": "pages/pdf-viewer",
"style": {
"navigationBarTitleText": "PDF查看",
"app-plus": {
"bounce": "none",
"titleNView": false
}
}
}
javascript复制// 在iframe内容中
document.addEventListener('touchmove', (e) => {
if (e.target.tagName === 'CANVAS') {
e.stopPropagation();
}
}, { passive: false });
Chrome调试iframe内容的两种方式:
javascript复制// 在Chrome Console
const iframe = document.querySelector('iframe');
cd(iframe.contentWindow);
封装全局错误捕获:
javascript复制// 在主应用中
window.addEventListener('message', (event) => {
if (event.data.cmd === 'PDF_ERROR') {
Sentry.captureException(new Error(`PDF Error: ${event.data.error}`));
}
});
// 在iframe中
window.addEventListener('error', (event) => {
window.parent.postMessage({
cmd: 'PDF_ERROR',
error: event.message
}, '*');
});
使用Performance API监控关键指标:
javascript复制// 在PDF加载完成后
const timing = {
loadTime: performance.now() - performance.timing.navigationStart,
renderTime: performance.now() - window.PDF_LOAD_START_TIME
};
window.parent.postMessage({
cmd: 'PERF_METRICS',
metrics: timing
}, '*');