SSGOI LogoSSGOI
← 返回博客
使用 Next.js 打造原生应用般的 Web 应用

使用 Next.js 打造原生应用般的 Web 应用

文大承
ssgoinextjsreactpage-transitiontutorial

想用 Web 技术制作应用?

您知道可以用 Web 技术制作应用吗?使用 Next.js 制作 Web 应用,然后用 Expo 或 Flutter 的 WebView 包装,就可以直接发布到应用商店。

但是 Web 应用有一个最大的弱点,那就是页面过渡不够自然

原生应用的页面过渡非常流畅。从列表进入详情时会滑动,点击图片时会放大展开。但是 Web 呢?页面只是生硬地切换。

如果 Web 也能实现原生般的页面过渡呢? 那样的 Web 应用会让人感觉就像真正的应用一样。

今天我们将使用 Next.js 制作一个具有原生应用般页面过渡效果的 Web 应用

什么是 SSGOI?

SSGOI(쓱오이)是一个在所有浏览器上都能运行的页面过渡库。

虽然 Chrome 有 View Transition API,但它在 Safari 和 Firefox 上无法运行。SSGOI 解决了这个问题。

SSGOI 的优势:

  • 支持所有现代浏览器(Chrome、Firefox、Safari、Edge)
  • 完美兼容 SSR/SSG
  • 基于弹簧物理动画,实现自然流畅的动效
  • 多种内置过渡效果(Drill、Slide、Pinterest、Instagram 等)

开始使用

让我们从 SSGOI 官方模板开始。

# 克隆仓库
git clone https://github.com/meursyphus/ssgoi

# 在根目录安装依赖(重要!)
pnpm install

# 进入 Next.js 模板文件夹
cd templates/nextjs

# 运行开发服务器
pnpm run dev

注意pnpm install 必须在根目录执行。因为是 Monorepo 结构,需要在根目录安装所有依赖。

在浏览器中打开 http://localhost:3000 即可查看模板演示。

了解文件夹结构

templates/nextjs/ 文件夹结构如下。

templates/nextjs/
├── src/
│   ├── app/
│   │   ├── layout.tsx          # 根布局
│   │   ├── posts/              # Drill 过渡示例
│   │   │   ├── page.tsx
│   │   │   └── [id]/page.tsx
│   │   ├── pinterest/          # Pinterest 过渡示例
│   │   │   ├── page.tsx
│   │   │   └── [id]/page.tsx
│   │   ├── profile/            # Instagram 过渡示例
│   │   │   ├── page.tsx
│   │   │   └── [id]/page.tsx
│   │   └── products/           # Slide 过渡示例
│   │       ├── layout.tsx
│   │       └── [category]/page.tsx
│   └── components/
│       ├── demo-layout.tsx     # Ssgoi 配置(核心!)
│       ├── demo-wrapper.tsx    # 移动端框架 UI
│       ├── posts/
│       ├── pinterest/
│       ├── profile/
│       └── products/

核心文件是 demo-layout.tsx。所有过渡配置都在这里。

配置 Ssgoi

打开 src/components/demo-layout.tsx

"use client";

import { Ssgoi } from "@ssgoi/react";
import { drill, pinterest, instagram } from "@ssgoi/react/view-transitions";
import { useMemo } from "react";

export default function DemoLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const config = useMemo(
    () => ({
      transitions: [
        // Pinterest 过渡
        {
          from: "/pinterest/*",
          to: "/pinterest",
          transition: pinterest(),
          symmetric: true,
        },
        // Posts - Drill 过渡
        {
          from: "/posts",
          to: "/posts/*",
          transition: drill({ direction: "enter" }),
        },
        {
          from: "/posts/*",
          to: "/posts",
          transition: drill({ direction: "exit" }),
        },
        // Profile - Instagram 过渡
        {
          from: "/profile",
          to: "/profile/*",
          transition: instagram(),
          symmetric: true,
        },
      ],
    }),
    [],
  );

  return (
    <main>
      <Ssgoi config={config}>{children}</Ssgoi>
    </main>
  );
}

config 配置说明:

  • from:起始路径(* 是通配符)
  • to:目标路径
  • transition:使用的过渡效果
  • symmetric:是否对反向路径应用相同过渡

使用 SsgoiTransition 包装页面

每个页面都需要用 SsgoiTransition 包装,并赋予唯一的 id

查看 src/components/posts/index.tsx

import { SsgoiTransition } from "@ssgoi/react";

export default function PostsDemo() {
  return (
    <SsgoiTransition id="/posts">
      <div className="min-h-screen bg-[#121212] px-4 py-6">
        {/* 文章列表 */}
      </div>
    </SsgoiTransition>
  );
}

查看 src/components/posts/detail.tsx

export default function PostDetail({ postId }: { postId: string }) {
  return (
    <SsgoiTransition id={`/posts/${postId}`}>
      <div className="min-h-screen bg-[#121212]">{/* 文章详情 */}</div>
    </SsgoiTransition>
  );
}

关键id 必须与 config 中的 from/to 匹配,SSGOI 才能应用过渡效果。

Drill 过渡

从列表进入详情的过渡效果。

Drill 过渡 - 从列表进入详情再返回

模板位置src/components/posts/

import { drill } from "@ssgoi/react/view-transitions";

// 列表 → 详情(进入时)
drill({ direction: "enter" });

// 详情 → 列表(退出时)
drill({ direction: "exit" });

config 配置:

{
  from: "/posts",
  to: "/posts/*",
  transition: drill({ direction: "enter" }),
},
{
  from: "/posts/*",
  to: "/posts",
  transition: drill({ direction: "exit" }),
},

选项:

  • direction"enter" | "exit"
  • opacity:设置为 true 时添加淡入淡出效果

Slide 过渡

选项卡 UI 左右滑动的效果。

Slide 过渡 - 点击选项卡时左右滑动

模板位置src/app/products/layout.tsx

import { slide } from "@ssgoi/react/view-transitions";

// 向左滑动
slide({ direction: "left" });

// 向右滑动
slide({ direction: "right" });

products/layout.tsx 中使用嵌套的 Ssgoi 来只对选项卡区域应用过渡:

export default function ProductsLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const config = useMemo(
    () => ({
      transitions: [
        {
          from: "/products/tab/left",
          to: "/products/tab/right",
          transition: slide({ direction: "left" }),
        },
        {
          from: "/products/tab/right",
          to: "/products/tab/left",
          transition: slide({ direction: "right" }),
        },
      ],
    }),
    [],
  );

  return (
    <SsgoiTransition id="/products">
      {/* 标题、选项卡按钮 */}
      <div className="flex-1 overflow-hidden">
        <Ssgoi config={config}>{children}</Ssgoi>
      </div>
    </SsgoiTransition>
  );
}

Pinterest 过渡

画廊图片放大并过渡到详情的效果。

Pinterest 过渡 - 图片放大过渡到详情

模板位置src/components/pinterest/

设置 Data 属性(必需!)

Pinterest 过渡需要通过 data 属性指定哪些图片之间建立连接。

src/components/pinterest/index.tsx(画廊):

function PinCard({ item }: { item: PinterestItem }) {
  return (
    <Link href={`/pinterest/${item.id}`}>
      <div className="relative" style={{ aspectRatio: item.aspectRatio }}>
        <img
          src={item.image}
          alt={item.title}
          className="w-full h-full object-cover"
          data-pinterest-gallery-key={item.id}
        />
      </div>
    </Link>
  );
}

src/components/pinterest/detail.tsx(详情):

export default function PinterestDetail({ pinId }: { pinId: string }) {
  const item = getPinterestItem(pinId);

  return (
    <SsgoiTransition id={`/pinterest/${pinId}`}>
      <img
        src={item.image}
        alt={item.title}
        style={{ aspectRatio: item.aspectRatio }}
        data-pinterest-detail-key={item.id}
      />
    </SsgoiTransition>
  );
}

关键data-pinterest-gallery-keydata-pinterest-detail-key 必须设置相同的 id,SSGOI 才能识别连接。

config 配置:

{
  from: "/pinterest/*",
  to: "/pinterest",
  transition: pinterest(),
  symmetric: true,
}

Instagram 过渡

从个人资料网格过渡到详情的效果。与 Pinterest 类似,但画廊保持不变。

Instagram 过渡 - 从网格过渡到详情,画廊保持显示

模板位置src/components/profile/

设置 Data 属性

src/components/profile/feed.tsx(网格):

function PostCard({ post }: { post: Post }) {
  return (
    <Link href={`/profile/${post.id}`}>
      <img
        src={post.coverImage.url}
        alt={post.title}
        className="w-full h-auto object-cover"
        data-instagram-gallery-key={post.id}
      />
    </Link>
  );
}

src/components/profile/feed-detail.tsx(详情):

export default function FeedDetail({ postId }: { postId: string }) {
  const post = getPost(postId);

  return (
    <SsgoiTransition id={`/profile/${postId}`}>
      <img
        src={post.coverImage.url}
        alt={post.title}
        data-instagram-detail-key={post.id}
      />
    </SsgoiTransition>
  );
}

config 配置:

{
  from: "/profile",
  to: "/profile/*",
  transition: instagram(),
  symmetric: true,
}

使用 Spring 选项调整时间

所有过渡都使用基于弹簧的物理动画。可以通过 spring 选项调整速度和感觉。

drill({
  direction: "enter",
  spring: {
    stiffness: 200, // 越高越快
    damping: 20, // 越高越少弹跳
    doubleSpring: true, // 启用 ease-in-out 效果
  },
});

Spring 选项说明

选项说明效果
stiffness刚度越高越快且响应越即时
damping阻尼越高弹跳越少,越平滑
doubleSpring双弹簧true/false - ease-in-out 效果

什么是 doubleSpring? 类似 CSS 的 ease-in-out,使开始和结束更平滑。详情请参考 Double Spring 博客文章

实用选项

symmetric 选项

自动设置双向过渡。

// 只需这样设置一次
{
  from: "/pinterest/*",
  to: "/pinterest",
  transition: pinterest(),
  symmetric: true,
}

// 反向也会自动应用

通配符路由

使用 * 匹配所有子路径。

// 匹配 /posts/1、/posts/abc 等所有路径
{ from: "/posts", to: "/posts/*", ... }

总结

现在您也可以在 Next.js Web 应用中应用原生般的页面过渡效果了!

资源

用 WebView 包装起来,制作真正的应用吧!