使用Viewer.js给文章配图加点击放大

2026-07-12 前端

2026-10-03 更新:本文改用 Viewer.js 官方文档、npm 官方元数据与 MDN 官方资料重写,删去原先的本机实测内容;示例都是可直接复制的代码,并标明每条结论的官方出处。

文章里的配图点一下就能放大看细节,这个交互自己写并不划算:要处理缩放级别、拖拽平移、键盘操作、触摸手势、焦点管理。交给成熟库更省事,Viewer.js 是这类需求里最常见的选择之一。下面按官方文档把接入方式、常用选项和几个真会绊住人的点讲清楚。

一、先确认库的官方事实

  • 初始化语法:new Viewer(element[, options])。element 可以是单张图片,也可以是图片容器——库会对容器内的 img 建立图集,所以"文章里所有配图共用同一个查看器"用容器初始化即可。
  • 安装方式:npm 上是 npm install viewerjs;直接在浏览器里用则引 viewer.css 与 viewer.js 两个文件。
  • 版本与许可:npm registry 上最新版为 1.15.2;官方 LICENSE 为 MIT(Copyright 2015-present Chen Fengyuan)。官方演示站同时提供 modal(弹层)与 inline(内嵌)两种模式的例子。
  • 键盘支持:README 专门有一节 Keyboard Support,且仅在 modal 模式可用。

二、把库放到自己的服务器上,别直接引外链

这不是洁癖,有两个官方层面的理由:

  1. CSP 管的是脚本来源。MDN 关于 CSP 的说明是:script-src 设定允许加载 JavaScript 的来源。把库限定在自有源,等于把"页面能执行谁的代码"这件事写死在响应头里,站外脚本被替换、被墙、被下架都不会影响你的站点。
  2. 第三方资源存在被篡改的风险。Subresource Integrity(SRI)这个特性的存在本身就说明了问题——MDN 的定义是:它让浏览器校验所获取的资源(例如来自 CDN 的资源)是否被意外篡改。用了外链就要连带维护 SRI 哈希,多数站点并不会做。

自托管还有一个运维上的好处:文件放在自己的 /assets/ 下,缓存策略由你决定;升级时换文件名(viewer-1.15.2.min.js)而不是原地覆盖,避免长缓存导致的"版本混用"。

目录结构可以这样放(换成你自己的路径即可):

assets/
└── vendor/
    └── viewerjs/
        ├── viewer-1.15.2.min.css
        └── viewer-1.15.2.min.js

三、最小可用接入(可直接复制)

前提:站点的 CSP 是 script-src 'self'; style-src 'self'。MDN 明确写过,这类策略下内联事件处理器同样会被阻止,官方建议改用 addEventListener——所以初始化必须放在外部 JS 文件里,不能写 onclick,也不能写内联 <script>。

HTML(只挂结构与外链资源):

<link rel="stylesheet" href="/assets/vendor/viewerjs/viewer-1.15.2.min.css">

<article class="article">
  <img src="/assets/images/demo/photo-a.jpg" alt="示例图 A:页面加载瀑布图">
  <img src="/assets/images/demo/photo-b.jpg" alt="示例图 B:渲染耗时对比">
</article>

<script src="/assets/vendor/viewerjs/viewer-1.15.2.min.js" defer></script>
<script src="/assets/js/viewer-init.js" defer></script>

外部 JS:

// /assets/js/viewer-init.js
(function () {
  var box = document.querySelector('.article');
  if (!box || typeof Viewer === 'undefined') return;

  var viewer = new Viewer(box, {
    navbar: 3,        // 见下节:数值是“断点”,不是“个数”
    toolbar: 4,
    title: function (img) { return img.alt; },  // 标题取 alt
    transition: true, // 为部分元素启用 CSS 过渡
    fullscreen: true, // 需要浏览器支持 Fullscreen API
    zoomRatio: 0.1,   // 鼠标滚轮缩放比例
    keyboard: true,   // 键盘支持仅在 modal 模式可用
    focus: true       // 初始化时聚焦导航栏中的活动项(需 keyboard 开启)
  });
})();

四、选项的取值是"断点",不是"个数"

这是最容易写错的地方。README 里对 navbar 的说明是:

  • 0 或 false:不显示;
  • 1 或 true:显示;
  • 2:屏幕宽度大于 768 像素才显示;
  • 3:大于 992 像素才显示;
  • 4:大于 1200 像素才显示。

toolbar 同理(4 等价于"屏幕宽度大于 1200 像素时显示全部按钮")。所以把它当成"显示几个按钮"去写,结果往往是窄屏下整条工具栏直接消失——不是坏了,是选项语义被误解了。

另外两点:

  • title: true 时,标题取自图片的 alt 属性,取不到则从 URL 解析文件名。也就是说 alt 既服务于无障碍(MDN 对 alt 的说明是:图片无法显示时用它替代,因此应尽量填写有意义的描述),也决定了灯箱里显示什么。
  • keyboard 只在 modal 模式有效;开启后 Esc 关闭、Tab 在按钮间切换焦点、方向键切换上一张/下一张与放大缩小、Ctrl+0 回到初始尺寸、Ctrl+1 查看原始尺寸。

五、动态插入的图片要手动 update()

配图懒加载、分页加载更多、或者文章内有 Tab 切换时,图片是后插进 DOM 的——已创建的实例不会自动认识它们。官方 README 对此有明确说明:当源图片发生新增、删除或重新排序时,用 update() 更新实例,并明示用 XHR 动态加载图片后可以调用它把新图纳入。

var box = document.querySelector('.article');
var viewer = new Viewer(box);

// 图片后插入 DOM 之后调用
var mo = new MutationObserver(function () {
  viewer.update();
});
mo.observe(box, { childList: true, subtree: true });

事件处理里可以用 this.viewer 拿到实例,这是 README 里写明的一点:

box.addEventListener('viewed', function () {
  // this.viewer 即当前实例
  console.log('current index:', this.viewer.index);
});

六、无障碍上必须补的一块

库提供键盘操作,但"点击图片能放大"这件事本身要让键盘用户也能做到:MDN 的 WCAG 键盘指南要求可点击元素必须可聚焦并具备交互语义,且可聚焦元素要有可见的焦点样式,同时建议避免使用大于 0 的 tabindex。

最省事的做法是把图片包在按钮里,或用 tabindex="0" + role="button" 并绑定 keydown(Enter/Space 时调用查看器):

box.querySelectorAll('img').forEach(function (img) {
  img.tabIndex = 0;              // 可聚焦(不要用大于 0 的值)
  img.setAttribute('role', 'button');
  img.addEventListener('keydown', function (e) {
    if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); viewer.view(); }
  });
});

七、能力边界:什么时候不需要这个库

  • 原生 <dialog>:MDN 说明模态 dialog 会阻止与其余 UI 的交互、使页面其余部分 inert,支持 showModal() / close()、Esc 关闭、::backdrop 伪元素样式。适合"点开看大图、点关闭退出",但没有缩放、旋转、拖拽与手势。
  • CSS zoom:MDN 说明它控制元素的放大级别,与 transform: scale() 不同的是会触发布局重算;只解决"变大",不解决查看器交互。
  • image-set():按像素密度从候选里挑图,是背景图的选择机制(MDN),跟交互无关。

结论很直白:只要"点开、点背景关闭",原生 dialog 足够;要缩放、拖拽、手势、键盘换图,就用 Viewer.js。

八、上线前检查清单

  • 图片都写了有意义的 alt(同时决定灯箱标题);
  • 库文件与初始化脚本都是外部文件,没有内联脚本,没有 on* 属性;
  • 自托管 + 文件名带版本号,升级时换名字;
  • 有动态插图的页面调用了 update();
  • 键盘能打开、能换图、能关闭,且焦点样式可见;
  • 移动端触摸缩放正常(库内置能力)。

参考来源