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
所接管