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,不过我看了下,好像还是要手动开启:
❯ 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:
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"><bodyclassName={`${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 组件,如:
const Meals = async () => {const meals = await getMeals();return <MealsGrid meals={meals} />;
};
加载状态
这里我主要新创建了一个 loading.js
文件,然后搭配了 Suspense
使用:
import React from "react";
import classes from "./loading.module.css";const MealsLoadingPage = () => {return <div className={classes.loading}>Fetching meals...</div>;
};export default MealsLoadingPage;
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. Magnamvoluptatibus fuga voluptas temporibus porro consequatur totam nihilquae omnis eos blanditiis asperiores, repudiandae itaque officiaoptio? 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
所接管