AWS
自定义覆盖

在某些情况下,简单的示例可能不够用,您可能希望对服务器进行更多自定义。这时就需要使用懒加载覆盖(lazy loaded overrides)。您可以通过提供一个返回 Promise 的函数来覆盖服务器的任何部分,该 Promise 会解析为覆盖对象。这在您想为服务器添加自定义逻辑(如添加自定义队列或自定义转换器)时非常有用。

💡

如果您使用 edge runtime(无论是在函数中还是通过外部中间件),请务必小心。我们会进行两次 open-next.config.ts 的编译:一次针对 node 环境,一次针对 edge runtime。如果您使用了一些自定义覆盖,可能需要添加:

edgeExternals: ['./customWrapper', './anyOtherOverrideUsed']

到您的 open-next.config.ts 文件中,以避免 edge runtime 尝试编译与其不兼容的覆盖内容。

自定义转换器

有时您可能需要修改 OpenNext 接收到的对象。例如,SST 中的 Config.YOUR_SECRET_KEY 无法在中间件中使用,因此您可能需要将其添加到 headers 中。这时就可以使用自定义转换器。您可以添加自定义转换器来在对象传递给 OpenNext 之前对其进行修改。

在开发环境中,您仍需使用回退值,因为开发服务器不会使用此转换器。

// customConverter.ts
import converter from "@opennextjs/aws/overrides/converters/aws-apigw-v2.js";
import type { Converter } from "@opennextjs/aws/types/overrides.js";
import { Config } from "sst/node/Config";
const mySecretKey = Config.YOUR_SECRET_KEY;
 
export default {
  convertFrom: async (event) => {
    const result = await converter.convertFrom(event);
    return {
      ...result,
      headers: {
        ...result.headers,
        "inserted-in-converter": "1",
        "my-super-secret-key": mySecretKey,
      },
    };
  },
  convertTo: async (intResult) => {
    const result = await converter.convertTo(intResult);
    return {
      ...result,
      headers: {
        ...result.headers,
        "x-converter-end": "1",
      },
    };
  },
  name: "custom-apigw-v2",
} as Converter;
// open-next.config.ts
import type { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";
 
const config = {
  default: {
    override: {
      converter: () => import("./customConverter").then((mod) => mod.default),
    },
  },
} as OpenNextConfig;

自定义包装器

这里我们提供一些自定义包装器的示例。

在中间件中定义全局变量以使用 Node.js

// customWrapper.ts
import defaultWrapper from "@opennextjs/aws/overrides/wrappers/aws-lambda.js";
 
// 这里可以定义一些全局变量
declare global {
  var myApi: () => Promise<number>;
}
globalThis.myApi = async () => {
  const crypto = await import("crypto");
  return {
    nb: crypto.randomInt(0, 100),
  };
};
 
export default defaultWrapper;
// open-next.config.ts
import type { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";
const config = {
  default: {
    override: {
      wrapper: () => import("./customWrapper").then((mod) => mod.default),
    },
  },
} as OpenNextConfig;
export default config;

但由于 Next.js 开发服务器运行在模拟的边缘运行时环境中,且全局变量仅在部署时定义,你需要在中间件中模拟这个全局变量。

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
 
// 如果全局变量不存在,需要在这里模拟它
// 一种避免不同实现问题的方法是创建一个 API 端点
// 该端点使用与你之前定义的全局变量完全相同的逻辑
// 并且仅在开发环境中可用,例如 /api/dev/myApi
if (!globalThis.myApi) {
  globalThis.myApi = async () => {
    return await fetch("http://localhost:3000/api/dev/myApi").then((res) => res.json());
  };
}
 
export function middleware(request: NextRequest) {
  // 你也可以在 API 端点本身发送错误
  // 或者将所有开发端点放在它们自己的 lambda 中
  // 这些 lambda 不会部署到生产环境
  if (request.nextUrl.pathname.startsWith("/api/dev") && process.env.NODE_ENV === "production") {
    return NextResponse("此路由仅在开发环境中可用", {
      status: 500,
    });
  }
  // 现在你可以在中间件中使用 Node.js
  const { nb } = await myApi();
 
  // ... 你的代码在这里
}

使用 middy.js 与 wrapper 结合

// customWrapper.ts
import streamingWrapper from "@opennextjs/aws/overrides/wrappers/aws-lambda.js";
import type { WrapperHandler } from "@opennextjs/aws/types/overrides.js";
import middy from "@middy/core";
import httpSecurityHeaders from "@middy/http-security-headers";
 
const handler: WrapperHandler = async (handler, converter) => {
  const defaultHandler = await streamingWrapper.wrapper(handler, converter);
  return middy().use(httpSecurityHeaders()).handler(defaultHandler);
};
 
export default {
  wrapper: handler,
  name: "custom-aws-lambda",
  supportStreaming: false,
};
// open-next.config.ts
import type { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";
const config = {
  default: {
    override: {
      wrapper: () => import("./customWrapper").then((mod) => mod.default),
    },
  },
} as OpenNextConfig;
export default config;

在预热事件中预加载某些路由

这个示例展示了如何使用自定义 wrapper 在首次请求前预加载一些重要路由。当您有一些在冷启动时加载较慢的路由(Next.js 仅在需要时才懒加载这些路由)时,这个功能特别有用。如果您想向服务器添加一些自定义逻辑(例如向响应添加自定义标头),这个功能也同样适用。

警告:此示例未经充分测试。它仅展示了您可以实现的功能,在生产环境使用前请务必进行充分测试。此外,预加载过多路由可能不是一个好主意。

// customWrapper.ts
import type { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda";
import { Writable } from "node:stream";
 
import { WarmerEvent, WarmerResponse } from "@opennextjs/aws/adapters/warmer-function.js";
import type { StreamCreator } from "@opennextjs/aws/types/open-next.js";
import type { WrapperHandler } from "@opennextjs/aws/types/overrides.js";
 
type AwsLambdaEvent = APIGatewayProxyEventV2 | WarmerEvent;
 
type AwsLambdaReturn = APIGatewayProxyResultV2 | WarmerResponse;
 
const serverId = Math.random().toPrecision(5).toString();
let isPreloaded = false;
 
function formatWarmerResponse(event: WarmerEvent) {
  return new Promise<WarmerResponse>((resolve) => {
    setTimeout(() => {
      resolve({ serverId, type: "warmer" } satisfies WarmerResponse);
    }, event.delay);
  });
}
 
const handler: WrapperHandler =
  async (handler, converter) =>
  async (event: AwsLambdaEvent): Promise<AwsLambdaReturn> => {
    console.log("custom wrapper");
    // 处理预热事件
    if ("type" in event) {
      if (!isPreloaded) {
        // 您可以在此处预加载所有需要的路由
        // 注意:当路由正在预加载时,lambda无法处理其他请求
        await handler({
          type: "core",
          url: "/myRoute",
          method: "GET",
          headers: {},
          query: {},
          rawPath: "/myRoute",
          cookies: {},
          remoteAddress: "",
        });
        isPreloaded = true;
      }
      return formatWarmerResponse(event);
    }
 
    const internalEvent = await converter.convertFrom(event);
    internalEvent.headers["inserted-in-wrapper"] = "hello from wrapper";
 
    // 这是一个临时解决方案,node中存在一个问题会导致如果OpenNextNodeResponse流未被消费,node会静默崩溃
    // 这种情况不会每次都发生,可能是由SSR中的挂起组件引起的(通过<Suspense>或loading.tsx)
    // 任何希望创建自己的wrapper而不使用StreamCreator的人都应该实现此解决方案
    // 如果底层handler不使用OpenNextNodeResponse,则不需要此方案(目前,node运行时服务器和图片服务器使用OpenNextNodeResponse)
    const fakeStream: StreamCreator = {
      writeHeaders: () => {
        return new Writable({
          write: (_chunk, _encoding, callback) => {
            callback();
          },
        });
      },
    };
 
    const response = await handler(internalEvent, { streamCreator: fakeStream });
    response.headers["x-wrapper"] = "hi";
 
    return converter.convertTo(response, event);
  };
 
export default {
  wrapper: handler,
  name: "custom-aws-lambda",
  supportStreaming: false,
};
// open-next.config.ts
import type { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";
const config = {
  default: {
    override: {
      wrapper: () => import("./customWrapper").then((mod) => mod.default),
    },
  },
} as OpenNextConfig;

自定义增量缓存

您可以参考我们提供的 fs-dev (opens in a new tab) 覆盖实现,它使用文件系统来存储增量缓存。您需要创建一个包含以下内容的 open-next.config.ts 文件:

import type { OpenNextConfig } from "@opennextjs/aws/types/open-next.ts";
const config = {
  default: {
    override: {
      // 可以使用我们内置的实现或您自定义的覆盖
      incrementalCache: () => import("./customIncrementalCache").then((mod) => mod.default),
    },
  },
} satisfies OpenNextConfig;
 
export default config;

内置的 (opens in a new tab)实现包括:'s3' | 's3-lite' | 'multi-tier-ddb-s3' | 'fs-dev' | 'dummy'

自定义队列

默认情况下,系统会使用 SQS 队列来重新验证过期的路由。您可以在此阅读更多相关信息。要创建自定义覆盖实现,可以参考我们内置的 (opens in a new tab)实现。您需要创建一个包含以下内容的 open-next.config.ts 文件:

import type { OpenNextConfig } from "@opennextjs/aws/types/open-next.ts";
const config = {
  default: {
    override: {
      // 可以使用我们内置的实现或您自定义的覆盖
      queue: () => import("./customQueue").then((mod) => mod.default),
    },
  },
} satisfies OpenNextConfig;
 
export default config;

内置的 (opens in a new tab)实现包括:'sqs' | 'sqs-lite' | 'direct' | 'dummy'

自定义标签缓存

要覆盖标签缓存功能,可以参考 fs-dev (opens in a new tab) 覆盖实现,它使用了文件系统。更多关于此覆盖的信息可在此查看。你需要创建一个包含以下内容的 open-next.config.ts 文件:

import type { OpenNextConfig } from "@opennextjs/aws/types/open-next.ts";
const config = {
  default: {
    override: {
      // 可以使用我们内置的覆盖实现或你自己的自定义覆盖
      tagCache: () => import("./customTagCache").then((mod) => mod.default),
    },
  },
} satisfies OpenNextConfig;
 
export default config;

内置 (opens in a new tab)的实现包括 'dynamodb' | 'dynamodb-lite' | 'fs-dev' | 'dummy'

自定义源解析器

此覆盖仅由 OpenNext 内部使用,用于在存在 external 中间件时解析请求的源。可以参考我们内置的 pattern-env (opens in a new tab) 覆盖实现。你需要创建一个包含以下内容的 open-next.config.ts 文件:

import type { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";
const config = {
  default: {},
  middleware: {
    // 必须设为 true 才能使用 originResolver
    external: true,
    // 可以使用我们内置的覆盖实现或你自己的自定义覆盖
    originResolver: () => import("./customOriginResolver").then((mod) => mod.default),
  },
} satisfies OpenNextConfig;
 
export default config;

内置 (opens in a new tab)的实现包括 'pattern-env' | 'dummy'

自定义图片加载器

该覆盖项用于图片优化服务器,从自定义源加载图片。您可以参考我们使用文件系统的实现这里 (opens in a new tab)。您需要在 open-next.config.ts 中添加以下配置:

import type { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";
const config = {
  default: {},
  imageOptimization: {
    loader: () => import("./customImageLoader").then((mod) => mod.default),
  },
} satisfies OpenNextConfig;
 
export default config;

内置 (opens in a new tab)的加载器类型包括 's3' | 's3-lite' | 'host' | 'fs-dev' | 'dummy'

自定义预热调用

要实现自定义的预热调用覆盖,可以参考我们的 aws-lambda (opens in a new tab) 实现。您需要在 open-next.config.ts 中添加以下配置:

import type { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";
const config = {
  default: {},
  warmer: {
    invokeFunction: () => import("./customWarmer").then((mod) => mod.default),
  },
} satisfies OpenNextConfig;
 
export default config;

内置 (opens in a new tab)的预热调用类型包括 'aws-lambda' | 'dummy'

自定义 CDN 缓存失效

要实现自定义的 CDN 缓存失效覆盖,您可以参考我们提供的 cloudfront (opens in a new tab) 覆盖实现。您需要创建一个 open-next.config.ts 文件,内容如下:

import type { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";
const config = {
  default: {
    override: {
      cdnInvalidation: () => import("./customCdnInvalidation").then((mod) => mod.default),
    },
  },
} satisfies OpenNextConfig;
 
export default config;

内置 (opens in a new tab)的实现包括 'cloudfront' | 'dummy'

自定义外部请求代理

OpenNext 使用此功能来代理重写后的外部服务请求。您可以在此了解更多。要实现自定义的外部请求代理覆盖,您可以参考我们提供的 fetch (opens in a new tab) 覆盖实现。您需要创建一个 open-next.config.ts 文件,内容如下:

import type { OpenNextConfig } from "@opennextjs/aws/types/open-next.js";
const config = {
  default: {
    override: {
      proxyExternalRequest: () => import("./customProxyExternalRequest").then((mod) => mod.default),
    },
  },
} satisfies OpenNextConfig;
 
export default config;

内置 (opens in a new tab)的实现包括 'fetch' | 'node' | 'dummy'