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