NextJS的路由设计十分特别,它采用一种基于文件系统的路由声明方式。在大约20年前流行的PHP等网站开发技术中,曾经并不存在“路由”这一概念,浏览器访问的路径就是PHP文件相对于网站根目录的文件系统相对路径,20年后NextJS回归了这一大道至简的路由方式(不过其底层原理完全不同)。这篇笔记我们学习NextJS中的路由系统。
在NextJS的早期版本中,文件路由使用Page Router结构,但最新的NextJS的13.4
版本将路由系统改为了App Router,路由相关的API发生了较大的变化。新版本的NextJS中,路由的根目录是app
文件夹(如果创建工程时采用src
目录,app
文件夹位于src
内;如果创建工程时不采用src
目录,app
文件夹位于工程根目录)。假如我们有以下目录结构:
|_app
|_page.jsx
|_dashboard
|_page.jsx
根据此目录结构,NextJS会生成如下2条路由:
/
/dashboard
如果我们嵌套多层目录结构,也会对应生成多层的嵌套路由。也就是说,NextJS中app
为路由根路径,app
下的目录结构对应路由;但要注意目录下必须存在page.jsx
,其对应的目录才会被识别为路由。page.jsx
是一个React组件,默认情况下NextJS会在服务端执行该组件,NextJS中如果需要使用客户端组件,需要在文件头声明"use client"
,有关混合渲染的内容将在后续章节介绍。
此外,如果目录名使用下划线开头,例如_components
形式,该目录及其子目录会被忽略,不会被识别为路由定义。我们可以将不需要被识别为路由的React组件放置在这种路径中。下面例子代码中,我们在page.jsx
中引入了_components
目录中定义的组件。
import Header from "@/app/dashboard/_components/Header";
const Page = () => {
return (
<>
<Header />
<div>Hello, NextJS!</div>
</>
);
};
export default Page;
和很多其它框架类似,NextJS也对import
路径进行了处理,@
是工程根路径的别名。
通过前面学习我们知道page.jsx
代表一个页面,HTML页面中通常包含标题、描述等头信息,NextJS的服务端组件中,约定使用metadata
属性配置页面的头信息,能正确设置这些头信息无疑对SEO有极大帮助,下面是一个例子。
export const metadata = {
title: "Welcome",
description: "Welcome to my website!",
};
const Page = () => {
return <div>Hello, NextJS!</div>;
};
export default Page;
实际上,我们只要在page.jsx
中导出一个metadata
属性即可。此外,实际开发中,我们可能还经常用到动态设置metadata
的情况,在服务端组件中,这需要用到generateMetadata
方法。
export const generateMetadata = async function () {
return {
title: "Welcome",
description: "Welcome to my website!",
};
};
const Page = () => {
return <div>Hello, NextJS!</div>;
};
export default Page;
代码中的generateMetadata()
函数内我们直接返回了相应的信息,不过实际上这里我们也可以进行更多操作,比如调用下接口查询等。此外要注意metadata
和generateMetadata
不能同时使用。有关metadata
属性的其它配置,我们可以参考文档中相关的章节。
Layout的概念在许多前端路由系统中都存在,它的作用类似一个“框”套住了路由的子组件,NextJS中我们可以在路由目录下创建layout.jsx
作为该路径下所有子路由的框架,我们看一个简单的例子。
|_app
|_dashboard
|_layout.jsx
|_page.jsx
|_... // 其它子目录
layout.jsx
const Layout = ({children}) => {
return <>
<span>首页</span>
<span>文章</span>
<span>关于</span>
<hr/>
{children}
</>;
};
export default Layout;
上面代码中,layout.jsx
会作用于同级的路由和子级的路由。layout.jsx
本身也是一个React组件,NextJS在它接收的props
中传入了children
属性,它是一个ReactNode
对象,代表嵌入其中的路由组件。
NextJS中路由分组使用小括号(分组名)
作为目录名创建,下面是一个例子。
|_app
|_(marketing)
|_layout.jsx
|_about
|_page.jsx
|_blog
|_page.jsx
|_(shop)
|_layout.jsx
|_account
|_page.jsx
上面例子中,例如(marketing)
并不会产生一个实际的路由,我们访问其中的页面还是使用类似/about
的路径,但它会产生一个分组,在分组中我们可以单独定义layout.jsx
框架,让不同的分组具有不同的Layout框架。
NextJS中动态路由指类似/users/3
的路由形式,其中3
其实是一个参数,这种动态路由使用中括号[参数名]
的形式指定。
|_app
|_users
|_[id]
|_page.jsx
page.jsx
const Page = ({ params }) => {
return <>{params.id}</>;
};
export default Page;
NextJS会将动态路由参数以params
属性的形式传入组件的props
中。
除此之外动态路由也支持多级,多级需要以[...参数数组名]
的形式指定,下面是一个例子。
|_app
|_users
|_[...slug]
|_page.jsx
page.jsx
const Page = ({ params }) => {
return (
<>
{params.slug.map((item, index) => {
return <div key={index}>{item}</div>;
})}
</>
);
};
export default Page;
此时如果访问/users/a/b/c
,params.slug
中的内容即为数组['a', 'b', 'c']
;如果访问/users
,则无法匹配[...slug]
内的页面。
<Link>
是路由系统提供的组件,用于链接跳转,它会在浏览器中渲染为<a>
标签。
import Link from "next/link";
const Page = () => {
return (
<>
<Link href={"/dashboard"}>Dashboard</Link>
</>
);
};
export default Page;
上面例子代码中创建了一个跳转到/dashboard
页面的链接。注意在NextJS工程中我们应该尽可能是使用<Link>
而不是<a>
,这是因为<Link>
支持prefetch预加载、缓存等高级特性,而<a>
标签则会被浏览器粗暴的理解为就是重新向服务器请求一个新的页面,使用<Link>
而不是<a>
有助于NextJS对我们网站响应速度进行优化。
除了<Link>
组件我们也可以使用NextJS提供的Hooks函数来访问和操作路由。
"use client";
import { useRouter } from "next/navigation";
const Page = () => {
const router = useRouter();
return (
<>
<button
onClick={() => {
router.push("/dashboard");
}}
>
Click Me!
</button>
</>
);
};
export default Page;
上面例子中我们使用了useRouter
来操作路由,点击按钮时跳转到/dashboard
页面。注意这里代码中包含了操作页面跳转的逻辑,我们很容猜到它底层是基于HistoryAPI来实现的,存在客户端操作,因此该React组件需要声明为客户端组件。有关混合渲染的内容将在后续章节详细介绍。
获取GET参数可以使用useSearchParams()
函数,注意使用该Hooks的组件也必须是客户端组件。
"use client";
import { useSearchParams } from "next/navigation";
const Page = () => {
const searchParams = useSearchParams();
const id = searchParams.get("id");
const username = searchParams.get("username");
return (
<>
<div>{id}</div>
<div>{username}</div>
</>
);
};
export default Page;
代码中我们读取了GET参数中的id
和username
。
我们可以使用usePathname
获取请求路径,这也是一个客户端组件的Hooks函数。
"use client";
import { usePathname } from "next/navigation";
const Page = () => {
const pathname = usePathname();
return (
<>
<div>{pathname}</div>
</>
);
};
export default Page;
代码中,假如我们访问了地址http://xxx/dashboard/a/b/c
,函数usePathname()
会返回/dashboard/a/b/c
。