Next.js 实战笔记 1.0:架构重构与 App Router 核心机制详解

Next.js 实战笔记 1.0:架构重构与 App Router 核心机制详解

上一次写 Next 相关的东西都是 3 年前的事情了,这 3 年里 Next 也经历了 2-3 次的大版本变化。当时写的时候 Next 是 12 还是 13 的,现在已经是 15 了,从 build 到实现都有一些重大变化,所以就想着重新过一下关键点

这部分内容没啥特别好的归纳,基本上学/写到哪里记到哪里

更多内容可以在官方文档里面看到,我觉得一个比较有用的部分是这个:**Project structure and organization,**里面讲了 Next 推荐的文件夹管理方式,以及路由、metadata、SEO 之类的关键信息

构造

早起的版本中 Next 还是使用 webpack 做 bundle 的,从 Next 12 之后慢慢引入了 Rust 编写的 SWC(Speedy Web Compiler),到现在的 15 版本,已经开始引入 turbopack 去渐渐代替 webpack

找到的资料说,dev 模式自动开启 turbo,不过我看了下,好像还是要手动开启:

bash 复制代码
❯ yarn dev --turbo
yarn run v1.22.22
$ next dev --turbo
   ▲ Next.js 14.0.3 (turbo)
   - Local:        http://localhost:3000
 ✓ Ready in 2.4s

❯ yarn dev
yarn run v1.22.22
$ next dev
   ▲ Next.js 14.0.3
   - Local:        http://localhost:3000

 ✓ Ready in 2.7s

可以看到有 --turbo flag 的才会开启 turbopack......

目前体感来说,使用 turbopack 会快不少,大概提速 30%-50%,不过我的练手项目都比较小,差别就在这几秒或者是几百毫秒的差别,不足以大到让我有明显的体感上的差别

❗ 看了一下,大概是 next 的 config 文件里面没有配置,所以默认 dev 没有开启 turbo

app router vs page router

新版的项目结构也有了一些的变化,比如说之前的 directory 叫 page,现在改成了 app 。Next 还有一个选项是把所有的代码包在 src 下面,我没选那个,这里提一句

这种转变,实际上是 Next 内部中实现的转变,即从 page router 转成了 app router,现在推荐使用的是 app router,因为 Next 基于 app router 实现了很多新的功能,同样也是未来的转变方向

二者核心对比:

功能 Page Router (pages/) App Router (app/)
路由机制 文件系统自动生成路由 文件系统自动生成嵌套路由
支持 Layout ❌ 仅支持 _app.js 全局包装 ✅ 支持嵌套 layout.tsx
支持 Server Components ❌ 仅客户端组件(可用 SSR) ✅ 默认是 Server Component
支持 Streaming ❌ 不支持 ✅ 支持分块传输 / loading UI
Data fetching getServerSideProps, getStaticProps, getInitialProps fetch() in Server Component
Middleware 支持
动态路由 [id].js [id]/page.tsx
API Routes pages/api/* ✅ 仍使用 pages/api/*
文件结构限制 只有一个页面文件 允许多个文件组合构成页面(如 loading.tsx, error.tsx
状态成熟度 ✅ 成熟稳定 🚧 仍在改进(尤其是缓存行为)

server component

这应该是 page router 和 app router 最大的区别了,旧版的 page router 中,默认的还是 client side rendering,在 build 的阶段将数据写入 HTML 中。新版的 app router 则是 app 文件夹下默认所有的组件都在服务端生成,其中的一些状态和日志不会在 client 端显示,只会在服务端显示,如下面这个 log:

page.js should render as page, and is server component, which will be rendered at server

前面的 server 标记了是 server 端的内容,在正式打包后就会被去除

另一个需要注意的是,server component 不能用 hooks,这是 client component 专用的。如果要使用 hooks 的话,需要在文件头标注 use client;,这样这个组件下所有的内容都会在 client 端生成,否则就会报错:

如果想要利用好 Next 的 server side rendering,那么就尽可能的抽象组件,尽可能的在末端使用 use client

路由

基础的路由比较简单,新加一个文件夹,并且创建对应的 page.js 文件即可:

动态路由

根据官方文档显示,显示的 directory 的名称应该如下:

[folder] Dynamic route segment
[...folder] Catch-all route segment
[[...folder]] Optional catch-all route segment

并且在对应的文件夹下创建 page.js 文件即可

路由组与私有路由

根据官方文档,实现如下:

(folder) Group routes without affecting routing
_folder Opt folder and all child segments out of routing

layout

这个也是 Next 提升了很多的地方,这是目前 template 中的 layout:

jsx 复制代码
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode,
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        {children}
      </body>
    </html>
  );
}

其中, metadata 就是当前页面绑定的关键词,这也是个保留词。使用当前 layout 的所有页面,都会共享这里面的布局和 metadata

除此之外,Next 做的改进就是,每个文件夹下面都可以有它独立的 layout,这是不影响外层布局的。如果有这个需求的话,这个用途/设定挺好的

Image

Next15 也对其做了不少的改进,之前主要用的是 lazy loading 的特性,这次发现了一个 priority,即与 lazy loading 相反的特性,很适合加在 logo/banner 等地方

加载数据

现在 Next 所有的组件默认都是 server component 了,因此也不太需要使用 useEffect 去渲染数据,而是可以直接创建新的 async 组件,如:

jsx 复制代码
const Meals = async () => {
  const meals = await getMeals();

  return <MealsGrid meals={meals} />;
};

加载状态

这里我主要新创建了一个 loading.js 文件,然后搭配了 Suspense 使用:

jsx 复制代码
import React from "react";
import classes from "./loading.module.css";

const MealsLoadingPage = () => {
  return <div className={classes.loading}>Fetching meals...</div>;
};

export default MealsLoadingPage;
jsx 复制代码
const MealsPage = () => {
  return (
    <>
      <header className={classes.header}>
        <h1>
          Delicious meals, created{" "}
          <span className={classes.highlight}>by you</span>
        </h1>
        <p>
          Lorem ipsum dolor sit amet consectetur adipisicing elit. Magnam
          voluptatibus fuga voluptas temporibus porro consequatur totam nihil
          quae omnis eos blanditiis asperiores, repudiandae itaque officia
          optio? Repudiandae recusandae sit sequi?
        </p>
        <p className={classes.cta}>
          <Link href={"/meals/share"}>Share Your Favorite Recipe</Link>
        </p>
      </header>
      <main className={classes.main}>
        <Suspense fallback={<MealsLoadingPage />}>
          <Meals />
        </Suspense>
      </main>
    </>
  );
};

效果如下:

如果不使用 Suspense 的话,那么整个页面都会被 loading.js 所接管