Vue 指南

学习如何用 @reelkit/vue.

触摸优先
带惯性和吸附的滑动
键盘导航
方向键 + Escape
滚轮滚动
可选,带防抖
虚拟化
10,000+ 条目,DOM 里只有 3 个
指示器
Instagram 风格的圆点滚动
编程式 API
通过模板 ref 调用 next()、prev()、goTo()
循环模式
无限循环导航
方向可选
竖向或横向
组合式 API
<script setup> 搭配组合式函数

基础滑动器

<Reel> 组件包装了核心的滑动控制器。用 #item 插槽渲染每张幻灯片,并自动虚拟化 —— 只有可见的幻灯片会被挂载。

App.vue
<script setup lang="ts">
import { Reel, ReelIndicator } from '@reelkit/vue';

const items = [
  { title: 'Virtualized', subtitle: 'Only 3 slides in DOM', color: '#6366f1' },
  { title: 'Touch First', subtitle: 'Native swipe gestures', color: '#8b5cf6' },
  { title: 'Zero Deps', subtitle: 'Tiny bundle size', color: '#7c3aed' },
  { title: 'Keyboard Nav', subtitle: 'Full a11y support', color: '#ec4899' },
  { title: 'SSR Ready', subtitle: 'Works everywhere', color: '#14b8a6' },
  { title: '60fps', subtitle: 'Smooth animations', color: '#f59e0b' },
];

const onAfterChange = (index: number) => {
  console.log('Current index:', index);
};
</script>

<template>
  <Reel
    :count="items.length"
    style="width: 100%; height: 100dvh"
    direction="vertical"
    :enable-wheel="true"
    @after-change="onAfterChange"
  >
    <template #item="{ index, size }">
      <div
        :style="{
          width: size[0] + 'px',
          height: size[1] + 'px',
          background: items[index].color,
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          color: '#fff',
        }"
      >
        <div style="font-size: 1.5rem; font-weight: bold">
          {{ items[index].title }}
        </div>
        <div style="font-size: 0.875rem; opacity: 0.8">
          {{ items[index].subtitle }}
        </div>
      </div>
    </template>

    <div style="position: absolute; right: 12px; top: 50%; transform: translateY(-50%); z-index: 10">
      <ReelIndicator direction="vertical" />
    </div>
  </Reel>
</template>

ReelIndicator

可选组件,显示 Instagram 风格的进度指示器,标出当前在滑动器中的位置。放在 <Reel>内部时,它会通过上下文自动连接到父级的 count 和 active 值,走的是 Vue 的 provide/inject —— 不需要手动接线。

vue-html
<!-- Auto-connect: count and active are inherited from parent Reel -->
<Reel :count="10" :size="[400, 600]">
  <template #item="{ index, size }"> ... </template>
  <ReelIndicator direction="vertical" />
</Reel>

<!-- Manual usage: pass count and active explicitly (e.g. outside a Reel) -->
<ReelIndicator :count="10" :active="currentIndex" />

命令式 API —— 模板引用

<Reel> 组件通过模板 ref 暴露 ReelExpose 接口。用 ref() 保存引用,即可调用命令式方法,比如 next(), prev()、 goTo().

vue
<script setup lang="ts">
import { ref } from 'vue';
import { Reel, type ReelExpose } from '@reelkit/vue';

const items = [
  { title: 'Slide 1', color: '#6366f1' },
  { title: 'Slide 2', color: '#8b5cf6' },
  { title: 'Slide 3', color: '#ec4899' },
];

const sliderRef = ref<ReelExpose | null>(null);
const currentIndex = ref(0);

const onAfterChange = (index: number) => {
  currentIndex.value = index;
};
</script>

<template>
  <Reel
    ref="sliderRef"
    :count="items.length"
    style="width: 100%; height: 100dvh"
    direction="vertical"
    :enable-wheel="true"
    @after-change="onAfterChange"
  >
    <template #item="{ index, size }">
      <div
        :style="{
          width: size[0] + 'px',
          height: size[1] + 'px',
        }"
      >
        {{ items[index].title }}
      </div>
    </template>
  </Reel>

  <div style="position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%)">
    <button
      :disabled="currentIndex === 0"
      @click="sliderRef?.prev()"
    >
      Prev
    </button>
    <button
      :disabled="currentIndex === items.length - 1"
      @click="sliderRef?.next()"
    >
      Next
    </button>
    <button @click="sliderRef?.goTo(2)">Go to 3</button>
  </div>
</template>

横向方向

把 direction="horizontal" 即可做成左右滑动的滑动器。指示器的方向也要一致。

vue-html
<Reel
  :count="items.length"
  :size="[400, 300]"
  direction="horizontal"
>
  <template #item="{ index, size }">
    <div :style="{ width: size[0] + 'px', height: size[1] + 'px' }">
      {{ items[index].title }}
    </div>
  </template>

  <ReelIndicator direction="horizontal" />
</Reel>

自动尺寸

size 属性是可选的。省略时, <Reel> 会通过 ResizeObserver 自动测量容器,并适配由 CSS 决定的布局。容器的尺寸必须由父级决定(例如 flex、grid 或显式的 CSS 尺寸)。

vue-html
<!-- Explicit size (fixed) -->
<Reel :count="items.length" :size="[400, 600]">
  <template #item="{ index, size }"> ... </template>
</Reel>

<!-- Auto-size (responsive — sized by CSS) -->
<Reel :count="items.length" style="width: 100%; height: 100dvh">
  <template #item="{ index, size }"> ... </template>
</Reel>

过渡动画

传入 transition 属性即可自定义幻灯片动画。ReelKit 提供五种可被 tree-shaking 的过渡: slideTransition (默认)、 fadeTransition, flipTransition, cubeTransition、 zoomTransition.

vue
<script setup lang="ts">
import { Reel, cubeTransition } from '@reelkit/vue';
</script>

<template>
  <Reel
    :count="items.length"
    :size="[400, 600]"
    :transition="cubeTransition"
  >
    <template #item="{ index, size }"> ... </template>
  </Reel>
</template>

循环模式

用 loop 属性开启无限循环导航。滑动器会从最后一张无缝回到第一张(反之亦然)。

vue-html
<Reel
  :count="items.length"
  :size="[400, 600]"
  :loop="true"
>
  <template #item="{ index, size }"> ... </template>
</Reel>

事件回调

<Reel> 组件会发出若干事件,用于跟踪滑动器状态:

vue
<script setup lang="ts">
import { Reel } from '@reelkit/vue';

const onBeforeChange = (index: number, nextIndex: number, rangeIndex: number) => {
  console.log('Transitioning from', index, 'to', nextIndex);
};

const onAfterChange = (index: number, rangeIndex: number) => {
  console.log('Arrived at slide', index);
};

const onSlideDragStart = (index: number) => {
  console.log('Started dragging slide', index);
};

const onSlideDragEnd = (index: number) => {
  console.log('Stopped dragging slide', index);
};
</script>

<template>
  <Reel
    :count="20"
    style="width: 100%; height: 100dvh"
    @before-change="onBeforeChange"
    @after-change="onAfterChange"
    @slide-drag-start="onSlideDragStart"
    @slide-drag-end="onSlideDragEnd"
  >
    <template #item="{ index, size }"> ... </template>
  </Reel>
</template>

内置的导航方式:

  • 触摸 / 滑动: 拖动即可翻页,带惯性和吸附
  • 键盘: 方向键和 Escape
  • 鼠标滚轮: 用 :enable-wheel="true"
  • 编程式: 用模板 ref 访问 next(), prev(), goTo()
vue
<script setup lang="ts">
import { ref } from 'vue';
import { Reel, type ReelExpose } from '@reelkit/vue';

const sliderRef = ref<ReelExpose | null>(null);
</script>

<template>
  <Reel
    ref="sliderRef"
    :count="10"
    :size="[400, 600]"
  >
    <template #item="{ index, size }">
      <div :style="{ width: size[0] + 'px', height: size[1] + 'px' }">
        Slide {{ index + 1 }}
      </div>
    </template>
  </Reel>

  <button @click="sliderRef?.prev()">Prev</button>
  <button @click="sliderRef?.next()">Next</button>
  <button @click="sliderRef?.goTo(5)">Go to 6</button>
</template>

URL 状态

useOverlayUrlState 为浮层构建一个 URL 状态控制器并整个返回,你再把它作为 <LightboxUrlOverlay> 的 :controller 属性。打开状态归地址栏所有,因此绑定后的浮层会自己打开,链接就是通常的打开方式。参数原本不存在时第一次写入压入一条历史记录,之后每次写入都是替换,所以翻页永远不会把返回键埋掉。留着控制器就能读取 value/position ,也能用 set(null)编程式关闭 —— 这正是浮层内部在切换幻灯片时使用的底层写入。

vue
<script setup lang="ts">
import { LightboxUrlOverlay, type LightboxItem } from '@reelkit/vue-lightbox';
import { useOverlayUrlState, urlIndexKey } from '@reelkit/vue';

const props = defineProps<{ images: LightboxItem[] }>();

const photo = useOverlayUrlState({
  param: 'photo',
  ...urlIndexKey(() => props.images.length),
});
</script>

<template>
  <!-- Opening is a link — the overlay reads the URL and opens itself. -->
  <RouterLink v-for="(img, i) in props.images" :key="img.src" :to="`?photo=${i}`">
    <img :src="img.src" />
  </RouterLink>

  <LightboxUrlOverlay :controller="photo" :items="props.images" />
</template>

选项对象接受 param, codec、 locator (这三个都是必填的),外加可选的 adapter。 codec 和 locator 是共用同一个 Id,因此对于普通的 ?photo=3 画廊,展开 ...urlIndexKey(() => props.images.length)即可,它会一次性返回两半。 urlIndexKey 把参数映射成幻灯片索引,并以 getter 返回的实时数量为上界,因此过期或越界的 ?photo=99 会被拒绝并自动从 URL 中消失,而不是打开一张从未指定的幻灯片。请传 getter 而不是数字:Vue 的 setup 只会执行一次,捕获下来的长度会随着分页信息流增长而过期。它包装了 createIndexLocator (the locator half) and pairs it with indexCodec。分页信息流或按身份寻址的画廊则自行提供配套的 codec + locator 。完整的选项表见 Vue API 参考.

#item 插槽模式

Vue 不用 React 的 render prop,而是用 #item 作用域插槽。它让虚拟化成为可能 —— 只有可见的幻灯片会被挂载。插槽作用域提供三个属性:

vue-html
<template #item="{ index, indexInRange, size }">
  <!--
    index        : number          — absolute slide index (0 to count-1)
    indexInRange  : number          — position in visible window (0, 1, or 2)
    size          : [number, number] — [width, height] of the container
  -->
  <MySlide
    :data="items[index]"
    :style="{ width: size[0] + 'px', height: size[1] + 'px' }"
  />
</template>

组合式函数

@reelkit/vue 为常见的浮层场景提供了组合式函数:

vue
<script setup lang="ts">
import { ref } from 'vue';
import { useBodyLock, useFullscreen, useReelContext } from '@reelkit/vue';

// Lock body scroll when an overlay is open
const isOpen = ref(true);
useBodyLock(isOpen);

// Fullscreen API with cross-browser support
const containerRef = ref<HTMLElement | null>(null);
const { isFullscreen, toggle } = useFullscreen({ elementRef: containerRef });

// Access parent Reel context (when inside a Reel)
const reelContext = useReelContext();
// reelContext?.index  — active slide index signal
// reelContext?.count  — total slide count signal
// reelContext?.goTo() — navigate programmatically

// Bridge a core Subscribable into a reactive Vue ref
import { toVueRef } from '@reelkit/vue';
const index = toVueRef(reelContext!.index); // Ref<number> — re-renders on change
</script>

要点

  • 组合式 API

    把 Reel, ReelIndicator以及各种组合式函数直接引入你的 <script setup> —— 不需要注册插件

  • #item 作用域插槽

    Vue 里对应 React itemBuilder 属性的写法 —— 用熟悉的模板语法实现虚拟化

  • 模板 ref

    使用 ref<ReelExpose>() 用于命令式导航 —— 不需要事件回调

  • @after-change

    发出 (index, rangeIndex) —— 跟踪当前索引以更新界面

  • provide/inject 上下文

    ReelIndicator 会自动连接到父级 Reel ,走 Vue 的 provide/inject —— 不必层层传属性

性能建议

  • 让幻灯片模板保持轻量

    #item 插槽会为每张可见幻灯片执行(通常同时 3 张)。不要在里面做重计算或写太深的嵌套结构。

  • 临近边缘时加载数据

    使用 @after-change 来检测用户是否接近末尾,并在幻灯片用完之前取下一批 —— 这样就能做无限滚动的信息流。

  • 命令式状态用 ref

    把 ReelExpose 引用和当前索引存在 Vue 的 ref()里,既能细粒度响应又不会产生多余的重渲染。

  • 在可滚动页面里关掉滚轮

    把 :enable-wheel="false" ,当滑动器嵌在可滚动布局里时,避免抢走页面滚动。

下一步