SSR 与 Node.js
本页回答:Wow 客户端能不能跑在服务端?一个进程服务多个用户时有什么不同?
@ahoo-wang/wow-client 和生成的客户端在 Node.js 中无需改动即可运行:它们用的是平台自带的 fetch、ReadableStream 和 TextDecoderStream,Node 都内置了。服务端真正不同的是归属。浏览器标签页只属于一个用户,所以在那里共用一个配置好的 Fetcher 是安全的;服务端进程同时处理许多用户的请求,所以凡是携带用户身份的东西都必须按请求创建。
要求
| 运行环境 | 要求 |
|---|---|
| Node.js | 22.12 或更高:各包声明了 engines.node >=22.12.0 |
| 模块格式 | wow-client 与 wow-generator 同时提供 ESM 和 CommonJS;wow-react 与 wow-view-engine 只提供 ESM |
| TypeScript | 生成的客户端需要 experimentalDecorators: true;module/moduleResolution 用 NodeNext 或 Bundler |
React(wow-react) | 19.3 或更高,构建产物导入 react/compiler-runtime;见兼容性 |
用 Node 运行编译产物的脚本需要 "type": "module" 才能使用顶层 await,快速开始就是这样运行的。
携带用户身份的请求,每个请求一个 Fetcher
Fetcher 的 default 实例和所有 NamedFetcher 都是进程级的。在服务端,永远不要把用户的令牌交给它们,也不要对它们应用 CoSecConfigurer:否则它的令牌存储会服务进程里的所有用户。改为按请求创建 Fetcher,传给客户端,随请求一起丢弃:
ts
import { Fetcher } from '@ahoo-wang/fetcher';
import { cartQueryClientFactory } from './generated/index.js';
export async function loadCart(
request: { authorization: string; ownerId: string },
signal: AbortSignal,
) {
// 为这个请求创建:这里的一切都不会比请求活得更久,也不会到达别的用户。
const fetcher = new Fetcher({
baseURL: process.env.CART_SERVICE_URL ?? 'http://localhost:8080',
headers: { Authorization: request.authorization },
timeout: 5_000,
});
const snapshots = cartQueryClientFactory.createSnapshotQueryClient({
fetcher,
urlParams: { path: { ownerId: request.ownerId } },
});
return snapshots.getStateById(request.ownerId, undefined, signal);
}对每个请求都相同的服务身份(例如机器令牌)可以放在共享的 Fetcher 上。把传入请求的 AbortSignal 传下去,这样页面被放弃时它的 Wow 调用也会停止。
Node.js 中的流
查询流和 sendAndWaitStream 在 Node 中与浏览器中一样:用 for await 读取,中途失败时抛出 WowError(见错误处理)。使用方离开时中止 signal 或 break 出循环;没读完的流会一直占着连接。
ts
import { listQuery, type SnapshotQueryClient } from '@ahoo-wang/wow-client';
export async function countActive<S>(snapshots: SnapshotQueryClient<S>, signal: AbortSignal) {
let count = 0;
for await (const _event of await snapshots.listStateStream(listQuery({ limit: 1_000 }), undefined, signal)) {
count++;
}
return count;
}Next.js App Router 与其他 React 服务端渲染
- Server Component 和 Route Handler 可以直接调用
wow-client和生成的客户端(按上文每个请求一个 Fetcher),把普通数据传给页面。 - Hook 只能在 Client Component 中运行。
@ahoo-wang/wow-react导出的是 Hook,其构建产物没有"use client"指令,所以调用它们的文件要以'use client'开头。Hook 本身见 wow-react 参考。 @ahoo-wang/wow-view-engine/ui的组件(尚未发布)同样使用 Hook,也没有该指令。在一个标了'use client'的文件里重新导出要渲染的组件,再从那里导入:
tsx
'use client';
// app/components/view-engine.tsx
export { DataWorkbench, EmbeddedView } from '@ahoo-wang/wow-view-engine/ui';- 在浏览器里发请求的 Client Component 使用浏览器端的 Fetcher 及其 CoSec 配置,见认证与拦截器。这些模块级实例只在客户端代码里创建,服务端的打包产物就不会在用户之间共享它们。
延伸阅读
- 认证与拦截器:浏览器中的 CoSec、具名 Fetcher、自定义请求头。
- 兼容性与版本:Node、React 与 TypeScript 的版本。
- Fetcher 文档:超时、拦截器与流。