Skip to content

中间件

中间件(Middleware),一个听起来就很高级、很强大的功能。实际上也确实如此。使用中间件,你可以拦截并控制应用里的所有请求和响应。

比如你可以基于传入的请求,重写、重定向、修改请求或响应头、甚至直接响应内容。一个比较常见的应用就是鉴权,在打开页面渲染具体的内容前,先判断用户是否登录,如果未登录,则跳转到登录页面。

定义

写中间件,你需要在项目的根目录定义一个名为 middleware.js 的文件:

  • 这里的根目录就是与package.json同级别 不是app下面 目录如下:
bash
├── app
   ├── about
   └── page.js
   ├── home
   └── page.js
├── middleware.js
├── package.json
└── tsconfig.json
  • 举例如下
ts
// middleware.js
import { NextResponse } from "next/server";

// 中间件可以是 async 函数,如果使用了 await
export function middleware(request) {
  return NextResponse.redirect(new URL("/home", request.url));
}

// 设置匹配路径
export const config = {
  matcher: "/about/:path*",
};

在这个例子中,我们通过 config.matcher 设置中间件生效的路径,在 middleware 函数中设置中间件的逻辑,作用是将 /about/about/xxx/about/xxx/xxx 这样的的地址统一重定向到 /home

matcher 配置

单独

ts
export const config = {
  matcher: "/about/:path*",
};

matcher 不仅支持字符串形式,也支持数组形式,用于匹配多个路径:

多个

ts
export const config = {
  matcher: ["/about/:path*", "/dashboard/:path*"],
};

正则

命名参数可以使用修饰符,其中 * 表示 0 个或 1 个或多个,?表示 0 个或 1 个,+表示 1 个或多个,比如

  • /about/:path* 匹配 /about/about/xxx/about/xxx/xxx

  • /about/:path? 匹配 /about/about/xxx

  • /about/:path+ 匹配 /about/xxx/about/xxx/xxx

也可以在圆括号中使用标准的正则表达式,比如/about/(.*) 等同于 /about/:path*,比如 /(about|settings) 匹配 /about/settings,不匹配其他的地址。/user-(ya|yu)匹配 /user-ya/user-yu

一个复杂常用的例子:

ts
export const config = {
  matcher: [
    /*
     * 匹配所有的路径除了以这些作为开头的:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    "/((?!api|_next/static|_next/image|favicon.ico).*)",
  ],
};

除此之外,还要注意,路径必须以 /开头。matcher 的值必须是常量,这样可以在构建的时候被静态分析。使用变量之类的动态值会被忽略。

路径参数

ts
export const config = {
  matcher: [
    {
      source: "/api/*",
      has: [
        { type: "header", key: "Authorization", value: "Bearer Token" },
        { type: "query", key: "userId", value: "123" },
      ],
      missing: [{ type: "cookie", key: "session", value: "active" }],
    },
  ],
};

在这个例子中,不仅匹配了路由地址,还要求 headerAuthorization 必须是 Bearer Token,查询参数的 userId 为 123,且 cookie 里的 session 值不是 active

条件语句

第二种方法是使用条件语句:

ts
import { NextResponse } from "next/server";

export function 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));
  }
}

中间件逻辑

ts
export function middleware(request) {
  // 如何读取和设置 cookies ?
  // 如何读取 headers ?
  // 如何直接响应?
}

如何读取和设置 cookies?

用法跟路由处理程序一致,使用 NextRequest 和 NextResponse 快捷读取和设置 cookies。

对于传入的请求,NextRequest 提供了 getgetAllsetdelete 方法处理 cookies,你也可以用 has 检查 cookie 或者 clear 删除所有的 cookies。

对于返回的响应,NextResponse 同样提供了 get、getAll、set 和 delete 方法处理 cookies。示例代码如下:

ts
import { NextResponse } from "next/server";

export function middleware(request) {
  // 假设传入的请求 header 里 "Cookie:nextjs=fast"
  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

  // 设置 cookies
  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: '/' }

  // 响应 header 为 `Set-Cookie:vercel=fast;path=/test`
  return response;
}

在这个例子中,我们调用了 NextResponse.next() 这个方法,这个方法专门用在 middleware 中,毕竟我们写的是中间件,中间件进行一层处理后,返回的结果还要在下一个逻辑中继续使用,此时就需要返回 NextResponse.next()。当然如果不需要再走下一个逻辑了,可以直接返回一个 Response 实例,接下来的例子中会演示其写法。

如何读取和设置 headers?

用法跟路由处理程序一致,使用 NextRequestNextResponse 快捷读取和设置 headers。示例代码如下:

ts
// middleware.js
import { NextResponse } from "next/server";

export function middleware(request) {
  //  clone 请求标头
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set("x-hello-from-middleware1", "hello");

  // 你也可以在 NextResponse.rewrite 中设置请求标头
  const response = NextResponse.next({
    request: {
      // 设置新请求标头
      headers: requestHeaders,
    },
  });

  // 设置新响应标头 `x-hello-from-middleware2`
  response.headers.set("x-hello-from-middleware2", "hello");
  return response;
}

这个例子比较特殊的地方在于调用 NextResponse.next 的时候传入了一个对象用于转发 headers

CORS

这是一个在实际开发中会用到的设置 CORS 的例子:

ts
import { NextResponse } from "next/server";

const allowedOrigins = ["https://acme.com", "https://my-app.org"];

const corsOptions = {
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type, Authorization",
};

export function middleware(request) {
  // Check the origin from the request
  const origin = request.headers.get("origin") ?? "";
  const isAllowedOrigin = allowedOrigins.includes(origin);

  // Handle preflighted requests
  const isPreflight = request.method === "OPTIONS";

  if (isPreflight) {
    const preflightHeaders = {
      ...(isAllowedOrigin && { "Access-Control-Allow-Origin": origin }),
      ...corsOptions,
    };
    return NextResponse.json({}, { headers: preflightHeaders });
  }

  // Handle simple requests
  const response = NextResponse.next();

  if (isAllowedOrigin) {
    response.headers.set("Access-Control-Allow-Origin", origin);
  }

  Object.entries(corsOptions).forEach(([key, value]) => {
    response.headers.set(key, value);
  });

  return response;
}

export const config = {
  matcher: "/api/:path*",
};

如何直接响应?

用法跟路由处理程序一致,使用 NextResponse 设置返回的 Response。示例代码如下:

ts
import { NextResponse } from "next/server";
import { isAuthenticated } from "@lib/auth";

export const config = {
  matcher: "/api/:function*",
};

export function middleware(request) {
  // 鉴权判断
  if (!isAuthenticated(request)) {
    // 返回错误信息
    return new NextResponse(
      JSON.stringify({ success: false, message: "authentication failed" }),
      { status: 401, headers: { "content-type": "application/json" } }
    );
  }
}

执行顺序

在 Next.js 中,有很多地方都可以设置路由的响应,比如 next.config.js 中可以设置,中间件中可以设置,具体的路由中可以设置,所以要注意它们的执行顺序:

注意

  1. headers(next.config.js)
  2. redirects(next.config.js)
  3. 中间件 (rewrites, redirects 等)
  4. beforeFiles (next.config.js 中的 rewrites)
  5. 基于文件系统的路由 (public/, _next/static/, pages/, app/ 等)
  6. afterFiles (next.config.js 中的 rewrites)
  7. 动态路由 (/blog/[slug])
  8. fallback 中的 (next.config.js 中的 rewrites)

注: beforeFiles 顾名思义,在基于文件系统的路由之前,afterFiles 顾名思义,在基于文件系统的路由之后,fallback 顾名思义,垫底执行。

中间件相关配置

skipTrailingSlashRedirect

skipTrailingSlashRedirect 顾名思义,跳过尾部斜杠重定向,当

你设置 skipTrailingSlashRedirect 为 true 后

假设再次访问 /about/,URL 依然会是 /about/

ts
// next.config.js
module.exports = {
  skipTrailingSlashRedirect: true,
};

skipMiddlewareUrlNormalize

可以获取路由原始的地址,常用于国际化场景中。

ts
// next.config.js
module.exports = {
  skipMiddlewareUrlNormalize: true,
};
  • 举例(开启后)
ts
// middleware.js
export default async function middleware(req) {
  const { pathname } = req.nextUrl;

  // GET /_next/data/build-id/hello.json

  console.log(pathname);
  // 如果设置为 true,值为:/_next/data/build-id/hello.json
  // 如果没有配置,值为: /hello
}

注意

注意

这意味着写 中间价 的时候,尽可能使用 Web API,避免使用 Node.js API

中间件分割

工具函数代码如下

  • 有的时候中间件太多了 不好维护 需要拆分
ts
import { NextResponse } from "next/server";

function chain(functions, index = 0) {
  const current = functions[index];
  if (current) {
    const next = chain(functions, index + 1);
    return current(next);
  }
  return () => NextResponse.next();
}

function withMiddleware1(middleware) {
  return async (request) => {
    console.log("middleware1 " + request.url);
    return middleware(request);
  };
}

function withMiddleware2(middleware) {
  return async (request) => {
    console.log("middleware2 " + request.url);
    return middleware(request);
  };
}

export default chain([withMiddleware1, withMiddleware2]);

export const config = {
  matcher: "/api/:path*",
};

调用

ts
import { chain } from "@/lib/utils";
import { withHeaders } from "@/middlewares/withHeaders";
import { withLogging } from "@/middlewares/withLogging";

export default chain([withLogging, withHeaders]);

export const config = {
  matcher: "/api/:path*",
};

具体每一个中间件

ts
export const withHeaders = (next) => {
  return async (request) => {
    // ...
    return next(request);
  };
};