
一句话:Edge Runtime 不是「Node.js 的另一个版本」,而是「一个基于 V8 isolate 的、只有 Web 标准 API 的轻量运行时」——它的价值是「冷启动 5ms 级 + 就近部署」,而它的约束(没有 fs/net/child_process、内存与 CPU 时间受限、不保证实例存活)决定了「它只适合『轻量的请求处理』」。
一、本质:V8 isolate ≠ Node.js 容器
⭐⭐⭐ 「Edge Runtime 是什么」的一句话版本:
它是「**把一段 JS 跑在离用户最近的边缘节点上,且用 V8 isolate(而不是容器)隔离**」的运行时。
⭐ 所以它不是「Node.js 的一个子集」,而是「**另一套宿主环境**」:
· ✅ 只有 **Web 标准 API**(fetch / Request / Response / crypto / streams…)
· ❌ 没有 **Node 内置模块**(fs / path / net / child_process / crypto(node)…)
· ❌ 没有 **原生扩展**(任何「需要编译原生代码」的依赖都跑不了)
⭐ 于是「能不能用 Edge」这个问题的答案通常是:
· ✅ 能:「鉴权、重定向、A/B 分桶、地理路由、抓取+改写、轻量 API、流式响应」
· ❌ 不能:「读写文件、连传统的 TCP 数据库、用原生依赖、跑长任务」
⭐⭐⭐ 与 Node.js Runtime 的对比(这是最该背下来的一张表):
| 维度 | Node.js Runtime | Edge Runtime |
| --- | --- | --- |
| 启动 | ⚠️ 容器/进程(**冷启动 100ms~1s**) | ⭐ **V8 isolate(冷启动约 5ms)** |
| 位置 | 固定区域(如 `us-east-1`) | ⭐ **就近(全球边缘节点)** |
| 可用 API | ⭐ 全部 Node 内置 + npm 生态 | ⚠️ **只有 Web 标准 API** |
| `fs`/`net`/`child_process` | ✅ | ❌ **完全没有** |
| 内存 | 较大(可配到 GB 级) | ⚠️ **受限**(如 128MB) |
| 执行时间 | 较长(可配几十秒到几分钟) | ⚠️ **受限**(CPU 时间通常几秒;墙钟时间看平台) |
| 实例存活 | ✅ 可以「常驻」 | ⚠️ **不保证**(会被回收,**不要依赖内存里的全局状态**) |
| 长连接 | ✅(WebSocket 服务端、TCP) | ⚠️ 受限(部分平台支持 WS,但**没有 TCP 客户端**) |
| 传统数据库驱动 | ✅(TCP 连接池) | ❌ **无 TCP** → 必须用 **HTTP 驱动**或**连接池代理** |
| 依赖体积 | 大(node_modules) | ⚠️ **越小越好**(影响冷启动/部署) |
| 适合 | ⭐ 「重活」:SSR 渲染、复杂 API、DB 密集 | ⭐ **「轻活」:中间件、鉴权、路由、改写** |
二、Edge 能用的 API:白名单(必须记)
⭐⭐⭐ 「Edge Runtime 有什么」——⭐ 背这张清单比背「没有什么」有用:
✅ 【Web 标准(核心)】
· `fetch`、`Request`、`Response`、`Headers`、`FormData`、`URL`、`URLSearchParams`
· `ReadableStream` / `WritableStream` / `TransformStream`、`TextEncoder`/`TextDecoder`
· `AbortController` / `AbortSignal`、`structuredClone`、`Blob` / `File`
· ⭐ **`crypto`(WebCrypto)**:`crypto.subtle`、`crypto.randomUUID`、`crypto.getRandomValues`
· `atob` / `btoa`、`setTimeout` / `setInterval` / `queueMicrotask`、`console`
· ⭐ `Cache` API(`caches.open()`,见 10.11)
✅ 【平台扩展(看具体平台)】
· ⭐ **`waitUntil(promise)`**(Vercel/Cloudflare)——「**响应返回后继续做的工作**」
⭐ 这是「边缘里做『异步副作用』(埋点/日志/缓存写)」的正确方式
· `after()`(Next.js 15 的新 API,语义与 `waitUntil` 类似)
· 平台专有:KV / D1 / R2 / Queues(Cloudflare)、`geolocation`、`request.cf`(Cloudflare 的地理信息)
❌ 【没有】
· `fs` / `path` / `os` / `net` / `tls` / `dns` / `child_process` / `worker_threads`
· ⚠️ **Node 的 `crypto` 模块**(但有 WebCrypto,⭐ 注意「API 完全不同」)
· `Buffer`(⭐ 有些平台提供 polyfill,但**不要依赖**;用 `Uint8Array`/`TextEncoder`)
· ⚠️ **`process`**(有些字段只读或被 polyfill;⭐ 别用它做「读环境变量之外的事」)
· ❌ **`eval` / `new Function`**(⭐ CSP 默认禁止;Cloudflare 明确不支持)
· ⚠️ **「请求结束后还在跑的定时器」**(会被杀掉;要延续就 `waitUntil`)
/** ⭐⭐ 一个「只在 Edge 能写」的中间件:鉴权 + A/B 分桶 + 地理路由 + 改写 */
// middleware.ts(Next.js 的 Edge Middleware,跑在**所有请求之前**)
import { NextResponse, type NextRequest } from 'next/server';
export const config = {
// ⭐ 声明「哪些路径走中间件」(⚠️ 中间件「每个请求都跑」,要尽量减少匹配范围)
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};
/** ⭐ 一致性哈希分桶(⭐ 与 8.9 的算法完全一致 —— 这一点极其重要) */
function bucketOf(flagKey: string, userId: string): number {
let hash = 0x811c9dc5;
const input = `${flagKey}:${userId}`;
for (let i = 0; i < input.length; i++) {
hash ^= input.charCodeAt(i);
hash = Math.imul(hash, 0x01000193) >>> 0;
}
return (hash % 10_000) / 10_000;
}
export async function middleware(request: NextRequest) {
const { pathname, origin } = request.nextUrl;
/** ============ ① 鉴权(⭐ Edge 最典型的用途:把「拦截」放到离用户最近处) ============ */
const session = request.cookies.get('session')?.value;
if (pathname.startsWith('/app') && !session) {
const loginUrl = new URL('/login', origin);
// ⭐ 带上「我想去哪」(⭐ 服务端拼接,天然防开放重定向 —— 因为「只拼站内路径」)
loginUrl.searchParams.set('redirect', pathname + request.nextUrl.search);
return NextResponse.redirect(loginUrl);
}
// ⭐ 注意:Edge 里「验证 session 签名」只能做「轻量校验」
// (用 WebCrypto 验签,⭐ 或「只检查 cookie 存在性 + 让下游做真正鉴权」)
if (pathname.startsWith('/app') && session) {
const valid = await verifySessionSignature(session);
if (!valid) {
const res = NextResponse.redirect(new URL('/login', origin));
res.cookies.delete('session');
return res;
}
}
/** ============ ② A/B 分桶(⭐ 在边缘做,用户「第一次请求」就拿到正确版本) ============ */
let response: NextResponse;
if (pathname === '/') {
// ⭐ 用「登录用户 id 或匿名 cookie」做分桶(⭐ 保证「同一用户稳定同组」)
const userId = session ? extractUserId(session) : (request.cookies.get('anon_id')?.value ?? crypto.randomUUID());
const variant = bucketOf('release_new_home', userId) < 0.5 ? 'b' : 'a';
// ⭐⭐ 关键:把「变体」写进请求头 → 下游的 Server Component / RSC 直接读它
// (⭐ 这样服务端「渲染时就出正确的版本」→ **零闪烁**,见 8.9)
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-ab-variant', variant);
response = NextResponse.next({ request: { headers: requestHeaders } });
// ⭐ 首次访问时「种下匿名 id」(⭐ 这样后续请求分桶稳定)
if (!request.cookies.get('anon_id')) {
response.cookies.set('anon_id', userId, {
httpOnly: true, sameSite: 'lax', maxAge: 60 * 60 * 24 * 365,
secure: process.env.NODE_ENV === 'production',
});
}
} else {
response = NextResponse.next();
}
/** ============ ③ 地理路由(⭐ 边缘的独特能力:拿到「就近节点」的地理信息) ============ */
// ⚠️ 「地理信息」的来源是「平台提供的头/属性」(⚠️ 不要「前端猜」)
// Vercel: request.geo;Cloudflare: request.cf.country
const country = (request as unknown as { geo?: { country?: string } }).geo?.country;
if (country && pathname.startsWith('/shop')) {
// ⭐ 把「地区」注入请求头(下游据此「选货币/语言/合规提示」)
response.headers.set('x-user-country', country);
}
/** ============ ④ 安全头(⭐ 中间件是「统一加安全头」的最佳位置) ============ */
const securityHeaders: Record<string, string> = {
'x-content-type-options': 'nosniff',
'referrer-policy': 'strict-origin-when-cross-origin',
'x-frame-options': 'SAMEORIGIN',
// ⚠️ CSP 要小心(⭐ 它会「影响 Worker / 内联脚本」,见 8.10)
};
for (const [k, v] of Object.entries(securityHeaders)) response.headers.set(k, v);
/** ============ ⑤ 边缘缓存(⭐ 让「可缓存的公开页面」在边缘直接命中) ============ */
if (pathname.startsWith('/blog/')) {
// ⭐⭐ 「边缘缓存」的正确姿势:让 CDN/平台缓存「这个响应」
// ⚠️ 但「带 cookie 的个性化页面」不能这样(⭐ 会「串用户」)
const isPersonalized = request.cookies.has('session');
if (!isPersonalized) {
response.headers.set('cache-control', 'public, s-maxage=300, stale-while-revalidate=600');
}
}
return response;
}
/** ⭐ Edge 里的「验签」(用 WebCrypto,⚠️ 不是 Node 的 crypto) */
async function verifySessionSignature(token: string): Promise<boolean> {
const [payload, signature] = token.split('.');
if (!payload || !signature) return false;
const secret = process.env.SESSION_SECRET;
if (!secret) return false;
// ⭐ WebCrypto 的 HMAC(⭐ 与 Node 的 API 完全不同,这是「Edge 迁移」的常见改动点)
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['verify']
);
const sigBytes = Uint8Array.from(atob(signature.replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0));
return crypto.subtle.verify('HMAC', key, sigBytes, new TextEncoder().encode(payload));
}
function extractUserId(token: string): string {
// ⚠️ 真实实现要「验签后解析」(这里只做「形态示意」)
try {
const payload = JSON.parse(atob(token.split('.')[0] ?? '')) as { sub?: string };
return payload.sub ?? 'anon';
} catch {
return 'anon';
}
}
三、Edge 的四条「硬约束」与应对
⭐⭐⭐ 约束 ①【没有 TCP → 传统数据库驱动用不了】
⚠️ 「在 Edge 里 `new PrismaClient()`」→ 直接失败(Prisma 需要 TCP)
✅ 三条出路:
· ⭐ **HTTP 驱动**:Neon serverless driver(`@neondatabase/serverless`)、
PlanetScale HTTP、Turso(libSQL over HTTP)、Drizzle 的 HTTP 模式
· ⭐ **连接池代理**:Prisma Accelerate / Data Proxy、Supabase Supavisor
(⭐ 「边缘 → 代理(HTTP/WS)→ 数据库(TCP)」,代理负责连接池)
· ⭐ **不在 Edge 里连库**:Edge 只做「鉴权/路由/改写」,真正的数据请求
「转发给 Node runtime 或后端服务」(⭐ 这是最稳的做法)
⭐⭐⭐ 约束 ②【不保证实例存活 → 不要依赖「内存里的全局状态」】
⚠️ 「把计数器放在模块级变量里」→ 在 Edge 上「不可靠」
(⭐ 因为「不同请求可能落在不同 isolate」+「实例会被回收」)
✅ 需要「共享状态」就用:
· 平台 KV(Cloudflare KV / Vercel KV)
· Durable Objects(Cloudflare,⭐ 能做「有状态」的强一致)
· 外部 Redis(⚠️ 需要 HTTP 接口)
⭐ 判据:「**这段状态需要跨请求/跨实例共享吗**」——需要就不该放内存。
⭐⭐⭐ 约束 ③【CPU 时间与内存受限 → 不适合「重活」】
⚠️ 「解析大文件、跑复杂计算、生成大报告」→ 会「超时」或「OOM」
✅ 这些交给:
· Node runtime(Serverless Function,可配更长超时与更大内存)
· ⭐ **后台任务/队列**(如「提交任务 → 异步 Worker 处理 → 轮询/推送结果」)
· ⭐ 前端 Worker(见 8.10,⭐ 如果「计算可以在客户端做」)
⭐⭐⭐ 约束 ④【依赖要「越小越好」】
⚠️ 每个 Edge 函数的代码都会「被打包并分发到全球节点」→
依赖越大,「部署时间」与「冷启动」都变差
✅ 做法:
· ⭐ 「按需 import」(避免 barrel 文件,见 10.14)
· 用「Web 标准」的轻量库(如 `jose` 做 JWT,而不是 `jsonwebtoken`)
· ⭐ 把「重依赖」留给 Node runtime 的接口
/** ⭐⭐ 一个「Edge 抓取 + 改写」的完整例子(这是 Edge 最典型的强项) */
// app/api/proxy/[slug]/route.ts
export const runtime = 'edge'; // ⭐ 声明「这个 route 跑在 Edge」
// ⚠️ 如果没有这行,默认是 Node runtime(Next.js 里)
export async function GET(
request: Request,
{ params }: { params: Promise<{ slug: string }> }
) {
const { slug } = await params;
// ⭐ ① 边缘读取缓存(⭐ Cache API 在所有 Edge 平台可用)
const cache = await caches.open('proxy-v1');
const cacheKey = new Request(new URL(request.url), { method: 'GET' });
const cached = await cache.match(cacheKey);
if (cached) {
// ⭐ 加一个头表明「来自缓存」(便于观测命中率,见 7.13)
const headers = new Headers(cached.headers);
headers.set('x-edge-cache', 'HIT');
return new Response(cached.body, { status: cached.status, headers });
}
// ⭐ ② 抓取上游(⭐ Edge 里 `fetch` 是「一等公民」)
const upstream = await fetch(`https://origin.example.com/content/${encodeURIComponent(slug)}`, {
headers: {
// ⭐ 透传「必要的头」(⚠️ 不要「无脑透传所有头」——可能带敏感信息)
'user-agent': request.headers.get('user-agent') ?? '',
// ⭐ 透传「追踪 id」→ 让前后端链路串起来(见 7.13)
traceparent: request.headers.get('traceparent') ?? '',
},
signal: AbortSignal.timeout(8000), // ⭐ Edge 里也要「设超时」
});
if (!upstream.ok) {
return new Response(JSON.stringify({ error: 'upstream failed' }), {
status: 502,
headers: { 'content-type': 'application/json' },
});
}
// ⭐ ③ 流式改写(⭐ 这是 Edge 的「独门能力」:不缓冲整个响应就能改写)
const contentType = upstream.headers.get('content-type') ?? '';
if (!contentType.includes('text/html') || !upstream.body) {
return new Response(upstream.body, { status: upstream.status, headers: upstream.headers });
}
// ⭐⭐ 用 TransformStream「边流边改」(⭐ 用户「更早看到内容」)
const rewriter = new TransformStream<Uint8Array, Uint8Array>({
transform(chunk, controller) {
let text = new TextDecoder().decode(chunk);
// ⭐ 典型改写:注入「边缘算出来的」信息 / 替换图片 CDN / 加埋点
text = text.replace('</head>', `<script>window.__EDGE__={region:"${geoOf(request)}"}</script></head>`);
controller.enqueue(new TextEncoder().encode(text));
},
});
const transformed = upstream.body.pipeThrough(rewriter);
const response = new Response(transformed, {
status: 200,
headers: {
'content-type': 'text/html; charset=utf-8',
// ⭐ 边缘缓存(⚠️ 「个性化页面」不能这样)
'cache-control': 'public, s-maxage=300, stale-while-revalidate=600',
'x-edge-cache': 'MISS',
},
});
// ⭐⭐ 关键:**「响应返回后」再写缓存**(⭐ 不阻塞用户看到内容)
// ⚠️ 用 `waitUntil` 而不是「await 缓存写入」——否则「用户要多等」
const cacheWrite = cache.put(cacheKey, response.clone());
// Cloudflare/Vercel 提供 waitUntil;Next.js 15 提供 `after()`
const ctx = (request as unknown as { waitUntil?: (p: Promise<unknown>) => void });
if (typeof ctx.waitUntil === 'function') ctx.waitUntil(cacheWrite);
else void cacheWrite;
return response;
}
function geoOf(request: Request): string {
// ⚠️ 地理信息「来自平台」(不同平台字段不同)
return (request as unknown as { cf?: { country?: string } }).cf?.country ?? 'unknown';
}
⭐⭐⭐ 这个例子里的四个「Edge 特有」的能力(Node runtime 做不到或更贵):
① 【「就近」执行】
用户在日本 → 请求落在东京边缘节点 → ⭐ 抓上游/改写/返回都在东京(少一次跨洋 RTT)
② 【「流式改写」】⭐
`body.pipeThrough(new TransformStream(...))` —— **不缓冲整个 HTML 就能改写**
⚠️ 这在 Node 里也能做(Node Stream),但在 Edge 里「更轻」(⭐ 且「冷启动」优势明显)
③ 【「响应后继续工作」】⭐
`waitUntil(cache.put(...))` —— ⭐ **「写缓存」不阻塞用户**
⚠️ 这是 Edge 里做「异步副作用」(写缓存/埋点/日志)的**唯一正确方式**
(因为「请求结束后,普通的定时器/Promise 可能被直接杀掉」)
④ 【「Cache API」】
`caches.open()` 是 Web 标准(⭐ Service Worker 里也有,见 10.11)→
⭐ 「边缘缓存」与「浏览器 Service Worker 缓存」用同一套 API(可复用代码)
四、选型:什么时候该用 Edge
⭐⭐⭐ 「该不该用 Edge」的四个判据(⭐ 按「是否命中」计数):
✅ 该用(命中 2 条以上):
① ⭐ **延迟敏感 + 需要「就近执行」**(鉴权、重定向、A/B、地理路由、改写)
② ⭐ **只是「中间层」**(不读文件、不连传统库、不做重计算)
③ ⭐ **需要「流式」处理**(SSE、流式改写、AI 流式输出)
④ ⭐ **全球用户 + 想要「冷启动低」**(⭐ 首字节时间敏感)
❌ 不该用(命中任意一条就要谨慎):
① **需要「传统数据库」**(除非有 HTTP 驱动/代理)
② **需要「原生依赖」**(sharp、bcrypt、canvas 等 → ⭐ 直接跑不了)
③ **重计算/长任务**(PDF 生成、大数据聚合、图像处理)
④ **需要「常驻内存状态」**(连接池、内存缓存、速率限制计数器)
⑤ ⚠️ **「想用 Edge 因为它「快」」但「其实只是 API 转发」**——
⭐ 如果边缘函数只是「把请求转给后端」,那「省下的 RTT」可能被
「边缘 → 后端」的那一跳抵消(⭐ 要算清楚)
⭐⭐⭐ 一个「混合架构」的实践建议(⭐ 这是生产里最稳的做法):
① 【Edge Middleware】—— 跑在「所有请求之前」,只做「轻活」:
鉴权(轻量)/ 重定向 / A/B 分桶 / 地理注入 / 安全头 / 部分缓存
⭐ 关键:**「不做数据访问」**(除了「读 KV」这类边缘原生的存储)
② 【Node Runtime】—— 做「重活」:
SSR 渲染(需要大量依赖)/ DB 密集的 API / 文件处理 / 长任务
③ 【前端 Worker】—— 做「客户端的重活」(见 8.10)
④ ⭐ **「按 route 声明 runtime」**(Next.js 的 `export const runtime = 'edge' | 'nodejs'`)
→ ⭐ 「一个项目里混用」(不必「全站二选一」)
五、本篇特有的坑
// ① 在 Edge 里 import Node 内置模块(构建期就失败,或运行时报错)
import fs from 'node:fs'; // ❌
// ✅ Edge 里「只 import Web 标准 + 明确支持的库」
// ② 在 Edge 里用「Node 的 crypto」(API 完全不同)
import { createHmac } from 'node:crypto'; // ❌
// ✅ `crypto.subtle.importKey/sign/verify`(⭐ 是 Promise API,写法完全不同)
// ③ 在 Edge 里用 `Buffer`
const b = Buffer.from(x); // ⚠️ 依赖 polyfill
// ✅ `new TextEncoder().encode(x)` / `Uint8Array`
// ④ ⭐ 在 Edge 里 new PrismaClient(要 TCP)
// ✅ HTTP 驱动(Neon/PlanetScale/Turso)或「连接池代理」或「转给 Node runtime」
// ⑤ ⭐ 用「模块级变量」做「共享状态」(Edge 上不可靠)⭐
let counter = 0; export function inc() { return ++counter; } // ❌ 跨实例/被回收后不可靠
// ✅ KV / Durable Objects / 外部 Redis
// ⑥ ⭐ 「请求结束后」继续跑异步(不 `waitUntil` 会被杀)
fetch('/analytics', { ... }); // ⚠️ 可能被中断
// ✅ `waitUntil(fetch(...))` 或 `after(() => ...)`
// ⑦ ⭐ 「用 Edge 做重计算」(超时/OOM)
// ✅ 交给 Node runtime / 后台任务 / 客户端 Worker
// ⑧ 「Edge 函数的依赖太大」(部署慢 + 冷启动变差)
// ✅ 按需 import + 用轻量库(`jose` 而不是 `jsonwebtoken`)
// ⑨ ⭐ 「A/B 分桶」在边缘与在 Node 用了「不同的哈希算法」(用户分组不一致)
// ✅ ⭐ 「分桶算法」必须是「**一份共享代码**」(⭐ 前端、Edge、Node 都用它,见 8.9)
// ⑩ 「中间件里读 cookie 但没验签」(把「伪造的 cookie」当作「已登录」)
// ⚠️ 症状:能通过「随便设个 cookie」绕过鉴权
// ✅ ⭐ 「轻量验签」(WebCrypto HMAC/JWT)或「只做存在性检查 + 下游真正鉴权」
// ⑪ 「中间件 matcher 太宽」(每个请求都跑,包括静态资源)
// ✅ 用 matcher 排除 `_next/static`、`_next/image`、`favicon`…
// ⑫ ⭐ 「边缘缓存了带 cookie 的个性化页面」(串用户)⭐ 安全级
// ✅ 「检测到 cookie/session → 不加 `s-maxage`」;⭐⭐ 并**绝不能把用户数据放进「共享缓存」**
// ⑬ 「中间件改写了请求头但下游读不到」
// ⚠️ Next.js 里要「用 `NextResponse.next({ request: { headers } })`」才算「把新头传给下游」
// ✅ 写 `requestHeaders`(而不是「在 response 上加」)
// ⑭ 「`redirect` 用了「用户可控的 URL」(开放重定向)」
loginUrl.searchParams.set('redirect', request.nextUrl.searchParams.get('next')!); // ❌
// ✅ 只拼「站内 pathname + search」;若必须接受外部输入 → 校验同源(见 6.12)
// ⑮ 「Edge 里 `setTimeout` 做延迟任务」(请求结束就被杀)
// ✅ 用「平台的任务队列」或「Node runtime + 后台任务」
// ⑯ 「以为 Edge 一定比 Node 快」(其实要「算清楚」)
// ⚠️ 「边缘 → 后端」的那一跳可能抵消「就近」的优势
// ✅ 用「真实数据」验证(⭐ 对比 P95 而不是「直觉」,见 7.13)
// ⑰ 「Edge 函数的错误处理缺失」(一个异常 → 500,用户看到「无法访问」)
// ✅ try/catch + 「降级到 Node runtime / 返回友好错误」
// ⑱ 「本地开发与线上行为不一致」(本地是 Node,线上是 Edge)
// ⚠️ 症状:「本地好好的,部署到 Edge 后报 `fs is not defined`」
// ✅ 用「支持 Edge 的本地运行时」(如 `next dev --turbo` 会标注 runtime;
// 或 ⭐ 「把 runtime 声明写进代码」并在 CI 里「用 Edge 环境构建」)
// ⑲ 「Edge 里的 `fetch` 默认缓存行为」(⭐ 与 Node 不同)
// ⚠️ 有些平台的 `fetch` 默认「不缓存」,有些「有 Cache API 但 fetch 不自动用」
// ✅ 显式声明缓存策略(⭐ 并用「响应的 `cache-control`」而不是「隐式行为」)
// ⑳ ⭐ 「把『需要强一致』的事放在边缘」(如「库存扣减」「幂等计数」)
// ⚠️ Edge 是「多实例 + 无共享内存」→ 用 KV 做「原子操作」很危险
// ✅ 「强一致」的操作放「有状态的服务」(Durable Objects / 数据库的原子操作,见 7.12)
// ⑥ 的完整处理(`waitUntil` 与 `after` 的正确用法)
import { after } from 'next/server';
export async function POST(request: Request) {
const data = await request.json();
// ⭐ 主流程(用户要等)
const result = await doMainWork(data);
// ⭐⭐ 副作用(用户不等)—— 「埋点 / 写缓存 / 发通知 / 日志」
after(async () => {
// ⚠️ 这里的错误「不会影响响应」,但也要 try/catch(避免「未处理的 rejection」,见 8.7)
try {
await Promise.all([
fetch('/api/analytics', { method: 'POST', body: JSON.stringify({ type: 'done' }) }),
cache.put(cacheKey, response.clone()),
logToService({ action: 'done' }),
]);
} catch (err) {
console.warn('[after] 副作用失败', err);
}
});
return Response.json(result);
}
// ⑫ 的完整处理(边缘缓存的「安全检查」)
function setEdgeCache(response: Response, request: Request, options: { maxAge: number; swr?: number }) {
// ⭐⭐ 三层检查(任何一层不过 → 不加缓存头)
// ① 带 cookie / Authorization → 可能是个性化内容 → 不缓存
if (request.headers.has('cookie') || request.headers.has('authorization')) return;
// ② 响应的 cache-control 已经声明「private/no-store」→ 尊重它
const cc = response.headers.get('cache-control') ?? '';
if (/private|no-store/i.test(cc)) return;
// ③ 显式「白名单」:只有「确定是公开内容」的路由才缓存
const url = new URL(request.url);
if (!/^\/(blog|docs|products)\//.test(url.pathname)) return;
response.headers.set(
'cache-control',
`public, s-maxage=${options.maxAge}${options.swr ? `, stale-while-revalidate=${options.swr}` : ''}`
);
}
六、面试延伸
- 「Edge Runtime 是什么?和 Node.js Runtime 有什么区别?」
⭐ 它不是一个「精简版 Node」,而是「一个基于 V8 isolate、只有 Web 标准 API 的运行时」。关键区别:① ⭐ 启动——V8 isolate(冷启动约 5ms)vs 容器/进程(100ms~1s);② ⭐ 位置——就近部署(全球边缘) vs 固定区域;③ ⭐ 可用 API——只有 Web 标准(fetch/Request/Response/crypto(WebCrypto)/streams/Cache)vs 全部 Node 内置 + npm 生态;④ ⭐ 没有fs/net/child_process,也没有 Node 的crypto/Buffer/process(完整);⑤ ⭐ 实例不保证存活(⚠️ 不要依赖内存里的全局状态)vs 可以常驻;⑥ CPU 时间与内存受限(不适合重活);⑦ ⭐ 没有 TCP → 传统数据库驱动用不了(必须用 HTTP 驱动或连接池代理)。 -
「Edge 里怎么连数据库?」
⭐ 核心障碍是「Edge 没有 TCP 客户端」——所以「MySQL/Postgres 的标准驱动(需要 TCP 连接池)」用不了。三条出路:① ⭐ HTTP 驱动——Neon serverless driver、PlanetScale HTTP、Turso(libSQL over HTTP)、Drizzle 的 HTTP 模式;② ⭐ 连接池代理——Prisma Accelerate/Data Proxy、Supabase Supavisor(「边缘 → 代理(HTTP/WS)→ 数据库(TCP)」,代理负责连接池);③ ⭐ 不在 Edge 里连库——Edge 只做「鉴权/路由/改写」,真正的数据请求转发给 Node runtime 或后端服务(⭐ 这是生产里最稳的做法)。另外要注意:⚠️ 「强一致」的操作(库存扣减、幂等计数)不要放在边缘(边缘是「多实例 + 无共享内存」,用 KV 做原子操作很危险)。 -
「Edge 里的『全局状态』为什么不可靠?」
因为 ⭐ Edge 是「多实例 + 实例会被回收」:① 不同请求可能落在不同的 isolate(⭐ 所以「模块级变量里的计数器」不是全局的,只是「这个实例的」);② 实例「不保证存活」(空闲时被回收 → 内存里的东西消失);③ ⚠️ 所以「连接池、内存缓存、速率限制计数器」这类都不可靠。解法:需要「跨请求/跨实例共享」的状态就用「平台提供的存储」——KV(Cloudflare KV / Vercel KV)、⭐ Durable Objects(Cloudflare,能做「有状态的强一致」)、外部 Redis(⚠️ 需要 HTTP 接口)。⭐ 判据:「这段状态需要跨请求/跨实例共享吗」——需要就不该放内存。 -
「
waitUntil/after是干什么的?」
它们解决「请求结束后,异步入队的工作会被杀掉」的问题:⚠️ 在 Edge 上,「响应返回后」普通的fetch/Promise/定时器「可能被直接终止」(因为「这个 isolate 可能被回收」)。⭐waitUntil(promise)(Vercel/Cloudflare)与after(() => ...)(Next.js 15)让「响应返回后继续做的工作」被「正确地等待」——它是 Edge 里做「异步副作用」的唯一正确方式:写缓存、埋点上报、写日志、发通知。⚠️ 两个注意:① 副作用里的错误「不会影响响应」,但仍要 try/catch(否则会「未处理的 rejection」,见 8.7);② 不要让「用户等的关键路径」放进after(那样「用户拿不到结果」)。 -
「Edge 缓存要注意什么?」
⭐ 最危险的是「缓存了带 cookie 的个性化页面」→ 串用户(安全级事故):① ⭐ 检查cookie/authorization头——有就不加s-maxage;② ⭐ 尊重响应自己的cache-control(private/no-store就不覆盖);③ ⭐ 用「路径白名单」(只有「确定是公开内容的路由」才缓存,如/blog/*、/docs/*);④ 用「s-maxage+stale-while-revalidate」(⭐ 与 5.16 的 HTTP 缓存黄金组合一致)。另外:① ⭐Cache API(caches.open())是 Web 标准——边缘与浏览器 Service Worker 用同一套 API(代码可复用,见 10.11);② ⭐ 「写缓存」要放进waitUntil(不阻塞用户);③ ⚠️ 「边缘缓存」与「Next.js 的 Data Cache / Full Route Cache」是两层(见 9.9,别混淆)。 -
「什么时候该用 Edge,什么时候不该?」
⭐ 该用(命中 2 条以上):① 延迟敏感 + 需要就近执行(鉴权、重定向、A/B 分桶、地理路由、响应改写);② 只是「中间层」(不读文件、不连传统库、不做重计算);③ 需要流式处理(SSE、流式改写、AI 流式输出);④ 全球用户 + 在意首字节时间。⭐ 不该用(命中任意一条就要谨慎):① 需要传统数据库(除非有 HTTP 驱动/代理);② 需要原生依赖(sharp/bcrypt/canvas → 直接跑不了);③ 重计算/长任务;④ 需要常驻内存状态;⑤ ⚠️ 「为了『快』而用 Edge,但其实只是 API 转发」——「边缘 → 后端」的那一跳可能抵消「就近」的优势(⭐ 要用真实 P95 数据验证,别靠直觉,见 7.13)。实践:⭐ 「Edge Middleware 做轻活 + Node Runtime 做重活 + 前端 Worker 做客户端重活」的混合架构,并在「按 route 声明 runtime」(export const runtime = 'edge' | 'nodejs')。 -
「Edge 里的鉴权怎么做?」
⭐ 关键纪律:中间件里的鉴权要「轻量」——因为它「每个请求都跑」(⚠️ 而且「中间件的耗时」会加到「每个请求的延迟」上)。做法:① ⭐ 「存在性检查 + 轻量验签」——用 WebCrypto 验「签名/JWT」(⚠️ 注意「Node 的crypto与WebCrypto的 API 完全不同」,这是迁移时最常见的改动点);② ⭐ 「不做重活」——不要在中间件里「查数据库」(那会「每次都多一次往返」);真正的授权(查权限点、查资源归属)应该在路由/接口里做(见 6.12);③ ⭐ 「重定向时只拼站内路径」——redirect参数如果「来自用户输入」就是开放重定向漏洞(见 6.12);④ 必须校验「伪造的 cookie」(⚠️ 「随便设个 cookie 就能过鉴权」是最常见的事故)——所以「存在性检查」不够,要验签。 -
「如果让你把一个 Node 的 API 迁到 Edge,你会遇到什么?」
六类问题:① ⭐ Node 内置模块(fs/path/net/crypto/Buffer)——⚠️ 尤其是crypto与Buffer(WebCrypto 是 Promise API、Buffer要用Uint8Array/TextEncoder);② ⭐ 依赖里有原生扩展(sharp/bcrypt/canvas)——直接跑不了,要么换「纯 JS/Web 标准」的实现(如bcryptjs、jose),要么保持在 Node;③ ⭐ 数据库驱动(TCP → 要 HTTP 驱动或代理);④ ⭐ 全局状态与连接池(⚠️ 内存计数/缓存不可靠);⑤ ⭐ 执行时间与内存(重活会超时/OOM);⑥ 本地开发与线上行为不一致(⚠️ 「本地是 Node,线上是 Edge」→ 部署后才发现报错)→ ✅ 「把 runtime 声明写进代码」+ 在 CI 里用 Edge 环境构建。⭐ 我的建议:「先只迁『中间件/轻量 API』,把『数据密集』的部分留在 Node」(⭐ 收益最大、风险最小)。
一句话速记
Edge Runtime = 「V8 isolate + 只有 Web 标准 API + 就近部署」——⭐ 冷启动约 5ms(vs Node 容器 100ms~1s)+ 全球就近,代价是「没有
fs/net/child_process,也没有 Node 的crypto/Buffer/完整process」+「实例不保证存活」+「CPU 时间与内存受限」+「⭐ 没有 TCP → 传统数据库驱动用不了」;⭐ 能用的白名单:fetch/Request/Response/Headers/FormData/URL/streams(TransformStream可做「流式改写」)/WebCrypto/CacheAPI/AbortController/atob/btoa+ ⭐waitUntil(或 Next 15 的after)——⭐ 它是「请求结束后做异步副作用」的唯一正确方式;⭐ 四条硬约束的应对:① 没 TCP → HTTP 驱动(Neon/PlanetScale/Turso)、连接池代理(Prisma Accelerate/Supavisor)、或「Edge 只做轻活、数据请求转给 Node」;② 实例不保证存活 → 共享状态用 KV/Durable Objects/Redis(⭐ 判据「需要跨实例共享吗」);③ 时间与内存受限 → 重活交给 Node runtime/后台任务/前端 Worker;④ 依赖越小越好(按需 import + 轻量库如jose);⭐ 选型判据:「延迟敏感 + 只是中间层 + 流式 + 全球用户」→ 该用;「需要传统 DB/原生依赖/重计算/常驻状态」→ 不该用,⚠️ 且「为了快而用 Edge 但只是 API 转发」可能被「边缘→后端那一跳」抵消(用 P95 验证);⭐ 鉴权要轻量 + 必须验签(不能只查存在性)+ redirect 只拼站内路径;⭐ 缓存三大安全纪律:有 cookie/Authorization 不缓存、尊重响应自己的private/no-store、路径白名单;生产最佳实践是混合架构(Edge Middleware 轻活 + Node 重活 + 按 route 声明runtime)。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!