先说结论
- 如果要开发的网站大部分是静态内容(如个人主页、文档、博客等),用 Astro。
- 如果要开发的网站是一个功能性的网站(aka Web App,比如用户中心、Todo Lists、AI Chat 等),用 Next.js。
- 如果静态内容和功能性各占一半,那么推荐用两个网站来实现。
- 常见做法是:www.example.com 用 Astro 开发,用作静态内容展示,然后 app.example.com 用 Next.js 开发,用作功能性网站。举例: www.netlify.com / app.netlify.com。
- 如果一定要整合在一起实现,我推荐用 Next.js。
正文开始
现在如果要开始开发一个新的网站,那么我一般都是无脑 Next.js。但是,最近开发的一个网站让我的想法有所改变。
这个网站是一个软件的官网,分为以下几个部分:
- 首页
/、关于我们/about等普通页面 - 文档页面
/docs/xxx。文档用 Markdown 形式书写,并且需要能自定义界面和样式 - 博客页面
/blog/xxx。博客用 Markdown 形式书写,并且需要能自定义界面和样式 - 多语言支持。
/blog表示英语,/zh-CN/blog表示中文,依此类推
Next.js 的问题
一开始,我也是使用了 Next.js,然后就遇到了一些问题。
由于网站大部分内容是 Markdown,我先是尝试使用了 Nextra,然后发现它并不适合我的需求:
- 它的可定制程度不高
- 虽然样式方面可以用
!important覆盖,但 html 结构无法修改
- 虽然样式方面可以用
- 它更适合于用来做“整个网站就是个单独的文档站”的情况,而我的网站实际上是需要将“普通页面”、“文档页面”、“博客页面”这三个部分整合在一起,但又都要能自定义界面
- Nextra 的默认样式是全局的,这意味着即使是首页这种跟文档无关的页面,也会需要用到跟文档页面统一的 header 和 footer
- 博客的部分也用了 markdown,但我不希望是文档站的那种 sidebar 样式,而这个样式又无法自定义
遂放弃 Nextra,打算自己根据 How to use markdown and MDX in Next.js 来实现一个,然后又遇到了问题。
Next.js 虽然支持将 Markdown 渲染成 page,但它仅有渲染能力,而如果要做成一个完整的文档系统,我还需要自己补齐以下功能:
- 自行读取一个文件夹下的所有 Markdown
- 根据文件夹 markdown 结构自动生成 sidebar 导航栏
- 自己编写动态参数 (例如
[...mdxPath]/page.tsx),然后根据参数读取 markdown 内容进行渲染 - 自行完成对 meta 数据的处理,比如
发布日期、作者、头图链接等。 - 兼容其它部分(“普通页面”和“博客页面”)的多语言支持
以上这些部分,如果我自己做完,那差不多就约等于一个 Nextra 了。
开始尝试 Astro
这时候我想到了 Astro,之前曾看到过它,它的官网声称它的使用量比 Next.js 还大。
一开始我也使用了基于 Astro 的文档系统 Starlight,但是它跟 Nextra 有同样的问题。虽然 Starlight 的文档上有“覆盖组件”的能力,但实际使用起来其实能改的不多,大致的页面框架仍是固定了的。
然后我就开始翻阅 Astro 的文档来自己实现文档和博客系统,找到了 Content collections - Astro。刚开始看的时候觉得有点复杂,实际使用起来直呼“真香”。
前面提到,如果我要在 Next.js 中实现文档系统,我需要做很多事情,而 Astro 基本上已经都做好了。
- 无需自行做任何配置,markdown 本身就是 Astro 支持的文件类型,只需要“导入”然后“作为组件使用”。
- Content collections 帮助我完成了所有 markdown 的收集以及渲染工作。
- markdown 本身就支持 meta 信息,Astro 称为
frontmatter YAML
我需要做的就只有一件事:把 url 里的动态参数映射到对应的文件路径,非常直观:
---
// /src/pages/[...locale]/blog/[id].astro
import { getCollection, render } from 'astro:content'
const defaultLocale = 'en'
// 1. 为每个语种 + markdown 文件生成一个对应的网址
export async function getStaticPaths() {
const posts = await getCollection('blog')
return posts.map((post) => {
// id 是文件系统里的路径,比如 `zh-cn/blog-1`、`en/blog-1`
const id = post.id
const firstSlashIndex = id.indexOf('/')
let blogLocale: string | undefined = id.slice(0, firstSlashIndex)
// 对于默认语言,不要在 url 里显示语种,也就是:
// 网址 `/blog/blog-1` 对应文件系统的 `/src/content/blog/en/blog-1.md`
// 网址 `/zh-cn/blog/blog-1` 对应文件系统的 `/src/content/blog/zh-cn/blog-1.md`
if (blogLocale === defaultLocale) {
blogLocale = undefined
}
// 剥离掉语种部分,即将原本的 `zh-cn/blog-1` 和 `en/blog-1` 变成 `blog-1`
const blogId = id.slice(firstSlashIndex + 1)
return {
// 最终,会为每个语种 + md 文件生成一个对应的网址,例如:
// `/zh-cn/blog/blog-1`
// `/blog/blog-1`
params: { locale: blogLocale, id: blogId },
props: { post },
}
})
}
// 2. 使用渲染好的 markdown content 组件
const { post } = Astro.props
const { Content } = await render(post)
---
<div>Markdown 内容:</div>
<Content />
Astro 真的做到了“简化 md 收集和渲染”以及“完全自定义界面”。
示例里的中文语种用的是全小写的 zh-cn 而不是标准的 zh-CN,这是有原因的
在用 Astro 做多语言的时候,要用全小写的语种,比如 zh-cn,而不是 zh-CN,不然会报 404。
这是因为,Astro 在很多地方都做了小写转化,比如 astro:i18n 里的方法里(可以用 normalizeLocale: false 关掉),以及前面用到的 getCollection() 得到的 post.id,即使你的文件夹用的是 zh-CN/blog-1.md,post.id 里也会是全小写的 zh-cn/blog-1,并且无法通过设置改变这一行为。
所以,最简单的办法就是语种也用小写,不然就要在很多地方都处理大小写问题。不过这样一来,URL 会是 /zh-cn/xxx 而不是 /zh-CN/xxx,现在大部分网站都是后者,但是可以观察到,使用了 Astro 网站都是小写的 /zh-cn/xxx,比如 https://www.cloudflare.com/zh-cn/
Astro 的问题
到目前为止,Astro 很好用,但是接下来,我想要做一些功能性的界面,例如登录 / 注册、用户中心等一些交互很重的界面,然后我就发现了 Astro 的不足。
Astro 本质上是一个“静态网页生成器”,它生成的是一个“多页面应用(MPA)”,这跟“单页面应用(SPA)”有很大不同,比如:
- 全局状态管理:如果是 SPA,由于没有页面刷新,所以很容易就可以做到跨 URL 的统一状态管理;但是 MPA 就不同了,每次跳转都是一次刷新,你需要把状态存进 localStorage 里,或者每次都从服务器重新读取。
- 交互性组件:Astro 可以使用交互性组件(或者在 Next.js 里叫
Client Component),但是实际上,你是在每一个页面都有一个(或多个) React App,而不是整个网站就是一个 React App。
这一点上,Next.js 就做了一个很好的折衷:首次加载会带有完整的静态 HTML,用户能快速看到内容,但是之后就会变成一个 SPA,有更好的使用体验;同时,它的开发过程会更接近在开发 SPA 的感受,你无需反复提醒自己“Astro 是个多页面应用,每个页面都是一个单独的 React App”。
架构上的不同之处
我们通过几个场景来解释 Astro 与 Next.js 在架构上的不同之处。
第一个场景:纯静态内容网站
假设我们的网站是一个纯静态网站,无任何 js,那么:
- Astro 输出的内容将没有任何 js 代码,零 js;全程都是静态 html,页面间的跳转会触发浏览器刷新
- Next.js 仍然会附带少量全局运行时 js 代码;首次加载是静态 html,但之后的页面跳转会变成 SPA 形式的客户端路由渲染,需要依赖 js
在这个场景下,Astro 无疑是完胜:
- 完全不依赖 js,所以性能肯定比 Next.js 高
- 开发体验上会比 Next.js 更好
第二个场景:30% 都是静态内容,70% 需要 js 进行交互
假设我们的网站只有首页、关于我们等页面是静态内容,其余都是交互性很重的页面,那么
- Astro 会输出很多 js;页面间的跳转仍然是静态 html 跳转,但在这种情况下,这反而会造成状态共享、逻辑混乱
- Next.js 的优势就体现出来了,因为本质上是一个重 js 的网站,所以少量的全局 js 运行时相比起来可以忽略不计,客户端路由也成了优点
Astro 的“群岛架构”与 Next.js 的“混合架构”
两者有相似之处(都利于 SEO、都能避免用户先看到空白再看到内容),但本质上不同:
- Astro 的“群岛架构“是,输出的全是静态 html 页面,而 html 页面里的交互部分是一个个小型的 SPA
- Next.js 的“混合架构”是,它本质上还是一个大的 SPA,只是首屏加载时用的是提前拼好的 HTML 片段,避免了用户先看到空白、再看到内容的问题
总结
一句话描述:Astro 在“内容网站” + “少量功能性交互”方面做到了极致,而 Next.js 在“大量功能性交互” + “少量内容”网站方面做到了很好的折衷效果。
于是就有了开头的结论。
希望未来会有一个 Next.js 插件,把 Astro 的 Content collections 功能移植到 Next.js 里。