博客前端重构:从 Astro 跑路到 Next.js
博客前端重构:从 Astro 跑路到 Next.js
起因
前段时间把博客从 Astro 迁移到了 Next.js,折腾了好一阵子,趁还没忘干净赶紧记录一下。
其实 Astro 用得好好的,为啥要换?主要是我自己作的。
博客一开始就是个简单的内容站,Astro 的岛屿架构确实香——页面默认纯静态 HTML,需要交互的地方才加载 JS,性能拉满。但后来我往里面塞了越来越多东西:登录注册、Markdown 在线编辑器、小说大纲设计器……交互组件越来越多,几乎每个页面都得挂 client:load,岛屿架构的优势基本没了。
另外当时选的 UI 框架是 SolidJS。SolidJS 本身没毛病,响应式做得比 React 优雅多了,性能也好。但问题是生态太小众了,很多 React 生态里现成的轮子没法用,得自己造或者找平替。时间长了就有点累。
想了想,既然交互这么多,干脆换成 Next.js + React 算了。React 生态成熟,想用什么库基本都有,Next.js 的 App Router 对 SSR 支持也很完善。
换了啥
简单列一下迁移前后的变化:
| 之前 | 之后 | |
|---|---|---|
| 框架 | Astro 5.8 | Next.js 16.1 |
| UI | SolidJS | React 19 |
| 样式 | Tailwind + DaisyUI | 没换,照搬 |
| 图标 | lucide-solid | lucide-react |
| Markdown 渲染 | 自己写的 WASM | react-markdown |
样式层基本是无缝迁移,Tailwind 的 class 直接复制过去就能用,这点省了不少事。
目录结构的变化
Astro 那边的结构是这样的:
blog-web/src/
├── pages/ # astro 页面
│ ├── index.astro
│ ├── blog/[slug].astro
│ └── ...
├── components/
│ ├── Header.astro # 静态组件
│ ├── Footer.astro
│ └── solid/ # 交互组件单独放
│ ├── BlogPosts.tsx
│ ├── AuthForm.tsx
│ └── ...
Astro 里静态组件和交互组件是分开的,.astro 写静态的,SolidJS 的 .tsx 放 solid/ 目录下,页面里用 client:load 激活。
换到 Next.js 之后:
frontend/
├── app/ # App Router
│ ├── layout.tsx
│ ├── page.tsx
│ ├── blog/[slug]/page.tsx
│ └── ...
├── components/ # 组件全放一起
│ ├── Header.tsx
│ ├── Footer.tsx
│ ├── BlogPosts.tsx
│ └── ...
不用再分「这个是静态组件」「那个是交互组件」了。Next.js 里想让组件在客户端跑就加个 "use client",否则默认服务端渲染,逻辑清晰很多。
踩的坑
1. SolidJS 转 React 的语法差异
这块是工作量最大的。虽然都是 JSX,但写法区别还挺多。
信号变状态:
// SolidJS - 信号要调用才能拿到值
const [count, setCount] = createSignal(0);
<span>{count()}</span>
// React - 直接用
const [count, setCount] = useState(0);
<span>{count}</span>
副作用:
// SolidJS - 自动追踪依赖,不用手动写
createEffect(() => {
console.log(count());
});
// React - 得手动声明依赖数组
useEffect(() => {
console.log(count);
}, [count]);
列表渲染:
// SolidJS
<For each={items()}>{(item) => <div>{item.name}</div>}</For>
// React
{items.map((item) => <div key={item.id}>{item.name}</div>)}
看着好像差不多,但十几个组件一个个改过来还是挺费神的。主要是怕漏改,SolidJS 的信号调用忘了去掉括号页面就白屏。
2. 主题切换闪一下的问题
我用的 DaisyUI,主题是通过 data-theme 属性切换的。用户选了深色主题,下次打开页面,因为服务端不知道用户偏好,先渲染了浅色,等客户端 JS 跑起来再切成深色——于是就闪了一下。
解决办法是在 <head> 里塞一段同步脚本,页面渲染之前就把主题设好:
const themeScript = `
try {
const savedTheme = localStorage.getItem("theme");
if (savedTheme) {
document.documentElement.setAttribute("data-theme", savedTheme);
} else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
document.documentElement.setAttribute("data-theme", "dark");
} else {
document.documentElement.setAttribute("data-theme", "light");
}
} catch (e) {
document.documentElement.setAttribute("data-theme", "light");
}
`;
然后 <html> 上加个 suppressHydrationWarning,不然 React 会警告服务端和客户端渲染结果不一致。
3. API 地址要分环境
这个坑卡了我一阵子。
博客是 Docker 部署的,前后端分开的容器。服务端渲染的时候,Next.js 容器要访问后端 API,这时候走 Docker 内网就行(比如 http://backend:8080)。但客户端浏览器发请求,得走公网域名(https://api.xxx.com)。
所以 axios 封装的时候得判断一下环境:
if (typeof window === 'undefined') {
// 服务端,走内网
apiUrl = process.env.INTERNAL_API_BASE_URL;
} else {
// 客户端,走公网
apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL;
}
之前没注意这个,服务端渲染直接用公网地址,绕了一大圈,慢得要死。
4. Markdown 渲染换方案
原来我自己用 Rust 写了个 Markdown 渲染器,编译成 WASM 在浏览器跑。性能是挺好的,但维护起来麻烦,而且换到 React 之后还得再封装一层。
想了想算了,直接换成 react-markdown,配合 rehype-highlight 做代码高亮,remark-gfm 支持 GitHub 风格的表格和删除线。生态成熟,插件丰富,博客这点内容渲染性能根本不是瓶颈。
5. SEO 相关
Astro 那边我是手写了个 SEO.astro 组件,手动拼 meta 标签,挺土的。
Next.js 内置了 Metadata API,直接导出一个 metadata 对象就行:
export const metadata: Metadata = {
title: "页面标题",
description: "页面描述",
openGraph: { ... },
};
动态页面(比如博客文章详情)还能用 generateMetadata 函数根据参数生成。比手拼舒服多了。
顺便还给文章页加了 JSON-LD 结构化数据,对搜索引擎收录友好一点。
6. Sitemap 和 Robots
Astro 用 @astrojs/sitemap 插件自动生成。Next.js 里改成在 app/ 下建 sitemap.ts 和 robots.ts 文件,编程式生成。好处是可以动态拉取所有文章的 slug,不用每次手动更新。
迁移完的感受
折腾完之后确实舒服多了:
- 不用在 Astro 和 SolidJS 两套语法之间来回切了
- 想找个什么组件库基本都有 React 版本
- 服务端组件直接 async/await 取数据,客户端组件用 useEffect,职责分得很清楚
- SEO 那块不用自己操心了
要说缺点的话,打包产物比 Astro 大一些,毕竟 React 运行时不是白给的。不过对于我这种已经重度交互的站来说,这点差异无所谓了。
给想迁移的朋友一点建议
- 样式层(Tailwind、DaisyUI)基本无缝迁移,不用担心
- SolidJS 转 React 主要是响应式模型不同,信号改状态,别忘了去掉调用括号
- SSR 场景下注意区分服务端和客户端的 API 地址
- 主题切换记得处理闪烁问题
- 不要一口气全改,先把框架搭好,再一个页面一个页面迁移,出问题好排查
就这些,希望对有类似需求的朋友有点帮助。