Medium Zoom - Vue3 使用文档
📦 简介
Medium Zoom 是一个轻量级 JavaScript 库,用于实现类似 Medium 网站的图片点击放大缩放效果。本文档专注于在 Vue3 项目中的集成和使用。
✨ 核心特性
- 📱 响应式设计 - 完美适配移动端和桌面端
- 🚀 高性能 - 优化至 60fps 流畅动画
- ⚡️ 高清图支持 - 通过
data-zoom-src属性加载高清大图 - 🎨 可定制化 - 支持自定义边距、背景、滚动偏移等
- 🎂 事件系统 - 完整的生命周期事件监听
- 🔌 零依赖 - 无任何运行时依赖
- 💎 框架友好 - 完美支持 Vue3 组合式 API
📥 安装
使用 npm
bash
npm install medium-zoom
使用 yarn
bash
yarn add medium-zoom
使用 pnpm
bash
pnpm add medium-zoom
🚀 Vue3 快速开始
方法一:封装组件(推荐)
创建一个可复用的 ImageZoom.vue 组件:
vue
<script setup lang="ts">
import { watch, type ImgHTMLAttributes, type ComponentPublicInstance } from 'vue'
import mediumZoom, { type Zoom, type ZoomOptions } from 'medium-zoom'
interface Props extends ImgHTMLAttributes {
options?: ZoomOptions
}
const props = defineProps<Props>()
let zoom: Zoom | null = null
function getZoom() {
if (zoom === null) {
zoom = mediumZoom(props.options)
}
return zoom
}
function attachZoom(ref: Element | ComponentPublicInstance | null) {
const image = ref as HTMLImageElement | null
const zoom = getZoom()
if (image) {
zoom.attach(image)
} else {
zoom.detach()
}
}
watch(() => props.options, (options) => {
const zoom = getZoom()
zoom.update(options || {})
})
</script>
<template>
<img :ref="attachZoom" v-bind="$attrs" />
</template>
使用封装的组件
vue
<script setup lang="ts">
import ImageZoom from './components/ImageZoom.vue'
</script>
<template>
<article>
<h1>我的图片画廊</h1>
<!-- 基础使用 -->
<ImageZoom src="/images/photo-1.jpg" alt="照片1" />
<!-- 自定义背景色 -->
<ImageZoom
src="/images/photo-2.jpg"
alt="照片2"
:options="{ background: '#000000' }"
/>
<!-- 自定义边距 -->
<ImageZoom
src="/images/photo-3.jpg"
alt="照片3"
:options="{ margin: 48, background: 'rgba(0,0,0,0.9)' }"
/>
<!-- 高清图支持 -->
<ImageZoom
src="/images/thumbnail.jpg"
data-zoom-src="/images/hd-image.jpg"
alt="高清图片"
/>
</article>
</template>
方法二:在 Composable 中使用
创建 useImageZoom.ts:
typescript
import { onMounted, onUnmounted, ref, type Ref } from 'vue'
import mediumZoom, { type Zoom, type ZoomOptions } from 'medium-zoom'
export function useImageZoom(
selector?: string | HTMLElement,
options?: ZoomOptions
) {
const zoom: Ref<Zoom | null> = ref(null)
onMounted(() => {
zoom.value = mediumZoom(selector, options)
})
onUnmounted(() => {
zoom.value?.detach()
})
return {
zoom,
open: () => zoom.value?.open(),
close: () => zoom.value?.close(),
toggle: () => zoom.value?.toggle(),
}
}
使用 Composable:
vue
<script setup lang="ts">
import { onMounted } from 'vue'
import { useImageZoom } from './composables/useImageZoom'
// 初始化 zoom
const { zoom, open, close } = useImageZoom('[data-zoomable]', {
margin: 24,
background: '#BADA55',
})
// 也可以在挂载后动态附加图片
onMounted(() => {
zoom.value?.attach('.gallery-image')
})
</script>
<template>
<div>
<img src="/image-1.jpg" data-zoomable alt="图片1" />
<img src="/image-2.jpg" data-zoomable alt="图片2" />
<img src="/image-3.jpg" class="gallery-image" alt="图片3" />
<!-- 手动控制按钮 -->
<button @click="open">放大第一张图片</button>
<button @click="close">关闭缩放</button>
</div>
</template>
方法三:直接在组件中使用
vue
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import mediumZoom from 'medium-zoom'
const zoomRef = ref(null)
onMounted(() => {
zoomRef.value = mediumZoom('.zoomable', {
margin: 24,
background: '#fff',
scrollOffset: 40,
})
})
onUnmounted(() => {
zoomRef.value?.detach()
})
</script>
<template>
<div>
<img src="/image.jpg" class="zoomable" alt="可缩放图片" />
</div>
</template>
⚙️ 配置选项 (ZoomOptions)
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
margin |
number |
0 |
缩放图片外的空白边距(像素) |
background |
string |
"#fff" |
遮罩层背景色,支持任何 CSS 颜色值 |
scrollOffset |
number |
40 |
滚动多少像素后自动关闭缩放 |
container |
`string | HTMLElement | object` |
template |
`string | HTMLTemplateElement` | null |
配置示例
typescript
const options: ZoomOptions = {
margin: 48, // 48px 边距
background: 'rgba(0,0,0,0.9)', // 半透明黑色背景
scrollOffset: 100, // 滚动 100px 后关闭
container: '#zoom-container', // 在指定容器内缩放
}
🎯 API 方法
open(options?: { target?: HTMLElement }): Promise<Zoom>
打开缩放,返回 Promise。
typescript
zoom.value?.open()
// 或指定目标图片
zoom.value?.open({ target: imgElement })
close(): Promise<Zoom>
关闭缩放,返回 Promise。
typescript
zoom.value?.close()
toggle(options?: { target?: HTMLElement }): Promise<Zoom>
切换缩放状态(打开/关闭)。
typescript
zoom.value?.toggle()
attach(...selectors): Zoom
附加图片到缩放实例。
typescript
zoom.value?.attach('#image-1', '#image-2')
zoom.value?.attach(document.querySelector('#image-3'))
detach(...selectors): Zoom
从缩放实例中移除图片。
typescript
zoom.value?.detach('#image-1')
zoom.value?.detach() // 移除所有图片
update(options: ZoomOptions): Zoom
更新配置选项。
typescript
zoom.value?.update({
background: '#000',
margin: 50
})
clone(options?: ZoomOptions): Zoom
克隆一个新的缩放实例,合并新配置。
typescript
const newZoom = zoom.value?.clone({ margin: 100 })
on(type: string, listener: Function, options?): Zoom
注册事件监听器。
typescript
zoom.value?.on('open', (event) => {
console.log('图片已打开', event.detail.zoom)
})
off(type: string, listener: Function, options?): Zoom
移除事件监听器。
typescript
const handler = (event) => { /* ... */ }
zoom.value?.on('open', handler)
zoom.value?.off('open', handler)
getOptions(): ZoomOptions
获取当前配置选项。
typescript
const currentOptions = zoom.value?.getOptions()
getImages(): HTMLElement[]
获取所有已附加的图片元素。
typescript
const images = zoom.value?.getImages()
getZoomedImage(): HTMLElement | null
获取当前正在缩放的图片元素。
typescript
const currentImage = zoom.value?.getZoomedImage()
🎭 事件系统
可用事件
| 事件名 | 触发时机 | 说明 |
|---|---|---|
open |
调用 open() 方法时立即触发 |
缩放动画开始前 |
opened |
缩放动画完成后 | 图片已完全放大 |
close |
调用 close() 方法时立即触发 |
关闭动画开始前 |
closed |
关闭动画完成后 | 图片已完全缩小 |
detach |
调用 detach() 方法时 |
图片从实例中移除 |
update |
调用 update() 方法时 |
配置选项已更新 |
Vue3 中使用事件
vue
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import mediumZoom from 'medium-zoom'
const zoom = ref(null)
const zoomCount = ref(0)
onMounted(() => {
zoom.value = mediumZoom('[data-zoomable]')
// 监听打开事件
zoom.value.on('open', (event) => {
console.log('开始缩放:', event.target.alt)
})
// 监听打开完成事件
zoom.value.on('opened', (event) => {
zoomCount.value++
console.log(`已缩放 ${zoomCount.value} 次`)
})
// 监听关闭事件
zoom.value.on('close', () => {
console.log('开始关闭缩放')
})
// 监听关闭完成事件
zoom.value.on('closed', () => {
console.log('缩放已关闭')
})
// 单次事件监听
zoom.value.on('open', (event) => {
console.log('这个只触发一次')
}, { once: true })
})
</script>
<template>
<div>
<img src="/image.jpg" data-zoomable alt="图片" />
<p>已缩放次数: {{ zoomCount }}</p>
</div>
</template>
🎨 高级用法
1. 高清图加载
使用 data-zoom-src 属性指定高清大图:
vue
<template>
<ImageZoom
src="/thumbnails/small.jpg"
data-zoom-src="/images/large-hd.jpg"
alt="高清图片"
/>
</template>
2. 在 Markdown 渲染中使用
适用于博客、文档等场景:
vue
<script setup lang="ts">
import { onMounted, ref, nextTick } from 'vue'
import mediumZoom from 'medium-zoom'
import MarkdownIt from 'markdown-it'
const md = new MarkdownIt()
const content = ref('')
const zoom = ref(null)
const markdown = `
# 我的文章


`
onMounted(async () => {
content.value = md.render(markdown)
await nextTick()
// 为所有 markdown 渲染的图片添加缩放
zoom.value = mediumZoom('.markdown-body img', {
background: 'rgba(255,255,255,0.95)',
margin: 24
})
})
</script>
<template>
<div class="markdown-body" v-html="content"></div>
</template>
3. 自定义容器
在指定容器内缩放:
vue
<script setup lang="ts">
import { onMounted } from 'vue'
import mediumZoom from 'medium-zoom'
onMounted(() => {
mediumZoom('[data-zoomable]', {
container: '#custom-container',
background: 'transparent'
})
})
</script>
<template>
<div id="custom-container" style="position: relative; height: 500px;">
<img src="/image.jpg" data-zoomable alt="图片" />
</div>
</template>
4. 动态添加/移除图片
vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import mediumZoom from 'medium-zoom'
const images = ref(['/img1.jpg', '/img2.jpg'])
const zoom = ref(null)
onMounted(() => {
zoom.value = mediumZoom('.gallery img')
})
function addImage() {
const newImg = `/img${images.value.length + 1}.jpg`
images.value.push(newImg)
// 下一帧后附加新图片
nextTick(() => {
const newElement = document.querySelector(`.img-${images.value.length - 1}`)
zoom.value?.attach(newElement)
})
}
function removeImage(index: number) {
const imgElement = document.querySelector(`.img-${index}`)
zoom.value?.detach(imgElement)
images.value.splice(index, 1)
}
</script>
<template>
<div class="gallery">
<img
v-for="(src, index) in images"
:key="index"
:src="src"
:class="`img-${index}`"
alt="图片"
/>
<button @click="addImage">添加图片</button>
</div>
</template>
5. 从外部按钮触发缩放
vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import mediumZoom from 'medium-zoom'
const zoom = ref(null)
const imageRef = ref(null)
onMounted(() => {
zoom.value = mediumZoom(imageRef.value)
})
function handleZoom() {
zoom.value?.toggle({ target: imageRef.value })
}
</script>
<template>
<div>
<img ref="imageRef" src="/image.jpg" alt="图片" />
<button @click="handleZoom">点击缩放图片</button>
</div>
</template>
6. 图片画廊示例
vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import mediumZoom from 'medium-zoom'
const zoom = ref(null)
const gallery = [
{ id: 1, src: '/gallery/img1.jpg', thumb: '/gallery/thumb1.jpg', alt: '风景1' },
{ id: 2, src: '/gallery/img2.jpg', thumb: '/gallery/thumb2.jpg', alt: '风景2' },
{ id: 3, src: '/gallery/img3.jpg', thumb: '/gallery/thumb3.jpg', alt: '风景3' },
]
onMounted(() => {
zoom.value = mediumZoom('.gallery-image', {
margin: 48,
background: '#000',
})
zoom.value.on('opened', (event) => {
console.log(`正在查看: ${event.target.alt}`)
})
})
</script>
<template>
<div class="gallery-grid">
<div v-for="item in gallery" :key="item.id" class="gallery-item">
<img
:src="item.thumb"
:data-zoom-src="item.src"
:alt="item.alt"
class="gallery-image"
/>
<p>{{ item.alt }}</p>
</div>
</div>
</template>
<style scoped>
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
}
.gallery-image {
width: 100%;
cursor: zoom-in;
transition: opacity 0.3s;
}
.gallery-image:hover {
opacity: 0.8;
}
</style>
🎨 样式定制
如果需要自定义 z-index(避免与其他 UI 框架冲突):
css
/* 在全局样式中添加 */
.medium-zoom-overlay,
.medium-zoom-image--opened {
z-index: 9999;
}
自定义动画时长:
css
.medium-zoom-image--opened {
transition: transform 300ms cubic-bezier(0.2, 0, 0.2, 1);
}
📱 TypeScript 支持
完整的 TypeScript 类型定义:
typescript
import mediumZoom, {
type Zoom,
type ZoomOptions,
type ZoomSelector,
type ZoomOpenOptions
} from 'medium-zoom'
// Zoom 实例类型
const zoom: Zoom = mediumZoom()
// 配置选项类型
const options: ZoomOptions = {
margin: 24,
background: '#fff',
scrollOffset: 40,
}
// 选择器类型
const selector: ZoomSelector = '.image' // string | HTMLElement | HTMLElement[] | NodeList
🔧 常见问题
Q: 缩放的图片看不见?
A: 某些 UI 框架可能设置了较高的 z-index,需要手动设置:
css
.medium-zoom-overlay,
.medium-zoom-image--opened {
z-index: 9999;
}
Q: 在 v-for 中使用无效?
A: 确保在 DOM 更新后再附加图片:
vue
<script setup>
import { nextTick } from 'vue'
async function addImages() {
images.value.push(newImage)
await nextTick()
zoom.value?.attach('.new-image')
}
</script>
Q: 如何在路由切换时清理?
A: 使用 onUnmounted 钩子:
vue
<script setup>
import { onUnmounted } from 'vue'
onUnmounted(() => {
zoom.value?.detach()
})
</script>
🌐 浏览器支持
- Chrome 36+
- Firefox 34+
- Safari 9+
- Edge 12+
- IE 10+ (需要 template polyfill)
📚 参考资源
提示: 本文档基于 medium-zoom 库分析生成,适用于 Vue 3.x 版本项目。