起因
凡事皆有因,有因才有果。一切还要从起因说起,2024年初,我开始做一个海外电商网站的二次开发工作,它的前端基于React技术栈,后端是基于PHP技术栈。这个网站主要是面对C端用户的,但是它的技术选型好像并不适合。下面我以产品列表页面来举例说明一下
当用户点击进入产品列表页面时,整体流程如下
- 浏览器通过前端路由导航到产品页面
- 等待DOM树渲染完毕后,发出XHR请求获取产品列表
- 使用js将产品列表渲染到DOM上
看起来好像没什么问题,但实际体验却有点糟糕
- 首先进入页面时渲染一次DOM,由于没有数据,因此页面是一片空白。
- 其次发送XHR请求,花费了一定的时间。
- 最后将XHR返回的数据再次渲染到DOM,这个过程也花费了一定的时间。
我们总结一下,一共渲染了两次DOM,发送一次XHR请求,并且这个初始时渲染DOM-发送XHR-再次渲染DOM过程无法并行。
graph
浏览器发送请求 --> 服务端返回内容 ---> 浏览器执行JS进行渲染
---> 发送XHR ---> 返回数据 ---> 再次渲染
那么如果说用户点击这个页面的时候,服务器直接返回已经渲染好数据的页面,浏览器只做一次渲染那体验会好很多。如果我们采用服务端服务端渲染技术,那么只需要在每次请求时生成一遍HTML页面,这个即是SSR(服务端渲染)技术。
graph
浏览器发送请求 --> 服务端渲染 ---> 返回内容 ---> 浏览器呈现
根据实际业务来看,由于产品并不是经常更新(几天或者几周才可以更新一个产品),所以最好的方案就是将这个页面静态化,然后设置下这个页面的有效期,过期则重新生成,这个即是SSG(服务端生成)技术。这样不仅节省了服务器生成HTML的时间,还可以充分利用CDN来缓存页面。
graph
浏览器发送请求 --> 服务端并返预先生成的内容 ---> 浏览器呈现
为了解决这些问题,我将目光投向了两个技术框架Next.js和Astro。我用两者分别写了一个demo之后,感觉Astro更倾向于SSG,编写交互性代码多有不便,而Next.js我做的网站交互性还是比较多的,两者性能方面所差无几,因为决定上Next.js这条船。
准备工作
在学习Next.js之前,我准备了供外网访问的预览环境,这样其他人也可以方便的查到我的网页。如果你需要的话,以下这些内容将会给你带来巨大的帮助
- 代码托管:github,这个不用多说,你也可以根据自己的爱好选择主流的代码托管平台
- 网站托管:netlify/vercel ,由于我很早就使用netlify了,所以这次还是选择的netlify
- 云数据库:cloud.mongodb.com 免费500M容量,自用足够了
- 域名:godaddy约10元1年,这个不是必须的,netlify的二级域名也不影响使用
- 云存储:我使用的腾讯云存储,这个也不是必须,具体看你的需求,按量收费,价格非常便宜
- 邮件服务:resend.com 免费版限量每天100封,每月3000封邮件
React vs Next.js
回想一下,当使用React开发的时候,你需要做什么?首先你需要选择一个打包工具:vite/webpack,其次安装React和React Router,接下来开始意大利面条,哦不,应该是苏格兰打卤面一样的路由配置文件,说真的我实在受够了,每个项目都要带上这个狗皮膏药一样的东西。当然你可以选择一个开发模板进行开发,但是寻找一个合适的模板也会花费时间。现在你明白了,React仅仅是一个渲染库而已,要完成一个基础的项目开发工作,往往需要配合其他工具。
和React不同,Next.js是一个框架,所需要的基本工具它已经帮你配置完成,你需要做的仅仅是安装Next.js,一切便已就绪。
Hello World
毫无疑问,学习一门新技术,最重要的先run起来。
前提条件:你已经安装了nodejs18.17版本以上
只需要运行以下命令
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
- 输入项目名称
- 选择是否使用TypeScript,请选择No,暂时不需要TypeScript
- 选择是否使用ESLint,请选择No,除了代码错误检查外,ESLint对代码格式还有一些要求,不要让格式错误阻断你的学习进度,当然如果你有使用ESLint的经验,可以选择Yes后自定义配置
- 选择是否使用Tailwind CSS,请选择Yes,即使你不了解Tailwind,选择Yes也不会影响你学习后续的内容
- 选择是否使用src文件夹,这个无关紧要,看你个人的爱好即可,我选择是No,这样文件夹更扁平一些
- 选择是否使用App Router模式,请选择Yes,App Router是新版本支持的模式,后续所内容也是基于App Router进行开发
- 是否自定义默认别名引用文件,请选择No,使用默认配置即可
选择完之后等待几秒创建成功后,进入项目文件夹执行安装依赖命令
npm install
之后执行启动命令即可
npm run dev
按下Ctrl + 鼠标左键点击http://localhost:3000即可在浏览器打开
OK,没有问题的话,现在项目就已经可以运行起来了
下面对代码进行简单的清理工作,进入app文件夹
- 打开globals.css,仅保留以下代码,其余的删掉
@tailwind base;
@tailwind components;
@tailwind utilities;
- 打开page.js,用以下代码替换掉
export default function Home() {
return (
<h1 className="text-xl font-semibold">
Hello World
</h1>
);
}
这个时候再看一眼http://localhost:3000,你会发现久违的Hello World又出来了
路由系统
在Next.js中,文件即路由。通过定义文件夹的名称和路径,便可以定义路由信息的基本信息。
首先要了解一下Next.js中的特殊文件 page,它是Next.js内定的用来专门展示页面的文件, 其后缀可以是.js,.jsx,.ts,.tsx,由于我们并没有使用TypeScript,因此使用.js或者.jsx后缀即可,我建议使用.jsx,这样可以享受到VS Code里插件的代码提示。
最简示例
来举一个最简单的例子,在app下面创建一个文件夹main,在main下面创建一个page.jsx文件写入
export default function Page() {
return (
<div>
Main
</div>
);
}
访问http://localhost:3000/main就可以看到写的内容了
嵌套路由
嵌套路由和嵌套文件夹形式是一模一样的,在main下面创建一个sub1 和sub2 文件夹,并在两者下面创建page.jsx文件写入
/main/sub1/page.jsx
export default function Page() {
return (
<div>
Sub1
</div>
);
}
/main/sub2/page.jsx
export default function Page() {
return (
<div>
Sub2
</div>
);
}
ok现在打开对应的地址就可以看到Sub1和Sub2,但是问题来了,main作为父页面路由,如何在/main/sub1和/main/sub2都显示父页面的某个内容呢
layout文件
和page文件一样,layout文件也是一个特殊类型文件,本级以及所有子级路由页面都将共享layout文件,来上代码,在main文件夹下面创建layout.jsx
export default function Layout({ children }) {
return (
<div>
<p>布局</p>
{children}
</div>
);
}
其中children代表路由对应的page文件,分别访问/main 、/main/sub1 、/main/sub2可以看到以下内容
需要注意的一点是,当访问完/main地址后,再访问/main/sub1和/main/sub2,layout只会在初始时加载一次,这个行为和react router里的父路由是一致的
ok,聪明的你可能会想到,在layout页面上添加一个导航栏切换sub1、sub2是一个不错的主意,那么开始吧
导航
在Next.js中有多种导航方式,我们来说最基本的一种,使用Link组件进行导航
打开main/layout.jsx文件进行修改,删掉其中的”布局“内容,替换为导航组件
import Link from "next/link";
export default function Layout({ children }) {
return (
<div>
<div>
<Link href="/main/sub1" className="text-blue-500 mr-2">sub1</Link>
<Link href="/main/sub2" className="text-blue-500">sub2</Link>
</div>
{children}
</div>
);
}
点击蓝色的导航,即可实现sub1和sub2的切换
路由参数
从路由地址从url上获取参数是非常简单的,只需要读取一个特殊变量searchParams即可
export default function Page({searchParams}) {
console.log(searchParams);
return (
<div>
sub1
</div>
);
}
访问一下 /main/sub1?name=张三,在控制台输出可以看到如下信息
GET /main/sub1?name=%E5%BC%A0%E4%B8%89 200 in 44ms
{ name: '张三' }
动态路由
截止到目前,我们定义的这几个路由 /main、/main/sub1、/main/sub2 都是静态的。但是有时候我们需要将user id作为路由段的内容:/user/1、/user/2、/user/3,然后展示user详情信息,这样不可能定义1 2 3等等全部用户id的静态路由,因此动态路由就派上用场了。
使用方括号包裹一个参数名称来创建文件夹,即可定义动态路由,我们在app下面创建user/[id]文件夹以及user/[id]/page.jsx
/user/[id]/page.jsx
export default function Page({params}) {
console.log(params);
return (
<div>
当前id是:{params.id}
</div>
);
}
除了这个应用场景之外,在Next.js中多语言切换也是使用的动态路由方案。
重定向
重定向的方法有很多,这里介绍最基本的一种,在user/[id]/page.jsx里试验一下
import { redirect } from "next/navigation";
export default function Page({params}) {
console.log(params);
redirect("/main");
return (
<div>
当前id是:{params.id}
</div>
);
}
API
Next.js使用特殊文件route来表示这是一个API,支持GET、POST、PUT、DELETE方法,我们写一个简单示例,创建/api/test/route.js文件
import { NextResponse } from "next/server";
export function GET(request) {
const searchParams = request.nextUrl.searchParams;
console.log(searchParams);
return NextResponse.json({
message: "Hello World"
})
}
其中request.nextUrl.searchParams是url上参数,在浏览器的控制台使用GET方法调用/api/test接口
fetch("/api/test?name=xiaoxiao")
可以看到返回信息是
同时可以在VS Code的控制台里输出了参数信息
服务端组件和客户端组件
在Next.js中代码默认在服务端执行,如果要想在客户端执行代码,需要在文件头部添加”use client“来说明。
服务端组件在服务端进行渲染(SSR)返回HTML结果,由于SSR可以有效减轻客户端的负担,因此我们推荐尽可能的使用服务端组件进行开发。
客户端组件的子组件也会被视为一个整体在客户端执行,因此,尽可能的将客户端组件放置于文件组织的末梢。
以下情况建议使用服务端组件
-
获取数据
Next.js在服务端实现了和浏览器一样的fetch方法,因此可以使用fetch获取远程数据
更改main/sub1/page.jsx
const getData = async () => { const res = await fetch('<http://localhost:3000/api/test>'); return await res.json(); }; export default async function Page() { const data = await getData(); return ( <div> {JSON.stringify(data)} </div> ); }
也可以直接使用nodejs获取任意数据,例如获取当前文件夹路径
const getData = async () => { const res = await fetch('<http://localhost:3000/api/test>'); return await res.json(); }; export default async function Page() { const data = await getData(); return ( <div> {JSON.stringify(data)} <br /> 服务端路径{process.cwd()} </div> ); }
-
权限认证,这个在后续的用户认证里写
-
重定向,在前面路由系统已经写了
必须要交互的时候,再使用客户端组件,例如编辑用户数据,可以使用服务端组件先获取到用户数据,然后将数据传递个子组件客户端,画个图比较直观
连接数据库
在nodejs环境下连接数据库还是很简单的,根据不同的数据库会使用对应的ORM进行操作。这里介绍一下如使用nodejs操作mongodb。
申请免费数据库
首先在cloud.mongodb.com申请一个免费的云数据库
-
点击创建Project
-
随便起一个名字,然后点击Next
-
继续下一步
-
这个时候会提示你创建集群,点击创建
-
选择M0,自定义集群名称(默认Cluster0),供应商不用变,选择地区之后创建即可
-
自定义账号密码,建议不用管,使用默认即可,然后点击创建用户
-
复制数据库连接地址,使用默认项,点击复制后先保存起来后面要用到
-
点击前往设置访问ip
-
设置ip
-
稍等一会设置完毕后就可以访问了,使用Mongdb Compass测试连接一切正常
-
创建一个数据库和users集合,然后添加一条数据
这部分工作告一段落,开始下一阶段的任务
使用ORM:mongoose
npm install mongoose
创建.env文件,写入数据库连接变量
MONGO_URI="刚才保存的连接地址"
创建文件夹lib,在下面创建connect-mongo.js
import mongoose from "mongoose";
const MONGO_URI = process.env.MONGO_URI;
const cached = {};
async function connectMongo() {
if (!MONGO_URI) {
throw new Error("Please define the MONGO_URI environment variable inside .env.local");
}
if (cached.connection) {
return cached.connection;
}
if (!cached.promise) {
const opts = {
bufferCommands: false,
};
cached.promise = mongoose.connect(MONGO_URI, opts);
}
try {
cached.connection = await cached.promise;
} catch (e) {
cached.promise = undefined;
throw e;
}
return cached.connection;
}
export default connectMongo;
创建models文件夹,在下面创建user.js,创建对应的model
import { model, models, Schema } from 'mongoose';
const UserSchema = new Schema(
{
name:String
}
);
const User = models.User || model('User', UserSchema, 'users');
export default User;
下面写一个查询用户示例,在app/user下面创建page.jsx页面,使用mongoose查询user
import User from '@/models/user';
import connectMongo from '@/lib/connect-mongo';
export default async function Page() {
await connectMongo();
let users = await User.find();
return (
<div>
{users.map(user => {
return <div key={user._id}>{user.name}</div>;
})}
</div>
);
}
打开对应的页面就可以看到
是不是非常简单
用户认证
在Next.js中,我们不需要手动实现认证功能,有非常多的第三方工具可供选择,我选择了最流行的NextAuth.js
配置用户认证功能要比连接数据难度稍微高一点,不过我已经替兄弟萌趟过雷了,放心
-
创建app/login/page.jsx
export default function LoginPage() { return ( <div> <form action=""> <label>邮箱</label> <input type="email" name="email" /> <br /> <label>密码</label> <input type="password" name="password" /> </form> </div> ); }
创建好页面留着备用
-
安装设置next-auth
先安装对应依赖
npm i next-auth@beta
然后生成一个key用于加密
openssl rand -base64 32
将key保存在.env文件中
AUTH_SECRET=刚才生成的key
-
添加登录地址选项
创建/auth-config.js,配置当未认证通过时要跳转的地址
export const authConfig = { pages: { signIn: '/login', }, };
-
还是在这个文件添加认证后的回调,根据认证结果和路由地址决定是否允许跳转
export const authConfig = { pages: { signIn: '/login', }, callbacks: { authorized({ auth, request: { nextUrl } }) { const isLoggedIn = !!auth?.user; // 是否登录 const isOnUser = nextUrl.pathname.startsWith('/user'); // 地址是否为user开头 // 如果访问的是user开头的地址,并且已登录,则允许继续跳转,否则重定向到上面配置的登录页面 if (isOnUser) { if (isLoggedIn) { return true; } else { return false; } } // 其他情况默认允许跳转 return true; }, }, providers: [] };
-
创建中间件middleware.js
import NextAuth from 'next-auth'; import { authConfig } from './auth.config'; export default NextAuth(authConfig).auth; export const config = { matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], };
除以下内容之外的所有内容:
- api
- _next/static
- _next/image
- 以 .png 结尾的任何内容 简单来说就是仅拦截浏览器上的路由地址
-
添加认证逻辑,创建auth.js
import NextAuth from "next-auth"; import Credentials from "next-auth/providers/credentials"; import { authConfig } from "./auth.config"; export const { signIn, signOut, auth, } = NextAuth({ ...authConfig, providers: [ Credentials({ credentials: { }, authorize: async (credentials) => { console.log(credentials); let user = null; if (credentials.email === "337634268@qq.com" && credentials.password === "password") { user = { id: 1, name: "波罗丁的菠萝", email: "337634268@qq.com", image: "<https://avatars.githubusercontent.com/u/10150404?v=4>", }; } return user; }, }), ], });
credentials是浏览器传过来的数据,判断邮箱和密码是否正确,如果正确则返回用户信息,否则返回null,表示认证失败
-
创建/lib/actions.js用于调用登录和登出功能
"use server"; import { signIn, signOut } from "@/auth"; import { AuthError } from "next-auth"; export async function authenticate( prevState, formData, ) { try { await signIn("credentials", formData); } catch (error) { if (error instanceof AuthError) { switch (error.type) { case "CallbackRouteError": return "认证失败"; default: return "服务器内部错误"; } } throw error; } } export async function logOut() { try { await signOut(); } catch (error) { if (error instanceof AuthError) { switch (error.type) { case "CallbackRouteError": return "认证失败"; default: return "服务器内部错误"; } } throw error; } }
-
更新登录页面代码 /app/login/page.jsx
"use client"; import { authenticate } from "@/lib/actions"; import { useState } from "react"; export default function LoginPage() { const [loginStatus, setLoginStatus] = useState({ success: false, error: null, loading: false }); const onSubmit = async (e) => { e.preventDefault(); const data = new FormData(e.target); setLoginStatus({ success: false, error: null, loading: true }) const result = await authenticate(null, data); if (!result) { setLoginStatus({ success: true, error: null, loading: false }); } else { setLoginStatus({ success: false, error: result, loading: false }); } }; return ( <div> <form onSubmit={onSubmit}> <label>邮箱</label> <input type="email" name="email" /> <br /> <label>密码</label> <input type="password" name="password" /> <br /> {loginStatus.loading && <div>正在提交</div>} { loginStatus.error && <p className="text-sm text-red-500">{loginStatus.error}</p> } <button type="submit" className="border bg-blue-400">提交</button> </form> </div > ); }
-
配置完成后打开/user页面,由于现在还没有登录,会跳转到login页面。输入邮箱密码认证失败会提示失败信息
-
登录成功后重定向到原来的页面
在/app/login/page.jsx添加以下代码,实现重定向
//...省略... import { useState, useEffect } from "react"; //...省略... useEffect(() => { if (loginStatus.success) { location.href = new URLSearchParams(location.search).get("callbackUrl"); } }, [loginStatus]);
-
登出
创建/app/logout/page.jsx,调用actions内的logout即可
"use client"; import { logOut } from "@/lib/actions"; import { useEffect, useState } from "react"; export default function Page() { const [logoutStatus, setLogoutStatus] = useState({ success: false, error: null, loading: false, }); const handleSubmit = async (event) => { event.preventDefault(); setLogoutStatus({ success: false, error: null, loading: true }); const result = await logOut(); if (!result) { setLogoutStatus({ success: true, error: null, loading: false }); } else { setLogoutStatus({ success: false, error: result, loading: false }); } }; useEffect(() => { if (logoutStatus.success) { location.href = "/main"; //改为你自己的地址即可 } }, [logoutStatus]); return ( <form onSubmit={handleSubmit}> <button type="submit" className="bg-blue-400"> 登出 </button> </form> ); }
到现在为止,整个用户认证结束了
多语言
在Next.js中使用动态路由可以方便的实现多语言切换。
简单的来说思路如下:首先在app目录下面创建一个动态路由文件夹[lng],将原来所有的路由文件夹都放进去。然后判断动态路由的参数以获取不同语言的配置信息进行显示。其中显示不同配置的方法,在服务端和客户端分别是不同的实现,但是调用起来是一样的。
-
安装依赖
npm install i18next react-i18next i18next-resources-to-backend i18next-browser-languagedetector react-cookie accept-language
-
创建[lng]文件夹,将所有路由文件夹放进去,再创建一个second路由仅用于演示多语言切换
修改代码/app/[lng]/page.js
import Link from 'next/link'; export default function Page({ params: { lng } }) { return ( <> <h1>Hi there!</h1> <Link href={`/${lng}/second`}> second page </Link> </> ) }
修改代码app/[lng]/layout.js
import { Inter } from "next/font/google"; import "@/app/globals.css"; import { dir } from 'i18next'; const languages = ['en', 'de']; export async function generateStaticParams() { return languages.map((lng) => ({ lng })); } const inter = Inter({ subsets: ["latin"] }); export const metadata = { title: "Create Next App", description: "Generated by create next app", }; export default function RootLayout({ children, params: { lng } }) { return ( <html lang={lng} dir={dir(lng)}> <body className={inter.className}>{children}</body> </html> ); }
修改代码app/[lng]/second/page.js
import Link from 'next/link'; export default function Page({ params: { lng } }) { return ( <> <h1>Hi from second page!</h1> <Link href={`/${lng}`}> back </Link> </> ) }
-
创建i18n文件夹,在下面创建配置文件settings.js、用于服务端翻译的index.js、用于客户端翻译的client.js,zhhk:繁体中文,zhcn:简体中文,en:英文,默认配置使用繁体。再创建每个语言对应的配置内容,文件结构如下
setting.js
export const fallbackLng = 'zhhk' export const languages = [fallbackLng, 'zhcn', 'en'] export const defaultNS = 'translation' export const cookieName = 'i18next' export function getOptions(lng = fallbackLng, ns = defaultNS) { return { // debug: true, supportedLngs: languages, fallbackLng, lng, fallbackNS: defaultNS, defaultNS, ns } }
index.js
import { createInstance } from 'i18next' import resourcesToBackend from 'i18next-resources-to-backend' import { initReactI18next } from 'react-i18next/initReactI18next' import { getOptions } from './settings' const initI18next = async (lng, ns) => { const i18nInstance = createInstance() await i18nInstance .use(initReactI18next) .use(resourcesToBackend((language, namespace) => import(`./locales/${language}/${namespace}.json`))) .init(getOptions(lng, ns)) return i18nInstance } export async function useTranslation(lng, ns, options = {}) { const i18nextInstance = await initI18next(lng, ns) return { t: i18nextInstance.getFixedT(lng, Array.isArray(ns) ? ns[0] : ns, options.keyPrefix), i18n: i18nextInstance } }
client.js
'use client' import { useEffect, useState } from 'react' import i18next from 'i18next' import { initReactI18next, useTranslation as useTranslationOrg } from 'react-i18next' import { useCookies } from 'react-cookie' import resourcesToBackend from 'i18next-resources-to-backend' import LanguageDetector from 'i18next-browser-languagedetector' import { getOptions, languages, cookieName } from './settings' const runsOnServerSide = typeof window === 'undefined' // i18next .use(initReactI18next) .use(LanguageDetector) .use(resourcesToBackend((language, namespace) => import(`./locales/${language}/${namespace}.json`))) .init({ ...getOptions(), lng: undefined, // let detect the language on client side detection: { order: ['path', 'htmlTag', 'cookie', 'navigator'], }, preload: runsOnServerSide ? languages : [] }) export function useTranslation(lng, ns, options) { const [cookies, setCookie] = useCookies([cookieName]) const ret = useTranslationOrg(ns, options) const { i18n } = ret if (runsOnServerSide && lng && i18n.resolvedLanguage !== lng) { i18n.changeLanguage(lng) } else { // eslint-disable-next-line react-hooks/rules-of-hooks const [activeLng, setActiveLng] = useState(i18n.resolvedLanguage) // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { if (activeLng === i18n.resolvedLanguage) return setActiveLng(i18n.resolvedLanguage) }, [activeLng, i18n.resolvedLanguage]) // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { if (!lng || i18n.resolvedLanguage === lng) return i18n.changeLanguage(lng) }, [lng, i18n]) // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { console.log('lng', lng); if (!lng) { return; } if (cookies.i18next === lng) return setCookie(cookieName, lng, { path: '/' }) }, [lng, cookies.i18next]) } return ret }
en/translation.json
{ "title": "title" }
zhhk/translation.json
{ "title": "標題" }
zhcn/translation.json
{ "title": "标题" }
-
配置middleware.js,修改为如下代码
import NextAuth from 'next-auth'; import { NextResponse } from 'next/server'; import { authConfig } from './auth.config'; import acceptLanguage from 'accept-language'; import { fallbackLng, languages, cookieName } from './app/i18n/settings'; export default NextAuth(authConfig).auth; acceptLanguage.languages(languages) export const config = { // <https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher> // matcher: [''], matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], }; export function middleware(req) { let lng if (req.cookies.has(cookieName)) lng = acceptLanguage.get(req.cookies.get(cookieName).value) if (!lng) lng = acceptLanguage.get(req.headers.get('Accept-Language')) if (!lng) lng = fallbackLng // Redirect if lng in path is not supported if ( !languages.some(loc => req.nextUrl.pathname.startsWith(`/${loc}`)) && !req.nextUrl.pathname.startsWith('/_next') ) { return NextResponse.redirect(new URL(`/${lng}${req.nextUrl.pathname}`, req.url)) } if (req.headers.has('referer')) { const refererUrl = new URL(req.headers.get('referer')) const lngInReferer = languages.find((l) => refererUrl.pathname.startsWith(`/${l}`)) const response = NextResponse.next() if (lngInReferer) response.cookies.set(cookieName, lngInReferer) return response } return NextResponse.next() }
-
调用服务端翻译方法,修改app/[lng]/page.jsx
import Link from 'next/link'; import { useTranslation } from '@/app/i18n'; export default async function Page({ params }) { const { t } = await useTranslation(params.lng); return ( <> <h1>Hi there!-----------{t('title')}</h1> <Link href={`/${params.lng}/second`}> second page </Link> </> ) }
可以看到下面两个不同的页面出现的内容也不一样
-
调用客户端翻译方法,修改app/[lng]/second/page.jsx
'use client' import Link from 'next/link'; import { useTranslation } from "@/app/i18n/client"; export default function Page({ params }) { const { t } = useTranslation(params.lng); return ( <> <h1>Hi from second page!----{t('title')}</h1> <Link href={`/${params.lng}`}> back </Link> </> ) }
-
语言切换
我一向认为简单粗暴是硬道理,所以只需在layout添加对应的a链接切换到不同的主页即可
<a href="/zhhk">zhhk</a> <a href="/zhcn">zhcn</a> <a href="/en">en</a>