在 “
NodeJS系列(8)- Next.js 框架 (一) | 安装配置、路由(Routing)、页面布局(Layout)
” 里,我们简单介绍了 Next.js 的安装配置,创建了 nextjs-demo 项目,讲解和演示了 Next.js 项目的运行、路由(Routing)、页面布局(Layout)等内容。
本文继续在 nextjs-demo 项目(Pages Router)基础上,讲解和演示国际化 (i18n)、中间件 (Middleware) 等内容。
NextJS: https://nextjs.org/
NextJS GitHub: https://github.com/vercel/next.js
1. 系统环境
操作系统:CentOS 7.9 (x64)
NodeJS: 16.20.0
NPM: 8.19.4
NVM: 0.39.2
NextJS: 13.4.12
2. 国际化 (i18n)
Next.js 自 v10.0.0 起就内置了对国际化(i18n)路由的支持。可以提供区域设置、默认区域设置和域特定区域设置的列表,Next.js 将自动处理路由。
Next.js 内置的国际化(i18n)路由支持主流的 i18n 库,如 react-intl、react-i18next、lingui、rosetta、next-intl、next-translate、next-multilanguage、typesafe-i18n、tolgee 等。
国际化(i18n)部分区域语言码对照表:
ar 阿拉伯语
fa 波斯语
tr 土耳其语
en 英语
fr 法语
de 德语
ru 俄国语
es 西班牙语
pt 葡萄牙语
it 意大利语
nl 荷兰语
el 希腊语
zh-CN 中文简体
ja 日本语
ko 韩国语
id 印尼语
ms 马来语
th 泰国语
vi 越南语
...
1) 本地化策略
有两种区域设置处理策略:子路径路由和域路由。
(1) 子路径路由
就是将区域设置放在 url 路径中,在 next.config.js 里的配置如下:
module.exports = {
i18n: {
locales: ['en', 'zh-CN'],
defaultLocale: 'en',
上述配置,locales 是区域语言列表,en 是默认的区域设置。比如 src/pages/test.js,可以使用以下网址:
/test
/zh-CN/test
默认区域设置没有前缀。
(2) 域路由
通过使用域路由,可以配置从不同域提供服务的区域设置,在 next.config.js 里的配置如下:
module.exports = {
i18n: {
locales: ['en', 'zh-CN'],
defaultLocale: 'en',
domains: [
domain: 'example.com',
defaultLocale: 'en',
domain: 'example.cn',
defaultLocale: 'zh-CN',
注:子域必须包含在要匹配的域值中,例如 www.example.com 使用域 example.com 。
2) React-intl 库
React-intl 是雅虎的语言国际化开源项目 FormatJS 的一部分,通过其提供的组件和 API 可以与 React 绑定。
React-intl 提供了两种使用方法,一种是引用 React 组建,另一种是直接调取 API,官方更加推荐在 React 项目中使用前者, 只有在无法使用 React 组件的地方,才应该调用框架提供的 API。
React-intl 提供的 React 组件有如下几种:
<IntlProvider /> 包裹在需要语言国际化的组建的最外层,为包含在其中的所有组建提供包含 id 和字符串的键值对。(如: "homepage.title": "Home Page"; )
<FormattedDate /> 用于格式化日期,能够将一个时间戳格式化成不同语言中的日期格式。
<FormattedTime> 用于格式化时间,效果与 <FormattedDate /> 相似。
<FormattedRelative /> 通过这个组件可以显示传入组件的某个时间戳和当前时间的关系,比如 "10 minutes ago" 。
<FormattedNumber /> 这个组件最主要的用途是用来给一串数字标逗号,比如 10000 这个数字,在中文的语言环境中应该是1,0000,是每隔 3 位加一个逗号,而在英语的环境中是 10,000,每隔3位加一个逗号。
<FormattedPlural /> 这个组件可用于格式化量词,在中文的语境中,其实不太会用得到。但是在英文的语言环境中,描述一个苹果的时候,量词是 apple,当苹果数量为两个时,就会变成 apples,这个组件的作用就在于此。
本文在 nextjs-demo 项目基础上,使用 react-intl 库处理多语言和格式化。首先把 React-intl 库安装到 nextjs-demo 项目上,进入 nextjs-demo 目录,运行如下命令:
$ npm install react-intl
(1) 修改配置
使用子路径路由,在 next.config.js 文件里,配置如下:
module.exports = {
i18n: {
locales: ['en', 'zh-CN'],
defaultLocale: 'en',
注:en 表示英语,zh-CHS 表示简体中文。
创建 src/locales 目录,并在 locales 目录下创建 en.json 和 zh-CN.json 文件。
(2) 多语言文件
以 nextjs-demo 项目的 test 页面为例,修改 en.json 文件,内容如下:
"page.test.title": "Test Page",
"page.test.description": "React intl locales"
修改 zh-CN.json 文件,内容如下:
"page.test.title": "测试页",
"page.test.description": "React intl 多语言组件"
(3) 修改 src/pages/_app.js 文件
import { useRouter } from "next/router";
import { IntlProvider } from "react-intl";
import 'antd/dist/antd.min.css';
import Layout from '../components/layout'
import en from "../locales/en.json";
import zhCN from "../locales/zh-CN.json";
const messages = {
"en": en,
"zh-CN": zhCN,
export default ({ Component, pageProps }) => {
const { locale } = useRouter();
return (
<IntlProvider locale={locale} messages={messages[locale]}>
<Layout>
<Component {...pageProps} />
</Layout>
</IntlProvider>
(4) 修改 src/pages/test.js 文件
import { FormattedMessage, useIntl } from "react-intl";
import { useRouter } from 'next/router';
import Link from "next/link";
import styles from '@/styles/Home.module.css'
export default (props) => {
const { locales } = useRouter();
const intl = useIntl();
const title = intl.formatMessage({ id: "page.test.title" });
const description = intl.formatMessage({ id: "page.test.description" });
return (
<header>
<div className={styles.languages}>
{[...locales].sort().map((locale) => (
<Link key={locale} href="/test" locale={locale}>
{locale + ' | '}
</Link>
</header>
<main className={styles.main}>
<h3>Title: {title}</h3>
<p className={styles.description}>
Description: {description}
</main>
运行 nextjs-demo 项目,使用浏览器访问 http://localhost:3000/test,显示内容如下:
Home Login # 菜单
en | zh-CN |
Title: Test Page
Description: React intl locales
Footer
鼠标点击 "zh-CN" 链接,跳转到 http://localhost:3000/zh-CN/test,显示中文内容。
3. 中间件 (Middleware)
中间件 (Middleware) 就是在请求完成之前运行的代码,中间件在缓存内容和路由匹配之前运行。中间件根据传入的请求,可以通过重写、重定向、修改请求或响应标头或直接响应来修改响应。
在 src 目录下(即和 pages 或 app 目录同级)添加 middleware.js (或 middleware.ts) 文件,该文件被命名为中间件。
Next.js 项目中的每条路由的运行都会调用到中间件。以下是执行顺序:
(1) next.config.js 中的标头
(2) 从 next.config.js 重定向
(3) 中间件(重写、重定向等)
(4) beforeFiles(重写)来自 next.config.js
(5) 文件系统路由(public/、_next/static/、pages/、app/ 等)
(6) afterFiles(重写)来自 next.config.js
(7) 动态路由(/bog/[slug])
(8) 从 next.config.js 回退(重写)
有两种方法可以定义中间件将匹配哪些路径:自定义匹配器、条件语句
1) 自定义匹配器
示例,src/middleware.js 代码如下:
import { NextResponse } from 'next/server'
// This function can be marked `async` if using `await` inside
export const middleware = (request) => {
return NextResponse.redirect(new URL('/test', request.url))
// See "Matching Paths" below to learn more
export const config = {
matcher: '/about/:path*',
注:把访问 /about/* 的请求重定向到 /test
可以使用数组语法匹配单个路径或多个路径:
export const config = {
matcher: ['/about/:path*', '/dashboard/:path*'],
}
匹配器配置允许使用完整的 regex,因此支持像定向排除 (negative look ahead) 或字符匹配这样的匹配。以下是一个定向排除的例子,以匹配除特定路径之外的所有路径:
export const config = {
matcher: [
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
'/((?!api|_next/static|_next/image|favicon.ico).*)',
注:matcher 值必须是常量,这样才能在构建时对其进行静态分析,将忽略变量等动态值。匹配器的规则:
(1) 以开头 /
(2) 可以包括命名参数:/about/:path 匹配 /about/a 和 /about/b,但不匹配 /about/a/c
(3) 命名参数上可以有修饰符(以 :开头): /about/:path* 与 /about/a/b/c 匹配,因为 * 为零或更多,?为零或 1个或多个
(4) 可以使用括号中的正则表达式:/about/(.*)与 /about/:path* 相同
2) 条件语句
示例,src/middleware.js 代码如下:
import { NextResponse } from 'next/server'
export const middleware = (request) => {
if (request.nextUrl.pathname.startsWith('/about')) {
return NextResponse.rewrite(new URL('/about-2', request.url))
if (request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.rewrite(new URL('/dashboard/user', request.url))
使用条件语句时,无需配置匹配器。以上自定义匹配器和条件语句,都使用 NextResponse 的 API 返回响应。
NextResponse 的 API 能完成如下工作:
(1) 将传入请求重定向到其他 URL
(2) 通过显示给定的 URL 重写响应
(3) 设置 API 路由、getServerSideProps 和重写目标的请求头
(4) 设置响应 cookie
(5) 设置响应标头
3) 使用 Cookie
Cookie 是常规标头。在请求时,它们存储在 Cookie 标头中。在响应中,它们位于 Set Cookie 标头中。Next.js 提供了一种方便的方式,可以通过 NextRequest 和 NextResponse 上的 cookie 扩展来访问和操作这些 cookie。
对于传入请求,cookie 具有以下方法:get、getAll、set 和 delete cookie。您可以使用 has 检查是否存在 cookie,也可以使用 clear 删除所有 cookie。
对于传出响应,cookie 具有以下方法 get、getAll、set 和 delete。
示例,代码如下:
import { NextResponse } from 'next/server'
export const middleware = (request) => {
// Assume a "Cookie:nextjs=fast" header to be present on the incoming request
// Getting cookies from the request using the `RequestCookies` API
let cookie = request.cookies.get('nextjs')
console.log(cookie) // => { name: 'nextjs', value: 'fast', Path: '/' }
const allCookies = request.cookies.getAll()
console.log(allCookies) // => [{ name: 'nextjs', value: 'fast' }]
request.cookies.has('nextjs') // => true
request.cookies.delete('nextjs')
request.cookies.has('nextjs') // => false
// Setting cookies on the response using the `ResponseCookies` API
const response = NextResponse.next()
response.cookies.set('vercel', 'fast')
response.cookies.set({
name: 'vercel',
value: 'fast',
path: '/',
cookie = response.cookies.get('vercel')
console.log(cookie) // => { name: 'vercel', value: 'fast', Path: '/' }
// The outgoing response will have a `Set-Cookie:vercel=fast;path=/test` header.
return response
4) 设置 Headers
可以使用 NextResponse API 设置请求和响应头(从 Next.js v13.0.0 开始提供设置请求头)。
示例,代码如下:
import { NextResponse } from 'next/server'
export const middleware = (request) => {
// Clone the request headers and set a new header `x-hello-from-middleware1`
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-hello-from-middleware1', 'hello')
// You can also set request headers in NextResponse.rewrite
const response = NextResponse.next({
request: {
// New request headers
headers: requestHeaders,
// Set a new response header `x-hello-from-middleware2`
response.headers.set('x-hello-from-middleware2', 'hello')
return response
注:避免设置大 Headers,因为根据后端 Web 服务器配置,这可能会导致 431 请求标题字段过大错误。
5) 产生响应
Next.js 从 v13.1.0 开始,可以通过返回 Response 或 NextResponse 实例直接从中间件进行响应。
示例,代码如下:
import { NextResponse } from 'next/server'
// Limit the middleware to paths starting with `/api/`
export const config = {
matcher: '/api/:function*',
export const middleware = (request) => {
if (true) {
// Respond with JSON indicating an error message
return new NextResponse(
JSON.stringify({ success: false, message: 'authentication failed' }),
{ status: 401, headers: { 'content-type': 'application/json' } }
注:访问 http://localhost:3000/api/*,中间件拦截请求,返回 401
6) 高级中间件标志
在 Next.js 的 v13.1 中,为中间件引入了两个额外的标志 skipMiddlewareUrlNormalize 和 skipTrailingLashRedirect,以处理高级用例。
skipTrailingSlashRedirect 允许禁用 Next.js 默认重定向以添加或删除尾部斜杠,从而允许在中间件内部进行自定义处理,这可以允许为某些路径维护尾部斜杠,但不允许为其他路径维护尾部斜线,从而允许更容易的增量迁移。
在 next.config.js 里的配置如下:
module.exports = {
skipTrailingSlashRedirect: true,
}
示例,代码如下:
const legacyPrefixes = ['/docs', '/blog']
export default middleware = async (req) => {
const { pathname } = req.nextUrl
if (legacyPrefixes.some((prefix) => pathname.startsWith(prefix))) {
return NextResponse.next()
// apply trailing slash handling
!pathname.endsWith('/') &&
!pathname.match(/((?!\.well-known(?:\/.*)?)(?:[^/]+\/)*[^/]+\.\w+)/)
req.nextUrl.pathname += '/'
return NextResponse.redirect(req.nextUrl)
skipMiddlewareUrlNormalize 允许禁用规范 Next.js 的 URL,以使处理直接访问和客户端转换相同。在一些高级情况下,您需要使用解锁的原始 URL 进行完全控制。
在 next.config.js 里的配置如下:
module.exports = {
skipMiddlewareUrlNormalize: true,
}
示例,代码如下:
export default middleware = async (req) => {
const { pathname } = req.nextUrl
// GET /_next/data/build-id/hello.json
console.log(pathname)
// with the flag this now /_next/data/build-id/hello.json
// without the flag this would be normalized to /hello