Appearance
next.js,React版本的服务端同构渲染框架。
一、搭建 Next 应用
Next和Nuxt使用的都是同一种渲染流程,首次渲染使用服务端渲染(响应当前页面以及SPA的资源),后续客户端进行路由跳转时就请求对应的路由组件,并通过客户端 Router 操作DOM进行页面切换。
也就是说,首次渲染是服务端渲染,后续路由更新就是客户端渲染的方式。
Next服务器在接收到请求时若匹配上了路由,会对目标路由组件编译并执行,最终返回HTML给客户端,再次请求该页面时,Next会跳过编译直接执行路由组件得到结果,然后返回HTML给客户端,得益于缓存,提示了响应时间。
使用脚手架
利用脚手架快速创建Next模板应用。
使用js构建Next:npx create-next-app@latest
使用ts构建Next:npx create-next-app@latest --typescript
D:\随便写写\学习\next>npx create-next-app@latest --typescript
√ What is your project named? ... study-next // 项目名称
√ Would you like to use ESLint? ... No / Yes // 是否使用eslint
√ Would you like to use Tailwind CSS? ... No / Yes // 使用tailwindcss
√ Would you like to use `src/` directory? ... No / Yes // 创建src文件
√ Would you like to use App Router? (recommended) ... No / Yes //使用App 路由
√ Would you like to customize the default import alias (@/*)? ... No / Yes //是否需要自定义创建路径别名
√ What import alias would you like configured? ... @/* //路径别名的值为D:\随便写写\学习\next>npx create-next-app@latest --typescript
√ What is your project named? ... study-next // 项目名称
√ Would you like to use ESLint? ... No / Yes // 是否使用eslint
√ Would you like to use Tailwind CSS? ... No / Yes // 使用tailwindcss
√ Would you like to use `src/` directory? ... No / Yes // 创建src文件
√ Would you like to use App Router? (recommended) ... No / Yes //使用App 路由
√ Would you like to customize the default import alias (@/*)? ... No / Yes //是否需要自定义创建路径别名
√ What import alias would you like configured? ... @/* //路径别名的值为进入项目根目录后使用npm run dev来启动项目。
通过next教授架创建的项目,目录结构为:
.next:这是Nextjs的缓存目录,在执行dev或者build等命令的时候,会在本地项目的根目录下生成此目录,开发不需要关注。想要了解更多的可以稍微研究一下,使用缓存/已生成的方式加速编译。
node_modules:项目的依赖文件。
public:这个主要放置静态资源,默认没有二级目录,为了方便可以简单创建几个目录来放相关资源。默认路径是在根目录,使用的时候可以使用类似/favicon.ico的形式引用。
src:这个目录是主要源代码的位置,初始目录下有app默认页和pages其他页面目录。
.eslintrc.json:主要是eslint的规则配置文件。
.gitignore:git排除文件。
next-env.d.ts:nextjs的一些ts相关内容,目前只有默认引用。
next.config.js:Nextjs的配置文件,这里默认只有appDir参数。
package-lock.json:项目依赖lock文件。
package.json:项目npm相关文件。
README.md:文档说明。
tsconfig.json:typescript相关配置文件。
不使用脚手架
Next执行机制
默认情况下,Next.js会预渲染每个页面。这意味着Next.js提前为每个页面生成HTML,而不是让客户端JavaScript完成所有工作。预渲染可以带来更好的性能和SEO。
每个生成的HTML都与该页所需的最少JavaScript代码相关联。当浏览器加载页面时,它的JavaScript代码运行并使页面完全交互式。(这个过程被称为水合作用。)
二、页面路由
Next有两种路由模式,页面和应用,建议学新版的应用级别的路由,直接跳到八、App Router。
Next.js的路由系统和Nuxt.js一样都是基于文件系统的路由。Next配置路由需要在/src/pages文件夹中定义,每次接收到请求,都会根据请求路径与pages文件夹中的路由进行匹配。编译并打包、渲染路由组件最终响应给客户端。
提示
若使用了脚手架创建的项目,请先删除src/app/page.tsx这个文件,再创建pages文件夹,因为App Router的优先级大于Page Router
1.定义路由表
在pages文件夹下创建路由后,其与路径的匹配关系为:
/pages/index.tsx —— /
/pages/posts.tsx —— /posts
/pages/my/profile.tsx —— /my/profile
/pages/posts/index.tsx —— /posts
2.路由导航
声明式
Link
使用Link组件进行路由导航,通过href属性指定需要导航的路径。
最终渲染效果就是是一个a标签,但这不是一个普通的a标签,他是一个被重写的超链接,在点击时不会请求完整页面,而是使用客户端渲染的方式加载路由组件资源,并使用 Router 来控制路由跳转。
tsx
import Link from "next/link";
export default function Home() {
return (
<div>
<h1>Home</h1>
<Link href="/posts">Look for article.</Link>
</div>
);
}import Link from "next/link";
export default function Home() {
return (
<div>
<h1>Home</h1>
<Link href="/posts">Look for article.</Link>
</div>
);
}如何验证Link是客户端渲染的?
你可以通过控制台给body设置一个背景色,然后点击这Link组件(客户端渲染),导航成功后看样式是否存在;再点击a标签(服务端渲染),导航成功后会发现样式消失了,这就是因为a标签会重新请求、渲染文档。
tsx
import Link from "next/link";
export default function Home() {
return (
<div>
<h1>Home</h1>
<Link href="/posts">Look for article.</Link>
<hr />
<a href="/posts">To Posts (Native a tag)</a>
</div>
);
}import Link from "next/link";
export default function Home() {
return (
<div>
<h1>Home</h1>
<Link href="/posts">Look for article.</Link>
<hr />
<a href="/posts">To Posts (Native a tag)</a>
</div>
);
}编程式
使用useRouter钩子来获取路由对象实例,可以用来导航、查看当前路由路径、查询参数、动态参数...
tsx
import MyLayout from "@/app/layout";
import { useRouter } from "next/router";
export default function Posts() {
const router = useRouter();
const handleClick = () => router.push("/");
return (
<MyLayout>
<div>
<h1>Posts</h1>
<button
className="border text-xs px-2 py-1 bg-sky-300"
onClick={handleClick}>
Back to home.
</button>
</div>
</MyLayout>
);
}import MyLayout from "@/app/layout";
import { useRouter } from "next/router";
export default function Posts() {
const router = useRouter();
const handleClick = () => router.push("/");
return (
<MyLayout>
<div>
<h1>Posts</h1>
<button
className="border text-xs px-2 py-1 bg-sky-300"
onClick={handleClick}>
Back to home.
</button>
</div>
</MyLayout>
);
}3.动态路由
动态路由也就是对路径参数匹配上同一个路由组件。
简单使用【一个参数】😍
如何创建一个动态路由呢?
创建一个/pages/user/[uid].tsx,然后就可以通过/user/1来匹配上该路由了。然后可以通过useRouter来获取路径参数的值。
tsx
import { useRouter } from "next/router";
export default function User() {
const router = useRouter();
// 对的,你没看错,query是动态参数,也是查询参数😅
const { uid } = router.query;
return (
<div>
<div>User id is {uid}</div>
</div>
);
}import { useRouter } from "next/router";
export default function User() {
const router = useRouter();
// 对的,你没看错,query是动态参数,也是查询参数😅
const { uid } = router.query;
return (
<div>
<div>User id is {uid}</div>
</div>
);
}稍有不适【多个参数】😅
我动态参数有两个,要怎么办呢?没问题,咱们Next对多个动态参数都进行了处理。
示例1
例如有一个/user/:uid/:page的路由,那么我们需要创建/pages/user/[uid]/[page].tsx的路由组件。是的你没看错,就是文件名有点难看。
然后我们就可以通过/user/52/2来匹配上[page].tsx路由了。
示例2
例如我有一个/user/:xxx/yyy/:zzz的路由,那么我们需要创建/pages/[xxx]/yyy/[zzz].tsx的路由组件。
既可以通过/user/4/yyy/77来匹配上路由。
有点恶心【捕获全部】🤢
Next拥有捕获所有路由参数的功能。可以在多个参数的情况下使用同一个路由组件,稍有不适那种情况参数个数不同对应的路由组件也不一样,而现在这个功能是多个参数公用一个组件,使用场景不一样。
例如我想要让/user/:uid和/user/:uid/:page都匹配同一个路由要咋整?
只需要创建/pages/user/[...params].tsx即可(不一定都叫params,可以自己取,router.query[xxxx],就是所有动态参数的值了),这样在访问这两种参数的路由时都可以匹配上该路由组件。
tsx
const router = useRouter()
console.log(router.query)const router = useRouter()
console.log(router.query)/user/1?ok=5 ---- {ok:"5",params:["1"]} // params属性名不固定根据你路由名称[...params]而定
/user/1/5 --- {params:["1","5"]}
/user/sasa/adsda/qwdqw/qwdwdq --- {params:["sasa","adsads"....]}/user/1?ok=5 ---- {ok:"5",params:["1"]} // params属性名不固定根据你路由名称[...params]而定
/user/1/5 --- {params:["1","5"]}
/user/sasa/adsda/qwdqw/qwdwdq --- {params:["sasa","adsads"....]}感到眩晕【可选参数】😵💫
假如我现在拥有/user/[...params].tsx的路由了,我直接访问/user会出现404,我想让我的网页可以让参数变得可选,从而可以正常访问/user匹配上/user/[...params].tsx,要怎么办呢?
很简单,只需要创建/user/[[...params]].tsx即可,这样就算未携带参数,也可以直接访问/user/[[...paras]].tsx组件了,就是这命名方式多少有点。。
非常难受【平行路由】🥵
App Router!!
https://juejin.cn/post/7296330137284788275
aaa/@xxx/page.tsx 可以在 layout.tsx 里引入多个,叫做平行路由
地狱级别【拦截路由】💀
App Router!!
aaa/(..)/bbb/page.js 可以拦截 /bbb 的路由,重写对应的组件,但是刷新后依然渲染原组件,叫做拦截路由。
https://next.nodejs.cn/docs/app/building-your-application/routing/intercepting-routes
天堂级别【分组路由】😇
App Router!!
路由组 (xxx) 加了个括号来表示分组,不参与导航。
4.路由传参
不论是动态参数还是查询参数都可以通过useRouter().query来获取。
例如:/user/:uid ,访问/user/1?page=5,则可以通过:
tsx
const router = useRouter()
router.query // {uid:"1",page:"5"}const router = useRouter()
router.query // {uid:"1",page:"5"}5.默认路由
index.tsx和page.tsx都是默认路由组件,例如:
/pages/index.tsx --- /
/pages/page.tsx --- /
/pages/user/index.tsx --- /user
/pages/user/page.tsx --- /user
/pages/user/[uid]/index.tsx --- /user/:uid/pages/index.tsx --- /
/pages/page.tsx --- /
/pages/user/index.tsx --- /user
/pages/user/page.tsx --- /user
/pages/user/[uid]/index.tsx --- /user/:uid6.路由钩子
useRouter
三、静态资源
在Next的项目中,静态资源存储在public文件夹下,应用启动时会将public下的所有文件都挂载在Next服务器上。
例如/public/dog.png,则我们可以通过/dog.png或localhost:3000/dog.png来访问静态资源。同样的,文件夹中的文件也同样会被挂载,例如/public/imgs/person/me.svg,则我们可以通过/imgs/person/me.svg来访问该文件。
其实,任何文件都可以挂载在public文件夹中,甚至我们可以在public中创建index.html,在localhost:3000/index.html同样可以访问。Next服务器在接收到请求时,会优先查看当前路径是否在public中存在,若存在则优先响应,若不存在才会通过路由系统匹配路由页面。
四、元数据
这里的元数据是指网页的元数据,meta标签。Next提供了Head组件允许你在里面写入一些标签,并将这些标签添加到文档的head标签中。
简单示例
Next推荐编写meta、link、title标签在Head中,而不是在里面写UI、脚本。Head组件以外的UI内容都会被移入到路由组件中,这一点不需要担心。
tsx
import Head from "next/head";
export default function Meta() {
return (
<>
<Head>
<meta
name="description"
content="免费的 Web 教程"></meta>
<meta
name="keywords"
content="HTML, CSS, JavaScript"></meta>
<meta
name="author"
content="Bill Gates"></meta>
<title>Meta Setting Page</title>
<link
ref="icon"
href="/next.svg"></link>
</Head>
<div>
<h1>Meta Page...</h1>
</div>
</>
);
}import Head from "next/head";
export default function Meta() {
return (
<>
<Head>
<meta
name="description"
content="免费的 Web 教程"></meta>
<meta
name="keywords"
content="HTML, CSS, JavaScript"></meta>
<meta
name="author"
content="Bill Gates"></meta>
<title>Meta Setting Page</title>
<link
ref="icon"
href="/next.svg"></link>
</Head>
<div>
<h1>Meta Page...</h1>
</div>
</>
);
}五、CSS
编码姿势
next内置开箱即用的tailwind css和css in js插件任君使用。如果你想使用 CSS Module、其他CSS in JS的差距,请随意!
styled-jsx
styled-jsx是Next.js内置插件,可以直接使用,使用方式非常简单(并且扩展了css,可以使用scss预处理器编写css),并且不会污染其他组件、全局的样式(scoped)。个人感觉除了没提示以外都挺好的。
tsx
export default function CSS() {
return (
<div>
<h1>CSS coding</h1>
<div>CSS in JS</div>
<button>click me</button>
<span className="fish-spin">🐟</span>
<style jsx>{`
h1 {
color: red;
}
div {
color: skyblue;
font-size: 20px;
}
button {
&:hover {
background-color: blue;
}
}
@keyframes Fish {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.fish-spin {
display:inline-block;
animation: Fish infinite 1s linear;
}
`}</style>
</div>
);
}export default function CSS() {
return (
<div>
<h1>CSS coding</h1>
<div>CSS in JS</div>
<button>click me</button>
<span className="fish-spin">🐟</span>
<style jsx>{`
h1 {
color: red;
}
div {
color: skyblue;
font-size: 20px;
}
button {
&:hover {
background-color: blue;
}
}
@keyframes Fish {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.fish-spin {
display:inline-block;
animation: Fish infinite 1s linear;
}
`}</style>
</div>
);
}全局样式
这里会创建一个类似于根组件的文件,创建/pages/_app.tsx文件,他可以作为应用的根组件,每次渲染路由组件时都会执行一次,并且可以接收到一个props,其中包含了这些属性:
pageProps:传入的props。
Components:需要渲染的路由组件,一个函数式组件。
router:路由对象,其中包含了当前路由路径。
tsx
// pages/_app.tsx
import { AppProps } from "next/app";
// 引入tindwind css或其他css文件
import "@/app/globals.css";
export default function App(props: AppProps) {
// 需要被渲染的一级路由组件
return <props.Component {...props.pageProps}></props.Component>;
}// pages/_app.tsx
import { AppProps } from "next/app";
// 引入tindwind css或其他css文件
import "@/app/globals.css";
export default function App(props: AppProps) {
// 需要被渲染的一级路由组件
return <props.Component {...props.pageProps}></props.Component>;
}_app.tsx作为根组件,适合用于全局样式导入、布局组件等功能。
使用CSS预处理器
六、布局组件
想让每个页面都有相似的结构(比如:后台管理系统,布局嵌入一级路由入口)?不妨自己编写一个布局组件,然后在路由组件中使用,这样就可以复用布局啦~。
根组件
在/app/layout.tsx中默认导出的组件将会作为根组件。
tsx
import { ReactNode } from "react";
export default function Layout({ children }: { children: ReactNode }) {
return (
<html lang="zh">
<body>{children}</body>
</html>
);
}import { ReactNode } from "react";
export default function Layout({ children }: { children: ReactNode }) {
return (
<html lang="zh">
<body>{children}</body>
</html>
);
}局部
通过编写组件的方式复用UI结构
布局组件
tsx
import { PropsWithChildren } from "react";
import { Inter } from "next/font/google";
// 引入tindwind css或其他css文件
import "./globals.css";
import Link from "next/link";
// 引入谷歌字体
const inter = Inter({ subsets: ["greek"] });
export default function Layout({ children }: PropsWithChildren) {
return (
<div className={inter.className}>
<nav className="flex justify-around bg-slate-100 items-center py-2">
<Link href="/">Home</Link>
<Link href="/css">CSS</Link>
<Link href="/meta">Meta</Link>
<Link href="/posts">Post</Link>
</nav>
<main>{children}</main>
</div>
);
}import { PropsWithChildren } from "react";
import { Inter } from "next/font/google";
// 引入tindwind css或其他css文件
import "./globals.css";
import Link from "next/link";
// 引入谷歌字体
const inter = Inter({ subsets: ["greek"] });
export default function Layout({ children }: PropsWithChildren) {
return (
<div className={inter.className}>
<nav className="flex justify-around bg-slate-100 items-center py-2">
<Link href="/">Home</Link>
<Link href="/css">CSS</Link>
<Link href="/meta">Meta</Link>
<Link href="/posts">Post</Link>
</nav>
<main>{children}</main>
</div>
);
}路由组件
tsx
import MyLayout from "@/components/layout";
import Link from "next/link";
export default function Home() {
return (
<Layout>
<div>
<h1>Home</h1>
<Link href="/posts">Look for article.</Link>
<hr />
<a href="/posts">To Posts (Native a tag)</a>
</div>
</Layout>
);
}import MyLayout from "@/components/layout";
import Link from "next/link";
export default function Home() {
return (
<Layout>
<div>
<h1>Home</h1>
<Link href="/posts">Look for article.</Link>
<hr />
<a href="/posts">To Posts (Native a tag)</a>
</div>
</Layout>
);
}七、数据获取(Pages Router)
获取有两种方式,根据应用的模式来决定,若是SSR则使用__getServerSideProps__,若是SSG、ISG(增量生成)则使用__getStaticProps__和__getStaticPaths__。
getServerSiderProps
是一个自己需要定义的函数,可以在路由组件加载时获取一些数据,其返回值可以作为参数传入路由组件中。
首先需要在路由组件文件中向外__按需暴露__getServerSiderProps函数,该函数必须是一个异步的函数。然后需要返回一个对象,对象中需要包含props属性,该props会被作为参数传入到路由组件中。
执行时机:
- 首屏渲染时,服务器会调用此函数实现预渲染,得到完整的html页面响应给客户端。
- 客户端请求该页面时,服务端直接响应该组件文件资源。在浏览器环境下渲染(执行)该组件时会以Ajax请求服务器接口,服务器再次调用该函数,最终响应结果。
1.定义
ts
import { GetServerSideProps } from 'next';
const Page = props => {
return <div>page</div>;
};
// 必须按需导出
export const getServerSideProps: GetServerSideProps = async context => {
return {
// 必须的返回值
props: {}
};
};
export default Page;import { GetServerSideProps } from 'next';
const Page = props => {
return <div>page</div>;
};
// 必须按需导出
export const getServerSideProps: GetServerSideProps = async context => {
return {
// 必须的返回值
props: {}
};
};
export default Page;2.简单示例
ts
import { GetServerSideProps } from "next";
interface Pokemon {
name: string;
url: string;
}
type PokemonList = Pokemon[];
interface Response {
count: number;
next: string;
results: PokemonList;
}
export default function Pokemons({ results }: { results: PokemonList }) {
return (
<div>
<h1>pokemons</h1>
<hr></hr>
<ul>
{results.map((item) => (
<li key={item.name}>
<span>{item.name}</span>
</li>
))}
</ul>
</div>
);
}
// 此函数只会在服务器环境下运行
// 执行时机:
// 1.首屏渲染会调用此函数实现预渲染
// 2.客户端请求并渲染对应路由组件时,会向服务器发送请求,服务器调用该函数并响应结果给客户端
export const getServerSideProps: GetServerSideProps = async (context) => {
const { results }: Response = await (
await fetch("https://pokeapi.co/api/v2/pokemon")
).json();
console.log("server side");
return {
// props将作为对应路由组件的props传入
props: {
results,
},
};
};import { GetServerSideProps } from "next";
interface Pokemon {
name: string;
url: string;
}
type PokemonList = Pokemon[];
interface Response {
count: number;
next: string;
results: PokemonList;
}
export default function Pokemons({ results }: { results: PokemonList }) {
return (
<div>
<h1>pokemons</h1>
<hr></hr>
<ul>
{results.map((item) => (
<li key={item.name}>
<span>{item.name}</span>
</li>
))}
</ul>
</div>
);
}
// 此函数只会在服务器环境下运行
// 执行时机:
// 1.首屏渲染会调用此函数实现预渲染
// 2.客户端请求并渲染对应路由组件时,会向服务器发送请求,服务器调用该函数并响应结果给客户端
export const getServerSideProps: GetServerSideProps = async (context) => {
const { results }: Response = await (
await fetch("https://pokeapi.co/api/v2/pokemon")
).json();
console.log("server side");
return {
// props将作为对应路由组件的props传入
props: {
results,
},
};
};3.context
getServerSideProps 中的 context 参数包含了常用的请求的 req、res、params、query 等参数,还包含了 preview、previewData、resolvedUrl、locale 等参数。
4.返回值或特殊处理
将参数注入给页面组件
返回props即可在组件渲染时获取这些props数据。
404
getServerSideProps 返回值除了可以设置 props 外还可以使用 notFound 来强制页面跳转到 404。
tsx
export async function getServerSideProps(context) {
const data = await getData();
if (!data) {
return {
notFound: true
};
}
return {
props: { data }
}export async function getServerSideProps(context) {
const data = await getData();
if (!data) {
return {
notFound: true
};
}
return {
props: { data }
}重定向
或者是使用 redirect 来将页面重定向。
tsx
export async function getServerSideProps(context) {
const data = await getData();
if (!data) {
return {
redirect: {
destination: '/',
permanent: false // 是否永久重定向
}
};
}
return {
props: { data }
};
}export async function getServerSideProps(context) {
const data = await getData();
if (!data) {
return {
redirect: {
destination: '/',
permanent: false // 是否永久重定向
}
};
}
return {
props: { data }
};
}500
如果 getServerSideProps 报错了,next.js 将会直接跳转到 500 页面,又省下一段异常处理代码,可喜可贺。
5.运行机制
当 getServerSideProps 所在页面为 SSR 服务端渲染(首屏渲染)时,getServerSideProps 中的数据将会被放到全局的 _NEXT_DATA 中,用于 hydrate(水合)。
而非 SSR 情况下,进入该页面 next.js 将会自动发请求到: _next/data/development/{url}.json?{query},其中 development 为开发环境下地址段,该请求的返回值为:
json
{
"pageProps": "返回的 props 数据内容",
"__N_SSP": true
}{
"pageProps": "返回的 props 数据内容",
"__N_SSP": true
}从而将 getServerSideProps 返回值在页面挂载时注入进去。
6.注意
getServerSideProps无法调用内部接口,只能调用外部接口。
getStaticProps
getStaticProps在SSG模式下使用,也就是静态网页。定义方式和getServerSideProps差不多,不过执行时机不一样,这个会在__build__的时候运行,用来生成静态网页。在渲染到使用了getStaticProps的组件时,会先执行获取后再执行组件渲染函数得到完整的静态页面。
例如Next应用中有个Home组件使用了getStaticProps,在打包时就会调用getStaticProps函数获取数据,并生成完整的页面。
getStaticPaths
说明
搭配着getStaticProps一起使用,也会在build时候执行。
使用场景:在生成静态页面时包含了动态路由,由于不知道用户会访问哪些路径参数,使用getStaticPaths可以预先生成一些参数页面,若访问了预先生成的参数页面以外的就会根据策略来是否开启SSR模式。
例如有个动态参数路径的路由组件/user/:id,在打包时不知道未来会访问哪个值,所以可以通过getStaticPaths预先生成一些页面,比如生成id为1-10的10个静态页面,这样用户在访问/user/1~10时就可以直接返回静态页面接口,但访问了预设以外的参数,则会根据策略是否开启SSR,重新生成最新的页面,并将这个页面保存为静态页面,增加下一次请求的速度。
生成的静态页面可以在/.next/server/pages中查看。
用法
__函数返回值__是包含两个属性paths和fallback:
paths:一个数组代表了生成的静态页面个数,每个元素是一个对象包含一个params参数,表明每次生成静态页面时的路径参数值。
fallback:有三种取值方式,true(若请求预设以外的参数,则直接渲染组件同时发送请求获取数据)、false(404)、‘blocking’(开启SSR模式渲染页面,并保存此路径参数结果作为静态页面)。
执行流程
在通过build生成静态页面时,会先执行getStaticPaths获取需要生成静态页面个数,以及fallback策略。然后遍历paths数组,每次遍历都会创建一个静态页面,调用getStaticProps并将当前元素(包含了本次的路径参数)作为参数传入给getStaticProps,得到一个静态页面,直到遍历完成。
fallback策略示例
blocking
会开启SSR渲染,客户端发送请求,会阻塞路由切换直到响应成功,服务器接收到请求后会调用getStaticProps,得到结果后响应给客户端,并保存此路径的静态页面。
tsx
import { GetServerSideProps, GetStaticPaths, GetStaticProps } from "next";
import Image from "next/image";
import { useRouter } from "next/router";
interface PokemonDetail {
id: string;
name: string;
height: number;
weight: number;
}
export default function Detail(props: PokemonDetail) {
const nav = useRouter();
const handleClick = () => nav.push(`/pokemons/${props.id + 1}`);
return (
<div>
<h1>Pokemon Detail</h1>
<div>ID:{props.id}</div>
<div>Name:{props.name}</div>
<div>
<span>Weigth:{props.weight}</span>
<span>Height:{props.height}</span>
</div>
<img
width={100}
height={100}
alt={props.name}
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${props.id}.png`}
/>
<button
onClick={handleClick}
className="rounded bg-sky-400 px-2 py-1 text-xs">
Next Pokemon
</button>
</div>
);
}
export const getStaticProps: GetStaticProps<{}, { id: string }> = async (
context
) => {
const { id } = context.params as { id: string };
try {
const data: PokemonDetail = await fetch(
`https://pokeapi.co/api/v2/pokemon/${id}`
).then((r) => r.json());
return {
props: data,
};
} catch (error) {
return {
redirect: {
destination: "/",
permanent: false,
},
};
}
};
export const getStaticPaths: GetStaticPaths = async () => {
return {
// 遍历生成20个静态页面,每次遍历都会执行getStaticProps并将这个返回值注入给context
paths: Array.from({ length: 20 }).map((_, index) => {
return {
params: {
id: index + 1 + "", // id为1-20
},
};
}),
// 访问了以外1-20以外的路径,则会开启SSR获取数据
fallback: "blocking",
};
};import { GetServerSideProps, GetStaticPaths, GetStaticProps } from "next";
import Image from "next/image";
import { useRouter } from "next/router";
interface PokemonDetail {
id: string;
name: string;
height: number;
weight: number;
}
export default function Detail(props: PokemonDetail) {
const nav = useRouter();
const handleClick = () => nav.push(`/pokemons/${props.id + 1}`);
return (
<div>
<h1>Pokemon Detail</h1>
<div>ID:{props.id}</div>
<div>Name:{props.name}</div>
<div>
<span>Weigth:{props.weight}</span>
<span>Height:{props.height}</span>
</div>
<img
width={100}
height={100}
alt={props.name}
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${props.id}.png`}
/>
<button
onClick={handleClick}
className="rounded bg-sky-400 px-2 py-1 text-xs">
Next Pokemon
</button>
</div>
);
}
export const getStaticProps: GetStaticProps<{}, { id: string }> = async (
context
) => {
const { id } = context.params as { id: string };
try {
const data: PokemonDetail = await fetch(
`https://pokeapi.co/api/v2/pokemon/${id}`
).then((r) => r.json());
return {
props: data,
};
} catch (error) {
return {
redirect: {
destination: "/",
permanent: false,
},
};
}
};
export const getStaticPaths: GetStaticPaths = async () => {
return {
// 遍历生成20个静态页面,每次遍历都会执行getStaticProps并将这个返回值注入给context
paths: Array.from({ length: 20 }).map((_, index) => {
return {
params: {
id: index + 1 + "", // id为1-20
},
};
}),
// 访问了以外1-20以外的路径,则会开启SSR获取数据
fallback: "blocking",
};
};true(异步加载)
fallback为真,则开启异步加载模式,若访问预设以外的参数,则会开启SSR渲染,客户端会对服务器发送Ajax请求获取最新数据,则服务器调用getStaticProps获取结果响应客户端,并以此参数路径生成最新的静态页面,提升下次请求统一参数值的请求速度。
tsx
import { GetServerSideProps, GetStaticPaths, GetStaticProps } from "next";
import Image from "next/image";
import { useRouter } from "next/router";
interface PokemonDetail {
id: string;
name: string;
height: number;
weight: number;
}
export default function Detail(props: PokemonDetail) {
const nav = useRouter();
if (props.id===undefined) {
return <div>加载中</div>;
} else {
const handleClick = () => nav.push(`/pokemons/${props.id + 1}`);
return (
<div>
<h1>Pokemon Detail</h1>
<div>ID:{props.id}</div>
<div>Name:{props.name}</div>
<div>
<span>Weigth:{props.weight}</span>
<span>Height:{props.height}</span>
</div>
<img
width={100}
height={100}
alt={props.name}
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${props.id}.png`}
/>
<button
onClick={handleClick}
className="rounded bg-sky-400 px-2 py-1 text-xs">
Next Pokemon
</button>
</div>
);
}
}
export const getStaticProps: GetStaticProps<{}, { id: string }> = async (
context
) => {
const { id } = context.params as { id: string };
try {
const data: PokemonDetail = await fetch(
`https://pokeapi.co/api/v2/pokemon/${id}`
).then((r) => r.json());
return {
props: data,
};
} catch (error) {
return {
redirect: {
destination: "/",
permanent: false,
},
};
}
};
export const getStaticPaths: GetStaticPaths = async () => {
return {
paths: Array.from({ length: 10 }).map((_, index) => {
return {
params: {
id: index + 1 + "",
},
};
}),
fallback: true,
};
};import { GetServerSideProps, GetStaticPaths, GetStaticProps } from "next";
import Image from "next/image";
import { useRouter } from "next/router";
interface PokemonDetail {
id: string;
name: string;
height: number;
weight: number;
}
export default function Detail(props: PokemonDetail) {
const nav = useRouter();
if (props.id===undefined) {
return <div>加载中</div>;
} else {
const handleClick = () => nav.push(`/pokemons/${props.id + 1}`);
return (
<div>
<h1>Pokemon Detail</h1>
<div>ID:{props.id}</div>
<div>Name:{props.name}</div>
<div>
<span>Weigth:{props.weight}</span>
<span>Height:{props.height}</span>
</div>
<img
width={100}
height={100}
alt={props.name}
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${props.id}.png`}
/>
<button
onClick={handleClick}
className="rounded bg-sky-400 px-2 py-1 text-xs">
Next Pokemon
</button>
</div>
);
}
}
export const getStaticProps: GetStaticProps<{}, { id: string }> = async (
context
) => {
const { id } = context.params as { id: string };
try {
const data: PokemonDetail = await fetch(
`https://pokeapi.co/api/v2/pokemon/${id}`
).then((r) => r.json());
return {
props: data,
};
} catch (error) {
return {
redirect: {
destination: "/",
permanent: false,
},
};
}
};
export const getStaticPaths: GetStaticPaths = async () => {
return {
paths: Array.from({ length: 10 }).map((_, index) => {
return {
params: {
id: index + 1 + "",
},
};
}),
fallback: true,
};
};false
若fallback为false,则请求预设以外的参数都会跳转到404页面。
八、App Router
App Router(Application Router)和Pages Router都可以用来做路由系统的,不过App Router优先级更高,功能更完善些。
App Router中的所有组件默认都在服务端中加载,首屏渲染是,CSR模式下是发送请求获取页面内容,而页面内容需要服务端执行路由组件并响应完整的HTML结构给客户端,客户端获取到后直接嵌入浏览器替换旧的路由完成路由跳转的功能。
在 Next.js 13 之前,Pages Router 是在 Next.js 中创建路由的主要方式。 它使用直观的文件系统路由将每个文件映射到路由。 新版本的 Next.js 仍然支持 Pages Router,但我们建议迁移到新的 应用路由 以利用 React 的最新功能。
1.文件约定
Next.js 提供了一组特殊文件来创建在嵌套路由中具有特定行为的 UI,并且有且只能由这些文件名:
| 文件名 | 含义 |
|---|---|
layout | 段及其子段的共享 UI |
page | 路线的独特 UI 并使路线可公开访问 |
loading | 加载段及其子段的 UI |
not-found | 未找到段及其子段的 UI |
error | 段及其子段的错误 UI |
global-error | 全局错误用户界面 |
route | 服务器端 API 端点 |
template | 专门重新渲染布局 UI |
default | 并行路由 的后备 UI |
文件结构---对应实际的组件结构。

layout
布局组件,作为Next应用的根组件(替代/pages/_app.tsx和/pages/_document.tsx)。
每个路由都可以拥有Layout组件作为路由的公共部分,用来实现嵌套路由,但只有/app/layout.jsx组件可以拥有根组件和body组件。
根布局在
app目录的顶层定义并适用于所有路由。 此布局使你能够修改从服务器返回的初始 HTML。
根布局
tsx
import { ReactNode } from "react";
import './globals.css'
export default function Layout({ children }: { children: ReactNode }) {
return (
<html lang="zh">
<body>{children}</body>
</html>
);
}import { ReactNode } from "react";
import './globals.css'
export default function Layout({ children }: { children: ReactNode }) {
return (
<html lang="zh">
<body>{children}</body>
</html>
);
}从
pages目录迁移: 根布局替换_app.js和_document.js文件。
非根组件
非根组件的布局可以用来做路由嵌套的功能或兄弟路由的公共部分,注意非根布局不能添加html、body组件。
兄弟路由
有/user/my和/user/edit两个路由,我想在my和edit路由组件显示一些公共的部分,可以创建/app/user/layout.tsx,并编写。
注意:这种方式意味着/user激活时依旧使用该布局组件!!
tsx
export default Layout({children}:{children:ReactNode}){
// children变量是当前激活的路由组件节点,可以直接用来渲染
return <div>
<div>我是公共部分,可以用来做子路由的导航</div>
{/*子路由入口,同viewrouter一样。*/}
<div>{children}</div>
</div>
}export default Layout({children}:{children:ReactNode}){
// children变量是当前激活的路由组件节点,可以直接用来渲染
return <div>
<div>我是公共部分,可以用来做子路由的导航</div>
{/*子路由入口,同viewrouter一样。*/}
<div>{children}</div>
</div>
}嵌套路由
page
一个路由页面。只有文件夹中存在page.tsx文件,这个路由才会可达。
例如:
/app/page.tsx---//app/about/page.tsx---/about/user/goods/likes/page.tsx---/user/goods/likes
loading
在CSR模式下,跳转到当前路由组件时,会向服务端发送请求获取路由组件片段,在请求过程中会显示loading组件。
SSR模式下不会渲染此组件,除非路由组件在执行过程中出现了错误,就会显示loading组件,并被替换成error组件。
tsx
// /user/loading
export default function Loading() {
return <h1>Loading</h1>;
}// /user/loading
export default function Loading() {
return <h1>Loading</h1>;
}error
error是客户端组件。和Reac Router的errorElement差不多,如果说路由组件出现了错误,就会根据路由表查找自身或祖先是否有error.tsx组件,有就将渲染error.tsx组件,而不是渲染拥有error.tsx的组件。
error组件只会在客户端中渲染,组件可以接收两个参数error和reset,一个是错误信息,一个是重新加载当前路由(不是刷新页面,只是部分刷新)
tsx
"use client"; // Error components must be Client Components
import { useEffect } from "react";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// Log the error to an error reporting service
console.error(error);
}, [error]);
return (
<div>
<h2>Something went wrong!</h2>
<button
onClick={
// Attempt to recover by trying to re-render the segment
() => reset()
}>
Try again
</button>
</div>
);
}"use client"; // Error components must be Client Components
import { useEffect } from "react";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// Log the error to an error reporting service
console.error(error);
}, [error]);
return (
<div>
<h2>Something went wrong!</h2>
<button
onClick={
// Attempt to recover by trying to re-render the segment
() => reset()
}>
Try again
</button>
</div>
);
}not-found
notFound是一个服务端组件。
手动激活
not-found组件只会在路由组件中调用了notFound函数才会激活,激活时路由组件时执行了notFound函数,就会将原路由组件替换成not-found组件。
示例:
tsx
// /app/user/[uid]/not-found.tsx
export default function NotFound() {
return <div>找不到该用户</div>;
}// /app/user/[uid]/not-found.tsx
export default function NotFound() {
return <div>找不到该用户</div>;
}tsx
// /app/user/[uid]/page.tsx
...
import { notFound } from "next/navigation";
type Props = {
params: {
uid: string;
};
};
export async function generateMetadata(props: Props): Promise<Metadata> {
const userData = await getUser(props.params.uid);
// getUser函数会在未找到用户时返回undefined
if (userData === undefined) {
// 设置标题
return {
title: "未找到用户",
};
}
return {
title: userData.name,
};
}
export default async function UserDetail({ params: { uid } }: Props) {
const userData = await getUser(uid);
if (userData === undefined) {
// 渲染notFound组件。
return notFound();
}
const userPosts_P = getUserPosts(uid);
return (
<>
<div>
<div>ID:{userData.id}</div>
<div>NAME:{userData.name}</div>
<div>PHONE:{userData.phone}</div>
<div>EMAIL:{userData.email}</div>
</div>
<hr></hr>
<Suspense fallback={<div>Loading</div>}>
{/*@ts-expect-error Server Component*/}
<UserPost resolve={userPosts_P}></UserPost>
</Suspense>
<hr></hr>
<Link
href="/users"
className="rounded px-2 py-1 bg-sky-400 text-xs">
Back to Users
</Link>
</>
);
}// /app/user/[uid]/page.tsx
...
import { notFound } from "next/navigation";
type Props = {
params: {
uid: string;
};
};
export async function generateMetadata(props: Props): Promise<Metadata> {
const userData = await getUser(props.params.uid);
// getUser函数会在未找到用户时返回undefined
if (userData === undefined) {
// 设置标题
return {
title: "未找到用户",
};
}
return {
title: userData.name,
};
}
export default async function UserDetail({ params: { uid } }: Props) {
const userData = await getUser(uid);
if (userData === undefined) {
// 渲染notFound组件。
return notFound();
}
const userPosts_P = getUserPosts(uid);
return (
<>
<div>
<div>ID:{userData.id}</div>
<div>NAME:{userData.name}</div>
<div>PHONE:{userData.phone}</div>
<div>EMAIL:{userData.email}</div>
</div>
<hr></hr>
<Suspense fallback={<div>Loading</div>}>
{/*@ts-expect-error Server Component*/}
<UserPost resolve={userPosts_P}></UserPost>
</Suspense>
<hr></hr>
<Link
href="/users"
className="rounded px-2 py-1 bg-sky-400 text-xs">
Back to Users
</Link>
</>
);
}自动激活
在/app/not-found创建notFound组件,则会捕获未匹配路由时的错误,跳转到该页面中。
route
编写接口的文件,支持这些请求方法。官方推荐的是将接口也放在对应路由中,但是这会导致一个问题就是/user/route.ts与/user/page.ts会发送冲突,导致会请求接口,而不是访问页面。
最好的做法就是在/app/api/user/route.ts创建接口文件,这样我们可以通过/user访问页面,/api/user来访问接口。
tsx
export async function GET(request: Request) {}
export async function HEAD(request: Request) {}
export async function POST(request: Request) {}
export async function PUT(request: Request) {}
export async function DELETE(request: Request) {}
export async function PATCH(request: Request) {}
// If `OPTIONS` is not defined, Next.js will automatically implement `OPTIONS` and set the appropriate Response `Allow` header depending on the other methods defined in the route handler.
export async function OPTIONS(request: Request) {}export async function GET(request: Request) {}
export async function HEAD(request: Request) {}
export async function POST(request: Request) {}
export async function PUT(request: Request) {}
export async function DELETE(request: Request) {}
export async function PATCH(request: Request) {}
// If `OPTIONS` is not defined, Next.js will automatically implement `OPTIONS` and set the appropriate Response `Allow` header depending on the other methods defined in the route handler.
export async function OPTIONS(request: Request) {}2.嵌套路由
通过文件夹来表示嵌套关系。例如:需要表示这种路由/user/goods/likes的嵌套关系并且激活时要显示组件路由的内容,对于则需要创建:
- /user/layout.tsx 用来展示二级路由,以及
/user激活时的公共部分 - /user/goods/layout.tsx 用来展示三级路由,以及
/user/goods/激活时的公共部分 - /user/goods/like/page.tsx
3.修改Head
在App Router中有很多种方法来修改网页的Head部分。
1.Metadata
通过按需导出metadata变量,在里面写入各种配置项,Next会自动在路由激活时为网页添加元数据。
注意:使用了Metadata的方式就会导致在组件中编写的Head失效。
tsx
import { Metadata } from "next";
export const metadata:Metadata={
title: "制作人员",
description:"Next学习"
}
export default function Person() {
return <div>制作人员:张三 李四 王麻子</div>;
}import { Metadata } from "next";
export const metadata:Metadata={
title: "制作人员",
description:"Next学习"
}
export default function Person() {
return <div>制作人员:张三 李四 王麻子</div>;
}2.自定义Head组件
如果你不想太内聚,可以把head的内容拆分到单独的文件中。Next同样会把该组件的内容提升到文档的head标签内部。
创建Head组件:
tsx
// about/head.tsx
export default function Head() {
return (
<>
<title>About页面</title>
<meta
name="description"
content="学习Next"></meta>
</>
);
}// about/head.tsx
export default function Head() {
return (
<>
<title>About页面</title>
<meta
name="description"
content="学习Next"></meta>
</>
);
}引入:
tsx
// about/page.tsx
import Head from "./head"
export default function About() {
return (
<>
<Head></Head>
<div>About</div>
</>
);
}// about/page.tsx
import Head from "./head"
export default function About() {
return (
<>
<Head></Head>
<div>About</div>
</>
);
}3.动态Head
例如帖子页面,需要让网页标题与帖子的标题同步,使用generateMetadata很有效。
他会在首次渲染时执行一次,服务器会执行generateMetadata以及__page__生成网页文档,执行完成(或Promise状态凝固)后,才会响应给客户端;在CSR模式下,客户端会请求该路由网页,服务器会执行generateMetadata以及__page__生成__网页片段__,执行完成(或Promise状态凝固)后,才会响应给客户端。
generateMetadata的执行结果是会被缓存的,缓存多久由Next决定。
同步
tsx
import { Metadata } from "next";
interface Props {
params: {
pid: string;
};
}
export function generateMetadata(props: Props):Metadata {
return {
title:`文章id ${props.params.pid}`
}
}
export default function (props: Props) {
return <div>{props.params.pid}</div>;
}import { Metadata } from "next";
interface Props {
params: {
pid: string;
};
}
export function generateMetadata(props: Props):Metadata {
return {
title:`文章id ${props.params.pid}`
}
}
export default function (props: Props) {
return <div>{props.params.pid}</div>;
}支持异步
在SSR下在__generateMetadata__完成后才会响应文档,在CSR下会发送请求,服务端执行__generateMetadata__、对应路由组件,都执行完成后才会响应文档片段。
tsx
import { Metadata } from "next";
interface Props {
params: {
pid: string;
};
}
export async function generateMetadata(props: Props): Promise<Metadata> {
await new Promise((r) => setTimeout(r, 3000));
return {
title: `文章id ${props.params.pid}`,
};
}
export default function (props: Props) {
return <div>{props.params.pid}</div>;
}import { Metadata } from "next";
interface Props {
params: {
pid: string;
};
}
export async function generateMetadata(props: Props): Promise<Metadata> {
await new Promise((r) => setTimeout(r, 3000));
return {
title: `文章id ${props.params.pid}`,
};
}
export default function (props: Props) {
return <div>{props.params.pid}</div>;
}4.动态路由
动态路由也就是对路径参数匹配上同一个路由组件。
简单使用【一个参数】😍
如何创建一个动态路由呢?
创建一个/pages/user/[uid].tsx,然后就可以通过/user/1来匹配上该路由了。然后可以通过useRouter来获取路径参数的值。
tsx
import { useRouter } from "next/router";
export default function User() {
const router = useRouter();
// 对的,你没看错,query是动态参数,也是查询参数😅
const { uid } = router.query;
return (
<div>
<div>User id is {uid}</div>
</div>
);
}import { useRouter } from "next/router";
export default function User() {
const router = useRouter();
// 对的,你没看错,query是动态参数,也是查询参数😅
const { uid } = router.query;
return (
<div>
<div>User id is {uid}</div>
</div>
);
}稍有不适【多个参数】😅
我动态参数有两个,要怎么办呢?没问题,咱们Next对多个动态参数都进行了处理。
示例1
例如有一个/user/:uid/:page的路由,那么我们需要创建/pages/user/[uid]/[page].tsx的路由组件。是的你没看错,就是文件名有点难看。
然后我们就可以通过/user/52/2来匹配上[page].tsx路由了。
示例2
例如我有一个/user/:xxx/yyy/:zzz的路由,那么我们需要创建/pages/[xxx]/yyy/[zzz].tsx的路由组件。
既可以通过/user/4/yyy/77来匹配上路由。
有点恶心【捕获全部】🤢
Next拥有捕获所有路由参数的功能。可以在多个参数的情况下使用同一个路由组件,稍有不适那种情况参数个数不同对应的路由组件也不一样,而现在这个功能是多个参数公用一个组件,使用场景不一样。
例如我想要让/user/:uid和/user/:uid/:page都匹配同一个路由要咋整?
只需要创建/pages/user/[...data].tsx即可(不一定都叫params,可以自己取,router.query[xxxx],就是所有动态参数的值了),这样在访问这两种参数的路由时都可以匹配上该路由组件。
/user/1?ok=5 ---- {params:{data:["1"]},searchParams:{ok:"5"}}
/user/1/5 --- {params:{data:["1","5"]},searchParams:{}}
/user/sasa/adsda/qwdqw/qwdwdq --- {params:{data:["sasa","adsda"]},searchParams:{}}/user/1?ok=5 ---- {params:{data:["1"]},searchParams:{ok:"5"}}
/user/1/5 --- {params:{data:["1","5"]},searchParams:{}}
/user/sasa/adsda/qwdqw/qwdwdq --- {params:{data:["sasa","adsda"]},searchParams:{}}感到眩晕【可选参数】😵💫
假如我现在拥有/user/[...params].tsx的路由了,我直接访问/user会出现404,我想让我的网页可以让参数变得可选,从而可以正常访问/user匹配上/user/[...params].tsx,要怎么办呢?
很简单,只需要创建/user/[[...params]].tsx即可,这样就算未携带参数,也可以直接访问/user/[[...paras]].tsx组件了,就是这命名方式多少有点。。
非常难受【平行路由】🥵
https://juejin.cn/post/7296330137284788275
aaa/@xxx/page.tsx 可以在 layout.tsx 里引入多个,叫做平行路由
地狱级别【拦截路由】💀
aaa/(..)/bbb/page.js 可以拦截 /bbb 的路由,重写对应的组件,但是刷新后依然渲染原组件,叫做拦截路由。
https://next.nodejs.cn/docs/app/building-your-application/routing/intercepting-routes
天堂级别【分组路由】😇
路由组 (xxx) 加了个括号来表示分组,不参与导航。
5.路由参数
App Router 获取路由参数大更新,路由参数可以直接在路由组件的props中获取。
路由组件的props只能获取两个参数params和searchParams。
tsx
interface PageProps {
// 路径参数
params: {
id: string;
};
// 查询参数
searchParams:Record<string,string>;
}
export default function User(props: PageProps) {
console.log(props); // {params:{...},searchParams:{...}}
return <div>User Id is {props.params.id}</div>;
}interface PageProps {
// 路径参数
params: {
id: string;
};
// 查询参数
searchParams:Record<string,string>;
}
export default function User(props: PageProps) {
console.log(props); // {params:{...},searchParams:{...}}
return <div>User Id is {props.params.id}</div>;
}在参数不同的情况下params的值:
1.一个参数
例如/user/[id]/page.tsx,则params的值为 {id:string}
2.多个参数
例如/data/[id]/xxx/[op]/page.tsx,则params的值为{id:string,op:string}
3.捕获全部
例如/user/[...data],则params的值为{data:[参数列表]}
4.可选参数
例如/optional/[[...haha]],则有两种情况:
未传入任何参数:则params的值为{}
若传入值,和捕获全部的情况一样。
6.数据获取
简单示例
1.创建获取数据的api
tsx
// src/lib/getAllUser.ts
export default async function getAllUser() {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
if (res.status !== 200) throw Error("Fetch Data Failed.");
return res.json();
}// src/lib/getAllUser.ts
export default async function getAllUser() {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
if (res.status !== 200) throw Error("Fetch Data Failed.");
return res.json();
}2.创建ts定义文件
创建types.d.ts文件
ts
type User = {
id: number;
name: string;
username: string;
email: string;
address: {
street: string;
suite: string;
city: string;
zipcode: string;
geo: {
lat: string;
lng: string;
};
};
phone: string;
website: string;
company: {
name: string;
catchPhrase: string;
bs: string;
};
};type User = {
id: number;
name: string;
username: string;
email: string;
address: {
street: string;
suite: string;
city: string;
zipcode: string;
geo: {
lat: string;
lng: string;
};
};
phone: string;
website: string;
company: {
name: string;
catchPhrase: string;
bs: string;
};
};任意名字都可以,只要是ts类型定义文件,ts编译器都会把他作为全局的类型定义,不需要引入就可以进行类型注释,因为在next项目中创建的ts.congfig.json自动配置了include选项,会自动把这些文件作为全局的类型定义文件。
json
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"] "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]在组件中使用
tsx
import getAllUser from "@/lib/getAllUser";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "测试数据获取",
};
export default async function Users() {
const usersData: Promise<User[]> = getAllUser();
const users = await usersData;
return (
<div>
<ul>
{users.map((item) => {
return (
<li
key={item.id}
className="space-x-2">
<span>name:{item.name}</span>
<span>phone:{item.phone}</span>
</li>
);
})}
</ul>
</div>
);
}import getAllUser from "@/lib/getAllUser";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "测试数据获取",
};
export default async function Users() {
const usersData: Promise<User[]> = getAllUser();
const users = await usersData;
return (
<div>
<ul>
{users.map((item) => {
return (
<li
key={item.id}
className="space-x-2">
<span>name:{item.name}</span>
<span>phone:{item.phone}</span>
</li>
);
})}
</ul>
</div>
);
}异步加载
我想让组件内部的某些部分进行异步加载,需要怎么操作?就像Vue的Suspense组件一样,这次是真的和Suspense组件一样了,加载异步组件,同步渲染显示fallback插槽效果,异步组件加载完成后,替换fallback的内容。
在服务端中允许返回一个Promise的JSX片段。比方说我有个用户页面,想要优先加载用户的部分页面,而不是阻塞路由跳转直到加载完成,就可以用Suspense+async组件。下面这个案例就是优先加载用户页面,异步加载UserPost组件。
使用异步的组件
使用异步组件需要注意,由于返回的是异步的JSX节点,会被TS报错,添加下列注释即可:
{/*@ts-expect-error Server Component*/}{/*@ts-expect-error Server Component*/}tsx
import UserPost from "@/app/components/UserPost";
import getUser from "@/lib/getUser";
import getUserPosts from "@/lib/getUserPosts";
import Link from "next/link";
import { Suspense } from "react";
type Props = {
params: {
uid: string;
};
};
export default async function UserDetail({ params: { uid } }: Props) {
// 获取
const userData = await getUser(uid);
const userPosts_P = getUserPosts(uid);
return (
<>
<div>
<div>ID:{userData.id}</div>
<div>NAME:{userData.name}</div>
<div>PHONE:{userData.phone}</div>
<div>EMAIL:{userData.email}</div>
</div>
<hr></hr>
<Suspense fallback={<div>Loading</div>}>
{/*@ts-expect-error Server Component*/}
<UserPost resolve={userPosts_P}></UserPost>
</Suspense>
<hr></hr>
<Link
href="/users"
className="rounded px-2 py-1 bg-sky-400 text-xs">
Back to Users
</Link>
</>
);
}import UserPost from "@/app/components/UserPost";
import getUser from "@/lib/getUser";
import getUserPosts from "@/lib/getUserPosts";
import Link from "next/link";
import { Suspense } from "react";
type Props = {
params: {
uid: string;
};
};
export default async function UserDetail({ params: { uid } }: Props) {
// 获取
const userData = await getUser(uid);
const userPosts_P = getUserPosts(uid);
return (
<>
<div>
<div>ID:{userData.id}</div>
<div>NAME:{userData.name}</div>
<div>PHONE:{userData.phone}</div>
<div>EMAIL:{userData.email}</div>
</div>
<hr></hr>
<Suspense fallback={<div>Loading</div>}>
{/*@ts-expect-error Server Component*/}
<UserPost resolve={userPosts_P}></UserPost>
</Suspense>
<hr></hr>
<Link
href="/users"
className="rounded px-2 py-1 bg-sky-400 text-xs">
Back to Users
</Link>
</>
);
}需要异步加载的组件定义
是的,你没看错,可以直接返回一个异步的组件。
tsx
export default async function ({resolve}:{resolve: Promise<Post[]>}) {
const posts = await resolve
return (
<ul>
<div>His Posts:</div>
{posts.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
);
}export default async function ({resolve}:{resolve: Promise<Post[]>}) {
const posts = await resolve
return (
<ul>
<div>His Posts:</div>
{posts.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
);
}7.渲染模式
8.路由导航
声明式
使用Link组件,和pages router一样的使用方式。Link组件在生产环境下,如果Link出现在浏览器视口中,则Link组件会根据其href属性,预先加载该页面的内容片段,再将来访问该页面时可以提高响应速度。
此外,在生产环境中,只要
<Link>组件出现在浏览器的视图中,Next.js就会自动在后台预取链接路由的代码。当用户单击链接时,目标页面的代码已经在后台加载,这使得页面转换几乎是即时的!
编程式
客户端组件使用useRouter、服务端组件使用redirect。
服务端导航
例如需要鉴权的页面,用户无权限,可以通过redirect重定向某个页面
tsx
import redisClient from "@/utils/redis";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
export default async function Posts() {
const cookieStore = cookies();
const sessionId = cookieStore.get("sessionId");
if (sessionId === undefined) {
// 无权访问页面,重定向到401
return redirect("/401");
}
redisClient.get(sessionId.value);
return <div>{sessionId.value}</div>;
}import redisClient from "@/utils/redis";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
export default async function Posts() {
const cookieStore = cookies();
const sessionId = cookieStore.get("sessionId");
if (sessionId === undefined) {
// 无权访问页面,重定向到401
return redirect("/401");
}
redisClient.get(sessionId.value);
return <div>{sessionId.value}</div>;
}9.客户端组件
要想使用React的钩子,例如useEffect、useState等,需要给组件文件顶部写入一个"use client"来标识组件是客户端组件,从而可以使用客户端React钩子,禁用React服务端的钩子。
客户端组件只是意味着会在客户端中渲染,而不会在服务端中渲染。
tsx
"use client";
import { useRouter } from "next/navigation";
import { FormEvent, useCallback, useEffect, useState } from "react";
export default function SearchBar() {
// 搜索关键词
const [value, setValue] = useState(() => {
// 请求获取一些搜索推荐词的操作
return "今天圣诞节🎄";
});
// 导航
const nav = useRouter();
useEffect(() => {
// 服务器端永远不会执行这个
console.log("client");
});
const handleInput = useCallback((e: FormEvent) => {
setValue((e.target as HTMLInputElement).value);
}, []);
const handleClick = useCallback(() => {
setValue("");
nav.push(`/search?keywords=${value}`);
}, []);
return (
<div className="flex">
<input
className="outline-none"
value={value}
onInput={handleInput}></input>
<button
className="text-xs px-2 py-1 text-white bg-sky-400 hover:bg-sky-500"
onClick={handleClick}>
Search
</button>
</div>
);
}"use client";
import { useRouter } from "next/navigation";
import { FormEvent, useCallback, useEffect, useState } from "react";
export default function SearchBar() {
// 搜索关键词
const [value, setValue] = useState(() => {
// 请求获取一些搜索推荐词的操作
return "今天圣诞节🎄";
});
// 导航
const nav = useRouter();
useEffect(() => {
// 服务器端永远不会执行这个
console.log("client");
});
const handleInput = useCallback((e: FormEvent) => {
setValue((e.target as HTMLInputElement).value);
}, []);
const handleClick = useCallback(() => {
setValue("");
nav.push(`/search?keywords=${value}`);
}, []);
return (
<div className="flex">
<input
className="outline-none"
value={value}
onInput={handleInput}></input>
<button
className="text-xs px-2 py-1 text-white bg-sky-400 hover:bg-sky-500"
onClick={handleClick}>
Search
</button>
</div>
);
}10.API Route
现在我们可以在Next中编写接口,文档规定的是
简单示例
创建/app/api/hello/route.ts,编写如下内容,就完成了一个简单的接口编写。
tsx
// GET /api/hello
export async function GET() {
return new Response('Hello,Next.js')
}// GET /api/hello
export async function GET() {
return new Response('Hello,Next.js')
} 当然接口也可以嵌套编写。
创建/app/api/hello/ok/route.ts,编写如下内容,就完成了一个嵌套路由。这样就可以通过/api/hello/ok来访问接口了。
tsx
import { NextResponse } from "next/server";
// GET api/hello/ok
export async function GET() {
return NextResponse.json({ msg: "ok", data: {} });
}import { NextResponse } from "next/server";
// GET api/hello/ok
export async function GET() {
return NextResponse.json({ msg: "ok", data: {} });
}请求方法
支持这些请求方法。
ts
export async function GET(request: Request) {}
export async function HEAD(request: Request) {}
export async function POST(request: Request) {}
export async function PUT(request: Request) {}
export async function DELETE(request: Request) {}
export async function PATCH(request: Request) {}
// If `OPTIONS` is not defined, Next.js will automatically implement `OPTIONS` and set the appropriate Response `Allow` header depending on the other methods defined in the route handler.
export async function OPTIONS(request: Request) {}export async function GET(request: Request) {}
export async function HEAD(request: Request) {}
export async function POST(request: Request) {}
export async function PUT(request: Request) {}
export async function DELETE(request: Request) {}
export async function PATCH(request: Request) {}
// If `OPTIONS` is not defined, Next.js will automatically implement `OPTIONS` and set the appropriate Response `Allow` header depending on the other methods defined in the route handler.
export async function OPTIONS(request: Request) {}请求上下文
获取查询参数
tsx
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
// URL是node的全局方法,可以用来解析URL中的每个部分
const { searchParams } = new URL(request.url)
// 获取查询参数 (通过迭代器构造一个对象)
const query = Object.fromEntries(searchParams.entries())
// 返回响应
return NextResponse.json({
msg: "ok",
data:query
});
}import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
// URL是node的全局方法,可以用来解析URL中的每个部分
const { searchParams } = new URL(request.url)
// 获取查询参数 (通过迭代器构造一个对象)
const query = Object.fromEntries(searchParams.entries())
// 返回响应
return NextResponse.json({
msg: "ok",
data:query
});
}获取请求体数据
定义
tsx
// /api/posts/routes.ts
import { NextResponse } from "next/server";
type CreatePostBody = {
title: string;
body: string;
};
export async function POST(request: Request) {
const data: CreatePostBody = await request.json();
request.json // 解析json数据
request.formData // 解析formData数据
console.log(data);
return NextResponse.json({
msg: "ok",
data,
});
}// /api/posts/routes.ts
import { NextResponse } from "next/server";
type CreatePostBody = {
title: string;
body: string;
};
export async function POST(request: Request) {
const data: CreatePostBody = await request.json();
request.json // 解析json数据
request.formData // 解析formData数据
console.log(data);
return NextResponse.json({
msg: "ok",
data,
});
}使用
tsx
"use client";
import { useRouter } from "next/navigation";
import { FormEvent, useState } from "react";
export default function PostCreate() {
const router = useRouter();
const [data, setData] = useState({
title: "",
body: "",
});
const handleUpdateTitle = (e: FormEvent) => {
setData((pre) => ({ ...pre, title: (e.target as HTMLInputElement).value }));
};
const handleUpdateBody = (e: FormEvent) => {
setData((pre) => ({ ...pre, body: (e.target as HTMLInputElement).value }));
};
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
fetch("/api/posts", {
method: "POST",
body: JSON.stringify(data),
}).then(() => {
alert("发帖成功!");
router.push("/");
});
};
return (
<div>
<h1 className="text-xl font-semibold">Create A Post</h1>
<form onSubmit={handleSubmit}>
<div className="flex flex-col">
<label>Article Title</label>
<input
value={data.title}
onInput={handleUpdateTitle}
className="border border-sky-300"></input>
</div>
<div className="flex flex-col">
<label>Article Body</label>
<textarea
value={data.body}
onInput={handleUpdateBody}
className="border border-sky-300"></textarea>
</div>
<div className="mt-4 flex justify-end">
<button className="rounded px-2 py-1 bg-sky-400 text-white">
Submit
</button>
</div>
</form>
</div>
);
}"use client";
import { useRouter } from "next/navigation";
import { FormEvent, useState } from "react";
export default function PostCreate() {
const router = useRouter();
const [data, setData] = useState({
title: "",
body: "",
});
const handleUpdateTitle = (e: FormEvent) => {
setData((pre) => ({ ...pre, title: (e.target as HTMLInputElement).value }));
};
const handleUpdateBody = (e: FormEvent) => {
setData((pre) => ({ ...pre, body: (e.target as HTMLInputElement).value }));
};
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
fetch("/api/posts", {
method: "POST",
body: JSON.stringify(data),
}).then(() => {
alert("发帖成功!");
router.push("/");
});
};
return (
<div>
<h1 className="text-xl font-semibold">Create A Post</h1>
<form onSubmit={handleSubmit}>
<div className="flex flex-col">
<label>Article Title</label>
<input
value={data.title}
onInput={handleUpdateTitle}
className="border border-sky-300"></input>
</div>
<div className="flex flex-col">
<label>Article Body</label>
<textarea
value={data.body}
onInput={handleUpdateBody}
className="border border-sky-300"></textarea>
</div>
<div className="mt-4 flex justify-end">
<button className="rounded px-2 py-1 bg-sky-400 text-white">
Submit
</button>
</div>
</form>
</div>
);
}路径参数
参数路径的接口可以这样创建。
tsx
// /app/api/posts/[pid]/route.ts
import { NextResponse } from "next/server";
// /api/posts/:pid
export async function GET(
_: Request,
context: { params: Record<string, string> }
) {
return NextResponse.json(
{
msg: "ok",
data: context.params,
},
{
status: 200,
statusText: "ok",
}
);
}// /app/api/posts/[pid]/route.ts
import { NextResponse } from "next/server";
// /api/posts/:pid
export async function GET(
_: Request,
context: { params: Record<string, string> }
) {
return NextResponse.json(
{
msg: "ok",
data: context.params,
},
{
status: 200,
statusText: "ok",
}
);
}响应Response
可以使用标准 Web API 方法在 Response 上设置 CORS 标头,Response可以配置响应头部。
ts
export const dynamic = 'force-dynamic' // defaults to force-static
export async function GET(request: Request) {
return new Response('Hello, Next.js!', {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
})
}export const dynamic = 'force-dynamic' // defaults to force-static
export async function GET(request: Request) {
return new Response('Hello, Next.js!', {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
})
}11.中间件
中间件就是在请求和响应过程中执行的操作,在Next.js中,中间件的地位好像有点尴尬,明明在服务端运行的但又不能完整使用Node.js的API,只能执行浏览器相关的API。
全局中间件
在根目录或src下创建middleware.ts文件既可作为全局中间件。
ts
import { NextResponse } from "next/server";
export function middleware(request: Request) {
console.log(new URL(request.url).pathname);
// 放行请求
return NextResponse.next();
}import { NextResponse } from "next/server";
export function middleware(request: Request) {
console.log(new URL(request.url).pathname);
// 放行请求
return NextResponse.next();
}匹配中间件
tsx
import { MiddlewareConfig } from "next/dist/build/analysis/get-page-static-info";
import { NextResponse } from "next/server";
export function middleware(request: Request) {
console.log(new URL(request.url).pathname);
// 放行请求
return NextResponse.next();
}
export const config = {
// 只有路径为/api的才会执行中间件
matcher: "/api/:path*",
};import { MiddlewareConfig } from "next/dist/build/analysis/get-page-static-info";
import { NextResponse } from "next/server";
export function middleware(request: Request) {
console.log(new URL(request.url).pathname);
// 放行请求
return NextResponse.next();
}
export const config = {
// 只有路径为/api的才会执行中间件
matcher: "/api/:path*",
};在中间件中执行重定向
tsx
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url))
}
// See "Matching Paths" below to learn more
export const config = {
matcher: '/about/:path*',
}import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url))
}
// See "Matching Paths" below to learn more
export const config = {
matcher: '/about/:path*',
}limiter
limiter是一个用于限制请求频率的Node.js库。它可以用于限制某些操作的频率,比如API请求、登录尝试、密码重置请求等,以防止恶意行为或意外的过度使用。
通过limiter库,你可以定义一个速率限制器,指定允许的请求速率(比如每秒多少次请求),然后在你的应用中使用这个限制器来控制请求的频率。如果请求超出了限制,limiter库可以帮助你拒绝请求或者采取其他自定义的行为。
这个库对于构建需要频率限制的应用程序非常有用,特别是在需要保护API端点或者控制用户行为的场景下。
创建单实例的limiter
tsx
import { RateLimiter } from "limiter";
export const limiter = new RateLimiter({
// 可以发送150次请求
tokensPerInterval: 150,
// 一小时
interval: "hour",
});import { RateLimiter } from "limiter";
export const limiter = new RateLimiter({
// 可以发送150次请求
tokensPerInterval: 150,
// 一小时
interval: "hour",
});tsx
import { NextResponse } from "next/server";
import { limiter } from "@/config/limiter";
export async function middleware(request: Request) {
const remaining =await limiter.removeTokens(1);
if (remaining === 0) {
// 没有请求次数了!
// ...
}
console.log("remaining:" + remaining);
return NextResponse.next();
}
export const config = {
// 只有路径为/api的才会执行中间件
matcher: "/api/:path*",
};import { NextResponse } from "next/server";
import { limiter } from "@/config/limiter";
export async function middleware(request: Request) {
const remaining =await limiter.removeTokens(1);
if (remaining === 0) {
// 没有请求次数了!
// ...
}
console.log("remaining:" + remaining);
return NextResponse.next();
}
export const config = {
// 只有路径为/api的才会执行中间件
matcher: "/api/:path*",
};12.Next.js函数
1.cookies
在App Router中,服务端组件可以调用cookies函数来获取本次请求客户端携带的cookie。这对于鉴权页面或需要通过身份返回不同的数据来说非常重要。
使用cookies就可以在服务端组件中操作客户端的cookie,增加、删除、修改、查询都是可以。
tsx
import { cookies } from 'next/headers'
export default function Page() {
const cookieStore = cookies()
const sessionId = cookieStore.get('sessionId')
if(sessionId){
return <div>你好,用户。</div>
}else{
return <div>请先登录</div>
}
}import { cookies } from 'next/headers'
export default function Page() {
const cookieStore = cookies()
const sessionId = cookieStore.get('sessionId')
if(sessionId){
return <div>你好,用户。</div>
}else{
return <div>请先登录</div>
}
}2.headers
在服务端组件中,你可以在组件中获取请求头部。
tsx
import { headers } from 'next/headers'
export default function Page() {
const headersList = headers()
const referer = headersList.get('referer')
return <div>Referer: {referer}</div>
}import { headers } from 'next/headers'
export default function Page() {
const headersList = headers()
const referer = headersList.get('referer')
return <div>Referer: {referer}</div>
}3.notFound
在服务端组件中,你可以在用户请求不存在的资源时重定向到notFound组件中。Next会通过文件结构查找最近的not-found组件并渲染。
tsx
import { notFound } from 'next/navigation'
async function fetchUser(id) {
const res = await fetch('https://...')
if (!res.ok) return undefined
return res.json()
}
export default async function Profile({ params }) {
const user = await fetchUser(params.id)
if (!user) {
notFound()
}
// ...
}import { notFound } from 'next/navigation'
async function fetchUser(id) {
const res = await fetch('https://...')
if (!res.ok) return undefined
return res.json()
}
export default async function Profile({ params }) {
const user = await fetchUser(params.id)
if (!user) {
notFound()
}
// ...
}4.redirect
redirect 函数允许你将用户重定向到另一个 URL。 redirect 可用于服务器组件、客户端组件、路由处理程序 和 服务器操作。在客户端组件中使用还是推荐useRouter钩子。
tsx
import { redirect } from 'next/navigation'
async function fetchTeam(id) {
const res = await fetch('https://...')
if (!res.ok) return undefined
return res.json()
}
export default async function Profile({ params }) {
const team = await fetchTeam(params.id)
if (!team) {
redirect('/login')
}
// ...
}import { redirect } from 'next/navigation'
async function fetchTeam(id) {
const res = await fetch('https://...')
if (!res.ok) return undefined
return res.json()
}
export default async function Profile({ params }) {
const team = await fetchTeam(params.id)
if (!team) {
redirect('/login')
}
// ...
}13.Next.js钩子
1.useRouter
可以进行导航、刷新、替换历史记录导航等功能。