学习 Next.js 不妨看看这篇文章

4,809 阅读19分钟

hi,我是风骨,作为前端工程师,除了掌握 CSR(客户端渲染)研发 ToB 中后台应用外,还需要具备 SSR(服务端渲染)技能来开发面向 ToC 终端用户的应用。现如今 Next.js + Tailwindcss 是 React 技术栈 SSR 的首选方式。

下面让我们一起进入主题,学习 Next.js!

image.png

一、创建 Next.js 项目

执行 npx create-next-app@latest 命令创建 Next.js 项目,并按照以下提示完成项目创建。

Tip: Node 要求 v18.18 版本及以上。

image.png

上述操作完成以后,进入项目目录,运行 npm run dev 启动开发环境,并在浏览器上访问 http://localhost:3000 查看启动的项目。

二、路由

Next.js 使用 基于文件系统 的路由器,即 每个文件夹都表示一个路由,因此无需安装类似 react-router 的工具。

在上一步创建项目时我们选择的是 App Router 路由模式,接下来 app 目录将作为所有路由的存储根目录。

1、创建一个路由(页面)

现在,我们要新建一个 /dashboard 路由,只需两步即可完成创建。

  1. 新建 app/dashboard 目录;
  2. 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 子页面,依据 每个文件夹都表示一个路由 规则同样适用于创建嵌套路由,只需两步即可完成创建。

  1. 新建 app/dashboard/customers 目录;
  2. 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],同样需要两步完成创建。

  1. 新建 app/dashboard/customers/[id] 目录;
  2. 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 idabcd,手动将浏览器地址导航到 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 调用的方式实现路由跳转。你可以根据场景选用 pushreplace

// 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。

路由结构如下:

image.png

由于 (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 左侧进行快捷搜索 或 使用浏览器关键字搜索,查找要想使用的实用程序类。

image.png

四、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 搜索引擎优化。如 titledescriptionkeywords 等信息。

Next.js 允许将元数据配置在 layout.tsxpage.tsx 文件中进行导出。

配置元数据的方式有两种:

  1. 静态元数据对象;
  2. 动态 generateMetadata 函数。

1、静态元数据对象

静态元数据对象 用法简单,已知元数据内容的情况下可以使用这种方式,在 app 或者任意页面的 layout.tsxpage.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 标签可以看到设置的元数据内容。

image.png

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 特点如下:

  1. 提供触发登录(signIn)和登出(signOut)过程的函数;
  2. 内置的提供商,允许使用 Google、GitHub 或邮箱 + 密码等方式登录;
  3. 基于 JWT 生成用户 session token,在成功登录后,设置到浏览器 cookies 中;
  4. 提供 auth 函数读取用户 session token;
  5. 提供 auth 函数作为 middleware 中间件处理路由跳转鉴权逻辑;

NextAuth 可以应用在这两处:

  1. 处理登录,它提供的函数(signIn、signOut、auth)处理 登录、登出、获取用户信息 等操作;
  2. 路由鉴权,它提供的函数(auth)可作为 middleware 中间件处理路由导航。

在使用前我们需要安排准备工作:安装 NextAuth 以及生成一个 secret 加密令牌:

  1. 安装:
npm install next-auth@beta
  1. 生成 secret
npx auth secret

在根目录下 .env.local 文件中会存放生成的环境变量 AUTH_SECRET

1、处理登录

下面我们使用 NextAuth 凭据(邮箱 + 密码)方式给网站添加登录流程。

  1. 首先,初始化一个 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;
      },
    }),
  ],
});
  1. 在登录界面将用户输入的 邮箱 和 密码 传递给 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 中我们能看到 buildstart 两个命令:

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  }
}

部署主要步骤如下:

  1. 运行 npm run build 生成用于生产环境的应用程序的优化版本;
  2. 使用 pm2 运行 npm run start 命令,基于 build 的构建产物启动 Next 服务器,默认服务器运行在 3000 端口上;
pm2 start npm --name "next-app" -- start
  1. 配置 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 用法。

文章参考:
会了 React 和 HTTP 协议,就等于会了 Next.js