@chieftrade/pdf-viewer
v1.6.0
Published
框架无关的轻量级 PDF 预览插件,基于 pdfjs-dist 构建
Readme
@chieftrade/pdf-viewer
框架无关的轻量级 PDF 预览插件,基于 pdfjs-dist 构建,使用纯 TypeScript Class 架构。
一套代码覆盖 React、Vue、Next.js、Angular、Svelte 等所有框架,同时支持桌面 Web 和移动端 H5(iOS/Android WebView)。
特性
- 🎯 框架无关 — 纯 TypeScript Class,任何框架都能用
- 📄 Canvas 2D 渲染 — 兼容所有目标环境(Electron、WebView、桌面浏览器)
- 📐 自适应宽度 — 自动适配容器宽度,响应式布局
- ⚡ 懒加载 — IntersectionObserver 按需渲染,大文档也不卡
- 🤏 双指缩放 — 移动端原生手势支持,rAF 节流实时响应
- 🖱️ 滚轮缩放 — PC 端滚轮直接缩放,触屏设备自动禁用
- 🔧 缩放工具栏 — 右下角 +/- 按钮,白色背景带阴影
- ✋ 鼠标拖动 — 按住拖动浏览,grab/grabbing 光标
- ⌨️ 键盘快捷键 — Ctrl/Cmd + (+/-/0) 缩放(默认关闭)
- 📥 下载 — 一行代码触发 PDF 下载
- ⏳ Loading 动画 — 内置加载动画,文档加载期间自动显示
- 🔍 防抖高清渲染 — 缩放后自动重新渲染高清 Canvas,无闪烁
- 🔐 加密 PDF 支持 — 通过异步回调获取密码,UI 完全由调用方控制
- 🧹 零 CSS — 所有样式 JS 内联,无需引入额外样式文件
- 🔒 SSR 安全 — 模块顶层不访问浏览器全局对象
- 📦 三种格式 — ESM / CJS / UMD + TypeScript 类型声明
- 🪶 轻量 — 核心包 gzip < 20KB(不含 pdfjs-dist)
安装
# pnpm
pnpm add @chieftrade/pdf-viewer pdfjs-dist
# npm
npm install @chieftrade/pdf-viewer pdfjs-dist
# yarn
yarn add @chieftrade/pdf-viewer pdfjs-distpdfjs-dist 是 peerDependency,需要单独安装。支持版本:
^3.11.174
快速上手
import PdfViewer from '@chieftrade/pdf-viewer';
import { GlobalWorkerOptions } from 'pdfjs-dist';
// 1. 配置 Worker(必须在创建实例之前)
GlobalWorkerOptions.workerSrc = '/path/to/pdf.worker.min.mjs';
// 2. 创建实例
const viewer = new PdfViewer({
container: document.getElementById('pdf-container')!,
source: 'https://example.com/document.pdf',
});
// 3. 使用完毕后销毁
viewer.destroy();数据源类型
支持四种数据源格式:
// URL 直链
new PdfViewer({ container, source: 'https://example.com/file.pdf' });
// ArrayBuffer(文件上传、接口返回)
const buffer = await file.arrayBuffer();
new PdfViewer({ container, source: buffer });
// Uint8Array
new PdfViewer({ container, source: new Uint8Array(buffer) });
// Blob / File(文件选择器)
new PdfViewer({ container, source: file });配置项
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| container | HTMLElement | 必填 | 容器元素 |
| source | PdfSource | 必填 | PDF 数据源 |
| pixelRatio | number | 2 | 渲染清晰度倍率(详见下方说明) |
| pageGap | number | 8 | 页面间距(px) |
| backgroundColor | string | '#f0f0f0' | 页面背景色 |
| fitWidth | boolean | true | 自适应容器宽度 |
| enablePinchZoom | boolean | true | 启用双指缩放(移动端) |
| enableWheelZoom | boolean | true | 启用滚轮缩放(PC 端) |
| enableZoomToolbar | boolean | true | 显示缩放工具栏(右下角) |
| enableDragScroll | boolean | true | 启用鼠标拖动滚动 |
| enableKeyboardZoom | boolean | false | 启用键盘缩放快捷键 |
| enableLazyLoad | boolean | true | 启用懒加载 |
| lazyLoadMargin | number | 500 | 懒加载预加载边距(px) |
| enableLoadingPlaceholder | boolean | true | 显示骨架屏占位 |
| enableLoading | boolean | true | 显示加载动画 |
| enablePageIndicator | boolean | true | 显示页码指示器(右上角) |
| workerSrc | string | - | pdfjs-dist Worker 路径 |
pixelRatio 说明
pixelRatio 控制 Canvas 渲染清晰度,是 CSS 像素与 Canvas 物理像素的比值:
- 1 — 标准清晰度(1:1 像素匹配),适合低端设备或超大文档省内存
- 2 — 高清(推荐),覆盖绝大多数场景,包括 Retina 屏和桌面 Electron
- 3 — 超高清,内存占用较大
与设备 DPR 无关,始终按传入的整数倍渲染。这样在 Electron(dpr=1)环境下也能保证清晰。
// 默认高清(所有设备统一)
new PdfViewer({ container, source });
// 超大文档省内存
new PdfViewer({ container, source, pixelRatio: 1 });
// 追求极致清晰
new PdfViewer({ container, source, pixelRatio: 3 });缩放交互
| 功能 | 配置项 | 默认 | 说明 |
|------|--------|------|------|
| 滚轮缩放 | enableWheelZoom | true | 滚轮向上放大、向下缩小,触屏设备自动禁用 |
| 缩放工具栏 | enableZoomToolbar | true | 右下角显示 +/- 按钮 |
| 鼠标拖动 | enableDragScroll | true | 按住左键拖动滚动,触屏设备自动禁用 |
| 键盘快捷键 | enableKeyboardZoom | false | Ctrl/Cmd + (+/-/0) 缩放,默认关闭避免与浏览器冲突 |
| 双指缩放 | enablePinchZoom | true | 移动端双指捏合缩放 |
回调
| 回调 | 参数 | 说明 |
|------|------|------|
| onLoadProgress | { loaded: number, total: number } | 加载进度 |
| onDocumentLoaded | { numPages: number } | 文档加载完成 |
| onPageRendered | pageNumber: number | 单页渲染完成 |
| onError | Error | 错误回调 |
| onScaleChange | scale: number | 缩放变化 |
| onPasswordRequest | reason: 'needPassword' \| 'incorrectPassword' | 加密 PDF 密码请求(返回 Promise<string>) |
API
const viewer = new PdfViewer(options);
// 页码导航
viewer.goToPage(3); // 跳转到第 3 页
viewer.getCurrentPage(); // 获取当前页码
viewer.getTotalPages(); // 获取总页数
// 缩放控制
viewer.setScale(1.5); // 设置缩放比例
viewer.getScale(); // 获取当前缩放比例
viewer.zoomIn(); // 放大(默认步长 0.25)
viewer.zoomIn(0.5); // 放大 0.5
viewer.zoomOut(); // 缩小
viewer.resetZoom(); // 重置到初始缩放
// 下载
viewer.download(); // 下载为 document.pdf
viewer.download('report.pdf'); // 自定义文件名
// 销毁
viewer.destroy(); // 释放所有资源加密 PDF 支持
通过 onPasswordRequest 回调处理加密 PDF。不传此回调时,加密 PDF 直接触发 onError,行为与旧版本完全一致。
new PdfViewer({
container,
source: encryptedPdfUrl,
onPasswordRequest: (reason) => {
// reason: 'needPassword'(首次请求)| 'incorrectPassword'(密码错误重试)
const msg = reason === 'incorrectPassword' ? '密码错误,请重试' : '请输入 PDF 密码';
return new Promise((resolve, reject) => {
showPasswordDialog({
message: msg,
onConfirm: (pwd) => resolve(pwd),
onCancel: () => reject(new Error('User cancelled')),
});
});
},
});工作流程:
- 加载 PDF → 检测到需要密码 → 调用
onPasswordRequest('needPassword') - 调用方弹窗获取密码 →
resolve(password)→ 插件用密码重新加载 - 密码错误 → 调用
onPasswordRequest('incorrectPassword')→ 调用方再次弹窗 - 重复 2-3,最多重试 3 次错误密码后停止,触发
onError - 调用方
reject()可随时终止流程
插件不包含任何密码 UI,密码的获取方式完全由调用方决定(弹窗、页面跳转、指纹验证等均可)。
框架集成示例
React(加密 PDF)
import { useEffect, useRef, useCallback } from 'react';
import PdfViewer from '@chieftrade/pdf-viewer';
function PdfPreview({ url }: { url: string }) {
const containerRef = useRef<HTMLDivElement>(null);
const handlePasswordRequest = useCallback(
(reason: 'needPassword' | 'incorrectPassword') => {
return new Promise<string>((resolve, reject) => {
const msg = reason === 'incorrectPassword' ? '密码错误,请重试' : '请输入密码';
const pwd = window.prompt(msg);
pwd ? resolve(pwd) : reject(new Error('User cancelled'));
});
},
[]
);
useEffect(() => {
if (!containerRef.current) return;
const viewer = new PdfViewer({
container: containerRef.current,
source: url,
workerSrc: '/pdf.worker.min.mjs',
onPasswordRequest: handlePasswordRequest,
});
return () => viewer.destroy();
}, [url, handlePasswordRequest]);
return <div ref={containerRef} style={{ width: '100%', height: '80vh' }} />;
}React(基础用法)
import { useEffect, useRef } from 'react';
import PdfViewer from '@chieftrade/pdf-viewer';
function PdfPreview({ url }: { url: string }) {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!containerRef.current) return;
const viewer = new PdfViewer({
container: containerRef.current,
source: url,
workerSrc: '/pdf.worker.min.mjs',
});
return () => viewer.destroy();
}, [url]);
return <div ref={containerRef} style={{ width: '100%', height: '80vh' }} />;
}Vue 3
<template>
<div ref="container" style="width: 100%; height: 80vh" />
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import PdfViewer from '@chieftrade/pdf-viewer';
const props = defineProps<{ url: string }>();
const container = ref<HTMLElement>();
let viewer: PdfViewer | null = null;
onMounted(() => {
if (!container.value) return;
viewer = new PdfViewer({
container: container.value,
source: props.url,
workerSrc: '/pdf.worker.min.mjs',
});
});
onUnmounted(() => viewer?.destroy());
</script>Worker 配置
pdfjs-dist 需要 Worker 文件来解析 PDF。插件不会从外部 CDN 加载 Worker,需要你自行配置:
import { GlobalWorkerOptions } from 'pdfjs-dist';
// 方式 1:指向本地托管的 Worker 文件
GlobalWorkerOptions.workerSrc = '/static/pdf.worker.min.mjs';
// 方式 2:通过 PdfViewer 配置项传入
new PdfViewer({
container,
source,
workerSrc: '/static/pdf.worker.min.mjs',
});Worker 文件位于 node_modules/pdfjs-dist/build/pdf.worker.min.mjs,需要复制到你的静态资源目录。
构建输出
| 格式 | 文件 | 用途 |
|------|------|------|
| ESM | dist/index.mjs | 现代打包工具(Vite、Webpack 5) |
| CJS | dist/index.cjs | Node.js / 旧版打包工具 |
| UMD | dist/index.umd.js | <script> 标签直接引入(全局变量 ChiefPdfViewer) |
| Types | dist/index.d.ts | TypeScript 类型声明 |
注意事项
pixelRatio默认为 2,在所有设备上保证清晰(包括 Electron dpr=1 环境)- 缩放范围限制在 [defaultScale × 0.25, defaultScale × 5]
- 缩放时先即时修改 CSS 尺寸响应,停止 300ms 后自动重新渲染高清 Canvas
- 滚轮缩放和鼠标拖动在触屏设备上自动禁用,由 PinchZoomManager 处理
- 键盘快捷键默认关闭,避免与浏览器默认缩放行为冲突
- 懒加载的页面在进入视口时会自动应用当前缩放比例
- 内置 loading 动画在第一页渲染完成或出错时自动移除,可通过
enableLoading: false关闭 destroy()后所有方法静默忽略,不会抛出异常onPasswordRequest未传入时,加密 PDF 的行为与旧版本完全一致(触发onError)- 密码错误最多重试 3 次,超过后触发
onError - 页面渲染错误不会阻塞其他页面
sideEffects: false,支持 tree-shaking
移动端兼容性
现代浏览器(Chrome 80+)
直接使用标准版本即可:
import PdfViewer from '@chieftrade/pdf-viewer';
new PdfViewer({
container,
source: pdfUrl,
workerSrc: '/pdf.worker.min.mjs',
});旧版 WebView(Chrome 54-79,如部分 Android App 内嵌浏览器)
pdfjs-dist v4 的标准版本使用了 ES2020+ 语法(可选链 ?.、globalThis 等),旧版 WebView 不支持。
同时旧版 WebView 不支持 ESM Worker(new Worker(url, { type: 'module' })),.mjs 格式的 Worker 文件会加载失败。
解决方案:使用 pdfjs-dist 的 legacy 版本 + 静态文件方式部署 Worker。
第一步:Vite 配置
// vite.config.ts
import { defineConfig } from 'vite';
import { viteStaticCopy } from 'vite-plugin-static-copy';
export default defineConfig({
resolve: {
alias: {
// 使用 legacy 版本的 pdfjs-dist(编译到 ES5,兼容 Chrome 54+)
'pdfjs-dist': 'pdfjs-dist/legacy/build/pdf.mjs',
},
},
plugins: [
// 把 legacy Worker 文件复制到构建产物目录
viteStaticCopy({
targets: [
{
src: 'node_modules/pdfjs-dist/legacy/build/pdf.worker.min.mjs',
dest: 'assets',
rename: 'pdf.worker.js', // 重命名为 .js,避免被 WebView 当作 ESM
},
],
}),
],
build: {
target: 'es2015', // 兼容 Chrome 54+
},
});需要安装插件:pnpm add -D vite-plugin-static-copy
第二步:组件代码
import PdfViewer from '@chieftrade/pdf-viewer';
import { GlobalWorkerOptions } from 'pdfjs-dist';
// 指向复制到 dist 的 legacy Worker 文件(.js 格式,非 ESM)
GlobalWorkerOptions.workerSrc = '/assets/pdf.worker.js';
const viewer = new PdfViewer({
container: document.getElementById('pdf')!,
source: pdfUrl,
workerSrc: '/assets/pdf.worker.js',
});不想装 vite-plugin-static-copy?
手动复制 Worker 文件到 public/ 目录:
cp node_modules/pdfjs-dist/legacy/build/pdf.worker.min.mjs public/pdf.worker.js然后 workerSrc: '/pdf.worker.js' 即可。
常见报错及原因
| 报错 | 原因 | 解决 |
|------|------|------|
| Unexpected token '{' | Worker 文件是 ESM 格式,旧 WebView 不支持 | 使用 legacy 版本 + 重命名为 .js |
| Failed to fetch dynamically imported module | pdfjs fallback 到 import() 加载 Worker,旧 WebView 不支持 | 正确配置 workerSrc 指向静态文件 |
| Setting up fake worker failed | Worker 加载失败,fallback 到 fake worker 也失败 | 确认 workerSrc 路径可访问 |
| globalThis is not defined | 使用了标准版而非 legacy 版 | alias 指向 pdfjs-dist/legacy/build/pdf.mjs |
License
ISC
