服务端渲染

所有 reelkit 包都能在服务端运行。可以在 Next.js、Remix、Angular SSR、Nuxt 3 或任何服务端渲染方案里直接引入和渲染。

工作原理

核心的滑动控制器是纯逻辑,构造时不碰 DOM。 手势监听、键盘事件和动画只会在客户端的生命周期钩子里挂载。

服务端渲染时,React 的 Reel 在有 size 时会渲染一个静态容器,里面是初始可见的幻灯片(通常是 3 张:上一张、当前、下一张);自行测量时则渲染一个空容器。水合之后,它会挂上手势、键盘和滚轮控制器, 让一切变得可交互。

Vue 的 Reel 在服务端渲染一个空容器,水合之后才绘制幻灯片。传了 size 时,这个容器和客户端的首次渲染对不上,所以在 Nuxt 里要把它放进 <ClientOnly>(见 Nuxt 3)。

包名服务端安全备注
@reelkit/core是纯逻辑,导入和构造时都不使用浏览器 API
@reelkit/react是Reel 和 ReelIndicator 在服务端渲染出合法的 HTML
@reelkit/angular是独立组件,兼容 Angular SSR(@angular/ssr)的服务端渲染
@reelkit/vue是可以在服务端安全导入和渲染;传了 size 时,在 Nuxt 3 里请把 Reel 放进 <ClientOnly>
@reelkit/react-reel-player是关闭时(isOpen=false)什么都不渲染
@reelkit/react-lightbox是关闭时(isOpen=false)什么都不渲染
@reelkit/angular-reel-player是关闭时(isOpen=false)什么都不渲染
@reelkit/angular-lightbox是关闭时(isOpen=false)什么都不渲染
@reelkit/vue-reel-player是关闭时(isOpen=false)什么都不渲染
@reelkit/vue-lightbox是关闭时(isOpen=false)什么都不渲染
@reelkit/stories-core是与框架无关,不访问 DOM
@reelkit/react-stories-player是关闭时(isOpen=false)什么都不渲染
@reelkit/angular-stories-player是关闭时(isOpen=false)什么都不渲染
@reelkit/vue-stories-player是关闭时(isOpen=false)什么都不渲染

Next.js App Router

Reel 会用到浏览器事件和 ref,所以它是客户端组件。请在使用 Reel 的文件顶部加上 "use client" 指令:

tsx
'use client';

import { Reel, ReelIndicator } from '@reelkit/react';

export function Feed({ items }: { items: FeedItem[] }) {
  return (
    <Reel
      count={items.length}
      size={[400, 700]}
      direction="vertical"
      enableWheel
      itemBuilder={(index) => (
        <div className="w-full h-full flex items-center justify-center">
          {items[index].title}
        </div>
      )}
    >
      <ReelIndicator />
    </Reel>
  );
}

数据可以在服务端组件里取好,再传下去:

tsx
// app/feed/page.tsx (Server Component)
import { Feed } from './Feed';

export default async function FeedPage() {
  const items = await fetchFeedItems();

  return <Feed items={items} />;
}

Next.js Pages Router

Pages Router 不需要额外配置。组件会在服务端渲染,然后在客户端水合:

tsx
// pages/feed.tsx
import { Reel } from '@reelkit/react';
import type { GetServerSideProps } from 'next';

interface Props {
  items: FeedItem[];
}

export const getServerSideProps: GetServerSideProps<Props> = async () => {
  const items = await fetchFeedItems();
  return { props: { items } };
};

export default function FeedPage({ items }: Props) {
  return (
    <Reel
      count={items.length}
      size={[400, 700]}
      itemBuilder={(index) => <Slide data={items[index]} />}
    />
  );
}

服务端渲染下的响应式尺寸

干脆不要传 size 属性。没有 size 时,Reel 会在客户端用 ResizeObserver 自动测量容器。服务端渲染阶段滑动器渲染成一个空容器; 水合之后马上测量并渲染幻灯片:

tsx
'use client';

import { Reel } from '@reelkit/react';

export function FullScreenFeed({ items }: { items: FeedItem[] }) {
  return (
    <Reel
      count={items.length}
      style={{ width: '100%', height: '100dvh' }}
      itemBuilder={(index) => <Slide data={items[index]} />}
    />
  );
}
自动测量是怎么工作的

省略 size 时,容器的尺寸必须由 CSS 决定(父级的 flex / grid、显式的宽高,或者百分比)。 在第一次测量完成之前滑动器什么都不渲染, 之后会填满测到的尺寸,并自动响应后续的尺寸变化。

显式尺寸(手动方案)

想要像素级的控制,就显式传 size 属性。由于服务端渲染时拿不到 window.innerWidth,请给一个默认值,并在挂载后更新:

tsx
'use client';

import { useState, useEffect } from 'react';
import { Reel } from '@reelkit/react';

// Default size for SSR — matches common mobile viewport
const DEFAULT_SIZE: [number, number] = [390, 844];

export function FullScreenFeed({ items }: { items: FeedItem[] }) {
  const [size, setSize] = useState<[number, number]>(DEFAULT_SIZE);

  useEffect(() => {
    const update = () =>
      setSize([window.innerWidth, window.innerHeight]);

    update();
    window.addEventListener('resize', update);
    return () => window.removeEventListener('resize', update);
  }, []);

  return (
    <Reel
      count={items.length}
      size={size}
      itemBuilder={(index) => <Slide data={items[index]} />}
    />
  );
}
小提示

默认尺寸尽量选你最常见的视口(比如移动端优先)。 如果实际视口不一样,滑动器会在水合时立刻重新调整。

浮层组件

ReelPlayerOverlay、LightboxOverlay 和 StoriesOverlay 在 isOpen={false} 时什么都不渲染,所以默认就是服务端安全的。 只有被打开时(通常来自客户端的用户交互)才会挂载它们的 portal:

tsx
'use client';

import { useState } from 'react';
import { ReelPlayerOverlay } from '@reelkit/react-reel-player';

export function VideoFeed({ content }: { content: ContentItem[] }) {
  const [isOpen, setIsOpen] = useState(false);
  const [startIndex, setStartIndex] = useState(0);

  return (
    <>
      <div className="grid grid-cols-3 gap-1">
        {content.map((item, i) => (
          <button
            key={i}
            onClick={() => { setStartIndex(i); setIsOpen(true); }}
          >
            <img src={item.thumbnail} alt="" />
          </button>
        ))}
      </div>

      <ReelPlayerOverlay
        isOpen={isOpen}
        onClose={() => setIsOpen(false)}
        content={content}
        initialIndex={startIndex}
      />
    </>
  );
}

Angular SSR

配合 @angular/ssr,所有 Angular 组件都是服务端安全的。滑动控制器在 ngOnInit 里创建,需要浏览器的工作(比如测量容器)在服务端会通过 isPlatformBrowser 跳过。浮层组件在 isOpen=false 时什么都不渲染,因此服务端渲染阶段不会产出任何标记。

typescript
import { Component, signal } from '@angular/core';
import {
  RkReelPlayerOverlayComponent,
} from '@reelkit/angular-reel-player';

@Component({
  selector: 'app-feed',
  standalone: true,
  imports: [RkReelPlayerOverlayComponent],
  template: `
    <rk-reel-player-overlay
      [isOpen]="isOpen()"
      [content]="content"
      (closed)="isOpen.set(false)"
    />
  `,
})
export class FeedComponent {
  isOpen = signal(false);
  content = [/* ... */];
}

Nuxt 3

ReelKit 的 Vue 组件可以在服务端正常导入和渲染,不会报错。Reel 从服务端发出一个空容器,水合之后才绘制幻灯片。当 Reel 自行测量尺寸时,从 @reelkit/vue 0.9.1 起这个容器可以干净地完成水合;更早的版本里容器没有高度,幻灯片根本不出现。传了明确的 size 时,客户端首次渲染就已经带有幻灯片,水合会出现不匹配。遇到这种情况,或者使用更早的版本时,请把 Reel 包在 <ClientOnly> 组件里,或者用 .client.vue 后缀:

vue
<!-- pages/feed.vue -->
<script setup lang="ts">
const items = await useFetch('/api/feed');
</script>

<template>
  <ClientOnly>
    <Feed :items="items.data.value" />
  </ClientOnly>
</template>

Feed 组件里照常使用 Reel:

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

defineProps<{ items: FeedItem[] }>();
</script>

<template>
  <Reel :count="items.length" direction="vertical" enable-wheel>
    <template #item="{ index, size }">
      <div :style="{ width: size[0] + 'px', height: size[1] + 'px' }">
        {{ items[index].title }}
      </div>
    </template>
    <ReelIndicator />
  </Reel>
</template>

服务端渲染下的响应式尺寸

省略 size 属性即可启用自动测量。Reel 会自动撑满父级的 100%,从 @reelkit/vue 0.9.1 起不再需要 <ClientOnly>:服务端发出一个占满尺寸的空容器,客户端测量它并渲染幻灯片。示例保留了这层包裹,因为它在更早的版本上同样可用:

vue
<template>
  <ClientOnly>
    <div style="width: 100%; height: 100dvh">
      <Reel :count="items.length">
        <template #item="{ index, size }">
          <Slide :data="items[index]" :size="size" />
        </template>
      </Reel>
    </div>
  </ClientOnly>
</template>

浮层组件

ReelPlayerOverlay、LightboxOverlay 和 StoriesOverlay 在 isOpen 为 false 时什么都不渲染,所以不需要 <ClientOnly>,服务端渲染阶段也不会产出任何标记。只有在客户端被打开后,它们才会把内容 teleport 到 body。

直接使用核心包

如果你直接用 @reelkit/core 做自定义框架集成,控制器可以在服务端创建。 attach() 和 observe() 则只在客户端调用:

typescript
import { createSliderController } from '@reelkit/core';

// Safe to call on the server — no DOM access
const controller = createSliderController({
  count: 10,
  direction: 'vertical',
});

// Only call on the client — attaches DOM event listeners
if (typeof window !== 'undefined') {
  controller.attach(element);
  controller.observe();
}

小结

开箱即用的部分
  • 在服务端导入任意 reelkit 包
  • 服务端渲染阶段渲染滑动器组件(产出合法的静态 HTML)
  • 在服务端创建控制器
  • 浮层组件在 isOpen=false 时的表现
需要留意的地方
  • 省略 size 以启用自动测量;如果要用基于视口的尺寸,请给一个默认值
  • 在 Nuxt 里,传了 size 或使用 0.9.1 之前的 @reelkit/vue 时,把 Vue 的 Reel 放在 <ClientOnly> 里渲染
  • 直接使用核心包时,不要在服务端调用 attach()/observe()