首页 / React 学习笔记 / 50

REACT · Vol.VII · LESSON 50 · AI 时代的前端进阶

前沿展望:Server Actions / 边缘计算

进阶#前沿#趋势

Server Actions:一个不写 endpoint 的远程调用

先看一个你写了几十遍的流程:新建书签 = 前端写 fetch 打 /api/bookmarks + 后端写 Controller 接 + 两边对请求体结构。四个字的功能,两端各写一遍「传输层胶水」。Server Actions 的野心就是撕掉这层胶水:

app/actions.ts · 加了 'use server' 的函数:客户端直接调用,服务端执行
"use server";

import { revalidatePath } from "next/cache";
import { db } from "@/lib/db";

export async function addBookmark(formData: FormData) {
  const title = String(formData.get("title") ?? "");
  const url = String(formData.get("url") ?? "");

  await db.bookmark.create({ data: { title, url } });   // 直接写库,没有中间 HTTP 层
  revalidatePath("/bookmarks");   // 让列表缓存失效、重新渲染(第 33 篇失效思路的服务端版)
}
app/bookmarks/new/page.tsx · 表单直接绑服务端函数
import { addBookmark } from "@/app/actions";

export default function NewBookmarkPage() {
  return (
    <form action={addBookmark}>
      <input name="title" placeholder="标题" />
      <input name="url" placeholder="https://example.com" />
      <button type="submit">保存</button>
    </form>
  );
}

没有 fetch、没有 URL、没有手写的 loading/error 分发。原理一句话:'use server' 让编译器把函数变成「只能服务端执行」的安全桩——客户端调用时框架自动生成 POST 端点,参数序列化发过去、服务端执行、结果发回来,再由 revalidatePath 让受影响页面缓存失效。第 42 篇你手工布线的「前端 → /api → 后端」,被折叠成一个函数调用。

后端类比:RPC / @Remote 的既视感

你大概率写过 Dubbo 或 OpenFeign:接口在本地、实现在远端,框架负责序列化与网络——「像调本地方法一样调远端方法」。Server Actions 就是 React 官方钦定的前端→服务端 RPC,契约都不用手写:函数签名即契约,框架两端自动生成桩。Next.js 全包的同进程全栈用 Actions 最顺;跨服务集成,该走 HTTP 还走 HTTP。

「客户端能 import 服务端函数」,像把数据库暴露给了浏览器?没有——客户端拿到的只是自动生成的端点引用,执行的永远是服务端代码。但这个端点默认公网可达,鉴权必须在函数体内自己做,不能指望「页面有守卫」挡住它。和你写 Controller 时「不能信前端传来的任何东西」同一条军规。

Partial Prerendering:静态壳秒开,动态洞流式补

第 40 篇见过静态站的速度,第 44 篇见过服务端渲染的灵活,能不能既要又要?Partial Prerendering(PPR,Next.js 15 起以实验特性提供)的答案:把页面拆成「静态壳 + 动态洞」:

站点导航 · 静态(构建时预渲染,秒开) 标签云 · 静态 动态洞 1:我的书签列表 请求时流式填充(Suspense fallback 先占位) 动态洞 2:实时统计 数据到一块补一块 壳先到(毫秒级,躺在 CDN 里),洞随后流式补齐
图 1PPR:整页只有「洞」需要等数据,静态壳构建时就渲染完毕、直接从 CDN 秒开。

声明方式你可能已经猜到——就是第 34 篇的 Suspense 边界:

app/bookmarks/page.tsx · Suspense 边界 = 缓存粒度声明
import { Suspense } from "react";

export default function BookmarksPage() {
  return (
    <>
      <SiteHeader />                    // 静态:构建时渲染好,秒开
      <Suspense fallback={<ListSkeleton />}>
        <BookmarkList />                // 动态洞:请求时流式填充
      </Suspense>
      <SiteFooter />                    // 静态
    </>
  );
}

静态壳毫秒级到达;动态洞先以骨架占位,数据到了流式补上。第 34 篇你设计的加载态,在这里被框架内建成渲染策略——你声明「哪里会慢」,框架保证「其余全都快」

后端类比:整页缓存 + 片段动态的老手艺

你干过「整页走缓存、个别模块 ajax 实时刷新」:页面 90% 缓存五分钟,用户余额那一格单独取。PPR 是同一思想被框架收编——不用自己拆接口、写占位,组件级 Suspense 边界就是「缓存粒度的声明」,失效对应 revalidatePath。

串起来看:第 5 篇渲染模型 → 第 44 篇 RSC 决定「在哪渲染」→ 本篇 PPR 决定「何时渲染」。概念一直在加,心智模型没换过——始终是那棵组件树,从不同维度切分。

Edge Runtime:把代码部署到离用户 20ms 的地方

第 42 篇的部署模型是「一簇源站服务全国」:上海 10ms,乌鲁木齐 80ms。CDN 解决了静态资源(hash 文件名天生为它而生),动态逻辑仍要回源。Edge Runtime 再进一步:把「执行代码」推到全球数百个边缘节点——渲染、鉴权、个性化在离用户最近的机房完成。

用起来只是多一行声明:

app/api/health/route.ts · 该路由跑在边缘运行时
export const runtime = "edge";

export async function GET(request: Request) {
  const city = request.headers.get("x-vercel-ip-city") ?? "unknown";
  return Response.json({ ok: true, servedFrom: city });
}

能力与代价要一起看。它不是「缩小版 Node」,而是不带标准库的轻量沙箱(V8 isolate):

维度Node 运行时(第 42 篇的部署方式)Edge 运行时
冷启动秒级(≈ JVM 冷启动)毫秒级,几乎无感
能力完整 Node API、文件系统、长连接受限沙箱:部分 npm 包与 Node API 不可用
部署位置你指定的机房全球数百节点,自动就近
适合重逻辑、数据库长连接、后台任务轻渲染、鉴权、个性化重定向、A/B 分流
后端类比:多活部署被产品化了

你见识过同城双活、异地多活:DNS 就近调度、数据同步、机房运维,复杂度翻倍。Edge Runtime 把它打包成平台能力:部署单元从「机器」变成「代码」,push 一次全球生效,节点、调度、扩缩容都不用你管。代价是上表那列沙箱限制——当「CDN 上的轻量函数」用,别当缩小版 JVM。

React Compiler 与 AI:优化和写码的方式都在变

React Compiler:memo 化被编译器接管

第 37 篇你练过 memo、useCallback、useMemo,那是「人肉保证引用稳定」。React Compiler 在构建期分析组件,自动插入等价的缓存代码——同样的优化,从「人肉纪律」变成「机器保证」:

TodoList.tsx · 手写优化 → 编译器接管后
// 第 37 篇的手工优化……
const visible = useMemo(
  () => todos.filter((t) => t.title.includes(keyword)),
  [todos, keyword]
);
const toggle = useCallback((id: number) => {
  setTodos((prev) =>
    prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
  );
}, []);

// 开启 babel-plugin-react-compiler 后,直接写朴素代码:
const visible = todos.filter((t) => t.title.includes(keyword));
const toggle = (id: number) => {
  setTodos((prev) =>
    prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
  );
};

前提还是第 37 篇那条规则:引用稳定才跳过重渲染——只是「检查引用是否被篡改」交给了编译器。第 37 篇没有白学:懂「为什么」才看得懂编译器「做了什么」,才能在它对不合规代码放弃优化时(渲染期改 props)手动兜底。JVM 替你管内存,GC 调优工程师依然值钱,同理。

AI:交付单元从「组件」升级到「功能」

第 47 篇你和 AI 结过对,第 48 篇理解了工具调用与状态机。往前看,两条曲线正在合流:

  • 自然语言到 UI:一句话生成可用的 React 页面,表单、列表、CRUD 这类「模式化代码」加速最猛——对 AI 这是「分布内」任务,见得多就写得好;
  • Agent 生成整个功能:「规划 → 工具调用 → 校验 → 自修复」用于编码 Agent,能跨文件改代码、跑测试、迭代到通过——交付单元从「你写一个组件」变成「你验收一个功能」。
核心要点
  • Server Actions = 官方 RPC:函数签名即契约,胶水由框架生成;鉴权要在函数体内自己做。
  • PPR:静态壳 + 动态洞,Suspense 边界就是缓存粒度声明;第 34 篇的加载态被框架内建。
  • Edge Runtime:代码即部署单元,全球就近执行;能力受限,当「CDN 上的轻量函数」用。
  • React Compiler 接管机械 memo 化;第 37 篇的心智模型是看懂它、兜底它的前提。
  • AI 改变「写」的效率;稀缺的是「判断」——契约、架构、归因,恰是本卷反复强调的。
当 boilerplate 能被 AI 一句话生成,「你」的价值剩什么?判断力:状态放哪(第 17 篇)、工具怎么选(第 20 篇)、瓶颈在哪(第 41 篇)、契约怎么设计(第 49 篇)。AI 最难替你的,恰是专栏花最多篇幅讲的那些「为什么」。

全系列回顾:UI = f(state) 是起点,也是终点

50 篇,7 卷。合上之前,把主线摊在一张表里:

主线问题关键收获
一 · 核心原语界面为什么这样画?UI = f(state) 心智模型、JSX、State、diff
二 · Hooks 精讲函数组件怎么「记住」与「响应」?useEffect、闭包陷阱、自定义 Hook
三 · 状态管理状态放哪、谁说了算?客户端/服务端状态分类、Zustand、选型
四 · 组件工程怎么组织才不会烂?组合优于继承、TS、职责边界
五 · 路由与取数SPA 的「后端半边天」怎么补?Router、TanStack Query、加载/错误/竞态
六 · 性能与部署怎么又快又稳地送到用户手上?memo、Profiler、构建、Nginx、CI
七 · AI 时代边界正在怎样消失?RSC、Server Actions、Agent、边缘计算

第 1 篇那句 UI = f(state),走了 50 篇,它长出了三层含义:

  • f 在长大:从组件函数长成 RSC(第 44 篇)、Server Actions、边缘函数——「渲染」被推到离数据和用户更近的地方;
  • state 在长大:从 useState 到 URL 参数(第 31 篇)、服务端缓存(第 33 篇)——「状态」的边界从内存扩展到整个系统;
  • UI 在长大:从 div 到流式 UI(第 45 篇)、AI 生成的界面(第 47 篇)——界面未必全由你手写,但「状态到界面的映射」从未变过。

框架会换代,API 会过时,但「声明式、组件化、状态驱动」这套模型稳定了十年,大概率再稳十年——这是笔记敢以模型为主线的底气。回头看第 43 篇那张转型路线图:基础、生态、工程化、全栈、AI 协作,50 篇下来你把整张图走了一遍。

最后三件事,留给合上专栏之后的你:

  1. 做项目:把第 49 篇的书签系统扩成简历级产品——加协作、导入导出、PWA。项目是知识唯一的保鲜手段;
  2. 读源码:从 zustand 这类几百行的小库读实现,到 react.dev 读设计——小库读「怎么写」,大框架读「为什么」;
  3. 保持在线:订阅 react.dev 与 Next.js 官方博客,盯 RSC、PPR、React Compiler 从实验走向稳定的时间线。

本章回顾 · 全系列收官

回到第 1 篇那个「点击加一」的计数器:那时你抱着一身 Spring 功力,对着一颗手动改 DOM 的心。50 篇之后,你有心智模型、Hooks、状态分层、组件工程、路由取数、性能部署的完整链路,看清了 Server Actions、边缘计算与 AI 重塑的方向,还交付过一个全栈产品。UI = f(state) 是起点也是终点——起点是它完成了你从后端到全栈的心智跃迁,终点是你有资格在更大的尺度上重新定义自己的 f 和 state。江湖路远,带着这套模型继续走。

Comments · 评论