- 前端
- UI库/组件
【免费下载链接】PhotoSwipe
JavaScript image gallery for mobile and desktop, modular, framework independent
PhotoSwipe(当前仓库:gh_mirrors/ph/PhotoSwipe)是一款面向移动端与桌面的模块化、框架无关的 JavaScript 图片画廊库。本文基于仓库中的事件参考文档 docs/events.md,系统讲解 PhotoSwipe 5 的整套事件体系:包括初始化事件、开关过渡事件、关闭销毁事件、指针手势事件与幻灯片内容事件共五大类,并深入结合 src/js/core/eventable.js、src/js/lightbox/lightbox.js、src/js/photoswipe.js 等核心源码,说明每个事件在何时、由谁、以怎样的参数触发,以及如何利用preventDefault()拦截默认行为。读完本文,你将能在自己的图库集成中精准地在正确的时机挂载/卸载逻辑,实现自定义 UI、自定义内容类型、加载状态提示、手势拦截等高级功能。
事件模型基础:绑定在 Lightbox 上,自动转发到 PhotoSwipe 核心
PhotoSwipe 的事件 API 遵循标准的发布/订阅模式,核心方法在事件基类 src/js/core/eventable.js 的Eventable类中定义:
on(name, fn):注册事件监听器;off(name, fn):移除监听器;dispatch(name, details):触发事件,返回事件对象(内部使用);addFilter / removeFilter / applyFilters:与事件平行的"过滤器"机制(见 docs/filters.md)。
文档开篇即给出了核心用法:所有事件都可以直接绑定到 Lightbox 实例上,当 PhotoSwipe 打开时它们会被自动映射到 PhotoSwipe 核心。
const lightbox = new PhotoSwipeLightbox({ // options... }); lightbox.init();这一"自动映射"的实现位于 src/js/lightbox/lightbox.js:Lightbox 在创建出 PhotoSwipe 核心实例(pswp)后,会把this._listeners中已注册的所有监听器逐一pswp.on(...)转发过去;过滤器同理。因此你只需要关心 Lightbox 这一个对象,无需关心实例何时创建。
监听器的存储与事件对象
Eventable内部用_listeners对象按事件名存储回调数组(src/js/core/eventable.js)。触发时,dispatch会构造一个PhotoSwipeEvent事件对象(src/js/core/eventable.js),它包含三个关键能力:
event.type:事件名称;event.defaultPrevented:布尔值,表示是否已被preventDefault()拦截;event.preventDefault():调用后置defaultPrevented = true,可拦截默认行为的事件会因此跳过 PhotoSwipe 的默认处理(文档中凡标注 "can be default prevented" 的事件均可用)。
事件对象还会把dispatch时传入的详情对象(如content、slide、width、isLazy等)直接展开合并到自身,所以回调里可以直接解构取值,例如lightbox.on('contentInit', ({ content }) => {...})。
完整的事件名称与参数类型定义在 src/js/core/eventable.js 的
PhotoSwipeEventsMap类型声明中,TypeScript 与 JSDoc 用户可直接获得补全与类型检查支持。
初始化事件:理解 PhotoSwipe 的启动时间线
文档给出的初始化事件示例完整如下,读者可结合源码逐行理解每个事件的确切触发时机:
import PhotoSwipeLightbox from '/photoswipe/photoswipe-lightbox.esm.js'; const lightbox = new PhotoSwipeLightbox({ gallery: '#gallery--test-init-events', children: 'a', pswpModule: () => import('/photoswipe/photoswipe.esm.js') }); lightbox.on('beforeOpen', () => { console.log('beforeOpen'); // photoswipe starts to open }); lightbox.on('firstUpdate', () => { console.log('firstUpdate'); // photoswipe keeps opening // you may modify initial index or basic DOM structure }); lightbox.on('initialLayout', () => { console.log('initialLayout'); // photoswipe measures size of various elements // if you need to read getBoundingClientRect of something - do it here }); lightbox.on('change', () => { // triggers when slide is switched, and at initialization console.log('change'); }); lightbox.on('afterInit', () => { console.log('afterInit'); // photoswipe fully initialized and opening transition is running (if available) }); lightbox.on('bindEvents', () => { console.log('bindEvents'); // photoswipe binds DOM events (such as pointer events, wheel, etc) }); lightbox.init();对照 src/js/photoswipe.js 的init()方法,这组事件的真实触发顺序是:
beforeOpen(src/js/photoswipe.js):紧接着遗留事件init之后触发。此时 PhotoSwipe 已标记为打开(isOpen = true),随后会创建主 DOM 结构(_createMainStructure())。适用于需要在打开前完成一次性准备的场景。firstUpdate(src/js/photoswipe.js):在currIndex/potentialIndex已从options.index初始化之后触发。文档明确提示:这是修改初始索引或基础 DOM 结构的机会——例如根据路由参数、缩略图点击位置等动态修正起始页。注意在此之后 PhotoSwipe 会对索引做一次合法性修正(NaN/越界时归零,见 src/js/photoswipe.js),所以在此事件中赋一个合法索引是安全的。initialLayout(src/js/photoswipe.js):在updateSize()强制同步布局、拿到初始缩略图边界(getThumbBounds())之后触发。此时各元素尺寸已经确定,如果需要读取某个元素的getBoundingClientRect(),应当在此事件中读取(否则布局可能尚未完成)。change:在中间幻灯片内容setContent(...)完成后触发(src/js/photoswipe.js)。文档明确说明:切页时会触发,初始化时也会触发一次(另外 src/js/main-scroll.js 在滚动切换目标后、src/js/photoswipe.js 在refreshSlideContent()后同样会派发change)。afterInit(src/js/photoswipe.js):在opener.open()(打开动画开始)之后触发。此时 PhotoSwipe已经完全初始化,且打开过渡动画(如果可用)正在运行。这是绝大多数业务逻辑(如埋点、自定义计数器初始化)的推荐挂载点。bindEvents(src/js/photoswipe.js):在openingAnimationEnd回调里、PhotoSwipe 绑定window.resize/scroll监听之后触发。文档注释指出此时 PhotoSwipe 绑定 DOM 事件(pointer 事件、滚轮事件等)。如果你有需要与 PhotoSwipe 生命周期同步的全局监听(如自定义键盘快捷键),可在此事件中绑定。
打开过程中的内部机制补充
bindEvents被放在openingAnimationEnd之后并非偶然:PhotoSwipe 打开时只会先为当前幻灯片设置内容(itemHolders[1]),相邻幻灯片的内容要等openingAnimationEnd后才填充(见 src/js/photoswipe.js 中itemHolders[0]、itemHolders[2]的处理与appendHeavy()、contentLoader.updateLazy()的调用)。这与文档中change"切页时触发"的描述相互印证——首屏之后每次滑动切换,change都会再次触发。
打开与关闭过渡事件:无论是否禁用动画都会触发
文档特别强调:即便过渡动画被禁用(如showHideAnimationType: 'none'或动画时长被置零),以下四个事件也依然会触发,因此它们非常适合作为"过渡开始/结束"的可靠信号。
import PhotoSwipeLightbox from '/photoswipe/photoswipe-lightbox.esm.js'; const lightbox = new PhotoSwipeLightbox({ gallery: '#gallery--test-opening-closing-events', children: 'a', pswpModule: () => import('/photoswipe/photoswipe.esm.js') }); lightbox.on('openingAnimationStart', () => { console.log('openingAnimationStart'); }); lightbox.on('openingAnimationEnd', () => { console.log('openingAnimationEnd'); }); lightbox.on('closingAnimationStart', () => { console.log('closingAnimationStart'); }); lightbox.on('closingAnimationEnd', () => { console.log('closingAnimationEnd'); }); lightbox.init();源码佐证:过渡编排集中在 src/js/opener.js 的Opener类中。
openingAnimationStart/closingAnimationStart:在_initiate()中派发(src/js/opener.js),同时会派发遗留事件initialZoomIn/initialZoomOut。此时过渡时长(--pswp-transition-duration)已被写入样式,pswp--ui-visible类被切换。openingAnimationEnd/closingAnimationEnd:在动画完成回调_onAnimationComplete()中派发(src/js/opener.js),同时派发遗留事件initialZoomInEnd/initialZoomOutEnd。若本次是关闭且动画完成,紧接着会调用pswp.destroy()进入销毁流程(见下文"关闭事件")。
另外值得留意:打开动画开始时 PhotoSwipe 会等待当前幻灯片占位图解码完成,最长 250ms、最短 50ms(src/js/opener.js),这保证了从缩略图缩放的过渡足够平滑。
关闭事件:在正确的时机卸载资源
import PhotoSwipeLightbox from '/photoswipe/photoswipe-lightbox.esm.js'; const lightbox = new PhotoSwipeLightbox({ gallery: '#gallery--test-closing-events', children: 'a', pswpModule: () => import('/photoswipe/photoswipe.esm.js') }); lightbox.on('close', () => { // PhotoSwipe starts to close, unbind most events here console.log('close'); }); lightbox.on('destroy', () => { // PhotoSwipe is fully closed, destroy everything console.log('destroy'); }); lightbox.init();close:由pswp.close()触发(src/js/photoswipe.js)。此时 PhotoSwipe 将isDestroying = true,派发close后立即移除所有内部 DOM 事件(this.events.removeAll())并开始关闭过渡。文档建议:在这里解绑大部分事件。destroy:在关闭过渡真正结束后(closingAnimationEnd之后的pswp.destroy())触发(src/js/photoswipe.js)。此时 PhotoSwipe 清空监听器、移除根元素、销毁所有幻灯片与内容加载器。文档建议:在这里销毁一切。若你在关闭动画期间pswp.close()未被调用就直接调用destroy(),PhotoSwipe 会把showHideAnimationType置为'none'再走一次close()流程,保证事件顺序依然正确(src/js/photoswipe.js)。
在 Lightbox 层面,destroy()后还会把window.pswp与this.pswp清理为undefined(src/js/lightbox/lightbox.js),因此同一个 Lightbox 实例是可以被重复打开多次的——这正是事件监听器能"自动映射、反复生效"的前提。
指针与手势事件:拦截触摸与拖拽行为
import PhotoSwipeLightbox from '/photoswipe/photoswipe-lightbox.esm.js'; const lightbox = new PhotoSwipeLightbox({ gallery: '#gallery--test-pointer-events', children: 'a', pswpModule: () => import('/photoswipe/photoswipe.esm.js') }); lightbox.on('pointerDown', (e) => { console.log('pointerDown', e.originalEvent); }); lightbox.on('pointerMove', (e) => { console.log('pointerMove', e.originalEvent); }); lightbox.on('pointerUp', (e) => { console.log('pointerUp', e.originalEvent); }); lightbox.on('pinchClose', (e) => { // triggered when using pinch to close gesture // can be default prevented console.log('pinchClose', e.bgOpacity); }); lightbox.on('verticalDrag', (e) => { // triggered when using vertical drag to close gesture // can be default prevented console.log('verticalDrag', e.panY); }); lightbox.init();这组事件的触发点在 src/js/gestures/gestures.js 的Gestures类中:
pointerDown/pointerMove/pointerUp:分别在指针按下(src/js/gestures/gestures.js)、移动(src/js/gestures/gestures.js)、抬起(src/js/gestures/gestures.js)时派发,事件参数为{ originalEvent },即浏览器原生 PointerEvent。三个事件均可通过preventDefault()拦截,从而完全接管某类指针交互。注意pointerDown在opener.isOpen为 false(即还在开关过渡中)时会直接preventDefault()并返回,此时不会派发事件。pinchClose:由双指捏合关闭手势触发,派发点位于 src/js/gestures/zoom-handler.js,参数{ bgOpacity }表示随捏合进度变化的背景透明度。可被preventDefault()拦截(拦截后背景透明度不会随捏合变化,相当于禁用了捏合关闭)。verticalDrag:由垂直拖拽关闭手势触发,派发点位于 src/js/gestures/drag-handler.js,参数{ panY }为当前幻灯片垂直位移。可被preventDefault()拦截(拦截后幻灯片不会产生带摩擦的垂直位移,相当于禁用了下拉关闭)。
除上述文档收录的手势事件外,
PhotoSwipeEventsMap中还声明了未在文档中展开说明的imageClickAction、bgClickAction、tapAction、doubleTapAction(点击/双击行为,均可拦截)以及keydown、wheel等事件,可在 src/js/core/eventable.js 中查阅。
幻灯片内容事件:内容从创建到销毁的完整生命周期
这是最庞大也最常用的一组事件,覆盖了每一条幻灯片内容(Content)从诞生到销毁的全部阶段。文档示例:
import PhotoSwipeLightbox from '/photoswipe/photoswipe-lightbox.esm.js'; import PhotoSwipe from '/photoswipe/photoswipe.esm.js'; const lightbox = new PhotoSwipeLightbox({ gallery: '#gallery--test-content-events', children: 'a', pswpModule: PhotoSwipe }); lightbox.on('contentInit', ({ content }) => { console.log('contentInit', content); }); lightbox.on('contentLoad', ({ content, isLazy }) => { // content starts to load // can be default prevented // assign elements to `content.element` console.log('contentLoad', content, isLazy); }); lightbox.on('contentLoadImage', ({ content, isLazy }) => { // similar to the previous one, but triggers only for image content // can be default prevented console.log('contentLoadImage', content, isLazy); }); lightbox.on('loadComplete', ({ content, slide }) => { console.log('loadComplete', content); }); lightbox.on('contentResize', ({ content, width, height }) => { // content will be resized // can be default prevented console.log('contentResize', content, width, height); }); lightbox.on('imageSizeChange', ({ content, width, height, slide }) => { // content.element is image console.log('imageSizeChange', content, width, height, slide, slide.index); }); lightbox.on('contentLazyLoad', ({ content }) => { // content start to lazy-load // can be default prevented console.log('contentLazyLoad', content); }); lightbox.on('contentAppend', ({ content }) => { // content is added to dom // can be default prevented // content.slide.container.appendChild(content.element); console.log('contentAppend', content); }); lightbox.on('contentActivate', ({ content }) => { // content becomes active (the current slide) // can be default prevented console.log('contentActivate', content); }); lightbox.on('contentDeactivate', ({ content }) => { // content becomes inactive // can be default prevented console.log('contentDeactivate', content); }); lightbox.on('contentRemove', ({ content }) => { // content is removed from DOM // can be default prevented console.log('contentRemove', content); }); lightbox.on('contentDestroy', ({ content }) => { // content will be destroyed // can be default prevented console.log('contentDestroy', content); }); lightbox.init();文档说明:这些事件的进阶用法示例请参阅 自定义内容。下面按内容对象在其实现类 src/js/slide/content.js 中的派发位置,逐一说明触发时机与参数:
创建与加载阶段
| 事件 | 触发时机 | 参数 | 可否拦截 |
|---|---|---|---|
contentInit | Content构造时立即派发(src/js/slide/content.js) | { content } | 否 |
contentLazyLoad | 内容开始懒加载前(lazyLoad(),src/js/slide/content.js) | { content } | 是 |
contentLoad | 内容开始加载前(load(),src/js/slide/content.js) | { content, isLazy } | 是 |
contentLoadImage | 仅图片内容、开始加载图片前(loadImage(),src/js/slide/content.js) | { content, isLazy } | 是 |
loadComplete | 加载成功(onLoaded(),src/js/slide/content.js)或失败(onError(),src/js/slide/content.js)时;失败时isError: true | { content, slide, isError? } | 否 |
理解这几个事件的关键在于 PhotoSwipe 的两阶段加载模型:
- 懒加载阶段(Lightbox 打开前就可能发生):
lazyLoadData()/lazyLoadSlide()(见 src/js/slide/loader.js)会先依据视口与初始缩放级别估算图片显示尺寸,然后调用content.lazyLoad()→ 派发contentLazyLoad→load(true)→ 派发contentLoad(isLazy: true)→ 若是图片再派发contentLoadImage。 - 正式显示阶段:幻灯片被激活后,
setDisplayedSize()首次获得显示尺寸时会触发loadImage(false)(src/js/slide/content.js),此时再派发一次contentLoad/contentLoadImage(isLazy: false)。
contentLoad的一个典型用法是文档注释中提到的:为自定义内容类型手动把元素赋给content.element(配合content.type与content.data.html,详见 docs/custom-content.md)。
尺寸与状态阶段
| 事件 | 触发时机 | 参数 | 可否拦截 |
|---|---|---|---|
contentResize | 内容即将应用新显示尺寸前(setDisplayedSize(),src/js/slide/content.js) | { content, width, height } | 是 |
imageSizeChange | 图片内容尺寸更新后(src/js/slide/content.js) | { content, width, height, slide } | 否 |
contentAppend | 内容被加入 DOM 前(append(),src/js/slide/content.js) | { content } | 是 |
contentActivate | 内容成为当前活动幻灯片前(activate(),src/js/slide/content.js) | { content } | 是 |
contentDeactivate | 内容变为非活动时(deactivate(),src/js/slide/content.js) | { content } | 是 |
contentRemove | 内容从 DOM 移除前(remove(),src/js/slide/content.js) | { content } | 是 |
contentDestroy | 内容销毁前(destroy(),src/js/slide/content.js) | { content } | 是 |
实践要点:
contentResize拦截后,PhotoSwipe 不会把新的宽高写入内容元素(src/js/slide/content.js),适合自定义尺寸管理;contentAppend拦截后,你需要自行把content.element挂到content.slide.container上(文档注释中直接给出了content.slide.container.appendChild(content.element)的替代写法);contentActivate内部在非 Safari 且图片解码中的情况下会强制提前挂载图片(src/js/slide/content.js),拦截该事件会影响此行为,使用时需谨慎;contentDeactivate同时负责把holderElement的aria-hidden置为true(src/js/slide/content.js),拦截后需自行处理无障碍属性。
图片解码优化
contentAppend之后,PhotoSwipe 会利用HTMLImageElement.decode()对非活动幻灯片(以及 Safari 下的所有幻灯片)做解码优化,解码完成后再调用appendImage()真正挂载图片(src/js/slide/content.js),并在其中派发未收录在文档中的contentAppendImage事件(src/js/slide/content.js)。这保证了"图片在完整加载前即可部分渲染",提升翻页流畅度。
事件触发时序总览
综合上述源码分析,一个完整的 PhotoSwipe 会话(打开 → 浏览 → 关闭)的核心事件时间线如下:
lightbox.init() └─ 点击缩略图 → loadAndOpen() → 加载 pswpModule PhotoSwipe.init() ├─ beforeOpen (打开流程开始,创建 DOM 结构) ├─ firstUpdate (可修改初始索引) ├─ initialLayout (尺寸已就绪,可读 getBoundingClientRect) ├─ change (初始化时首次触发;之后每次切页都会触发) ├─ contentInit → contentLazyLoad → contentLoad → contentLoadImage → loadComplete ├─ openingAnimationStart ├─ openingAnimationEnd → bindEvents(相邻幻灯片内容填充、挂载 resize/scroll) ├─ contentAppend / contentActivate / imageSizeChange / contentResize(浏览期间反复触发) ├─ ... ├─ close (关闭流程开始,解绑大部分事件) ├─ closingAnimationStart ├─ closingAnimationEnd └─ destroy (彻底销毁)实战建议与常见场景
- 初始索引修正:在
firstUpdate中根据业务条件改写pswp.currIndex,比在构造后修改更可靠(源码在 src/js/photoswipe.js 中先赋值后派发事件)。 - 自定义加载状态提示:监听
contentLoad(开始加载)与loadComplete(结束,注意isError参数区分成败),在content.element或 UI 上切换加载指示器。 - 拦截手势关闭:在
pinchClose/verticalDrag中调用e.preventDefault()即可禁用捏合/下拉关闭;需要同时保留手势动画则只读取bgOpacity/panY做自定义处理。 - 自定义内容类型:在
contentLoad中为content.type非'image'的内容创建并赋值content.element,完整范式见 docs/custom-content.md。 - 埋点与统计:打开埋点放
afterInit,切页埋点放change(结合slide.index或pswp.currIndex),关闭埋点放close,销毁清理放destroy。 - 绑定 vs 卸载对称性:
bindEvents(打开后绑定全局监听)与close(解绑)是天然的对称配对;close中务必解绑你在bindEvents中挂到window上的监听器。
最后提醒:以上代码示例中的/photoswipe/photoswipe-lightbox.esm.js与/photoswipe/photoswipe.esm.js是官方文档站的资源路径;在本仓库中,构建产物位于 demo-docs-website/static/photoswipe/(含 ESM 与 UMD 格式),生产项目中也可通过 npm 安装后按需引入(Lightbox 与核心模块分离,核心通过pswpModule动态加载以减小首屏体积)。若需在源码层面进一步深入事件派发细节,可重点阅读 src/js/core/eventable.js、src/js/photoswipe.js、src/js/opener.js、src/js/gestures/gestures.js 与 src/js/slide/content.js。
- 前端
- UI库/组件
【免费下载链接】PhotoSwipe
JavaScript image gallery for mobile and desktop, modular, framework independent
相关推荐
mojs动画事件系统:从触发到完成的全生命周期
mojs动画事件系统:从触发到完成的全生命周期 动画交互是现代Web应用提升用户体验的核心手段,但要实现流畅自然的动画效果,离不开对事件生命周期的精准控制。mo
前端Resumable.js事件系统完全指南:从文件添加到上传完成的完整生命周期
Resumable.js事件系统完全指南:从文件添加到上传完成的完整生命周期 Resumable.js是一个强大的JavaScript库,专门用于实现可恢复的大
前端Uppy插件生命周期终极指南:从初始化到销毁的完整过程
Uppy插件生命周期终极指南:从初始化到销毁的完整过程 Uppy作为一款功能强大的开源文件上传器,其插件系统是实现灵活扩展的核心。本文将深入解析Uppy插件从初
前端UI组件后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考