黑白梦

Next 14 升级与 app 路由、服务端组件、Server Actions 等新特性应用

对项目进行了 Next 14 升级,并学习了一些新特性,如 app 路由、服务端组件、Server Actions 等作等。新版本在路由、项目结构组织、声明客户端组件、服务端获取数据、异步服务端操作等方面,提升了不少的便捷性。

Next 14 升级

https://nextjs.org/docs/app/building-your-application/upgrading/version-14

升级依赖:

npm i next@latest react@latest react-dom@latest eslint-config-next@latest
npm i -D @types/react @types/react-dom 

打包命令改动:

  • 打包命令从 next build && next expor 中去掉 next export ,直接 next build 即可
  • 配置文件增加 output: 'export',

这样就可以了,跑起来以及打包都一切正常,不得不说这升级也太简单了。

app 目录

https://nextjs.org/docs

app 目录是新版本增加的路由模式,是可选的。文档默认使用 app router ,旧版的 pages router 仍然支持,在文档左上角可切换使用的路由模式。

动态参数

https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes

动态路由, 可通过路径中的 [id] 表示参数。

获取参数:

const IdPage = ({params}: {params: {id:string}}) => {
    return <div>ID page: {params.id}</div>
}

下划线开头目录

https://nextjs.org/docs/app/building-your-application/routing/colocation#project-organization-features

在 app 目录中, 以 _ 下划线开头的目录不会作为路由页面使用,可用于放组件。

括号创建不用于 URL 路径的路由组

https://nextjs.org/docs/app/building-your-application/routing/colocation#route-groups

可以通过将文件夹括在括号中来创建路由组: (folderName) ,这表示该文件夹用于组织目的,不应包含在路由的 URL 路径中。如:

  • (test)/page.tsx 页面,访问路径为 /
  • (test)/something/page.tsx 页面,访问路径为 /something

layout

https://nextjs.org/docs/app/api-reference/file-conventions/layout

layout.tsx 可用于编写布局文件,生效于所有子目录的文件。

const TestLayout = ({
    children
}: {children: React.ReactNode}) => {
    return <div className="bg-red-400 h-full">
    {children}
    </div>
}

export default TestLayout

配置 metadate

https://nextjs.org/docs/app/building-your-application/optimizing/metadata

可以直接 export 一个对象来设置,配置标题、描述、网站 icon,比以前方便不少。

export const metadata: Metadata = {
  title: {
    default: siteConfig.name,
    template: `%s | ${siteConfig.name}`
  },
  description: siteConfig.description,
  icons: [
    {
      url: "/logo.svg",
      href: "/logo.svg"
    }
  ]
};

sitemap

https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap

在 app 目录下创建 sitemap.ts 可动态创建 sitemap.xml 文件。如:

import { MetadataRoute } from 'next'
 
export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: 'https://heibaimeng.com',
      lastModified: new Date(),
      changeFrequency: 'weekly',
      priority: 1,
    },
    {
      url: 'https://heibaimeng.com/collections/1',
      lastModified: new Date(),
      changeFrequency: 'monthly',
      priority: 0.8,
    },
  ]
}

API route

https://nextjs.org/docs/app/building-your-application/routing/route-handlers

创建 route.ts 文件,可以导出 GET、POST、PATCH 等函数,表示对应的请求方法。

import { NextResponse } from "next/server";

export function GET() {
    return NextResponse.json({
        hello: 'trello'
    })
}

服务端组件

声明客户端组件

https://nextjs.org/docs/app/building-your-application/rendering/client-components

默认,app 中的页面都是服务端组件。如需使用客服端组件,可在头部声明:

"use client";

服务端渲染异步组件

https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating

服务端渲染的组件支持异步写法。这样就能很方便的进行服务端渲染数据初始化,减少了样板代码。

const ProtectedPage = async () => {
    const user = await currentUser()

    return (
        <div>{user?.firstName}</div>
    )
}

Server Actions

https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations

可以很方便地通过异步方法触发服务端操作,使用 "use server" 指令定义服务端操作,发起一个 POST 请求,完成服务端的访问。

组件默认都是服务端渲染,这个 "use server" 并不是定义服务端渲染,而是定义异步的服务端操作。

声明异步函数

可以将指令放在 async 函数的顶部以将该函数标记为服务器操作。

import { db } from "@/lib/db";

const OrganizationPage = () => {
  async function create(formData: FormData) {
    "use server"

    const title = formData.get('title') as string

    await db.board.create({
        data: {
            title
        }
    })
  }
  return (
    <div>
      <form action={create}>
        <input
          id="title"
          name="title"
          required
          className="border-black border p-1"
          placeholder="输入面板标题"
        />
      </form>
    </div>
  );
};

export default OrganizationPage;

声明异步操作文件

可提取到单独的文件中,也在文件的顶部放将该文件的所有导出标记为服务器操作(客户端渲染的组件只能用这种模式):

"use server";

import { db } from "@/lib/db";

export async function create(formData: FormData) {
  const title = formData.get("title") as string;

  await db.board.create({
    data: {
      title,
    },
  });
}