关于 next
@emulators/adapter-next 将 API 模拟器嵌入 Next.js App Router 应用,并通过同源路径处理请求,适用于需要在预览部署中测试 OAuth 的开发者。它支持多个模拟服务及可选状态持久化。
主要功能
- 01嵌入 Next.js App Router
- 02同源路径路由多个模拟器
- 03支持可选持久化适配器
- 04自动保存变更请求后的状态
- 05配置 serverless 字体追踪
适用场景
- 01在 Vercel 预览部署中测试 OAuth 回调
- 02将多个 API 模拟器挂载到 Next.js 路由
- 03通过自定义适配器跨冷启动保存状态
- 04本地开发时将状态写入 JSON 文件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ vercel-labs/ emulate --skill next安装后在 Claude Code 中输入/next确认已加载。
详细介绍
Next.js 集成
@emulators/adapter-next 包可将模拟器直接嵌入 Next.js App Router 应用,并使其运行在同一来源上。这对于 Vercel 预览部署尤其有用,因为每次部署都会更改 OAuth 回调 URL。
安装
npm install @emulators/adapter-next @emulators/github @emulators/google
只安装所需的模拟器。每个 @emulators/* 包都独立发布,从而保持无服务器打包体积较小。
Route Handler
创建一个捕获所有请求的路由来处理模拟器流量:
// app/emulate/[...path]/route.ts
import { createEmulateHandler } from '@emulators/adapter-next'
import * as github from '@emulators/github'
import * as google from '@emulators/google'
export const { GET, POST, PUT, PATCH, DELETE, OPTIONS } = createEmulateHandler({
services: {
github: {
emulator: github,
seed: {
users: [{ login: 'octocat', name: 'The Octocat' }],
repos: [{ owner: 'octocat', name: 'hello-world', auto_init: true }],
},
},
google: {
emulator: google,
seed: {
users: [{ email: '[email protected]', name: 'Test User' }],
},
},
},
})
这会创建以下路由:
/emulate/github/**提供 GitHub 模拟器/emulate/google/**提供 Google 模拟器
GitHub 安装令牌元数据可在服务端通过 /emulate/github/_emulate/installation-tokens 获取。
Auth.js / NextAuth 配置
将提供程序指向同一来源上的模拟器路径:
import GitHub from 'next-auth/providers/github'
const baseUrl = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: 'http://localhost:3000'
GitHub({
clientId: 'any-value',
clientSecret: 'any-value',
authorization: { url: `${baseUrl}/emulate/github/login/oauth/authorize` },
token: { url: `${baseUrl}/emulate/github/login/oauth/access_token` },
userinfo: { url: `${baseUrl}/emulate/github/user` },
})
无需预先填充 oauth_apps。未配置任何条目时,模拟器会跳过对 client_id、client_secret 和 redirect_uri 的验证。
无服务器环境的字体追踪
模拟器 UI 页面使用内置字体。请包装 Next.js 配置,将这些字体包含在无服务器追踪中:
// next.config.mjs
import { withEmulate } from '@emulators/adapter-next'
export default withEmulate({
// your normal Next.js config
})
如果将捕获所有请求的路由挂载到自定义路径,请传入匹配的前缀:
export default withEmulate(nextConfig, { routePrefix: '/api/emulate' })
持久化
默认情况下,模拟器状态保存在内存中,并会在每次冷启动时重置。若要在重启后保留状态,请传入 persistence 适配器。
自定义适配器(Vercel KV、Redis 等)
import { createEmulateHandler } from '@emulators/adapter-next'
import * as github from '@emulators/github'
const kvAdapter = {
async load() { return await kv.get('emulate-state') },
async save(data: string) { await kv.set('emulate-state', data) },
}
export const { GET, POST, PUT, PATCH, DELETE, OPTIONS } = createEmulateHandler({
services: { github: { emulator: github } },
persistence: kvAdapter,
})
文件持久化(本地开发)
用于本地开发时,@emulators/core 提供基于文件的适配器:
import { filePersistence } from '@emulators/core'
// persists to a JSON file
persistence: filePersistence('.emulate/state.json'),
GitHub App 种子数据可以省略 private_key。请保留处理程序并调用仅限服务端使用的 generatedSecrets();显式密钥不会被排除在外。请妥善保管持久化快照,并以原子方式实现 initialize。
持久化工作原理
- 冷启动:适配器从持久化适配器加载状态。如果找到状态,则恢复完整的 Store 和令牌映射(跳过种子初始化)。如果未找到,则根据配置填充种子数据并保存初始状态。
- 变更请求后(POST、PUT、PATCH、DELETE):保存状态。保存操作通过内部队列串行执行,以防止竞态条件。
- 未配置持久化:退回到纯内存模式。每次冷启动时都会重新初始化种子数据。
工作原理
- 收到请求:
/emulate/github/login/oauth/authorize?client_id=... - 解析:service =
github,rest =/login/oauth/authorize - 移除前缀:创建一个路径已移除前缀的新
Request,并将其转发给 GitHub 服务应用 - 重写响应:为 HTML
action和href属性、CSSurl()字体引用以及Location标头添加服务前缀 - 持久化:变更请求后,通过持久化适配器保存状态
限制
- 需要使用 Node.js 运行时(而非 Edge),因为模拟器使用
crypto.randomBytes - 并发变更采用最后写入胜出语义。生成的身份要求
initialize在多次冷启动之间以原子方式选择初始快照。
配置参考
createEmulateHandler(config)
| 字段 | 类型 | 描述 |
|---|---|---|
services | Record<string, EmulatorEntry> | 将服务名称映射到模拟器配置 |
persistence? | PersistenceAdapter | 可选的持久化适配器,用于在冷启动之间保存状态 |
每个 EmulatorEntry:
| 字段 | 类型 | 描述 |
|---|---|---|
emulator | EmulatorModule | 模拟器包(例如 import * as github from '@emulators/github') |
seed? | Record<string, unknown> | 符合该服务配置架构的种子数据 |
withEmulate(nextConfig, options?)
包装 Next.js 配置,将模拟器字体文件包含在无服务器输出追踪中。在 next.config.mjs 或 next.config.ts 中,对导出的配置进行包装。
| 选项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
routePrefix | string | "/emulate" | 捕获所有请求的路由所挂载的路径前缀 |
PersistenceAdapter
interface PersistenceAdapter {
load(): Promise<string | null>
save(data: string): Promise<void>
initialize?(data: string): Promise<string>
}
initialize 必须以原子方式创建初始值,或返回另一个实例先创建的值。请使用比较并交换语义实现,例如 Redis SET NX。@emulators/core 提供的内置 filePersistence(path) 可用于本地开发。
自定义模拟器
适配器接受由 emulate 中的 defineEmulator 创建的定义,并将其放在 services[name].emulator 中。CLI 和测试可复用该定义;自定义检查器为可选项。相对于根目录的重定向位置会保留在服务挂载路径下,而自定义 HTML 正文则会原样传递。在测试清理阶段,等待返回的处理程序的 close() 完成。对于部署的定义,请将 emulate 安装为运行时依赖项。有关种子、快照、持久化和生命周期规则,请参阅 https://emulate.dev/docs/custom-emulators。