- 发布日期
第 39 讲:给 Next.js 生态做扩展(custom server / cache handler / adapter / codemod)
开发 custom server、cache handler、adapter 与 codemod 扩展 Next.js 生态
前 38 讲都是"使用 Next.js"和"读懂 Next.js"。本讲反过来:生态扩展点是什么、各自适用什么场景、内部 API 怎么稳定使用。讲完这一讲,你不再只是 Next.js 用户,而是能为公司或开源社区贡献基础设施层的人。
学习目标
- 列出 Next.js 7 个官方扩展点:custom server、cache handler、adapter、codemod、plugin(webpack/SWC)、middleware、route handlers。
- 看懂
NextServer类的 public 接口(prepare/getRequestHandler/getUpgradeHandler/close),能写一个 Express / Hono 适配。 - 用
cacheHandler接 Redis / S3,知道为什么 default 不够。 - 写一个 next-codemod 给团队批量迁移代码。
- 知道 adapter 是什么、Vercel / Cloudflare / AWS 怎么各自实现的、自家私有云怎么做。
一、7 个官方扩展点速览
| 扩展点 | 用途 | 公开 API 稳定度 |
|---|---|---|
| Custom Server | Express/Koa/Hono 接管 HTTP 层 | 高(next package 顶层导出) |
| Cache Handler | 替换 IncrementalCache 的存储后端 | 高(next.config.js 配置) |
| Adapter | 把 Next.js 输出适配到 Vercel/Cloudflare/AWS/自家平台 | 中(output: 'standalone' + 文档约定) |
| Codemod | 自动改用户代码(升级、API 迁移) | 高(独立 npm 包) |
| Webpack/SWC Plugin | 加构建时 transform | 中(不保证主版本兼容) |
| Middleware | 请求级 hook | 高(Web API) |
| Route Handler | 自定义 HTTP endpoint | 高(File-based) |
中两个是"生态友好",高的是"显式稳定"。下面逐个讲。
二、Custom Server
2.1 使用场景
你需要:
- 在请求到达 Next.js 之前做特殊处理(自定义 logging、auth header injection、rate limit)
- 把 Next.js 嵌入到已有 Express/Koa/Hono 项目里
- 实现 Next.js 不支持的 WebSocket 端点
- 共享同一个进程做 cron job / 后台 worker
⚠️ 取舍:用 custom server 会失去 Vercel 部署支持 + 大量优化(standalone、edge 部署等)。能不用就不用。
2.2 最小例子
// server.ts
import next from "next";
import { createServer } from "node:http";
import { parse } from "node:url";
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const handle = app.getRequestHandler();
await app.prepare();
const server = createServer((req, res) => {
const parsed = parse(req.url ?? "/", true);
// 这里可以加任何前置逻辑
handle(req, res, parsed);
});
server.listen(3000);
3 行核心:
next({ dev })创建 NextServerapp.prepare()启动 router-server + render workersapp.getRequestHandler()拿到(req, res, parsedUrl) => Promise<void>
2.3 NextServer 的核心接口
export class NextServer implements NextWrapperServer {
// ...
getRequestHandler(): RequestHandler { ... }
getRequestHandlerWithMetadata(meta: RequestMeta): RequestHandler { ... }
async prepare(serverFields?: ServerFields) { ... }
async close(): Promise<void> { ... }
}
prepare():加载 manifest、启动 worker pool、初始化 IncrementalCachegetRequestHandler():每次请求传(req, res, parsedUrl)close():graceful shutdown(第 29 讲讲过)
2.4 Hono / Express 适配模式
Express:
import express from "express";
import next from "next";
const app = next({ dev: false });
await app.prepare();
const handle = app.getRequestHandler();
const server = express();
server.use("/legacy/*", legacyHandler); // 老路由保留
server.all("*", (req, res) => handle(req, res));
server.listen(3000);
Hono(HTTP/Node adapter):
import { Hono } from "hono";
import { serve } from "@hono/node-server";
import next from "next";
const nextApp = next({ dev: false });
await nextApp.prepare();
const handle = nextApp.getRequestHandler();
const app = new Hono();
app.all("*", async (c) => {
await handle(c.req.raw as any, c.res as any);
});
serve({ fetch: app.fetch, port: 3000 });
2.5 WebSocket 升级
const server = createServer(...)
// HTTP 走 Next.js
server.on('request', (req, res) => handle(req, res))
// WebSocket 自己处理
server.on('upgrade', async (req, socket, head) => {
if (req.url?.startsWith('/api/ws')) {
myWsServer.handleUpgrade(req, socket, head, ...)
} else {
// 把其它 upgrade 交给 next(HMR 用)
const upgradeHandler = app.getUpgradeHandler()
upgradeHandler(req, socket, head)
}
})
dev 模式下 Next.js 用 WebSocket 做 HMR,所以必须正确转交 getUpgradeHandler(),否则 dev 不工作。
三、Cache Handler
第 30 讲讲过用法,这里讲设计原则。
3.1 5 种典型场景
| 业务 | 选哪个 cache handler |
|---|---|
| 单实例小项目 | 默认 FileSystemCache 即可 |
| Vercel 部署 | Vercel 自动接管,无需配 |
| 多 K8s pod | Redis-based 自写 |
| 边缘多区域 | Cloudflare KV / Vercel KV / Upstash |
| 海量长期 cache | S3 + DynamoDB metadata |
3.2 接口契约要点
第 30 讲贴过 CacheHandler 接口。3 个细节官方文档不强调:
1. set 必须幂等:相同 key 重复 set 等价。
2. revalidateTag 必须对所有实例可见:意味着不能只在 in-memory Map 里改。
3. get 返回 null != cache miss:返回 null 时下游可能仍然走 render → set;但返回 stale value 时下游不再 render(stale-while-revalidate)。
3.3 调试技巧
NEXT_PRIVATE_DEBUG_CACHE=true 开启 cache 路径的所有 log,能看到每个 key 的 GET/SET。
四、Adapter:把 Next.js 跑到任何平台
Adapter 是把 next build 输出转换为目标平台可部署格式的工具。常见:
| Adapter | 目标平台 | 形态 |
|---|---|---|
| Vercel(默认) | Vercel | Serverless Functions + Edge Functions + Static |
@cloudflare/next-on-pages | Cloudflare Pages | Workers + KV |
@opennextjs/aws | AWS | Lambda + CloudFront + S3 |
@opennextjs/cloudflare | Cloudflare | Workers + R2 + KV |
@netlify/plugin-nextjs | Netlify | Functions + CDN |
| custom | 自家 PaaS | 自定义 runtime |
4.1 Adapter 工作流程
next build
↓
.next/
├── server/ ← server bundles
├── static/ ← public + _next/static
├── standalone/ ← 自包含 server(如果 output: 'standalone')
├── server/app/ ← per-page HTML/RSC for prerendered
└── *manifest.json ← 各种 metadata
↓
adapter 读取 manifest + 文件
↓
adapter 把每条 route 转成目标平台的 handler
↓
deploy
adapter 主要做:
- 解析 routes-manifest / app-paths-manifest / middleware-manifest → 得到路由表
- 给每条动态路由生成 platform-specific function(Lambda / Worker)
- 给静态文件配 CDN
- 处理 ISR:用平台原生的 cache(CloudFront / R2)替代 FileSystemCache
- 处理 image optimization:调平台原生(Cloudflare Images / Vercel Image / AWS Lambda + sharp)
4.2 写自家 adapter 的关键
如果公司 PaaS 与上述不同(如自研 Serverless),核心步骤:
next build后用output: 'standalone'拿到自包含 server.js- 把 standalone 整个目录 +
static打成镜像 - 平台跑
node server.js,HTTP 路由到容器 - 平台 CDN 处理
/_next/static/*、/public/*直出 - 自家 cache handler 接 PaaS 提供的 KV
或者更激进:
- 跑一次 build 得到所有 manifest
- 自己写一个 split tool 把每条 route 拆成 isolated handler bundle
- 部署到 FaaS
参考 @opennextjs/aws 的源码(opennext.com),它示范了如何把 Next.js 完整 build 输出转成 AWS Lambda + CloudFront 部署。
4.3 Standalone vs custom server
| output: 'standalone' | custom server | |
|---|---|---|
| 用 NextServer API 直接 | ❌ 内部用 server.js | ✅ 显式 |
| Vercel 兼容 | ✅ | ❌ |
| Edge support | ✅ | ❌ |
| WebSocket | ✅ 通过 upgrade handler | ✅ 自由 |
| HMR (dev) | ❌ standalone 是 prod | ✅ |
写 adapter 推荐走 standalone,除非平台不支持 Node.js 长进程。
五、Codemod
5.1 next-codemod 包
npx @next/codemod@latest <transform> <path>
next 团队官方 codemod 都在这里:
packages/next-codemod/transforms/
├── built-in-next-font.ts ← 把社区 next/font 迁移到内置
├── new-link.ts ← v12 → v13 Link 改造
├── next-async-request-api.ts ← cookies()/headers() 改异步
├── next-og-import.ts ← @vercel/og → next/og
├── next-image-experimental.ts ← next/image-experimental 迁移
└── ...
5.2 用 jscodeshift 自写一个
@next/codemod 内部用 jscodeshift(Facebook 的 codemod 工具)。
最小例子:把项目里所有 useRouter from 'next/router' 改成 from 'next/navigation':
// my-codemod.js
module.exports = function (file, api) {
const j = api.jscodeshift;
const root = j(file.source);
root
.find(j.ImportDeclaration, { source: { value: "next/router" } })
.forEach((p) => {
p.node.source.value = "next/navigation";
});
return root.toSource();
};
npx jscodeshift -t my-codemod.js src/
可以批量改 1000 个文件,比手工或 grep+sed 安全(AST 解析,不会误伤字符串)。
5.3 团队级实用 codemod
每次大版本升级(v13 → v14 → v15)总有 breaking change。你团队也可以写自家的:
- 公司 internal
@my-org/api改 import path - 把所有
console.log换成logger.info(接入第 34 讲的可观测性) - 把
revalidate = 60抽到 const、加 doc 注释 - 检测危险模式(如第 38 讲案例 2:
unstable_cachekey 漏 userId)→ 报警或自动修复
六、Webpack / SWC plugin
6.1 Webpack plugin
next.config.js:
module.exports = {
webpack(config, { isServer, dev, webpack }) {
if (!isServer) {
config.plugins.push(
new webpack.DefinePlugin({
"process.env.BUILD_TIME": JSON.stringify(new Date().toISOString()),
}),
);
}
return config;
},
};
用途:
- 加自定义 plugin(如
@sentry/webpack-plugin上传 source map) - 改 alias、加 fallback、改 module rules
- 注入构建期常量
⚠️ Turbopack 不支持 webpack plugin。如果项目用 turbopack,需要重写为 turbopack rules(功能有限)。
6.2 SWC plugin
module.exports = {
experimental: {
swcPlugins: [
['@swc/plugin-styled-components', { ... }],
['my-swc-plugin', { ... }],
],
},
}
SWC plugin 是 Rust 编译成 wasm 的。门槛较高,但能在源码 AST 级做转换(比 webpack loader 早)。第 25 讲讲过 next-custom-transforms 就是这种 plugin。
七、Middleware 与 Route Handlers
这两类已经在前面讲过:
- Middleware(第 21 讲、31 讲、32 讲):请求级 hook,跑在 Edge runtime
- Route Handlers(第 22 讲):自定义 HTTP endpoint,支持 Node / Edge runtime
它们也是"扩展点"——而且是最稳定、最被推荐的扩展方式:
- 公司业务做自定义鉴权 → middleware
- 暴露 webhook、健康检查、SSE → route handler
- 反向代理外部服务 → middleware rewrite
八、扩展点选择决策树
我想加一个能力
├── 是请求级前置处理?
│ └─ Yes → middleware
├── 是 HTTP endpoint?
│ └─ Yes → route handler
├── 是改用户代码?
│ └─ Yes → codemod
├── 是构建期 transform?
│ ├─ AST 级源码改写 → SWC plugin(如果用 turbopack)/ webpack loader
│ └─ 普通插件 → webpack plugin
├── 是替换 cache 存储?
│ └─ Yes → cache handler
├── 是接入 HTTP 协议层(WS / 共享进程)?
│ └─ Yes → custom server
└── 是适配到新部署平台?
└─ Yes → adapter(基于 output: 'standalone')
九、生态贡献清单
学完想真正参与生态?这些方向都是 high impact:
| 方向 | 例子 |
|---|---|
| 自家 PaaS adapter | 类似 @my-paas/next-adapter |
| Redis cache handler 改进 | 加 multi-region 支持 |
| 中文社区 codemod | 公司内部规范自动迁移 |
| Webpack plugin | 集成自家 i18n / analytics |
| 文档翻译 | nextjs.org 中文 |
| Bug fix | 找 good-first-issue label |
| 性能改进 PR | 第 36 讲讲的 V8 优化 |
十、配套 fixture
fixtures/lecture-39/ 提供 3 个扩展点示例:
server.ts:custom server with Express style preprocesscache-handler.js:教学版 KV cache handler(pretend redis)codemod/replace-console.js:自写 jscodeshift codemod,把 console.log 换 logger.infoapp/api/health/route.ts&middleware.ts:稳定扩展点示例
启动:
cd learning/nextjs-40-lectures/fixtures/lecture-39
pnpm install
# 1. 试试 custom server
pnpm tsx server.ts
# 2. 试 codemod
pnpm jscodeshift -t codemod/replace-console.js app/
十一、本讲小结
- 7 个官方扩展点,按场景挑:90% 业务用 middleware + route handler 足够;只有 platform / framework 层才动 adapter / cache handler。
- Custom server 是把双刃剑:失去 Vercel 与 standalone 优势,能不用就不用。
- Cache handler 是生产必备:单机用默认,多机必须自写或用平台原生。
- Adapter 把 Next.js 跑到任何平台:基于
output: 'standalone'是最稳路径。 - Codemod 是大型团队升级 / 治理代码的杀手锏,jscodeshift 学习成本不高,性价比极高。
- Webpack/SWC plugin 是构建期介入,turbopack 时代要重新审视兼容性。
下讲预告
第 40 讲(最后一讲)《给 Next.js 主仓库提 PR:从克隆到合并》。会带你走完一次贡献流程:fork → 找 good-first-issue → 改代码 → 写测试 → 跑 CI → review → merge。并附一份"如何持续追主仓库变更"的长期清单。