npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-dist

pdfjs-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')),
      });
    });
  },
});

工作流程:

  1. 加载 PDF → 检测到需要密码 → 调用 onPasswordRequest('needPassword')
  2. 调用方弹窗获取密码 → resolve(password) → 插件用密码重新加载
  3. 密码错误 → 调用 onPasswordRequest('incorrectPassword') → 调用方再次弹窗
  4. 重复 2-3,最多重试 3 次错误密码后停止,触发 onError
  5. 调用方 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