Waku:脱离 Next.js 使用 React 服务端组件
Waku 是一款轻量级 React 框架,无需 Next.js 即可使用 React Server Components。通过示例应用了解路由、渲染模式与部署。
Waku 是一个基于 Vite 构建的轻量级 React 框架,支持运行 React 19 的服务端组件(Server Components)和服务端操作(Server Actions)。上手 Waku 主要需要掌握两点:基于 src/pages 的文件系统路由,以及每个页面导出的 getConfig,它用于指定静态渲染或动态渲染。
如果你一直在 Next.js App Router 中使用服务端组件,可能很难分清哪些特性属于 React,哪些属于 Next。Waku 保留了 React 的组件模型,同时去掉了外围的大部分框架层。本文将从脚手架搭建到部署,完整构建一个小型应用,帮助你判断是否值得在业余项目中尝试 Waku。Waku 1.0 目前以候选发布版本(Release Candidate)的形式发布,其中 v1.0.0-rc.2 发布于 2026 年 9 月 28 日。请查看 Waku 发布页面,确认稳定版 1.0 是否已经发布。
核心要点
- Waku 是一个基于 Vite 的极简 React 框架,使用
src/pages实现文件系统路由。每个布局和页面都可以导出getConfig,返回render: 'static'或render: 'dynamic'。 - Waku 中的布局、页面和切片(slices)默认采用静态渲染。只有当页面的
getConfig返回render: 'dynamic'时,才会在每次请求时渲染。 - 对于
src/pages/releases/[slug].tsx这类静态动态段路由,必须在getConfig中返回一个staticPaths数组。 - Waku 默认部署到 Node.js,同时支持纯静态输出、Vercel、Netlify 和 Cloudflare Workers。Deno Deploy、Bun 和 AWS Lambda 的支持目前标记为实验性。
- Waku 1.0 目前处于候选发布阶段,最新版本为 v1.0.0-rc.2(2026 年 9 月 28 日),适合能够接受预发布版本频繁变动的小型项目。
如何创建 Waku React 项目?
运行 npm create waku@latest 即可创建 Waku React 项目。之后的日常开发只需用到三个 CLI 命令:waku dev、waku build 和 waku start。
npm create waku@latest
# then, inside the project:
npx waku dev # local dev server
npx waku build # production build
npx waku start # serve the production build
根据 Waku 入门文档,支持的 Node.js 版本为 ^26.0.0、^24.0.0 或 ^22.15.0。在底层实现上,Waku 使用 Vite 官方的 @vitejs/plugin-rsc 来打包服务端组件,因此其配置文件中提供了用于配置插件的 vite 字段。
获取数据的异步页面
在 Waku 中,页面就是 src/pages 下默认导出一个组件的文件。该组件可以是异步服务端组件,直接通过 await 获取数据。如果你熟悉 Next.js 中使用服务端组件获取数据的方式,会发现组件主体的写法完全一致,区别仅在于具名导出的 getConfig。
为了避免依赖外部 API,本示例读取一个本地 JSON 文件。Waku 文档指出,服务端组件可以安全地读取项目根目录下 private 文件夹中的文件。
// private/releases.json
[
{ "slug": "v2-0", "version": "2.0.0", "summary": "New plugin API." },
{ "slug": "v1-9", "version": "1.9.0", "summary": "Bug fixes." }
]
// src/pages/index.tsx
import { readFile } from 'node:fs/promises';
type Release = { slug: string; version: string; summary: string };
export default async function HomePage() {
const releases: Release[] = JSON.parse(
await readFile('./private/releases.json', 'utf8'),
);
return (
<>
<title>Release notes</title>
<h1>Release notes</h1>
<ul>
{releases.map((release) => (
<li key={release.slug}>
<a href={`/releases/${release.slug}`}>{release.version}</a>
</li>
))}
</ul>
</>
);
}
export const getConfig = async () => {
return { render: 'dynamic' } as const;
};
由于 getConfig 返回了 render: 'dynamic',该页面会在每次请求时进行服务端渲染。如果去掉这一行,页面将在构建时预渲染。<title> 标签之所以能生效,是因为 Waku 会将 title、meta 和 link 标签自动提升到文档的 head 中。
使用 ‘use client’ 添加客户端组件
在文件顶部添加 'use client' 后,当该文件被服务端组件导入时,就会形成一个服务端与客户端的边界。从这个边界往下,该文件引入的所有组件都会被水合(hydrate),并在浏览器中运行。这与你在 App Router 中使用的指令和规则完全相同。
// src/components/like-button.tsx
'use client';
import { useState } from 'react';
export const LikeButton = () => {
const [likes, setLikes] = useState(0);
return <button onClick={() => setLikes((n) => n + 1)}>👍 {likes}</button>;
};
客户端组件不能导入服务端组件,但可以通过 children 或其他 prop 接收服务端渲染的输出:
// src/components/collapsible.tsx
'use client';
import { useState, type ReactNode } from 'react';
export const Collapsible = ({ children }: { children: ReactNode }) => {
const [open, setOpen] = useState(false);
return (
<div>
<button onClick={() => setOpen((o) => !o)}>{open ? 'Hide' : 'Details'}</button>
{open && children}
</div>
);
};
// src/pages/index.tsx (inside the map, with both components imported)
<li key={release.slug}>
<a href={`/releases/${release.slug}`}>{release.version}</a>
<LikeButton />
<Collapsible>
<p>{release.summary}</p>
</Collapsible>
</li>
摘要段落在服务端渲染,然后作为 prop 传递给 Collapsible,客户端文件本身从未导入它。关于 React 服务端组件的一个常见误区是 'use server' 的作用。根据 Waku 文档,该指令用于标记服务端操作,而非服务端组件,因此不应放在服务端组件文件的顶部。另外,客户端组件在水合之前同样会先在服务端渲染为 HTML。通过对此类页面进行会话回放(session replay),可以清楚地看到点赞按钮是在首次点击时就能响应,还是要等水合完成后才有反应。
路由:布局、动态段与渲染模式
除非另行指定,Waku 会对布局、页面和切片进行静态渲染。API 处理函数则恰好相反,默认采用动态渲染。渲染模式按文件单独设置,因此静态布局中可以包裹动态页面。
src/
pages/
_layout.tsx
index.tsx
releases/
[slug].tsx
components/
like-button.tsx
collapsible.tsx
private/
releases.json
_layout.tsx 文件作用于其所在路由及其下所有嵌套路由,且其组件必须接收 children prop:
// src/pages/_layout.tsx
import type { ReactNode } from 'react';
export default async function RootLayout({ children }: { children: ReactNode }) {
return (
<>
<header><a href="/">Release notes</a></header>
<main>{children}</main>
</>
);
}
export const getConfig = async () => {
return { render: 'static' } as const;
};
文件名带方括号的文件即为动态段路由。段的值会作为 prop 传入组件,可以使用 waku/router 中的 PageProps 为其添加类型。根据 Waku 的路由文档,当动态段路由采用静态渲染时,getConfig 必须返回一个 staticPaths 数组,列出需要预渲染的值。由于 getConfig 可以是异步函数,这个列表可以直接从数据中生成:
// src/pages/releases/[slug].tsx
import { readFile } from 'node:fs/promises';
import type { PageProps } from 'waku/router';
type Release = { slug: string; version: string; summary: string };
const loadReleases = async (): Promise<Release[]> =>
JSON.parse(await readFile('./private/releases.json', 'utf8'));
export default async function ReleasePage({ slug }: PageProps<'/releases/[slug]'>) {
const release = (await loadReleases()).find((r) => r.slug === slug);
if (!release) return <p>Release not found.</p>;
return (
<>
<title>{`Release ${release.version}`}</title>
<h1>{release.version}</h1>
<p>{release.summary}</p>
</>
);
}
export const getConfig = async () => {
const releases = await loadReleases();
return { render: 'static', staticPaths: releases.map((r) => r.slug) } as const;
};
Waku 还提供了切片、拦截器(interceptors)和 Hono 中间件等功能,可按需使用,不过本示例应用并未用到。
Waku 可以部署到哪些平台?
Waku 默认部署到 Node.js,同时支持纯静态输出、Vercel、Netlify 和 Cloudflare Workers。Waku 部署文档将 Deno Deploy、Bun 和 AWS Lambda 标记为实验性支持。
- Node.js: 使用
waku start运行生产服务器。如需独立部署,可将dist文件夹部署到服务器上,然后运行node dist/serve-node.js。 - 纯 SSG: 将
dist/public上传到任意静态托管服务。 - Vercel:
vercel - Netlify: 先执行
NETLIFY=1 npm run build,再执行netlify deploy - Cloudflare Workers: 先执行
CLOUDFLARE=1 npm run build,再执行wrangler deploy - Deno Deploy、Bun、AWS Lambda(实验性): 在
src/waku.server.tsx中导入waku/adapters/deno、waku/adapters/bun或waku/adapters/aws-lambda。
纯静态输出会舍弃所有需要在请求时依赖服务器的功能,包括动态渲染、服务端操作和 API 路由。因此,上文中的动态首页需要改为 render: 'static' 才能以 SSG 方式部署。
什么情况下 Next.js 仍是更好的选择?
如果你需要稳定版本,Next.js 是更稳妥的选择。Waku 1.0 在 v1.0.0-rc.2 时仍处于候选发布阶段,而且 rc.2 的发布说明中包含”breaking: drop deprecated apis”(破坏性变更:移除已弃用的 API),因此版本之间可能会有变动。此外,Next.js 拥有更庞大的生态系统、更多的托管平台集成和更丰富的教程资源,还内置了更多功能,而使用 Waku 时这些功能需要你自行组合实现。Waku 文档指出,选择哪个框架取决于你期望的架构,而非项目规模。Waku 将框架本身保持得非常精简,其余功能依赖生态系统中的第三方库实现;而更重量级的框架则会自行承担更多此类工作。如果你希望由框架替你做这些决策,那么继续使用 Next.js 即可。
总结
Waku 提供了与 App Router 相同的服务端组件模型,但外围的框架层要精简得多:异步页面、相同的 'use client' 边界、基于 children 的组合方式,以及一个按文件显式指定静态或动态渲染的 getConfig 导出。下一步,你可以运行 npm create waku@latest,用 Waku 重写一个小型 Next.js 路由,并在 package.json 中锁定确切的 RC 版本号,以免后续的候选发布版本在不知不觉中改变应用行为。
常见问题
如何在 Waku 中添加 API 路由?
在 src/pages/_api 下添加文件,并为该路由需要处理的每种 HTTP 方法导出一个函数,例如 GET、POST、PUT、PATCH 或 DELETE。路由路径由文件名决定。每个处理函数接收一个标准的 Request 并返回一个标准的 Response。API 处理函数默认采用动态渲染;如果 getConfig 返回 render 'static',则会在构建时预渲染,适用于 RSS 订阅源之类的输出。
Waku 中的环境变量如何使用?
服务端代码可以通过从 'waku' 导入的 getEnv 函数读取环境变量,在 Node.js 环境中也可以使用 process.env。客户端组件只能通过 import.meta.env 读取带有 WAKU_PUBLIC_ 前缀的变量,例如 import.meta.env.WAKU_PUBLIC_HELLO。所有带此前缀的变量都会以明文形式打包进生产环境的 JavaScript 包中,因此切勿为 API 密钥或其他敏感信息添加 WAKU_PUBLIC_ 前缀。
在 Waku 中进行内部导航时,应该使用 a 标签还是 Link 组件?
内部链接请使用从 'waku' 导入的 Link 组件。其 to prop 既可以接收路由字符串,也可以接收包含 to、params、search 和 hash 的结构化对象,导航会通过 Waku 的路由器在客户端完成。普通的 a 标签则会触发浏览器的常规页面跳转。如需进行编程式导航或读取当前路径和查询参数,可在客户端组件中调用从 'waku' 导入的 useRouter Hook。
Waku 的服务端操作默认是安全的吗?
不是。用 'use server' 标记的函数会成为一个可供客户端调用的端点。Waku 文档警告,除非你在函数内部自行编写身份验证和授权检查,否则这些端点不受任何保护。请在每个服务端操作中校验用户身份和权限,并且只为确实需要对外暴露的函数添加该指令,以免意外创建不必要的端点。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k