NodeJS系列(9)- Next.js 框架 (二) | 国际化 (i18n)、中间件 (Middleware)
在 “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> ))} </div> </header> <main className={styles.main}> <h3>Title: {title}</h3> <p className={styles.description}> Description: {description} </p> </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 if ( !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 }