使用 React Router v7 重构博客
最近我使用 React Router v7 重构了原本使用 Next.js 搭建的个人博客。这篇文章记录了重构的动机、技术选型和实现细节。
1. 为什么要重构?
Next.js 是一个优秀的全栈框架,在 React 基础上预置了 SSR(服务端渲染)和 RSC(React Server Components)等能力。
然而,对于一个简单的静态博客来说,这些能力反而带来了不必要的心智负担:
- 复杂的约定:需要理解 Next.js 的文件路由约定、特殊生命周期函数,还要时刻考虑代码究竟在 Server 端还是 Client 端执行。
- 指令限制:
"use server"和"use client"文件在 Hook 使用上有不同的限制。 - 组件依赖规则:Server Components 可以依赖 Client Components,但反过来不行。
这些约定对于大型团队项目来说,有助于统一架构风格;但对于个人博客,只会增加不必要的复杂度。
于是,我决定换一个更轻量的方案。
2. 为什么选择 React Router v7?
2.1. 技术尝鲜
React Router v7 (RR7) 发布后不久,我就想体验一下。作为个人项目,稳定性不是首要考虑——好玩才是。
2.2. 原生 SSG 支持
RR7 内置支持 SSG(静态站点生成)。配置非常简单,只需在 react-router.config.ts 中设置:
export default {
prerender: true, // 开启预渲染
ssr: false, // 禁用 SSR,使用纯静态
appDirectory: "src",
} satisfies Config;
构建时生成的静态页面和资源,可以直接托管到 Vercel、Cloudflare Pages 等平台,无需自己维护服务器。
3. 博客核心功能
3.1. 自定义嵌套文件路由
使用 SSG 的博客通常采用「文件路由」(File Based Route)——通过目录结构组织 Markdown 文件,运行时按相同层级访问页面。
React Router 通过 @react-router/fs-routes 提供了文件路由支持,但它对嵌套路由的处理方式不太理想。例如:
posts/index.mdx→/postsposts/hello-world.mdx→/posts/hello-world
官方方案要求将文件放在同一级目录,使用 . 分隔层级:
posts._index.mdx→/postsposts.hello-world.mdx→/posts/hello-world
这种扁平结构不便于管理,也不够直观。
解决方案:React Router 支持自定义 RouteConfig。我在 routes.ts 中编写了一个递归扫描函数,构建时读取 pages 目录结构,自动生成符合要求的路由配置:
const scanDirectory = (dir: string, basePath: string = "") => {
const items = readdirSync(dir, { withFileTypes: true });
for (const item of items) {
const fullPath = path.join(dir, item.name);
if (item.isDirectory()) {
// 递归扫描子目录
scanDirectory(fullPath, path.join(basePath, item.name));
} else if (item.isFile()) {
const ext = path.extname(item.name);
if (fileExtensions.includes(ext)) {
const fileName = path.basename(item.name, ext);
// 构建路由路径
let routePath = fileName === "index"
? `/${basePath}`
: `/${basePath}/${fileName}`;
routes.push(route(routePath, fullPath));
}
}
}
};
这样就实现了真正的嵌套文件路由,目录结构与 URL 路径一一对应。
3.2. 项目架构
静态网站通常由两部分组成:内容(Markdown 文件)和生成器(解析、渲染逻辑)。
我的博客也遵循这个分离原则:
├── pages/ # 内容层:Markdown/MDX 文件
│ ├── index.mdx # 首页
│ └── posts/ # 文章目录
│ ├── index.mdx
│ └── hello-world.mdx
└── src/ # 生成层:React 组件和业务逻辑
├── components/
├── logic/
└── routes.ts
pages 文件夹相当于博客的「数据库」。这种按变更频率拆分的方式,有利于项目的稳定迭代——写新文章只需在 pages 目录下添加 MDX 文件,无需修改核心代码。
3.3. Content Collections 管理文章元数据
使用 @content-collections/core 来管理文章的 Frontmatter 元数据。这个库可以:
- 自动解析 MDX 文件的 frontmatter
- 提供类型安全的数据访问
- 在构建时生成文章列表
配置示例:
const posts = defineCollection({
name: "posts",
directory: "pages/posts",
include: "**/*.mdx",
schema, // Zod schema 定义 frontmatter 结构
transform: (data, context) => ({
...data,
// 自动从 # 标题提取 title
title: data.title ?? getTitleFromContent(data.content),
_meta: {
...data._meta,
path: getPathnameFromContext(context.collection.directory, data._meta.path),
},
}),
});
这样,在组件中就可以直接导入类型安全的文章列表:
import { allPosts } from "content-collections/generated";
3.4. 自定义 MDX 组件
通过 MDXProvider 可以为 MDX 内容注入自定义组件。例如:
PostList:渲染文章列表PostWrapper:为每篇文章添加 SEO 元数据- 自定义
<a>标签:统一处理外链
export default {
PostList,
wrapper: (props) => <PostWrapper {...props} />,
a: Anchor,
} satisfies MDXComponents;
3.5. View Transition 主题切换动画
为了让深色/浅色模式切换更加优雅,我使用了 View Transition API 实现了一个圆形扩散的过渡动画。
核心实现思路:
- 点击切换按钮时,记录点击坐标
- 使用
document.startViewTransition()启动过渡 - 通过 CSS
clip-path实现圆形扩散效果
const x = trigger?.clientX ?? window.innerWidth / 2;
const y = trigger?.clientY ?? window.innerHeight / 2;
const endRadius = Math.hypot(
Math.max(x, window.innerWidth - x),
Math.max(y, window.innerHeight - y)
);
document.startViewTransition(() => {
document.documentElement.classList.toggle("dark", nextIsDark);
});
CSS 动画部分:
:root[data-theme-transition="to-dark"]::view-transition-old(theme) {
clip-path: circle(var(--vt-radius) at var(--vt-x) var(--vt-y));
animation: theme-wipe-out 420ms ease-in forwards;
}
@keyframes theme-wipe-out {
to {
clip-path: circle(0 at var(--vt-x) var(--vt-y));
}
}
动画会从点击位置开始,以圆形扩散至全屏,视觉效果非常流畅。
4. 部署
博客通过 Vite 进行构建,最终生成纯静态文件。部署策略如下:
- 静态资源:上传至阿里云 OSS,通过 CDN 加速
- HTML 页面:部署到自有服务器,配置 Nginx 反向代理
构建命令:
pnpm build # 生成静态文件
# postbuild 脚本自动上传资源到 OSS
5. 总结
这次重构让我的博客变得更加轻量、灵活。主要收获:
- 更轻的心智负担:不再需要考虑 Server/Client 边界
- 更灵活的架构:自定义路由、自定义组件,一切尽在掌控
- 更好的开发体验:Vite 的 HMR 速度很快,写作体验流畅
- 有趣的技术探索:View Transition API 等新特性的实践
如果你也在考虑搭建个人博客,React Router v7 + MDX + Content Collections 是一个值得尝试的技术栈组合。
💡 本博客源码开源在 GitHub: suenyiyang/suenyiyang.com