• Next.js 13 服务器组件和应用目录完整指南


    通过关于使用服务器组件和应用程序目录的最完整和最权威的教程,释放 Next.js 13 的全部潜力。

    Next.js 13 带来了什么?

    Next.js 13 是最流行的 React 框架的主要版本:事实上,它附带了一个新的路由系统,也称为 App Router。在许多方面,这个新的路由系统是对前一个路由系统的完全重写,从根本上改变了我们将编写Next.js应用程序的方式

    app 目录为现有 pages 目录带来了许多改进,这是在经过一段时间的实验性发布后编写 Next.js 应用的新默认方式。

    服务器组件 (RSC)

    新 App Router 中最大的变化是引入了服务器组件,这是一种在服务器上运行并返回发送到客户端的已编译 JSX 的新型 React 组件。服务器组件可用于呈现页面的骨架、从服务器并行获取数据并将其传递给“客户端组件”。

    服务器组件是新 app 目录中的默认组件类型。

    布局

    布局由服务器组件提供支持,是环绕页面的基础组件。下一步.js布局不仅可用于跨页面显示通用 UI,还可用于跨页面重用逻辑和数据提取。

    布局还解决了瀑布问题,这是当前 Next.js 路由系统的常见问题。事实上,使用新的 App Router,我们可以并行获取数据,并将其传递给页面组件:这是对当前路由系统的实质性性能改进

    Server Actions 服务器操作

    仍处于 alpha 阶段,服务器操作是一种在服务器上执行函数的新方法,而不是通过 API 处理程序连接它们。服务器操作对于执行服务器端逻辑(如发送电子邮件或更新数据库)非常有用。服务器操作对于提交表单、执行服务器端逻辑以及将用户重定向到新页面非常有用。

    此外,服务器操作使我们能够重新验证从服务器组件获取的数据,从而消除了由于数据突变(例如更新 Redux 存储)而导致的复杂客户端状态管理的需求。

    Enhanced Router 增强型路由器

    使用常规文件名,我们可以在 app 目录中添加各种类型的组件。这是对当前路由系统的一大改进,当前路由系统要求我们使用特定的目录结构来定义页面、API 处理程序等。

    这是什么意思?从现在开始,我们可以使用特定于 Next.js 的特定文件名约定在 app 目录中创建各种类型的组件:

    • 页面定义为 page.tsx
    • 布局定义为 layout.tsx
    • 错误定义为 error.tsx
    • 加载状态定义为 loading.tsx

    什么是服务器组件?

    服务器组件是一种新型的 React 组件,它们在服务器上运行并返回发送到客户端的已编译 JSX。Next.js 在 Next.js 13 中发布了新的应用目录,通过将服务器组件设置为默认类型组件,完全接受了服务器组件。

    这是与在服务器和客户端上运行的传统 React 组件的重大转变。事实上,正如我们所指定的,React Server 组件不会在客户端上执行。

    因此,我们需要记住使用服务器组件的一些约束:

    • 服务器组件不能使用仅限浏览器的 API
    • 服务器组件不能使用 React 钩子
    • 服务器组件无法使用 Context 上下文

    那么,它们是干什么用的呢?

    React Server 组件对于渲染页面的骨架很有用,同时将交互式位留给所谓的“客户端组件”。

    尽管名称如此,但“客户端组件”(恕我直言,不幸的是)也是服务器呈现的,它们在服务器和客户端上运行。

    React 服务器组件可能很有用,因为它们允许我们:

    • 更快地呈现页面
    • 减少需要发送到客户端的 JavaScript 数量
    • 提高服务器呈现页面的路由性能

    简而言之,我们使用服务器组件从服务器获取数据并呈现页面的骨架:然后,我们可以将数据传递给“客户端组件”。

    服务器组件与客户端组件

    正如我们所看到的,服务器组件对于呈现页面的框架很有用,而客户端组件是我们今天所知道的组件。

    Next.js文档中的这种比较是了解两者之间区别的好方法。

    定义服务器组件

    服务器组件不需要这样定义表示法:服务器组件在应用程序目录中呈现时是默认组件。

    我们不能在服务器组件中使用 React 钩子、上下文或仅限浏览器的 API。但是,我们只能使用服务器组件 API,例如 headerscookies等。

    服务器组件可以导入客户端组件

    无需指定表示法来定义服务器组件:实际上,服务器组件是新 app 目录中的默认组件类型。

    假设该组件不是客户端组件的子组件 ServerComponent ,它将在服务器上呈现并作为编译的 JSX 发送到客户端:

    export default function ServerComponent() {
      return <div>Server Component</div>;
    }
    
    • 1
    • 2
    • 3

    定义客户端组件

    相反,在 Next.js app 目录中,我们需要专门定义客户端组件

    我们可以通过在文件顶部指定 use client 指令来做到这一点:

    'use client';
     
    export default function ClientComponent() {
      return <div>Client Component</div>;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    当我们使用客户端组件时,我们可以使用 React 钩子、上下文和仅限浏览器的 API。但是,我们不能仅使用某些服务器组件 API,例如 headerscookies 等。

    注意:客户端组件无法导入服务器组件,但您可以将服务器组件作为客户端组件的子组件或属性传递。

    App 应用目录

    Next.js 13 中发布的新“app”目录是一种构建 Next.js 应用程序的新实验性新方法。它与目录共存,我们可以使用它来增量地将现有项目迁移到新的 pages 目录结构。

    这种新的目录结构不仅仅是编写应用程序的新方法,它是一个全新的路由系统,比当前的路由系统强大得多。

    文件结构

    新的 Next.js 13 文件结构是什么样的?让我们看一下我们将在本教程中使用的示例应用。

    下面是具有新 app 目录的 Next.js 13 应用的示例:

    - app
      - layout.tsx
      - (site)
        - page.tsx
        - layout.tsx
      - app
        - dashboard
          - page.tsx
        - layout.tsx
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    如您所见,文件的名称反映了组件的类型。例如,是布局组件,而 page.tsx 是页面组件, layout.tsx 依此类推。

    别担心,我们将在下一节中介绍所有不同类型的组件。

    托管

    app 目录的一个重要副作用是它允许我们共置文件。由于文件名是约定俗成的,我们可以定义 app 目录中的任何文件,而不会成为页面组件。

    例如,我们可以将特定页面的组件直接放在定义它的文件夹中:

    - app
      - (site)
        - components
          - Dashboard.tsx
        - hooks
          - use-fetch-data-hook.ts
        - page.tsx
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    为什么在 (site) 括号里?通过使用括号,我们使目录 site “无路径”,这意味着我们可以在 site 目录中创建新的布局、加载文件和页面,而无需向路由添加新的路径段。

    (site) 下的所有页面都将从根路径 / 访问:例如,该页面 app/(site)/page.tsx 将位于 /

    Layouts 布局

    布局是新应用路由器实现的最大新功能之一。

    布局是包装页面的基础组件:这不仅可用于跨页面显示通用 UI,还可用于重用数据获取和逻辑。

    Next.js 需要一个根布局组件:

    export const metadata = {
      title: 'Next.js Tutorial',
      description: 'A Next.js tutorial using the App Router',
    };
     
    async function RootLayout({
      children,
    }: {
      children: React.ReactNode;
    }) {
      return (
        <html lang={'en'}>
          <body>{children}</body>
        </html>
      );
    }
     
    export default RootLayout;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    布局是使用 app 目录中的约定 layout.tsx 定义的:Next.js将自动包装定义布局的文件夹中的所有页面。

    例如,如果我们在 中 app/(site)/layout.tsx 定义了一个布局,Next.js 将使用此布局包装 app/(site) 目录中的所有页面:

    export default async function SiteLayout({
      children,
    }: {
      children: React.ReactNode;
    }) {
      return (
        <div>
          <main>
            {children}
          </main>
        </div>
      );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    结果 - app/(site) 目录中的所有页面都将用组件 SiteLayout 包装。

    在布局组件中加载数据

    如果您需要加载目录的所有页面所需的一些数据,布局组件也非常有用:例如,我们可以在布局组件中加载用户的配置文件,并将其传递给页面组件。

    要在 Next.js 的布局组件中获取数据,我们可以使用新 use 钩子,这是 React 中的一个实验性钩子,用于 Suspense 获取服务器上的数据。

    import { use } from "react";
     
    export default function SiteLayout({
      children,
    }: {
      children: React.ReactNode;
    }) {
      const data = use(getData());
     
      return (
        <div>
          <header>
            { data.user ? <ProfileDropown /> : null }
          </header>
     
          <main>
            {children}
          </main>
        </div>
      );
    }
     
    function getData() {
      return fetch('/api/data').then(res => res.json());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    在上面的示例中:

    1. 我们使用 use 钩子在布局组件中获取数据
    2. 我们基于 data.user 属性有条件地呈现 ProfileDropdown 组件

    注意:我们使用 use 钩子以(看似)同步的方式获取数据。这是因为 use 钩子在引擎盖下使用 Suspense ,这允许我们以同步方式编写异步代码。

    在服务器组件中使用Async/Await

    另一种方法是使组件成为 async 组件,并使用 async/await 从以下位置 getData 获取数据:

    export default async function SiteLayout({
      children,
    }: {
      children: React.ReactNode;
    }) {
      const data = await getData()
     
      return (
        <div>
          <header>
            { data.user ? <ProfileDropown /> : null }
          </header>
     
          <main>
            {children}
          </main>
        </div>
      );
    }
     
    function getData() {
      return fetch('/api/data').then(res => res.json());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    读取 Cookies 和 Headers

    如果您使用的是服务器组件,则可以从 next/headers 包中使用读取 Cookies 和 Headers。

    注意:在撰写本文时,我们只能使用这些函数来读取它们的值,而不能设置或删除它们。

    import { cookies } from 'next/headers';
     
    export function Layout(
      { children }: { children: React.ReactNode },
    ) {
      const lang = cookies.get('lang');
     
      return (
        <html lang={lang}>
          <body>
            {children}
          </body>
        </html>
      );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    如果您觉得缺少某些东西,请不要担心,不仅仅是您。实际上,与 不同 getServerSideProps ,我们无法访问该 request 对象。这就是 Next.js 公开这些实用程序以从请求中读取数据的原因。

    从布局重定向

    在布局中,我们还可以将用户重定向到不同的页面。

    例如,如果我们想将用户重定向到登录页面(如果他们未经身份验证),我们可以在布局组件中执行此操作:

    import { use } from 'react';
    import { redirect } from 'next/navigation';
     
    function AuthLayout(
      props: React.PropsWithChildren,
    ) {
      const session = use(getSession());
     
      if (session) {
        return redirect('/dashboard');
      }
     
      return (
        <div className={'auth'}>
          {props.children}
        </div>
      );
    }
     
     
    function getSession() {
      return fetch('/api/session').then(res => res.json());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    现在,我们可以在布局组件中使用 loadSession 函数:

    import { use } from 'react';
     
    function AuthLayout(
      props: React.PropsWithChildren,
    ) {
      const response = use(loadSession());
      const data = response.data;
     
      // do something with data
     
      return (
        <div className={'auth'}>
          {props.children}
        </div>
      );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    Next.js 重定向副作用

    新的 Next.js 函数 redirect 将抛出错误:实际上,它的返回类型是 never 。如果发现错误,则需要小心并确保遵循错误引发的重定向。

    为此,我们可以使用 Next.js 包导出的一些实用程序:

    import { use } from 'react';
     
    import {
      isRedirectError,
      getURLFromRedirectError,
    } from 'next/dist/client/components/redirect';
     
    import { redirect } from "next/navigation";
     
    async function loadData() {
      try {
        const data = await getData();
     
        if (!data) {
          return redirect('/login');
        }
     
        const user = data.user;
     
        console.log(`User ${user.name} logged in`);
     
        return user;
      } catch (e) {
        if (isRedirectError(e)) {
          return redirect(getURLFromRedirectError(e));
        }
     
        throw e;
      }
    }
     
    function Layout(
      props: React.PropsWithChildren,
    ) {
      const data = use(loadData());
     
      // do something with data
     
      return (
        <div>
          {props.children}
        </div>
      );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    Pages 页面

    要在新的应用程序目录中定义页面,我们使用 特殊约定 page.tsx .这意味着,如果我们想在 app 目录中定义一个页面,我们需要 命名文件 page.tsx .

    例如,如果我们想定义您网站的主页,我们可以将页面放在 app/(site) 目录中并命名为 page.tsx

    function SitePage() {
      return <div>Site Page</div>;
    }
     
    export default SitePage;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    页面元数据和 SEO 搜索引擎优化

    要指定页面的元数据,我们可以导出 page.tsx 文件中的 constant metadata 属性:

    export const metadata = {
      title: 'Site Page',
      description: 'This is the site page',
    };
    
    • 1
    • 2
    • 3
    • 4

    如果需要访问动态数据,可以使用以下 generateMetadata 函数:

    // path: app/blog/[slug]/page.js
    export async function generateStaticParams() {
      const posts = await getPosts();
     
      return posts.map((post) => ({
        slug: post.slug,
      }));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    查看生成静态路径的完整文档

    加载指示器

    在页面之间导航时,我们可能希望显示加载指示器。为此,我们可以使用可以在每个目录中定义的 loading.tsx 文件:

    // path: /app/loading.tsx
    export default function Loading() {
      return <div>Loading...</div>;
    }
    
    • 1
    • 2
    • 3
    • 4

    在这里,您可以添加要在页面加载时显示的任何组件,例如顶部栏加载器或加载微调器,或两者兼而有之。

    错误处理

    目前,您可以使用约定 not-found.tsx 定义“404 未找到”页面:

    export default function NotFound() {
      return (
        <>
          <h2>Not Found</h2>
          <p>Could not find requested resource</p>
        </>
      );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    仅当与 notFound 函数结合使用时,才会显示此文件。这就是为什么仍然建议使用旧 pages 目录使用自定义 400 和 500 页面的原因。

    自定义 404 和 500 页

    在撰写本文时,我们需要坚持使用常规 pages 目录来定义自定义 404 和 500 页面。这是因为 Next.js 不支持 app 目录中的自定义 404 和 500 页面。

    字体

    我们可以使用该包 next/font 在我们的应用程序中加载字体。

    为此,我们需要定义一个客户端组件,并将其导入根布局 app/layout.tsx 文件中:

    // path: app/font.tsx
    'use client';
     
    import { Inter } from 'next/font/google';
    import { useServerInsertedHTML } from 'next/navigation';
     
    const heading = Inter({
      subsets: ['latin'],
      variable: '--font-family-heading',
      fallback: ['--font-family-sans'],
      weight: ['400', '500'],
      display: 'swap',
    });
     
    export default function Fonts() {
      useServerInsertedHTML(() => {
        return (
          <style
            dangerouslySetInnerHTML={{
              __html: `
              :root {
                --font-family-sans: '-apple-system', 'BlinkMacSystemFont',
                  ${sans.style.fontFamily}, 'system-ui', 'Segoe UI', 'Roboto',
                  'Ubuntu', 'sans-serif';
     
                --font-family-heading: ${heading.style.fontFamily};
              }
            `,
            }}
          />
        );
      });
     
      return null;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    之后,我们可以在根布局中导入 Fonts 组件:

    import Fonts from '~/components/Fonts';
     
    export default async function RootLayout({
      children,
    }: {
      children: React.ReactNode;
    }) {
      return (
        <html>
          <Fonts />
     
          <body>{children}</body>
        </html>
      );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    API 接口路由

    新的应用目录还支持 API 路由。定义 API 路由的约定是在 app 目录中创建名为 route.tsx 的文件。

    API 路由现在使用标准 Request 对象,而不是 express -like reqres 对象。

    当我们定义 API 路由时,我们可以导出我们想要支持的方法的处理程序。例如,如果我们想支持 GET 和 POST 方法,我们可以导出 GET 和 POST 函数:

    // path: app/api/route.tsx
    import { NextResponse } from 'next/server';
     
    export async function GET() {
      return NextResponse.json({ hello: 'world' });
    }
     
    export async function POST(
      request: Request
    ) {
      const body = await request.json();
      const data = await getData(body);
     
      return NextResponse.json(data);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    如果我们想操纵响应,例如通过设置 cookie,我们可以使用以下 NextResponse 对象:

    export async function POST(
      request: Request
    ) {
      const organizationId = getOrganizationId();
      const response = NextResponse.json({ organizationId });
     
      response.cookies.set('organizationId', organizationId, {
        path: '/',
        httpOnly: true,
        sameSite: 'lax',
      });
     
      return response;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在 API 路由中,就像在服务器组件中一样,我们也可以使用从以下位置 next/navigation 导入的 redirect 函数重定向用户:

    import { redirect } from 'next/navigation';
     
    export async function GET(
      request: Request
    ) {
      return redirect('/login');
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    处理 Webhooks

    处理 webhook 是 API 路由的常见用例,现在获取原始正文请求要简单得多。实际上,我们可以使用以下 request.text() 方法获取原始正文请求:

    // path: app/api/webhooks.tsx
    export async function POST(
      request: Request
    ) {
      const rawBody = await request.text();
     
      // handle webhook here
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    Server Actions 服务器操作

    服务器操作是 Next.js 13 中引入的新概念。它们是定义可从客户端调用的服务器端操作的一种方法。您需要做的就是定义一个函数并使用顶部的 use server 关键字。

    例如,下面是一个有效的服务器操作:

    async function myActionFunction() {
      'use server';
     
      // do something
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如果要从客户端组件定义服务器操作,则需要从单独的文件导出该操作,并将其导入到客户端组件中。该文件需要顶部的关键字 use server

    'use server';
     
    async function myActionFunction() {
      // do something
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    要从客户端调用服务器操作,您可以通过多种方式

    • 将操作 action 定义为 form 组件的属性
    • 使用 formAction 属性从 button 组件调用操作
    • 使用 useTransition 钩子调用操作(如果它改变了数据)
    • 简单地像普通函数一样调用操作(如果它不改变数据)

    如果您想了解有关服务器操作的更多信息,请查看关于 Next.js 服务器操作的文章

    结论

    在本文中,我们学习了如何使用 Next.js 13 中的新实验性应用路由器。

    我们在本文中学到的模式和约定仍处于实验阶段,将来可能会发生变化。但是,它们已经非常有用,我们已经可以在项目中开始使用它们。

    您是否知道您可以我们的 Next.js 13 SaaS 初学者工具包来构建您自己的 SaaS 应用程序?这是一个功能齐全的 SaaS 初学者工具包,其中包含使用 Next.js 13 构建 SaaS 应用程序所需的一切,包括身份验证、计费等。

    虽然它仍处于实验阶段,但你可以立即开始使用 Next.js 13 开始构建 SaaS 应用,并为未来提供面向未来的应用,而无需从旧架构进行痛苦的迁移。

  • 相关阅读:
    SpringMVC
    2022-03-18-SpringBoot
    Postgresql的一个bug_涉及归档和pg_wal
    【STM32】定时器+基本定时器
    CE-Net: Context Encoder Network for 2D MedicalImage Segmentation
    冰河十年前的预测如今被阿里实现了,非常震撼
    Flutter系列文章-Flutter进阶2
    职场中为人处世那些事!
    字符串拼接re.sub()用法
    前端架构-分层而治,铁打的MV流水的C
  • 原文地址:https://blog.csdn.net/jslygwx/article/details/132392323