hi,我是风骨,作为前端工程师,除了掌握 CSR(客户端渲染)研发 ToB 中后台应用外,还需要具备 SSR(服务端渲染)技能来开发面向 ToC 终端用户的应用。现如今 Next.js + Tailwindcss 是 React 技术栈 SSR 的首选方式。
下面让我们一起进入主题,学习 Next.js!
一、创建 Next.js 项目
执行 npx create-next-app@latest 命令创建 Next.js 项目,并按照以下提示完成项目创建。
Tip: Node 要求 v18.18 版本及以上。
上述操作完成以后,进入项目目录,运行 npm run dev 启动开发环境,并在浏览器上访问 http://localhost:3000 查看启动的项目。
二、路由
Next.js 使用 基于文件系统 的路由器,即 每个文件夹都表示一个路由,因此无需安装类似 react-router 的工具。
在上一步创建项目时我们选择的是 App Router 路由模式,接下来 app 目录将作为所有路由的存储根目录。
1、创建一个路由(页面)
现在,我们要新建一个 /dashboard 路由,只需两步即可完成创建。
- 新建
app/dashboard目录; - 在
app/dashboard目录下新建page.tsx文件,并导出一个 React 组件。
// app/dashboard/page.tsx
export default function Dashboard() {
return <div>Dashboard Page.</div>;
}
打开浏览器访问 http://localhost:3000/dashboard 即可看到 dashboard 页面内容。
2、创建嵌套路由
接下来,我们需要在 /dashboard 下创建一个 /dashboard/customers 子页面,依据 每个文件夹都表示一个路由 规则同样适用于创建嵌套路由,只需两步即可完成创建。
- 新建
app/dashboard/customers目录; - 在
app/dashboard/customers目录下新建page.tsx文件,并导出一个 React 组件。
// app/dashboard/customers/page.tsx
export default function Customers() {
return <div>Customers Page.</div>;
}
手动将浏览器地址导航到 http://localhost:3000/dashboard/customers 即可看到 customers 页面内容。
3、创建动态路由
将 数据 id 作为路由段也是一个常见的路由形式,一般称为动态路由。
由于数据 id 是动态字段无法提前预知,Next.js 支持 将文件夹名称括在方括号中 来创建动态路由,格式为 [folderName],例如 [id]。路由上的数据 id 将作为 props.params.id 传递给页面组件。
我们在 /dashboard/customers 下创建一个动态路由 /dashboard/customers/[id],同样需要两步完成创建。
- 新建
app/dashboard/customers/[id]目录; - 在
app/dashboard/customers/[id]目录下新建page.tsx文件,并导出一个 React 组件。
// app/dashboard/customers/[id]/page.tsx
export default function CustomerDetail({ params }: { params: { id: string } }) {
return <div>CustomerDetail Page. {params.id}</div>;
}
假设 customers id 是 abcd,手动将浏览器地址导航到 http://localhost:3000/dashboard/customers/abcd 即可看到 customers/[id] 页面内容。
4、路由导航
我们要在 /dashboard/page.tsx 视图上放置一个按钮,点击可以进入 /dashboard/customers 页面。
我们可以使用 Next.js 所提供的 next/link Link 组件作为按钮 UI,并将 路由 Path 作为 Link 的 href 属性。同时可以使用 usePathname() hook 获取当前路由 来给 Link 设置活动状态。
这里我们新建一个 app/dashboard/nav-link.tsx 文件,在里面加入 Link 导航逻辑。
Tip: 注意在组件中使用
usePathname() 等 hooks,需要在文件顶部声明use client表示这是一个客户端渲染组件。有关服务端组件和客户端组件下文会做介绍。
// app/dashboard/nav-link.tsx
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
export default function Dashboard() {
const pathname = usePathname();
return (
<Link
style={{ color: pathname === "/dashboard/customers" ? "#333" : "#999" }}
href="/dashboard/customers"
>
Customers
</Link>
);
}
然后在 /dashboard/page.tsx 中引入 nav-link 组件:
// app/dashboard/page.tsx
+ import NavLink from "./nav-link";
export default function Dashboard() {
return (
<div>
Dashboard Page.
+ <NavLink />
</div>
);
}
此时在 http://localhost:3000/dashboard 页面点击 Link 元素便会跳转进入 http://localhost:3000/dashboard/customers 页面。
除了 Link 视图组件外,Next.js 还提供了 useRouter API 调用的方式实现路由跳转。你可以根据场景选用 push 和 replace。
// app/dashboard/nav-link.tsx
"use client";
import { usePathname, useRouter } from "next/navigation";
export default function Dashboard() {
const pathname = usePathname();
+ const { replace, push } = useRouter();
return (
<div
+ onClick={() => push("/dashboard/customers")}
style={{ color: pathname === "/dashboard/customers" ? "#333" : "#999" }}
>
Customers
</div>
);
}
5、路由组
在 app 目录下每一个文件夹都是一个路由,当你期望一个文件夹仅是用来分组或是包裹其他路由,不会作为 URL 访问路径出现,那么 路由分组 可以帮你实现这个功能。
通过创建一个名为 (group-name) 的文件夹来定义一个路由组,其中 group-name 可以是任意名称。
假设在 /dashboard 路由下有两个子页面:
- 数据分析模块(analytics),对应路由:
/dashboard/analytics - 用户管理模块(users),对应路由:
/dashboard/users
其中,数据分析模块 需要使用布局 UI,而 用户管理模块 并不需要。
PS:
关 Lyout 布局的使用,下文会有介绍,如对这里不太理解,可以跳过先认识 layout 布局
针对这类需求,我们可以创建 路由组 (use-layout) 分组,并定义 layout.tsx 文件来为这个分组下的路由定义相同的布局 UI;而另外一个分组 (nonuse-layout) 不创建 layout.tsx 文件,因此这个分组下的路由没有统一的布局 UI。
路由结构如下:
由于 (use-layout) 和 (nonuse-layout) 属于路由组,不会出现在 URL 访问路径上,因此 数据分析模块 和 用户管理模块 可以正常使用 /dashboard/模块路径 进行访问。
路由组的优势在于:
- 分割模块:当您的应用变得越来越大时,可以使用路由组来划分不同的功能模块。
- 共享布局:如果多个页面共享相同的布局,您可以将它们放在同一个路由组内,并为该组定义一个共同的
layout.tsx文件。
三、style 样式
Next.js 支持的样式书写形式有:Tailwind CSS、CSS Module、sass、CSS-in-JS,推荐使用 Tailwind CSS 完成样式书写。
Tailwind CSS 是一个 CSS 原子类框架,我们可以直接在节点上快速编写实用程序类,代替之前的起类名、在类名下编写 CSS 属性的方式。
比如使用 flex 实现一个两栏布局,可以这样编写:
export default function Container() {
return (
<div className="flex h-screen">
<div className="flex-1 bg-green-600 text-white">left</div>
<div className="flex-1 bg-blue-600 text-white">right</div>
</div>
);
}
每一个实用程序类都对应一个 css 属性,比如 flex 就表示 display: flex。
初次接触我们不太熟悉有哪些实用程序类,大家可以在 Tailwind docs 左侧进行快捷搜索 或 使用浏览器关键字搜索,查找要想使用的实用程序类。
四、layout 布局
布局是定义在多个路由之间共享的 UI。在一个网站中常见的布局是:顶部 header、左侧 navbar 是共享 UI,切换路由导航仅是改变中间区域内容。
在 Next.js 中,你可以在 app 或任意 文件夹路由目录 下定义一个 layout.tsx 文件,导出 React 组件来进行页面布局。
其中 /app/layout.tsx 称为 根布局,这个是必须的,添加到根布局的任何 UI 都将在应用程序中的所有页面之间共享。
下面我们新建 /app/dashboard/layout.tsx 文件作为 dashboard 及其子页面的共享 UI。
// app/dashboard/layout.tsx
import SideNav from "./nav-link";
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="flex h-screen">
<div className="w-64">
<SideNav />
</div>
<div className="flex-1">{children}</div>
</div>
);
}
在上面的布局中,左侧展示 nav-link,右侧展示页面的具体内容。当点击 Link 切换到 /dashboard/customers 页面后,nav-link 依旧可以共享。
五、组件渲染方式(Server/Client Components)
在 Next.js 中组件渲染方式有两种:Server Components 服务端组件渲染 和 Client Components 客户端组件渲染。
Server Components 服务端组件是指组件 render 运行在服务器端,而 Client Components 客户端组件则和编写 CSR 应用一样,组件 render 会在浏览器上执行。
默认 Next.js 会将每个组件视为 Server Components 进行服务端渲染。如果需要使用 Client Components 客户端组件,可以在文件顶部添加 "use client" 指令。
一个客户端渲染组件编写示例如下:
"use client";
import { useState } from "react";
export default function Dashboard() {
const [count, setCount] = useState(0);
return <div onClick={() => setCount(count + 1)}>计数:{count}</div>;
}
如何选择两者?
服务端渲染通常用于首屏渲染页面组件,它不支持渲染有关 交互性 的内容。如当需要使用 useState、useEffect等 hooks 或 onClick 等事件交互时,应当将内容拆分到单独组件文件内,并在文件顶部添加 "use client" 指令,声明这是客户端渲染。
六、数据获取
服务端渲染组件 和 客户端渲染组件 的数据获取方式有所不同。
1、服务端渲染的获取方式
如果是 Server Components 服务端组件渲染,可以直接在组件 render 内使用 await + fetch,等待拿到数据以后再渲染出 UI 视图。
// 模拟从数据库获取 posts
const fetchPosts = async (url: string) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: "1", title: "post1" },
{ id: "2", title: "post2" },
]);
}, 1000);
});
};
export default async function Page() {
// 使用 await + fetch 等待获取数据
const posts = (await fetchPosts("https://api/posts")) as any[];
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
2、客户端渲染的获取方式
如果是 Client Components 客户端组件渲染,和常规 CSR 中用法相似:在 useEffect 中发起 fetch 请求,期间可以展示 loading 视图,等待拿到数据后再渲染到视图上。
"use client";
import { useState, useEffect } from "react";
// 模拟从数据库获取 posts
const fetchPosts = async (url: string) => {...};
export default function Page() {
const [posts, setPosts] = useState<null | any[]>(null);
useEffect(() => {
// 发起 ajax 请求
async function fetchData() {
const posts = (await fetchPosts("https://api/posts")) as any[];
setPosts(posts);
}
fetchData();
}, []);
if (!posts) return <div>Loading...</div>;
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
七、处理加载(流式渲染)
在加载数据期间,通常可以展示 loading 给用户。
在 Next.js 中,有两种方法可以处理加载:
- 1)在页面级别,使用
loading.tsx 文件; - 2)对于特定模块,使用
<Suspense> 组件。
1、loading 文件
以 /dashboard 为例,假设页面需要展示 顾客消费排行榜(下文简称 内容块 A)、热销商品排行榜(下文简称 内容块 B) 两个模块内容。
我们首先在 app/dashboard/page.tsx 中查询这两项数据:
// app/dashboard/page.tsx
// 模拟从数据库获取顾客消费排行榜
const fetchCustomers = async () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: "1", title: "customer1" },
{ id: "2", title: "customer2" },
]);
}, 1000);
});
};
// 模拟从数据库获取热销商品排行榜
const fetchCommodity = async () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: "1", title: "commodity1" },
{ id: "2", title: "commodity2" },
]);
}, 500);
});
};
export default async function Dashboard() {
// 数据获取
await Promise.all([fetchCustomers(), fetchCommodity()]);
return (
<div className="flex h-screen">
<div className="flex-1">顾客消费排行榜</div>
<div className="flex-1">热销商品排行榜</div>
</div>
);
}
现在我们访问 http://localhost:3000/dashboard,看到的效果是:页面无响应,会等到 1s 展示出页面内容。然而在这 1s 等待获取数据期间,其实可以向用户友好展示一个 loading 效果(如骨架屏)。
我们新建 app/dashboard/loading.tsx 文件并编写 loading UI:
// app/dashboard/loading.tsx
export default function Loading() {
return <div>loading.</div>;
}
再次访问 http://localhost:3000/dashboard 你会先看到 loading 视图,之后才是展示实际数据。这样的体验会好一些。
2、Suspense 组件
基于 Suspense 特性可以实现类似 流式渲染 的效果,将页面的分解为较小的块,并逐步将这些块从服务器发送到客户端。这样可以更快地显示页面的某些部分,而无需等待所有数据加载后才能呈现任何 UI。
上面使用 loading.tsx 会存在一个问题:内容块 B 在 500ms 内拿到数据了,但是要等待 内容块 A 的 1000ms 拿到数据后一起渲染在视图。
其实两者可以分开渲染,只要任意一个拿到数据以后,就可以渲染在视图上。
我们改造一下组件结构,将 内容块 A 和 内容块 B 作为单独组件,数据请求也放在各自组件内,并在注册组件时使用 <Suspense> 包裹,同时提供 fallback 渲染 loading 视图。
// app/dashboard/page.tsx
import { Suspense } from "react";
// 模拟从数据库获取顾客消费排行榜
const fetchCustomers = async () => {...};
// 模拟从数据库获取热销商品排行榜
const fetchCommodity = async () => {...};
async function Customers() {
await fetchCustomers();
return <div className="flex-1">顾客消费排行榜</div>;
}
async function Commodity() {
await fetchCommodity();
return <div className="flex-1">热销商品排行榜</div>;
}
export default async function Dashboard() {
return (
<div className="flex h-screen">
<Suspense fallback={<div>顾客消费排行榜 loading.</div>}>
<Customers />
</Suspense>
<Suspense fallback={<div>热销商品排行榜 loading.</div>}>
<Commodity />
</Suspense>
</div>
);
}
现在你访问 http://localhost:3000/dashboard 看到的效果将是:内容块 A 和 B 先展示 loading view,500ms 后内容块 B 展示实际内容,此时内容块 A 还是展示它的 loading view,在 1000ms 后内容块 A 展示出实际内容。
Suspense 流式处理的方式可以让用户尽早看到已加载完成的内容块,在一些场景下比使用 loading.tsx 体验会更好。
Tip:上述实现的 loading view 过于简单,在实际工作场景中可以编写体验更好的 骨架屏。
八、处理错误 error 文件
Next.js 使用特殊的 error.tsx 文件来捕获路由段中的代码错误,并向用户显示回退 UI。
我们新建 app/dashboard/error.tsx 文件,并导出一个 React 组件,并声明为 Client Component:
// app/dashboard/error.tsx
"use client";
import { useEffect } from "react";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error(error);
}, [error]);
return (
<main className="flex h-full flex-col items-center justify-center">
<h2 className="text-center">Something went wrong!</h2>
<button
className="rounded-md bg-blue-500 px-4 py-2 text-white"
onClick={() => reset()}
>
Try again
</button>
</main>
);
}
当 /dashboard 页面内出现错误时(如:throw new Error("code error.")),页面将显示 error.js 回退 UI,并在 useEffect 中打印错误信息。
九、设置元数据
SSR 服务端渲染一大优势是可以设置元数据帮助网站进行 SEO 搜索引擎优化。如 title、description、keywords 等信息。
Next.js 允许将元数据配置在 layout.tsx 或 page.tsx 文件中进行导出。
配置元数据的方式有两种:
- 静态元数据对象;
- 动态 generateMetadata 函数。
1、静态元数据对象
静态元数据对象 用法简单,已知元数据内容的情况下可以使用这种方式,在 app 或者任意页面的 layout.tsx 或 page.tsx 中导出一个 metadata 对象:
Tip: 不能在声明了
"use client"的文件中导出 metadata 对象,仅支持 Server Component 进行配置。
// app/dashboard/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Dashboard",
description: "This is Dashboard page.",
keywords: ["dashboard"],
};
...
打开浏览器,访问 http://localhost:3000/dashboard 在 head 标签可以看到设置的元数据内容。
2、动态 generateMetadata 函数
动态 generateMetadata 函数 适用于动态向数据库查询到有关数据后再设置元数据,并且在 generateMetadata 方法中可以获取到路由参数 params 信息。
比如 customers/[id] 页面,我们在 app/dashboard/customers/[id]/page.tsx 中使用 generateMetadata 函数:
// app/dashboard/customers/[id]/page.tsx
import type { Metadata, ResolvingMetadata } from "next";
export async function generateMetadata(
{ params, searchParams }: any,
parent: ResolvingMetadata
): Promise<Metadata> {
// 读取参数
const id = params.id;
// 模拟 fetch data
const customer = (await new Promise((resolve) =>
setTimeout(
() =>
resolve({
title: `customer-${id}`,
description: `customer-${id} desc...`,
}),
1000
)
)) as any;
return {
title: customer.title,
description: customer.description,
};
}
...
打开浏览器,访问 http://localhost:3000/dashboard/customers/abcd 在 head 标签可以看到设置的元数据内容。
十、Server Action 服务端行为
在前端将数据提交到数据库中需要经过这些步骤:
- 前端发起 ajax POST 请求将数据给到服务端;
- 服务端将数据存储到数据库。
Next.js 提供了一种更为简单的方式来进行数据提交(称为 Server Action 服务端行为):省略了开发者在客户端书写 ajax POST 请求的步骤,处理数据的回调函数可直接在服务端运行并与数据库对接。
Tip: 注意,这仅是为开发者提交数据提供了便利,实际在 Next.js 底层还是会把它处理成一个 POST 请求从浏览器上进行发起。
假设我们在 /dashboard/customers 页面有一个输入框,输入用户名称,并点击按钮创建一个用户,内容如下:
// app/dashboard/customers/page.tsx
"use client";
import { useState } from "react";
import { createCustomer } from "./action";
export default function Customers() {
const [name, setName] = useState("");
const submitData = () => {
const formData = new FormData();
formData.set("name", name);
createCustomer(formData);
};
return (
<div>
<input
type="text"
placeholder="请输入用户名称"
value={name}
onChange={(event) => setName(event.target.value)}
/>
<button onClick={submitData}>创建一个用户</button>
</div>
);
}
其中 createCustomer 是关键,它来自 action.ts,重点是在 action.ts 文件顶部需要声明 "use server",目的是标记此文件内导出的函数作为 Server Actions 形式去使用。当在客户端组件中使用时,会自动发送 POST 请求,并将 formData 作为 POST 请求参数,传递给 createCustomer 处理函数。
// // app/dashboard/customers/action.tsx
"use server";
export async function createCustomer(data: FormData) {
console.log("data: ", data);
// TODO... 存储数据到数据库中
}
十一、middleware 中间件
中间件可以接收每个传入的请求(页面),可以通过重写、重定向、修改请求/响应标头等方式来修改响应内容。
一个常见的场景是:验证用户身份和授权,在访问特定页面或 API 路由之前,确认用户身份并检查会话 Cookie。
我们在项目根目录下新建 middleware.ts 文件来定义中间件,并加入一个重定向逻辑:当访问 /dashboard 路由时自动重定向到 /dashboard/customers。
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
console.log("middleware request: ", request.nextUrl.pathname);
if (request.nextUrl.pathname === "/dashboard") {
// 重定向到 /dashboard/customers 页面
return NextResponse.redirect(new URL("/dashboard/customers", request.url));
}
}
export const config = {
// 匹配器,让中间件在指定的路径上运行(排除 `/api`、`/_next/static`、`/_next/image` 和 `/favicon.ico` 路径)
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
middleware 中间件还有更多详细的用法可以参考:Middleware。
十二、Auth 登录鉴权
在 Next.js 中用户身份认证可以采用 NextAuth 作为首选方案,它提供了多种登录方式,包括 OAuth 提供商(如 Google、GitHub 等)、凭据(经典的邮箱 + 密码)。
NextAuth 特点如下:
- 提供触发登录(
signIn)和登出(signOut)过程的函数; - 内置的提供商,允许使用 Google、GitHub 或邮箱 + 密码等方式登录;
- 基于 JWT 生成用户 session token,在成功登录后,设置到浏览器 cookies 中;
- 提供
auth函数读取用户 session token; - 提供
auth函数作为middleware中间件处理路由跳转鉴权逻辑;
NextAuth 可以应用在这两处:
- 处理登录,它提供的函数(signIn、signOut、auth)处理 登录、登出、获取用户信息 等操作;
- 路由鉴权,它提供的函数(auth)可作为 middleware 中间件处理路由导航。
在使用前我们需要安排准备工作:安装 NextAuth 以及生成一个 secret 加密令牌:
- 安装:
npm install next-auth@beta
- 生成
secret
npx auth secret
在根目录下 .env.local 文件中会存放生成的环境变量 AUTH_SECRET。
1、处理登录
下面我们使用 NextAuth 凭据(邮箱 + 密码)方式给网站添加登录流程。
- 首先,初始化一个
NextAuth实例对象得到signIn、signOut、auth等方法,我们在项目根目录下新建auth.ts文件来完成这件事情。
// auth.ts
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
export const { auth, signIn, signOut } = NextAuth({
providers: [
// 使用 凭据 作为登录方式
Credentials({
// 提供 凭据 的检验函数
async authorize(credentials) {
// credentials 包含 用户邮件 + 密码 信息,在这里可以编写数据库查询用户信息,若登录信息有效,需要返回查询到的用户信息
const user = await getUser(credentials.email, credentials.password); // getUser 可以向查询数据库用户信息
if (user) {
return user;
}
console.log("Invalid credentials");
// 返回 null 阻止用户登录。
return null;
},
}),
],
});
- 在登录界面将用户输入的 邮箱 和 密码 传递给
signIn进行登录检验:
// app/login/page.tsx
"use client";
import { useActionState } from "react";
import { authenticate } from "@/app/lib/actions";
export default function Login() {
const [errorMessage, formAction] = useActionState(authenticate, undefined);
return (
<form action={formAction}>
<div>
邮箱:
<input type="email" name="email" />
</div>
<div>
密码:
<input type="password" name="password" />
</div>
<button>提交</button>
</form>
);
}
authenticate 是一个 Server Action,它的实现在 app/lib/actions.ts 中:
// app/lib/actions.ts
"use server";
// Server Action 处理登录操作
export async function authenticate(
prevState: string | undefined,
formData: FormData
) {
try {
const email = formData.get("email");
const password = formData.get("password");
// 核心,调用 NextAuth signIn 方法进行身份登录
await signIn("credentials", { email, password });
} catch {
return "Invalid credentials.";
}
}
我们梳理一下:当用户提交登录信息时会调用 signIn 将邮箱和密码传递给 Credentials authorize 进行身份认证,若认证成功需要 return 一个 user 信息给 NextAuth,它会基于 user 信息生成一个 JWT session token 存储在 cookie 中。
另外,如果需要退出登录,执行 signOut() 方法,需要获取当前用户身份时执行 auth() 方法。
2、路由鉴权
在未登录状态下访问 身份认证的页面 时,我们需要自动将路由重定向到 /login 页面。NextAuth 的 auth 方法可以作为 middleware 来完成这一项工作。
首先在根目录下新建 auth.config.ts 配置文件,并提供 callbacks.authorized 作为路由鉴权的处理函数。它接收一个 auth 对象,当 auth.user 不存在时说明当前处于未登录状态。
// auth.config.ts
import type { NextAuthConfig } from "next-auth";
export const authConfig = {
pages: {
signIn: "/login",
},
// 添加中间件,以保护您的路由
callbacks: {
// 验证请求是否被授权通过,auth 属性包含用户的会话,request 属性包含传入请求。
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user; // 是否已登录
if (isLoggedIn) {
if (nextUrl.pathname.startsWith("/login")) {
// 在登录页,重定向到首页,如 /dashboard
return Response.redirect(new URL("/dashboard", nextUrl));
}
// 其他页面,返回 true 保持不动
return true;
}
// 返回 false 重定向到登录页面
return false;
},
},
} satisfies NextAuthConfig;
然后在根目录创建 middleware.ts 并将 NextAuth auth 方法作为中间件函数导出。
// middleware.ts
import NextAuth from "next-auth";
import { authConfig } from "./auth.config";
// 使用 Middleware 完成此任务的优点是,在 Middleware 验证身份验证之前,受保护的路由甚至不会开始渲染,从而增强了应用程序的安全性和性能。
// 使用 authConfig 对象初始化 NextAuth.js 并导出 auth 函数
export default NextAuth(authConfig).auth;
export const config = {
// 使用 Middleware 中的 matcher 选项来指定它应该在特定路径上运行。
matcher: ["/dashboard/:path*"],
};
现在,进行页面切换时,会进入 callbacks.authorized 回调函数进行身份验证,若没有身份信息则重定向到登录页面。
十三、编写服务端 API
Next.js 作为 SSR 服务端框架,同时提供了开发服务端 API 的能力。因此可以用作 全栈项目开发 或做 Mock 数据。
和 Next.js 路由规则相似,编写 API 需要在 app/api 目录下使用特殊文件 route.ts,支持 GET、POST、DELETE、PUT 等所有 HTTP method。
我们新建 app/api/dashboard/route.ts 文件,并导出一个 GET 方法,返回一个列表数据。
// app/api/dashboard/route.ts
export function GET() {
const list = [
{ id: "1", title: "title 1", content: "content 1" },
{ id: "2", title: "title 2", content: "content 2" },
{ id: "3", title: "title 3", content: "content 3" },
];
return Response.json({ list });
}
export function POST() {...}
...
在浏览器访问 http://localhost:3000/api/dashboard 访问 GET 请求即可看到 list 数据。
十四、部署 Next.js
Next.js 属于一个 Node 应用,可以采用 NodeJS 服务器方式进行部署。
在 package.json 中我们能看到 build 和 start 两个命令:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
}
部署主要步骤如下:
- 运行
npm run build生成用于生产环境的应用程序的优化版本; - 使用
pm2运行npm run start命令,基于 build 的构建产物启动 Next 服务器,默认服务器运行在 3000 端口上;
pm2 start npm --name "next-app" -- start
- 配置 Web 服务器 Nginx 代理到 3000 端口,实现通过 域名/IP 访问到 Next 项目。
server {
listen 80;
server_name 域名或 IP;
location / {
proxy_pass http://127.0.0.1:3000/;
}
}
十五、社区开源模板
最后给大家推荐一个适合参考和学习 Next.js 的一些开源项目:在 Next.js templates 这里可以找寻相关领域的模板,借鉴和学习他们的 Next.js 用法。