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 模式可用。
二、把库放到自己的服务器上,别直接引外链
这不是洁癖,有两个官方层面的理由:
- CSP 管的是脚本来源。MDN 关于 CSP 的说明是:
script-src设定允许加载 JavaScript 的来源。把库限定在自有源,等于把"页面能执行谁的代码"这件事写死在响应头里,站外脚本被替换、被墙、被下架都不会影响你的站点。 - 第三方资源存在被篡改的风险。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(); - 键盘能打开、能换图、能关闭,且焦点样式可见;
- 移动端触摸缩放正常(库内置能力)。
参考来源
- Viewer.js 官方 README(初始化、选项、事件、方法、键盘支持)— https://github.com/fengyuanchen/viewerjs
- Viewer.js 官方 LICENSE(MIT)— https://github.com/fengyuanchen/viewerjs/blob/main/LICENSE
- npm registry:viewerjs 最新版本元数据 — https://registry.npmjs.org/viewerjs/latest
- MDN, Content-Security-Policy: script-src(内联事件处理器会被阻止,建议改用 addEventListener)— https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/script-src
- MDN, Content Security Policy (CSP) — https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP
- MDN, Subresource Integrity — https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
- MDN, EventTarget: addEventListener() — https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
- MDN,
<img>: The Image Embed element(alt 语义)— https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img - MDN, Keyboard accessible(可聚焦、可见焦点样式、避免 tabindex > 0)— https://developer.mozilla.org/en-US/docs/Web/Accessibility/Guides/Understanding_WCAG/Keyboard
- MDN,
<dialog>: The Dialog element — https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog - MDN, zoom — https://developer.mozilla.org/en-US/docs/Web/CSS/zoom
- MDN, image-set() — https://developer.mozilla.org/en-US/docs/Web/CSS/image/image-set