使用 Next.js 打造原生应用般的 Web 应用
想用 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 过渡
从列表进入详情的过渡效果。

模板位置: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 左右滑动的效果。

模板位置: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 过渡
画廊图片放大并过渡到详情的效果。

模板位置: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-key和data-pinterest-detail-key必须设置相同的 id,SSGOI 才能识别连接。
config 配置:
{
from: "/pinterest/*",
to: "/pinterest",
transition: pinterest(),
symmetric: true,
}
Instagram 过渡
从个人资料网格过渡到详情的效果。与 Pinterest 类似,但画廊保持不变。

模板位置: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 应用中应用原生般的页面过渡效果了!
资源
- GitHub:https://github.com/meursyphus/ssgoi
- 官方文档:https://ssgoi.dev
- Next.js 模板:https://github.com/meursyphus/ssgoi/tree/main/templates/nextjs
用 WebView 包装起来,制作真正的应用吧!