Skip to content

Latest commit

 

History

History
1457 lines (1231 loc) · 42.2 KB

第30章—API篇next.config.js(上).md

File metadata and controls

1457 lines (1231 loc) · 42.2 KB

前言

Next.js 可以通过根目录的 next.config.js 进行配置:

/** @type {import('next').NextConfig} */
const nextConfig = {
  /* config options here */
}
 
module.exports = nextConfig

正如文件的扩展名是 .jsnext.config.js 是一个常规的 Node.js 模块,而不是一个 JSON 文件。它会在 Next.js server 和构建阶段被用到,并且不包含在浏览器构建中(代码不会打包到客户端)。

如果你需要 ECMAScript 模块,你可以使用 next.config.mjs

/**
 * @type {import('next').NextConfig}
 */
const nextConfig = {
  /* config options here */
}
 
export default nextConfig

你也可以使用一个函数:

export default (phase, { defaultConfig }) => {
  /**
   * @type {import('next').NextConfig}
   */
  const nextConfig = {
    /* config options here */
  }
  return nextConfig
}

从 Next.js 12.1.0 起,你还可以使用一个异步函数:

module.exports = async (phase, { defaultConfig }) => {
  /**
   * @type {import('next').NextConfig}
   */
  const nextConfig = {
    /* config options here */
  }
  return nextConfig
}

其中 phase 表示配置加载的当前上下文。通过查看源码,可以知道 phase 的值一共有 5 个:

export const PHASE_EXPORT = 'phase-export'
export const PHASE_PRODUCTION_BUILD = 'phase-production-build'
export const PHASE_PRODUCTION_SERVER = 'phase-production-server'
export const PHASE_DEVELOPMENT_SERVER = 'phase-development-server'
export const PHASE_TEST = 'phase-test'

可以通过 next/constants 导入,根据不同的阶段进行自定义配置:

const { PHASE_DEVELOPMENT_SERVER } = require('next/constants')
 
module.exports = (phase, { defaultConfig }) => {
  if (phase === PHASE_DEVELOPMENT_SERVER) {
    return {
      /* 这里放 development 配置选项 */
    }
  }
 
  return {
    /* 除了 development 阶段的其他阶段的配置 */
  }
}

在这个例子中,注释行的地方就是你可以放配置的地方,实际上,Next.js 定义的配置非常多,可以查看源码配置文件

然而,这些配置又都不是必须的,也没有必要清楚的了解每个配置的作用,大致看一下,有个印象即可,需要用到的时候再去细查。

因为要讲解的配置有 36 个,内容繁琐细节且庞大,所以 next.config.js 的配置部分拆分为上下两篇。上篇讲解请求相关的 headers、redirects、rewrites,这是 Next.js 中常用的配置,且内容有很多相似之处,放在一起方便触类旁通。下篇讲解剩余的 33 个配置,每个配置内容都不多,了解即可。

现在让我们开始学习吧!

1. headers

1.1. 介绍

Headers 用于设置自定义 HTTP 标头,使用 next.config.jsheaders字段:

module.exports = {
  async headers() {
    return [
      {
        source: '/about',
        headers: [
          {
            key: 'x-custom-header',
            value: 'my custom header value',
          },
          {
            key: 'x-another-custom-header',
            value: 'my other custom header value',
          },
        ],
      },
    ]
  },
}

此时访问 /about,可以看到:

截屏2023-11-09 下午3.40.56.png

headers是一个异步函数,该函数返回一个包含 soruceheaders 属性的对象数组,其中:

  • source 表示传入的请求路径
  • headers 是一个包含 key 和 value 属性的响应标头对象数组

除了这两个值外,还可以设置:

  • basePathfalse 或者 undefined。当值为 false ,匹配时不会包含 basePath,只能用于外部重写
  • localefalse 或者 undefined,匹配时是否应该包含 locale
  • has:一个有 typekeyvalue 属性的对象数组
  • missing:一个有 typekeyvalue 属性的对象数组

headers 会在文件系统(包括页面和 /public 文件)之前被触发。

这些字段我们来一一举例介绍。

1.2. source

source 表示传入的请求路径,除了可以匹配具体的值,还支持三种匹配模式:

路径匹配

普通的路径匹配,举个例子,/blog:slug 会匹配 /blog/hello-world(无嵌套路径,也就是说 /blog/hello-world/about不会匹配)

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/blog/:slug',
        headers: [
          {
            key: 'x-slug',
            value: ':slug', // 匹配参数可以在 value 中使用
          },
          {
            key: 'x-slug-:slug', // 匹配参数可以在 key 中使用
            value: 'my other custom header value',
          },
        ],
      },
    ]
  },
}

访问 /blog/hello-world,可以看到:

截屏2023-11-09 下午4.00.05.png

但访问 /blog/hello-world/about就不会有自定义标头。

通配符路径匹配

在参数后使用 * 实现通配符路径匹配,举个例子:/blog/:slug* 会匹配 /blog/a/b/c/d/hello-world

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/blog/:slug*',
        headers: [
          {
            key: 'x-slug',
            value: ':slug*',
          },
          {
            key: 'x-slug-:slug*',
            value: 'my other custom header value',
          },
        ],
      },
    ]
  },
}

访问 /blog/hello-world/about,可以看到:

image.png

访问 /blog/hello-world 也是有的:

image.png

正则表达式路径匹配

在参数后用括号将正则表达式括住实现正则表达式匹配,举个例子:blog/:slug(\\d{1,}) 匹配 /blog/123 而不匹配 /blog/abc

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/blog/:post(\\d{1,})',
        headers: [
          {
            key: 'x-post',
            value: ':post',
          },
        ],
      },
    ]
  },
}

访问 /blog/123,可以看到:

image.png

注意:这 8 个字符 (){}:*+? 都会用于正则表达式匹配,所以需要用到这些字符本身的时候,使用 \\转义

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        // 匹配 `/english(default)/something`
        source: '/english\\(default\\)/:slug',
        headers: [
          {
            key: 'x-header',
            value: 'value',
          },
        ],
      },
    ]
  },
}

1.3. headers

headers 无须多说,我们聊聊 headers 的覆盖行为。

如果两个 headers 匹配相同的路径以及设置了相同的 header key,最后一个 header 的 key 会覆盖前一个。举个例子:

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          {
            key: 'x-hello',
            value: 'there',
          },
        ],
      },
      {
        source: '/hello',
        headers: [
          {
            key: 'x-hello',
            value: 'world',
          },
        ],
      },
    ]
  },
}

在这个例子中,当访问 /hello 时,既匹配 /:path*,又匹配 /hello,而两个 source 对应设置的 x-hello 的 key 值不同,因为/hello 是最后一个 header,所以最终的值是 world

那如果匹配了相同的路径,但设置的 header key 不冲突呢?那就都会添加,举个例子:

module.exports = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          {
            key: 'hello',
            value: 'hello',
          },
          {
            key: 'hello2',
            value: 'hello2',
          }
        ],
      },
      {
        source: '/hello',
        headers: [
          {
            key: 'hello',
            value: 'world',
          },
          {
            key: 'hello3',
            value: 'hello3',
          },
        ],
      },
    ]
  },
}

最终的结果为:

image.png

1.4. basePath

basePath的值为 false 或者 undefined。当值为 false ,匹配时不会包含 basePath,举个例子:

// next.config.js
module.exports = {
  basePath: '/docs',
 
  async headers() {
    return [
      {
        source: '/with-basePath', // 匹配 /docs/with-basePath
        headers: [
          {
            key: 'x-hello',
            value: 'world',
          },
        ],
      },
      {
        source: '/without-basePath', // 匹配 /without-basePath
        headers: [
          {
            key: 'x-hello',
            value: 'world',
          },
        ],
        basePath: false, // 因为设置了 false
      },
    ]
  },
}

在这个例子中,设置了 basePath/docs,正常 headers 中的 source 会匹配 basePath + source 构成的链接,除非你设置了 basePathfalse

1.5. locale

locale 的值为 false 或者 undefined,决定匹配时是否应该包含 locale,其实效果跟 basePath 类似.

考虑到部分同学对 locale 不太熟悉,我们先简单的讲下 locale配置项,locale 的作用就是国际化(i18n),next.config.js 针对 Pages Router 提供了 i18n 配置项,注意是在 Pages Router 下,在 App Router 下 Next.js 已经不再提供直接的支持,具体内容查看小册国际化章节。

比如我们在 pages 目录下新建一个 article.js 文件:

// pages/article.js
export default function Home() {
  return  <h1>Hello Article!</h1>
}

然后 next.config.js 修改配置项:

// next.config.js
module.exports = {
  i18n: {
    locales: ['en', 'fr', 'de', 'zh'],
    defaultLocale: 'zh',
  }
}

此时,访问 /en/article/fr/article/de/article 都会重写为 /article,注意是重写,就是路由地址不变,但内容是 /article的内容。访问 /zh/article 会重定向到 /article

而如果你在 app/article目录下新建一个 article.js 文件,文件内容同上。

此时,访问 /en/article/fr/article/de/article 都会 404 错误。访问 /zh/article 会重写为 /article。说明在 App Router 下只有 i18n.defaultLocale 是生效的。

好了,基本介绍完毕,主要是为了让大家了解配置项中的 i18n 的作用。我们再看 headers 中的 locales 设置,举个例子:

module.exports = {
  i18n: {
    locales: ['en', 'fr', 'de'],
    defaultLocale: 'en',
  },
 
  async headers() {
    return [
      {
        // 自动处理所有的 locales
        // 也就是 `/en/with-locale`、`/fr/with-locale`、`/de/with-locale`、`/with-locale` 都会匹配
        source: '/with-locale', 
        headers: [
          {
            key: 'x-hello',
            value: 'world1',
          },
        ],
      },
      {
        // 因为 locale 设置为 false,所以不会自动处理 locales
        // 也就是只匹配 `/nl/with-locale-manual`
        source: '/nl/with-locale-manual',
        locale: false,
        headers: [
          {
            key: 'x-hello',
            value: 'world2',
          },
        ],
      },
      {
        // 匹配 '/' 因为 `en` 是 defaultLocale
        // 也就是只匹配 `/`、`/en`
        source: '/en',
        locale: false,
        headers: [
          {
            key: 'x-hello',
            value: 'world3',
          },
        ],
      },
      {
        // 会转换为 /(en|fr|de)/(.*) 所以不会匹配顶层
        // 也就是 `/` 和 `/fr` 都不会匹配到
        // 如果要匹配到这两个,可以用 `/:path*`
        source: '/(.*)',
        headers: [
          {
            key: 'x-hello',
            value: 'world4',
          },
        ],
      },
    ]
  },
}

注意,虽然 i18n.locales 配置在 App Router 下不生效,但这也只是导致页面出现 404 错误而已,并不会影响处理标头,即便页面 404,你可以正常的查看标头。

1.6. has 和 missing

hasmissing 是用来处理请求中的 header、cookie 和请求参数是否匹配某些字段,或者不匹配某些字段的时候,才应用 header。

举个例子,比如请求 /article?id=1&author=yayuhas 可以要求请求中必须有 id 参数,或者 id 参数等于 xxx 的时候才返回某个标头。missing 可以要求请求中必须没有 id 参数,或者 id 参数不等于 xxx 的时候才返回某个标头。

hasmissing 对象有下面这些字段:

  • type: String类型,值为 headercookiehostquery 之一
  • key: String类型,所选类型(也就是上面的四种值)中要匹配的 key
  • valueString 或者 undefined,要检查的值。如果值为 undefiend,任何值都不会匹配。支持使用一个类似正则的字符串捕获值的特殊部分。比如 first-(?<paramName>.*)用于匹配 first-second,然后就可以用 :paramName获取 second 这个值

听起来有些复杂,看个例子其实就懂了:

// next.config.js
module.exports = {
  async headers() {
    return [
      // 如果 header 中 `x-add-header` 字段存在
      // 那就返回 `x-another-header` 标头
      {
        source: '/:path*',
        has: [
          {
            type: 'header',
            key: 'x-add-header',
          },
        ],
        headers: [
          {
            key: 'x-another-header',
            value: 'hello',
          },
        ],
      },
      // 如果 header 中 `x-no-header` 字段不存在
      // 就返回 `x-another-header` 标头
      {
        source: '/:path*',
        missing: [
          {
            type: 'header',
            key: 'x-no-header',
          },
        ],
        headers: [
          {
            key: 'x-another-header',
            value: 'hello',
          },
        ],
      },
      // 如果 source、query、cookie 都匹配
      // 就返回 `x-authorized` 标头
      {
        source: '/specific/:path*',
        has: [
          {
            type: 'query',
            key: 'page',
            value: 'home',
          },
          {
            type: 'cookie',
            key: 'authorized',
            value: 'true',
          },
        ],
        headers: [
          {
            key: 'x-authorized',
            value: 'hello',
          },
        ],
      },
      //如果 header 中 `x-authorized` 存在且等于 yes 或 true
      // 就返回 `x-another-header` 标头
      {
        source: '/:path*',
        has: [
          {
            type: 'header',
            key: 'x-authorized',
            value: '(?<authorized>yes|true)',
          },
        ],
        headers: [
          {
            key: 'x-another-header',
            value: ':authorized',
          },
        ],
      },
      // 如果 host 是 `example.com`,
      // 应用 header
      {
        source: '/:path*',
        has: [
          {
            type: 'host',
            value: 'example.com',
          },
        ],
        headers: [
          {
            key: 'x-another-header',
            value: 'hello',
          },
        ],
      },
    ]
  },
}

注意,has 和 missing 判断的都是请求头中的值。 type 的四种类型为 header、cookie、host、query,其中下图中的值都是 header:

image.png

cookie 指的是其中的 Cookie 标头,Next.js 已经自动做了解析,所以可以直接判断 Cookie 中的字段值:

image (1).png

host 就是主机名 + 端口,query 表示参数。以 'http://user:[email protected]:8080/p/a/t/h?query=string#hash'为例的话,host 的值为 host.com:8080queryquery=string

1.7. Cache-Control

你不能在 next.config.js 中为页面或静态资源设置 Cache-Control标头,因为该标头会在生产中被覆盖,以确保有效缓存响应和静态资源。

1.8. 选项

X-DNS-Prefetch-Control

X-DNS-Prefetch-Control 头控制着浏览器的 DNS 预读取功能。DNS 预读取是一项使浏览器主动去执行域名解析的功能,其范围包括文档的所有链接,无论是图片的,CSS 的,还是 JavaScript 等其他用户能够点击的 URL。

因为预读取会在后台执行,所以 DNS 很可能在链接对应的东西出现之前就已经解析完毕。这能够减少用户点击链接时的延迟。

{
  key: 'X-DNS-Prefetch-Control',
  value: 'on'
}

Strict-Transport-Security

Strict-Transport-Security(通常简称为 HSTS)响应标头用来通知浏览器应该只通过 HTTPS 访问该站点,并且以后使用 HTTP 访问该站点的所有尝试都应自动重定向到 HTTPS。

使用下面的配置,所有当前和未来的子域都将使用 max-age 为 2 年的 HTTPS:

{
  key: 'Strict-Transport-Security',
  value: 'max-age=63072000; includeSubDomains; preload'
}

X-Frame-Options

X-Frame-Options HTTP 响应头是用来给浏览器指示允许一个页面可否在 <frame><iframe><embed> 或者 <object> 中展现的标记。站点可以通过确保网站没有被嵌入到别人的站点里面,从而避免点击劫持 (en-US)攻击。

此标头已经被 frame-ancestors 替代,它在现代浏览器中有更好的支持。

Permissions-Policy

Permissions-Policy 响应标头提供了一种可以在本页面或包含的 iframe 上启用或禁止浏览器特性的机制,之前叫做 Feature-Policy

{
  key: 'Permissions-Policy',
  value: 'camera=(), microphone=(), geolocation=(), browsing-topics=()'
}

X-Content-Type-Options

如果 Content-Type 标头没有被显示设置,X-Content-Type-Options 会阻止浏览器尝试猜测内容类型。这可以防止允许用户上传和共享文件的网站受到 XSS 攻击。

这个标头只有一个有效值是 nosniff

{
  key: 'X-Content-Type-Options',
  value: 'nosniff'
}

Referrer-Policy

Referrer-Policy 控制当从当前网页导航到另一个网页时携带的信息内容:

{
  key: 'Referrer-Policy',
  value: 'origin-when-cross-origin'
}

2. redirects

2.1. 介绍

重定向,顾名思义,将请求路径重定向到其他目标路径。配置重定向,使用 next.config.jsredirects,示例如下:

module.exports = {
  async redirects() {
    return [
      {
        source: '/about',
        destination: '/',
        permanent: true,
      },
    ]
  },
}

redirects 是一个异步函数,该函数返回一个包含 sourcedestinationpermanent 属性的对象数组,其中:

  • source 表示传入的请求路径
  • destination 表示你重定向的的目标路径
  • permanent 值为 true 或者 false。如果为 true,使用 308 状态码,表示客户端或搜索引擎永久缓存重定向。如果是 false,使用 307 状态码表示临时未缓存。

为什么 Next.js 使用 307 和 308 呢?传统都是使用 302 表示临时重定向,301 表示永久重定向,但是很多浏览器会将重定向的请求方法修改为 GET,而不管原本的方法是什么。举个例子,如果浏览器发送了一个 POST 请求,/v1/users ,然后返回了 302 状态码,新地址是 /v2/users,则后续的请求会是 GET /V2/users 而不是 POST /v2/users,Next.js 用 307 临时重定向和 308 永久重定向状态码就是为了显示保留之前使用的请求方法。

除了这三个值外,还可以设置:

  • basePathfalse 或者 undefined。当值为 false ,匹配时不会包含 basePath,只能用于外部重写
  • localefalse 或者 undefined,匹配时是否应该包含 locale
  • has:一个有 typekeyvalue 属性的对象数组
  • missing:一个有 typekeyvalue 属性的对象数组

重定向会在文件系统(包括页面和 /public 文件)之前被触发。

重定向不会应用于客户端路由(Linkrouter.push),除非使用了中间件,且有匹配的路径。

当应用重定向的时候,请求路径的参数也会传递给重定向目标路径。举个例子:

{
  source: '/old-blog/:path*',
  destination: '/blog/:path*',
  permanent: false
}

当请求/old-blog/post-1?hello=world时,客户端会重定向到 /blog/post-1?hello=world

2.2. source

路径匹配

普通的路径匹配,举个例子,比如 /old-blog/:slug会匹配 /old-blog/hello-world(无嵌套路径,也就是说 /old-blog/hello-world/about不会匹配)

// next.config.js
module.exports = {
  async redirects() {
    return [
      {
        source: '/old-blog/:slug',
        destination: '/news/:slug',
        permanent: true,
      },
    ]
  },
}

通配符路径匹配

在参数后使用 * 实现通配符路径匹配,举个例子:/blog/:slug* 会匹配 /blog/a/b/c/d/hello-world

// next.config.js
module.exports = {
  async redirects() {
    return [
      {
        source: '/blog/:slug*',
        destination: '/news/:slug*',
        permanent: true,
      },
    ]
  },
}

正则表达式路径匹配

在参数后用括号将正则表达式括住实现正则表达式匹配,举个例子:/post/:slug(\\d{1,}) 匹配 /post/123 而不匹配 /post/abc

// next.config.js
module.exports = {
  async redirects() {
    return [
      {
        source: '/post/:slug(\\d{1,})',
        destination: '/news/:slug',
        permanent: false,
      },
    ]
  },
}

注意:这 8 个字符 (){}:*+? 都会用于正则表达式匹配,所以需要用到这些字符本身的时候,使用 \\转义

// next.config.js
module.exports = {
  async redirects() {
    return [
      {
        // 匹配 `/english(default)/something`
        source: '/english\\(default\\)/:slug',
        destination: '/en-us/:slug',
        permanent: false,
      },
    ]
  },
}

2.3. basePath

当使用 basePath 的时候,每一个 sourcedestination 都会自动添加 basePath 作为前缀,除非你为重定向设置 basePath: false

// next.config.js
module.exports = {
  basePath: '/docs',
 
  async redirects() {
    return [
      {
        source: '/with-basePath', // 自动变成 /docs/with-basePath
        destination: '/another', // 自动变成 /docs/another
        permanent: false,
      },
      {
        // does not add /docs since basePath: false is set
        source: '/without-basePath',
        destination: 'https://example.com',
        basePath: false,
        permanent: false,
      },
    ]
  },
}

2.4. locale

当使用 i18n的时候,每一个 sourcedestination 都会自动根据 locales添加前缀进行处理,除非你为重定向设置 locale: false。如果设置 locale: false,你必须使用一个 locale 作为 sourcedestination 的前缀才能够正确匹配,让我们看个例子:

// next.config.js
module.exports = {
  i18n: {
    locales: ['en', 'fr', 'de'],
    defaultLocale: 'en',
  },
 
  async redirects() {
    return [
      {
        // /with-locale -> /another
        // /en/with-locale -> /en/another
        // /fr/with-locale -> /fr/another
        // /de/with-locale -> /de/another
        source: '/with-locale',
        destination: '/another',
        permanent: false,
      },
      {
        // 因为 locale 设置为 false,所以不会自动处理
        // /nl/with-locale-manual -> /nl/another
        source: '/nl/with-locale-manual',
        destination: '/nl/another',
        locale: false,
        permanent: false,
      },
      {
        // 因为 `en` 是 defaultLocale,所以匹配 '/'
        // /en -> /en/another
        // / -> /en/another
        source: '/en',
        destination: '/en/another',
        locale: false,
        permanent: false,
      },
      // 尽管 locale 设置为 false,但匹配所有 locale
      // /page -> /en/newpage
      // /en/page -> /en/newpage
      // /fr/page -> /fr/newpage
      // /de/page -> /de/newpage
      {
        source: '/:locale/page',
        destination: '/en/newpage',
        permanent: false,
        locale: false,
      },
      {
        // 转换为 /(en|fr|de)/(.*) 所以不会匹配 `/`
        // /page -> /another2
        // /fr/page -> /fr/another2
        // 匹配 `\` 或 `/fr` 使用 /:path*
        source: '/(.*)',
        destination: '/another2',
        permanent: false,
      },
    ]
  },
}

2.5. has 和 missing

hasmissing 是用来处理请求中的 header、cookie 和请求参数是否匹配某些字段,或者不匹配某些字段的时候,才发生重定向。

举个例子,比如请求 /article?id=1&author=yayuhas 可以要求请求中必须有 id 参数,或者 id 参数等于 xxx 的时候才重定向。missing 可以要求请求中必须没有 id 参数,或者 id 参数不等于 xxx 的时候才重定向。

hasmissing 对象有下面这些字段:

  • type: String类型,值为 headercookiehostquery 之一
  • key: String类型,所选类型(也就是上面的四种值)中要匹配的 key
  • valueString 或者 undefined,要检查的值。如果值为 undefiend,任何值都不会匹配。支持使用一个类似正则的字符串捕获值的特殊部分。比如 first-(?<paramName>.*)用于匹配 first-second,然后就可以用 :paramName获取 second 这个值

其实跟 headers 是一样的,只不是过一个是返回标头,一个是发生重定向。

// next.config.js
module.exports = {
  async redirects() {
    return [
      // 如果 header `x-redirect-me` 存在,
      // 才应用重定向
      {
        source: '/:path((?!another-page$).*)',
        has: [
          {
            type: 'header',
            key: 'x-redirect-me',
          },
        ],
        permanent: false,
        destination: '/another-page',
      },
      // 如果 `x-dont-redirect` 存在,
      // 不会应用重定向
      {
        source: '/:path((?!another-page$).*)',
        missing: [
          {
            type: 'header',
            key: 'x-do-not-redirect',
          },
        ],
        permanent: false,
        destination: '/another-page',
      },
      // 如果 source, query, 和 cookie 匹配,
      // 会应用重定向
      {
        source: '/specific/:path*',
        has: [
          {
            type: 'query',
            key: 'page',
            value: 'home',
          },
          {
            type: 'cookie',
            key: 'authorized',
            value: 'true',
          },
        ],
        permanent: false,
        destination: '/another/:path*',
      },
      // 如果 header `x-authorized` 存在,并且是 yes huozhe true,
      // 会应用重定向
      {
        source: '/',
        has: [
          {
            type: 'header',
            key: 'x-authorized',
            value: '(?<authorized>yes|true)',
          },
        ],
        permanent: false,
        destination: '/home?authorized=:authorized',
      },
      // 如果 host 是 `example.com`,
      // 会应用重定向
      {
        source: '/:path((?!another-page$).*)',
        has: [
          {
            type: 'host',
            value: 'example.com',
          },
        ],
        permanent: false,
        destination: '/another-page',
      },
    ]
  },
}

3. rewrites

3.1. 介绍

重写允许你将传入的请求路径映射到其他目标路径。它与重定向的不同之处在于,重写相当于扮演了 URL 代理的角色,会屏蔽目标路径,地址还是这个地址,但路由逻辑发生了变化。而重定向则是导航至新的页面,浏览器中的 URL 也会发生更改。配置重定向,使用 next.config.jsrewrites,示例如下:

// next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        source: '/about',
        destination: '/',
      },
    ]
  },
}

重写会应用于客户端路由,在这个例子中,如果使用<Link href="/about"> 会应用重写。

rewrites 是一个异步函数,该函数可以返回一个包含 sourcedestination 属性的对象数组,其中:

  • source 表示传入的请求路径
  • destination 表示你重写的的目标路径
  • basePathfalse 或者 undefined。当值为 false ,匹配时不会包含 basePath,只能用于外部重写
  • localefalse 或者 undefined,匹配时是否应该包含 locale
  • has:一个有 typekeyvalue 属性的对象数组
  • missing:一个有 typekeyvalue 属性的对象数组

如果返回的是这种数组,重写会在检查文件系统(页面和 /public 文件)之后和动态路由之前应用。

也可以返回一个具有特定属性的对象,这是为了实现更精细的控制,示例代码如下:

// next.config.js
module.exports = {
  async rewrites() {
    return {
      beforeFiles: [
        // 在 headers/redirects 之后
        // 在 _next/public files 文件之前触发
        {
          source: '/some-page',
          destination: '/somewhere-else',
          has: [{ type: 'query', key: 'overrideMe' }],
        },
      ],
      afterFiles: [
        // 在 pages/public 之后,在动态路由之前触发
        {
          source: '/non-existent',
          destination: '/somewhere-else',
        },
      ],
      fallback: [
        // 在 pages/public files 和动态路由之后触发
        {
          source: '/:path*',
          destination: `https://my-old-site.com/:path*`,
        },
      ],
    }
  },
}

这个时候就要说到 Next.js 的路由的检查顺序是:

  1. headers
  2. redirects
  3. beforeFiles 重写
  4. public 目录下的静态文件、_next/static 文件、非动态的页面
  5. afterFiles 重写,如果每次匹配,
  6. fallback 重写,会在渲染 404 页面之前和动态路由、所有静态资源检查前被引用

3.2. 重写参数

如果 destination没有使用参数(例子中的:path*),那么 source 的中的参数会以查询字符串的形式(query)默认传递给 destination

// next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        source: '/old-about/:path*',
        destination: '/about',
      },
    ]
  },
}

假设 app/about/page.js的代码为:

// app/about/page.js
export default function Page(props) {
  console.dir(props)
  return  <h1>Hello About!</h1>
}

访问 /old-about/article?id=1,打印的值为:

image.png

source 中的参数 article 可以在 searchParams 中查到。

如果 destination使用了参数,则不会自动传递任何参数:

// next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        source: '/docs/:path*',
        destination: '/:path*',
      },
    ]
  },
}

访问 /docs/about?id=1,打印的值为:

image.png

如果 destination使用了参数,你依然可以手动传递参数:

// next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        source: '/:first/:second',
        destination: '/:first?second=:second'
      },
    ]
  },
}

在这个例子中,因为 destination 使用了 :first 参数,所以 :second 参数不会自动被添加到 query 中,但我们可以通过例子中的方式手动添加,使得能够在 query 中获取。

访问 /about/article?id=1,打印的值为:

image.png

3.3. source

路径匹配

普通的路径匹配,举个例子,比如 /blog/:slug会匹配 /blog/hello-world(无嵌套路径,也就是说 /blog/hello-world/about不会匹配)

// next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        source: '/blog/:slug',
        destination: '/news/:slug',
      },
    ]
  },
}

通配符路径匹配

在参数后使用 * 实现通配符路径匹配,举个例子:/blog/:slug* 会匹配 /blog/a/b/c/d/hello-world

// next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        source: '/blog/:slug*',
        destination: '/news/:slug*', // Matched parameters can be used in the destination
      },
    ]
  },
}

3.3.3. 正则表达式路径匹配

在参数后用括号将正则表达式括住实现正则表达式匹配,举个例子:/post/:slug(\\d{1,}) 匹配 /post/123 而不匹配 /post/abc

// next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        source: '/old-blog/:post(\\d{1,})',
        destination: '/blog/:post',
      },
    ]
  },
}

注意:这 8 个字符 (){}:*+? 都会用于正则表达式匹配,所以需要用到这些字符本身的时候,使用 \\转义

// next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        // this will match `/english(default)/something` being requested
        source: '/english\\(default\\)/:slug',
        destination: '/en-us/:slug',
      },
    ]
  },
}

3.4. basePath

当使用 basePath 的时候,每一个 sourcedestination 都会自动添加 basePath 作为前缀,除非你为重写设置 basePath: false

// next.config.js
module.exports = {
  basePath: '/docs',
 
  async rewrites() {
    return [
      {
        source: '/with-basePath', // 自动变成 /docs/with-basePath
        destination: '/another', // 自动变成 /docs/another
      },
      {
        // 不会添加 /docs 到 /without-basePath 因为 basePath 设置为 false 
        source: '/without-basePath',
        destination: 'https://example.com',
        basePath: false,
      },
    ]
  },
}

3.5. locale

当使用 i18n的时候,每一个 sourcedestination 都会自动根据 locales添加前缀进行处理,除非你为重写设置 locale: false。如果设置 locale: false,你必须使用一个 locale 作为 sourcedestination 的前缀才能够正确匹配,让我们看个例子:

// next.config.js
module.exports = {
  i18n: {
    locales: ['en', 'fr', 'de'],
    defaultLocale: 'en',
  },
 
  async rewrites() {
    return [
      {
        // /with-locale -> /another
        // /en/with-locale -> /en/another
        // /fr/with-locale -> /fr/another
        // /de/with-locale -> /de/another
        source: '/with-locale',
        destination: '/another',
      },
      {
        // 因为 locale 设置为 false,所以不会自动处理
        // /nl/with-locale-manual -> /nl/another
        source: '/nl/with-locale-manual',
        destination: '/nl/another',
        locale: false,
      },
      {
        // 因为 `en` 是 defaultLocale,所以匹配 '/'
        // /en -> /en/another
        // / -> /en/another
        source: '/en',
        destination: '/en/another',
        locale: false
      },
      // 尽管 locale 设置为 false,但匹配所有 locale
      {
        source: '/:locale/api-alias/:path*',
        destination: '/api/:path*',
        locale: false,
      },
      {
        // 转换为 /(en|fr|de)/(.*) 所以不会匹配 `/`
        // /page -> /another
        // /fr/page -> /fr/another
        // 匹配 `\` 或 `/fr` 使用 /:path*
				source: '/(.*)',
        destination: '/another',
      },
    ]
  },
}

3.6. has 和 missing

hasmissing 是用来处理请求中的 header、cookie 和请求参数是否匹配某些字段,或者不匹配某些字段的时候,才发生重写。

举个例子,比如请求 /article?id=1&author=yayuhas 可以要求请求中必须有 id 参数,或者 id 参数等于 xxx 的时候才重写。missing 可以要求请求中必须没有 id 参数,或者 id 参数不等于 xxx 的时候才重写。

hasmissing 对象有下面这些字段:

  • type: String类型,值为 headercookiehostquery 之一
  • key: String类型,所选类型(也就是上面的四种值)中要匹配的 key
  • valueString 或者 undefined,要检查的值。如果值为 undefiend,任何值都不会匹配。支持使用一个类似正则的字符串捕获值的特殊部分。比如 first-(?<paramName>.*)用于匹配 first-second,然后就可以用 :paramName获取 second 这个值

其实跟 redirects 是一样的,只不是过一个是重定向,一个是重写。

// next.config.js
module.exports = {
  async rewrites() {
    return [
      // 如果 header `x-rewrite-me` 存在,
      // 会应用重写
      {
        source: '/:path*',
        has: [
          {
            type: 'header',
            key: 'x-rewrite-me',
          },
        ],
        destination: '/another-page',
      },
      // 如果 `x-rewrite-me` 不存在
      // 会应用重写
      {
        source: '/:path*',
        missing: [
          {
            type: 'header',
            key: 'x-rewrite-me',
          },
        ],
        destination: '/another-page',
      },
      // 如果 source, query, 和 cookie 匹配,
      // 会应用重写
      {
        source: '/specific/:path*',
        has: [
          {
            type: 'query',
            key: 'page',
            value: 'home',
          },
          {
            type: 'cookie',
            key: 'authorized',
            value: 'true',
          },
        ],
        destination: '/:path*/home',
      },
      // 如果 header `x-authorized` 存在且为 yes 或 true
      // 会应用重写
      {
        source: '/:path*',
        has: [
          {
            type: 'header',
            key: 'x-authorized',
            value: '(?<authorized>yes|true)',
          },
        ],
        destination: '/home?authorized=:authorized',
      },
      // 如果 host 是 `example.com`,
      // 会应用重写
      {
        source: '/:path*',
        has: [
          {
            type: 'host',
            value: 'example.com',
          },
        ],
        destination: '/another-page',
      },
    ]
  },
}

3.7. 重写到外部 URL

rewrites 可以重写到外部 url,这在增量采用 Next.js 的项目中特别有用,比如这个例子就是将应用中的 /blog 路由全部重写到外部网址:

// next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        source: '/blog',
        destination: 'https://example.com/blog',
      },
      {
        source: '/blog/:slug',
        destination: 'https://example.com/blog/:slug',
      },
    ]
  },
}

如果设置了 trailingSlash:true,你也需要在 source 中插入一个尾部斜杠。如果目标地址也需要尾部斜杠,也应该包含在 destination 参数中。

// next.config.js
module.exports = {
  trailingSlash: true,
  async rewrites() {
    return [
      {
        source: '/blog/',
        destination: 'https://example.com/blog/',
      },
      {
        source: '/blog/:path*/',
        destination: 'https://example.com/blog/:path*/',
      },
    ]
  },
}

3.8. 增量采用 Next.js

可以让 Next.js 在检查所有 Next.js 路由后,如果没有对应的路由,那就代理现有的网站。这样你将更多页面迁移成 Next.js 时,就无需重写配置:

// next.config.js
module.exports = {
  async rewrites() {
    return {
      fallback: [
        {
          source: '/:path*',
          destination: `https://custom-routes-proxying-endpoint.vercel.app/:path*`,
        },
      ],
    }
  },
}

参考链接

  1. https://nextjs.org/docs/app/api-reference/next-config-js
  2. https://nextjs.org/docs/app/api-reference/next-config-js/headers
  3. https://nextjs.org/docs/app/api-reference/next-config-js/redirects
  4. https://nextjs.org/docs/app/api-reference/next-config-js/rewrites