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
            }

 

posted @ 2023-08-13 13:06  垄山小站  阅读(2193)  评论(0编辑  收藏  举报