Skip to content
返回

Astro vs Next.js

先说结论


正文开始

现在如果要开始开发一个新的网站,那么我一般都是无脑 Next.js。但是,最近开发的一个网站让我的想法有所改变。

这个网站是一个软件的官网,分为以下几个部分:

  1. 首页 /、关于我们 /about 等普通页面
  2. 文档页面 /docs/xxx。文档用 Markdown 形式书写,并且需要能自定义界面和样式
  3. 博客页面 /blog/xxx。博客用 Markdown 形式书写,并且需要能自定义界面和样式
  4. 多语言支持。/blog 表示英语,/zh-CN/blog 表示中文,依此类推

Next.js 的问题

一开始,我也是使用了 Next.js,然后就遇到了一些问题。

由于网站大部分内容是 Markdown,我先是尝试使用了 Nextra,然后发现它并不适合我的需求:

遂放弃 Nextra,打算自己根据 How to use markdown and MDX in Next.js 来实现一个,然后又遇到了问题。

Next.js 虽然支持将 Markdown 渲染成 page,但它仅有渲染能力,而如果要做成一个完整的文档系统,我还需要自己补齐以下功能:

以上这些部分,如果我自己做完,那差不多就约等于一个 Nextra 了。

开始尝试 Astro

这时候我想到了 Astro,之前曾看到过它,它的官网声称它的使用量比 Next.js 还大。

一开始我也使用了基于 Astro 的文档系统 Starlight,但是它跟 Nextra 有同样的问题。虽然 Starlight 的文档上有“覆盖组件”的能力,但实际使用起来其实能改的不多,大致的页面框架仍是固定了的。

然后我就开始翻阅 Astro 的文档来自己实现文档和博客系统,找到了 Content collections - Astro。刚开始看的时候觉得有点复杂,实际使用起来直呼“真香”。

前面提到,如果我要在 Next.js 中实现文档系统,我需要做很多事情,而 Astro 基本上已经都做好了。

我需要做的就只有一件事:把 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.mdpost.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)”有很大不同,比如:

这一点上,Next.js 就做了一个很好的折衷:首次加载会带有完整的静态 HTML,用户能快速看到内容,但是之后就会变成一个 SPA,有更好的使用体验;同时,它的开发过程会更接近在开发 SPA 的感受,你无需反复提醒自己“Astro 是个多页面应用,每个页面都是一个单独的 React App”。

架构上的不同之处

我们通过几个场景来解释 Astro 与 Next.js 在架构上的不同之处。

第一个场景:纯静态内容网站

假设我们的网站是一个纯静态网站,无任何 js,那么:

在这个场景下,Astro 无疑是完胜:

第二个场景:30% 都是静态内容,70% 需要 js 进行交互

假设我们的网站只有首页、关于我们等页面是静态内容,其余都是交互性很重的页面,那么

Astro 的“群岛架构”与 Next.js 的“混合架构”

两者有相似之处(都利于 SEO、都能避免用户先看到空白再看到内容),但本质上不同:

总结

一句话描述:Astro 在“内容网站” + “少量功能性交互”方面做到了极致,而 Next.js 在“大量功能性交互” + “少量内容”网站方面做到了很好的折衷效果。

于是就有了开头的结论。

希望未来会有一个 Next.js 插件,把 Astro 的 Content collections 功能移植到 Next.js 里。


分享这篇文章:

上一篇
3 年 Monorepo 的使用感受
下一篇
我的 Zustand 最佳实践