在某些情况下,简单的示例可能不够用,您可能希望对服务器进行更多自定义。这时就需要使用懒加载覆盖(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'