发布日期

第 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 用户,而是能为公司或开源社区贡献基础设施层的人。

学习目标

  1. 列出 Next.js 7 个官方扩展点:custom server、cache handler、adapter、codemod、plugin(webpack/SWC)、middleware、route handlers。
  2. 看懂 NextServer 类的 public 接口(prepare / getRequestHandler / getUpgradeHandler / close),能写一个 Express / Hono 适配。
  3. cacheHandler 接 Redis / S3,知道为什么 default 不够。
  4. 写一个 next-codemod 给团队批量迁移代码。
  5. 知道 adapter 是什么、Vercel / Cloudflare / AWS 怎么各自实现的、自家私有云怎么做。

一、7 个官方扩展点速览

扩展点用途公开 API 稳定度
Custom ServerExpress/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 行核心:

  1. next({ dev }) 创建 NextServer
  2. app.prepare() 启动 router-server + render workers
  3. app.getRequestHandler() 拿到 (req, res, parsedUrl) => Promise<void>

2.3 NextServer 的核心接口

packages/next/src/server/next.ts
export class NextServer implements NextWrapperServer {
  // ...
  getRequestHandler(): RequestHandler { ... }
  getRequestHandlerWithMetadata(meta: RequestMeta): RequestHandler { ... }
  async prepare(serverFields?: ServerFields) { ... }
  async close(): Promise<void> { ... }
}
  • prepare():加载 manifest、启动 worker pool、初始化 IncrementalCache
  • getRequestHandler():每次请求传 (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 podRedis-based 自写
边缘多区域Cloudflare KV / Vercel KV / Upstash
海量长期 cacheS3 + 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(默认)VercelServerless Functions + Edge Functions + Static
@cloudflare/next-on-pagesCloudflare PagesWorkers + KV
@opennextjs/awsAWSLambda + CloudFront + S3
@opennextjs/cloudflareCloudflareWorkers + R2 + KV
@netlify/plugin-nextjsNetlifyFunctions + 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 主要做:

  1. 解析 routes-manifest / app-paths-manifest / middleware-manifest → 得到路由表
  2. 给每条动态路由生成 platform-specific function(Lambda / Worker)
  3. 给静态文件配 CDN
  4. 处理 ISR:用平台原生的 cache(CloudFront / R2)替代 FileSystemCache
  5. 处理 image optimization:调平台原生(Cloudflare Images / Vercel Image / AWS Lambda + sharp)

4.2 写自家 adapter 的关键

如果公司 PaaS 与上述不同(如自研 Serverless),核心步骤:

  1. next build 后用 output: 'standalone' 拿到自包含 server.js
  2. 把 standalone 整个目录 + static 打成镜像
  3. 平台跑 node server.js,HTTP 路由到容器
  4. 平台 CDN 处理 /_next/static/*/public/* 直出
  5. 自家 cache handler 接 PaaS 提供的 KV

或者更激进:

  1. 跑一次 build 得到所有 manifest
  2. 自己写一个 split tool 把每条 route 拆成 isolated handler bundle
  3. 部署到 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.tscookies()/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_cache key 漏 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 preprocess
  • cache-handler.js:教学版 KV cache handler(pretend redis)
  • codemod/replace-console.js:自写 jscodeshift codemod,把 console.log 换 logger.info
  • app/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/

十一、本讲小结

  1. 7 个官方扩展点,按场景挑:90% 业务用 middleware + route handler 足够;只有 platform / framework 层才动 adapter / cache handler。
  2. Custom server 是把双刃剑:失去 Vercel 与 standalone 优势,能不用就不用。
  3. Cache handler 是生产必备:单机用默认,多机必须自写或用平台原生。
  4. Adapter 把 Next.js 跑到任何平台:基于 output: 'standalone' 是最稳路径。
  5. Codemod 是大型团队升级 / 治理代码的杀手锏,jscodeshift 学习成本不高,性价比极高。
  6. Webpack/SWC plugin 是构建期介入,turbopack 时代要重新审视兼容性。

下讲预告

第 40 讲(最后一讲)《给 Next.js 主仓库提 PR:从克隆到合并》。会带你走完一次贡献流程:fork → 找 good-first-issue → 改代码 → 写测试 → 跑 CI → review → merge。并附一份"如何持续追主仓库变更"的长期清单。