Next.js 14 开发环境搭建指南:从安装到项目结构解析

什么是 Next.js

Next.js 是一个用于构建全栈 Web 应用程序的 React 框架。您可以使用 React Components 来构建用户界面,并使用 Next.js 来实现附加功能和优化。

安装项目

请确保你的操作系统上安装了 Node.js 18.17 或更高版本。

我们采用官网上的自动安装,这样会方便很多

1
2
3
4
5
6
7
8
9
10
npx create-next-app@latest
# 你会看到如下提示
# What is your project named? my-app
# Would you like to use TypeScript? No / Yes
# Would you like to use ESLint? No / Yes
# Would you like to use Tailwind CSS? No / Yes
# Would you like to use `src/` directory? No / Yes
# Would you like to use App Router? (recommended) No / Yes
# Would you like to customize the default import alias (@/*)? No / Yes
# What import alias would you like configured? @/*

这样就得到了一个 Next.js 的项目

安装其他的插件

1
2
pnpm add zod // 数据校验工具,通常在表单提交的时候使用
pnpm add bcrypt // 加密工具,通常是用户密码的校验

项目简介

这里提醒一下 next.js 是默认在服务端渲染的框架,对于需要在浏览器中渲染的内容,当然这也就说明你需要浏览器的 api 或者对应的 react hooks。那么你需要在文件的最顶部添加 use client 。记住 use server 使用在函数里面的,而不是 tsx 的样式组件中,表明该函数在服务端使用。

app

由于我们采用的 APP 路由模式,在 Next.js 中文件既是路由。也就是说这是约定熟成的规则。

1
2
app\\layout.tsx # 这里是布局页面内容
app\\page.tsx # 这里的内容就是 localhost:3000/ 网页展示的内容

除了这 2 个必要的 tsx 文件,还有其他几个常用的文件,剩余的可以查询官方文档

layout .js .jsx .tsx Layout 这里是布局页面内容
page .js .jsx .tsx Page 对应路由页面展示的内容
loading .js .jsx .tsx Loading UI 页面加载中的样式页面
not-found .js .jsx .tsx Not found UI 404 页面内容
error .js .jsx .tsx Error UI 当页面上出现了错误的时候可以在此页面中处理
global-error .js .jsx .tsx Global error UI
route .js .ts API endpoint 你可以在这里编写接口用来处理 get post 这样的接口内容
template .js .jsx .tsx Re-rendered layout
default .js .jsx .tsx Parallel route fallback page

这里提供我遇到的文件写法案例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// layout.tsx
import TitleHeader from "@/app/ui/mainLayout/title-header";
import SideNav from "@/app/ui/mainLayout/side-nav";
import RightContainer from "@/app/ui/mainLayout/right-container";

export default function Layout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<main className="flex h-screen">
<SideNav></SideNav>
<RightContainer>
<TitleHeader></TitleHeader>
<div className="w-full px-8">{children}</div>
</RightContainer>
</main>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// page.tsx

// localhost:3000/front/2?name=david 以这个例子来说 searchParams拿到的就是{name:david}
// params 拿到的就是2 当然你的page.tsx必须是front/[id]/page.tsx 这样的文件目录下
export default function Page({
searchParams,
params,
}: {
searchParams?: {
query?: string;
page?: string;
};
params?: {
id: number;
};
}) {
const query = searchParams?.query || "";
const currentPage = Number(searchParams?.page || 1);

return <> {query} </>;
}
1
2
3
4
5
6
7
8
9
10
11
12
// loading.tsx
// 加载的时候显示的页面
// 你可以使用如下语句在page.tsx中测试
//await new Promise((resolve) => {
// setTimeout(() => {
// resolve(true);
// }, 3000);
//});

export default function Loading() {
return <>加载中...</>;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// error.tsx

"use client";
import ConversationFallback from "@/components/shared/conversation/ConversationFallback";
import { useRouter } from "next/navigation";
import { useEffect } from "react";

export default function Error({ error }: { error: Error }) {
const router = useRouter();

useEffect(() => {
router.push("/conversations");
}, [error, router]);

return <ConversationFallback />;
}
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
45
46
47
48
49
50
51
52
53
54
55
// app/dashboard/api.tsx

export async function POST() {
try {
// 使用 Response 对象返回
return new Response(JSON.stringify({ code: 200, msg: "传输成功" }), {
headers: {
"Content-Type": "application/json",
},
});
} catch (error) {
// 处理可能发生的错误
return new Response(JSON.stringify({ code: 500, msg: "服务器内部错误" }), {
status: 500,
headers: {
"Content-Type": "application/json",
},
});
}
}

// 之后可以通过 localhost:3000/dashboard/api POST请求来获取内容
const response = await fetch("/dashboard/api", { method: "POST" });

// 或者GET请求
import { type NextRequest } from "next/server";
import { prisma } from "@/app/lib/prisma";

export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const coserId = searchParams.get("coserId");
const query = searchParams.get("query");
const limit = parseInt(searchParams.get("limit") || "10", 10);
const offset = parseInt(searchParams.get("offset") || "0", 10);

let results = [];

return new Response(
JSON.stringify({
results: results,
ok: true,
}),
{
headers: { "Content-Type": "application/json" },
}
);
}

// 可以在页面中这样请求接口
let res = await fetch(
`/dashboard/cosers/filter?query=${filterText}&offset=0&limit=20&coserId=${coserId}`,
{
signal,
}
);
1
2
3
4
5
// not-found.tsx

export default function NotFound() {
return <main>404咯</main>;
}

public

公共资源文件夹,这儿通常存放图片或一些公共文件。例如public\\next.svg 在组件上使用的地址就是/next.svg

next.config.mjs

next.js 的配置文件,这里的具体情况可以查看官网文档。我这里提供我用的一个案例,就是重定向。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** @type {import('next').NextConfig} */
const nextConfig = {
async redirects() {
return [
{
source: "/",
destination: "/conversations",
permanent: true
},
]
}
};

export default nextConfig;

数据库连接(可选)

说明以及安装

在连接数据库之前,我们需要了解数据库和 orm 之间的关系,orm 是一种数据关系映射的工具,它用作编写纯 SQL 或使用其他数据库访问工具。这样的话,我们不用关心多种数据库的连接,改变数据库版本之后要修改代码等等问题。

1
pnpm install prisma --save-dev
1
2
npx prisma // 获取命令
npx prisma init // 初始化prisma

初始化之后会得到 prisma 的文件夹(如果你初始化失败,八成是你的网络连不上国外的网络)。

新建.env 文件,这个是本地的环境变量设置

1
2
3
4
DATABASE_URL="file:./dev.db" // SQLite 数据库是简单的文件,所以你建立一个dev.db的文件就好了
DATABASE_URL="postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=public" // postgresql的数据库连接
DATABASE_URL="mysql://USER:PASSWORD@HOST:PORT/DATABASE" // mysql的数据库连接
// 上面的3个选择你自己的数据库就可以了

prisma 配置

上面新建的环境变量文件,那么就需要 prisma 来连接数据库了。

打开prisma\\schema.prisma 文件

1
2
3
4
5
6
7
8
9
10
generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "postgresql" // 如果你是MySQL/SQLite 对应的字段为"mysql"、"sqlite"
url = env("DATABASE_URL")
}
// 上面的就是postgresql的连接
// 如果你使用了第三方的postgresql数据库,那么配置上的修改安装他们的提示

创建数据库

添加如下的模型到prisma\\schema.prisma 文件中

1
2
3
4
5
6
// ... 其他的配置
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
}

之后做如下操作,同步本地的表结构到数据库中。

1
npx prisma migrate dev --name init

当然你可能会修改数据库的表结构什么,为了方便可以在package.json把启动命令配置一下。更新表结构至线上数据库中使用npx prisma db push 如果是一个已经有数据的项目,就不能使用这个命令了,转而使用 prisma migrate 迁移。本文先不涉及。

1
2
3
4
5
6
7
8
9
10
11
{
.....
"scripts": {
"dev": "npm run prisma:generate && next dev",
"build": "npm run prisma:generate && next build",
"start": "next start",
"lint": "next lint",
"prisma:generate": "prisma generate",
},
.......
}

Next.js 客户端上使用

安装客户端

1
pnpm add @prisma/client

然后在 lib/prisma.ts 中写入

1
2
import { PrismaClient } from "@prisma/client";
export const prisma = new PrismaClient();

之后你可以在任意地方引入然后编写数据库操作语句。例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { prisma } from "@/app/lib/prisma";
export async function fetchCosplayPagesByCoserId({
coserId,
itemsPrePage = ITEMS_PER_PAGE,
}: {
coserId: number | string;
itemsPrePage?: number;
}) {
try {
const count = await prisma.posts.count({
where: {
coser_id: Number(coserId),
status: {
not: 2,
},
},
});
const totalPages = Math.ceil(count / itemsPrePage);
return totalPages;
} catch (error) {
console.error("数据库错误", error);
throw new Error(`获取指定Coser的作品数量错误${error}`);
}
}

你也可以使用npx prisma --help查看更多的语法

Next-auth(可选)

这是 Next.js 出品的用户验证插件,主要是完成像用户登录、会话管理、校验流程。具体查看官网

1
pnpm add next-auth

首选正常我们在页面中登录。

1
2
3
4
5
6
7
8
9
10
11
import { authenticate } from "@/app/lib/actions";

export default function Page() {
return (
<form action={authenticate}>
<input type="email" name="email" placeholder="Email" required />
<input type="password" name="password" placeholder="Password" required />
<button type="submit">Login</button>
</form>
);
}

我们在app/lib/actions.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
"use server";

import { signIn } from "@/auth";
import { authorror } from "next-auth";

export async function authenticate(
prevState: string | undefined,
formData: FormData
) {
try {
await signIn("credentials", formData);
} catch (error) {
if (error instanceof authorror) {
switch (error.type) {
case "CredentialsSignin":
return "Invalid credentials.";
default:
return "Something went wrong.";
}
}
throw error;
}
}

/auth.ts中写入。这里主要处理的就是账号密码验证

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
45
46
47
import NextAuth from "next-auth";
import { authConfig } from "./auth.config";
import Credentials from "next-auth/providers/credentials";
import { z } from "zod";
import bcrypt from "bcrypt";
import { prisma } from "./app/lib/prisma";

async function getUser(email: string) {
try {
const user = await prisma.users.findUnique({
where: {
email: email,
},
});
return user;
} catch (error) {
console.error("Failed to fetch user:", error);
throw new Error("Failed to fetch user.");
}
}

export const { auth, signIn, signOut } = NextAuth({
...authConfig,
providers: [
Credentials({
async authorize(credentials) {
const parsedCredentials = z
.object({ email: z.string().email(), password: z.string().min(6) })
.safeParse(credentials);

if (parsedCredentials.success) {
const { email, password } = parsedCredentials.data;
const user = await getUser(email);
if (!user) return null;
const passwordsMatch = await bcrypt.compare(password, user.password);
if (passwordsMatch) {
// 在这里转换你的 User 对象到 NextAuth 预期的格式
const nextAuthUser = { ...user, id: user.id.toString() }; // 将 id 转换为字符串
return nextAuthUser;
}
}
console.log("Invalid credentials");
return null;
},
}),
],
});

配置需要登录才能访问的路由。在auth.config.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import type { NextAuthConfig } from "next-auth";

export const authConfig = {
pages: {
signIn: "/login",
},
callbacks: {
async redirect({ url, baseUrl }) {
// 如果用户刚刚登录成功,则重定向到 /dashboard
return `${baseUrl}/dashboard`;
},
async authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isOnDashboard = nextUrl.pathname.startsWith("/dashboard");
if (!isLoggedIn && isOnDashboard) {
return false;
}
return true;
},
},
providers: [], // Add providers with an empty array for now
} satisfies NextAuthConfig;

然后用中间件保护路由 /middleware.ts

1
2
3
4
5
6
7
8
9
import NextAuth from "next-auth";
import { authConfig } from "./auth.config";

export default NextAuth(authConfig).auth;

export const config = {
// <https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher>
matcher: ["/((?!api|_next/static|_next/image|.*\\\\.png$).*)"],
};

这样就完成了用户的登录验证问题。

Fetch(可选)

这个主要是为了将 Next.js 纯作为一个 SSR 项目使用,只做前端页面开发。

值得注意的是,Next.js 毕竟是混合开发,也就是混合客户端和服务端的开发。所以你的接口请求虽然说是请求第三方,但是还是得考虑这个请求是发生在服务端还是客户端上。

对 fetch 做一个封装,让它能够自动对头部做参数添加。以及对于 401 的错误处理。

由于要考虑服务端所以,服务端上的 token 处理就采用 cookie 的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* @Author: HideInMatrix
* @Date: 2024-07-16
* @LastEditors: HideInMatrix
* @LastEditTime: 2024-07-18
* @Description: 这是一则说明
* @FilePath: /next.js-template/lib/setCookie.ts
*/
"use server";

import { cookies } from "next/headers";

export async function setCookies(name: string, data: any) {
cookies().set(name, data);
}

export async function getCookies(name: string) {
return cookies().get(name)?.value;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* @Author: HideInMatrix
* @Date: 2024-07-16
* @LastEditors: HideInMatrix
* @LastEditTime: 2024-07-18
* @Description: 这是一则说明
* @FilePath: /next.js-template/lib/utils.ts
*/

/**
* 判断是否客户端
* @returns {boolean}
*/
export const isBrowser = typeof window !== "undefined";
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
/*
* @Author: HideInMatrix
* @Date: 2024-07-15
* @LastEditors: HideInMatrix
* @LastEditTime: 2024-07-17
* @Description: 请求封装
* @FilePath: /next.js-template/lib/customFetch.ts
*/

import { redirect } from "next/navigation";
import { isBrowser } from "@/lib/utils";
import { getCookies } from "./setCookie";

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
const backPreUrl =
process.env.NODE_ENV === "development"
? "<http://localhost:3000>"
: process.env.NEXT_PUBLIC_BACK_PRE_URL;
const urlPreTag =
process.env.NODE_ENV === "development"
? process.env.NEXT_PUBLIC_BACK_PRE_TAG
: "";

interface FetchOptions extends RequestInit {
headers?: Record<string, string>;
}

interface ApiResponse<T = any> {
data?: T;
error?: string;
status?: number;
}

const apiClient = <T,>(method: HttpMethod) => {
return async (
url: string,
data?: any,
options: FetchOptions = {}
): Promise<ApiResponse<T>> => {
const controller = new AbortController();
const { signal } = controller;
let token = "";
let defaultLocale = "";
token = (await getCookies("NEXT_TOKEN")) || "";
defaultLocale = (await getCookies("NEXT_LOCAL")) || "";

const config: FetchOptions = {
method,
signal,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
...options.headers,
},
...options,
};

if (method !== "GET" && data) {
config.body = JSON.stringify(data);
}

const response = await fetch(`${backPreUrl}${urlPreTag}${url}`, config);

if (response.status === 401) {
// 处理 401 状态码
if (isBrowser) {
location.href = `/login`;
} else {
redirect(`/login`);
}
}

const result = await response.json();

if (!response.ok) {
return {
error: result.message || "Request failed",
status: response.status,
};
}

return result;
};
};

export const getRequest = apiClient("GET");
export const postRequest = apiClient("POST");
export const putRequest = apiClient("PUT");
export const deleteRequest = apiClient("DELETE");

使用案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"use client";
import { getRequest } from "@/lib/customFetch";

import { useCallback, useEffect, useState } from "react";
export default function UserName() {
// 客户端请求方式
const [data, setData] = useState<{
data?: any;
error?: string;
status?: number;
}>();
const loaderProfile = useCallback(async () => {
const result = await getRequest(`/auth/profile`);
if (!result.error) {
setData(result);
}
}, []);

useEffect(() => {
loaderProfile();
}, [loaderProfile]);

return <>userName组件 {data?.data.name}</>;
}
1
2
3
4
5
6
7
8
9
10
11
12
import { getRequest } from "@/lib/customFetch";
export default async function UserName() {
// 服务端请求方式
const result = await getRequest(`/auth/profile`);
if (!result.error) {
console.log(result);
}

const data = result.data as { name: string };

return <>userName组件 {data?.name || "为获取到数据"}</>;
}

设置开发代理

next.config.mjs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** @type {import('next').NextConfig} */

const isProd = ["production"].includes(process.env.NODE_ENV);
// 转发
const rewrites = () => {
if (!isProd) {
return [
{
source: "/api/:slug*",
destination: "<http://localhost:7000/api/:slug*>",
},
];
} else {
return [];
}
};

const nextConfig = {
rewrites: rewrites,
};

export default nextConfig;

添加状态管理插件

本项目状态管理没有选择传统的 redux 而是选择了比较轻巧的 zsutand该状态管理对于一般的项目已经足够用了

1
pnpm install zustand

然后在store/user.ts 中写入如下内容

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
import { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
import { defaultLocale, locales } from '@/static/locales';

// Custom types for theme
interface SettingState {
defaultLocale: string;
locales: string[];
setDefaultLocale: (newVal: string) => void;
}

const useSettingStore = create<SettingState>()(
persist(
(set, get) => ({
defaultLocale: get()?.defaultLocale ? get()?.defaultLocale : defaultLocale,
locales: locales,
setDefaultLocale: (newVal) => set((state: any) => ({
defaultLocale: state.defaultLocale = newVal,
})),
}),
{
name: 'setting',
storage: createJSONStorage(() => sessionStorage), // default localstorage },
),
);

export default useSettingStore;

使用案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import useSettingStore from "@/store/useSettingStore";
export default function CHangeLanguage() {
const options = [
{ label: "EN", value: "en" },
{ label: "中", value: "zh" },
];
const setDefaultLocale = useSettingStore((state) => state.setDefaultLocale); //设置函数
const defaultLocale = useSettingStore((state) => state.defaultLocale); // 读取内存的值
const [value, setValue] = useState(defaultLocale);

const handleChange = ({ target: { value } }: RadioChangeEvent) => {
setValue(value);
setDefaultLocale(value);
};
return (
<>
<Group
options={options}
onChange={onLanguageChange}
value={value}
key={value}></Group>
</>
);
}

国际化

  • next-i18next​: 一款流行的 Next.js 国际化插件,它提供了丰富的功能,包括多语言路由、服务器端渲染和静态生成的支持,以及简单的翻译文件管理。
  • next-intl​: 用于 Next.js 的国际化插件,它提供了基于 React Intl 的国际化解决方案,支持多语言文本和格式化。
  • next-translate​: 这个插件为 Next.js 提供了简单的国际化解决方案,支持静态生成和服务器端渲染,并且易于配置和使用。

使用 next-intl 插件,主要是网上看了对比的文章,这款插件从扩展和使用灵活性上都非常不错。

1
pnpm install next-intl

官方文档中提供了 2 种方式,一种基于路由地址,一种是不基于路由地址。我这里就选择路由地址。

同时这里我改用自定义的路径,以便项目更加好管理。并这里使用静态渲染,因为我喜欢在文件中使用异步函数。非静态渲染不能使用异步函数也就是它不在服务端渲染的。

  1. i18n/messages/en.json 中写入下面的内容

    1
    2
    3
    4
    5
    {
    "HomePage": {
    "title": "Hello world"
    }
    }

    同理在i18n\\messages\\zh.json 写入

    1
    2
    3
    4
    5
    {
    "HomePage": {
    "title": "你好"
    }
    }
  2. 现在,设置插件,该插件创建别名以向服务器组件提供 i18n 配置(在下一步中指定)。
    next.config.mjs

    1
    2
    3
    4
    5
    6
    7
    8
    import createNextIntlPlugin from "next-intl/plugin";

    const withNextIntl = createNextIntlPlugin("./i18n/i18n.ts");

    /** @type {import('next').NextConfig} */
    const nextConfig = {};

    export default withNextIntl(nextConfig);

    next.config.js

    1
    2
    3
    4
    5
    6
    7
    8
    const createNextIntlPlugin = require("next-intl/plugin");

    const withNextIntl = createNextIntlPlugin("./i18n/i18n.ts");

    /** @type {import('next').NextConfig} */
    const nextConfig = {};

    module.exports = withNextIntl(nextConfig);
  3. i18n\\i18n.ts 中使用next-intl 创建一个请求范围的配置对象,可用于根据用户的区域设置提供消息和其他选项,以便在服务器组件中使用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import { notFound } from "next/navigation";
    import { getRequestConfig } from "next-intl/server";

    // Can be imported from a shared config
    const locales = ["en", "zh"];

    export default getRequestConfig(async ({ locale }) => {
    // Validate that the incoming `locale` parameter is valid
    if (!locales.includes(locale as any)) notFound();

    return {
    messages: (await import(`./messages/${locale}.json`)).default,
    };
    });
  4. middleware.ts
    中间件匹配请求的区域设置并相应地处理重定向和重写。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import createMiddleware from "next-intl/middleware";

    export default createMiddleware({
    // A list of all locales that are supported
    locales: ["en", "zh"],

    // Used when no locale matches
    defaultLocale: "zh",
    });

    export const config = {
    // Match only internationalized pathnames
    matcher: ["/", "/(zh|en)/:path*"],
    };
  5. app\\[locale]\\layout.tsx
    这里需要调整一下自定义生成的文件,将 app 下面的约定文件例如 layout.tsx 移入到[locale]文件夹下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import { NextIntlClientProvider } from "next-intl";
    import { getMessages } from "next-intl/server";

    export default async function LocaleLayout({
    children,
    params: { locale },
    }: {
    children: React.ReactNode;
    params: { locale: string };
    }) {
    // Providing all messages to the client
    // side is the easiest way to get started
    const messages = await getMessages();

    return (
    <html lang={locale}>
    <body>
    <NextIntlClientProvider messages={messages}>
    {children}
    </NextIntlClientProvider>
    </body>
    </html>
    );
    }
  6. app/[locale]/page.tsx
    在页面组件或其他任何地方使用翻译!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import { useTranslations } from "next-intl"; //  非异步组件
    // 如果你想用异步组件也就是在下面的函数中使用async await
    // import {getTranslations} from 'next-intl/server';

    export default function HomePage() {
    const t = useTranslations("HomePage");
    // const t = await getTranslations('ProfilePage');
    return <div>{t("title")}</div>;
    }

到此国际化,axios 等设置就基本完成了,下面我使用一个 next.js 的 UI 框架继续完善这个项目