vue3中使用medium-zoom

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 = `
# 我的文章
![图片1](/image-1.jpg)
![图片2](/image-2.jpg)
`

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 版本项目。

相关推荐
xump1 小时前
如何在DevTools选中调试一个实时交互才能显示的元素样式
前端·javascript·css
折翅嘀皇虫1 小时前
fastdds.type_propagation 详解
java·服务器·前端
Front_Yue1 小时前
深入探究跨域请求及其解决方案
前端·javascript
wordbaby1 小时前
React Native 进阶实战:基于 Server-Driven UI 的动态表单架构设计
前端·react native·react.js
抱琴_1 小时前
【Vue3】我用 Vue 封装了个 ECharts Hooks,同事看了直接拿去复用
前端·vue.js
风止何安啊1 小时前
JS 里的 “变量租房记”:闭包是咋把变量 “扣” 下来的?
前端·javascript·node.js
老华带你飞1 小时前
社区养老保障|智慧养老|基于springboot+小程序社区养老保障系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·小程序·毕设·社区养老保障
Danny_FD1 小时前
用 ECharts markLine 标注节假日
前端·echarts
程序员西西1 小时前
SpringBoot无感刷新Token实战指南
java·开发语言·前端·后端·计算机·程序员