跳到主要内容

服务端函数

服务端函数允许你在与前端代码同源的地方编写后端逻辑,并在 React 组件中像调用本地函数一样调用它们。虽然我们不强制要求,但建议将服务端函数文件以 .server.ts 结尾。构建系统会自动将它们转换为 RPC 调用。

基本用法

// src/api/users.server.ts
"use server";

export async function getUsers() {
return await db.users.findMany();
}

export async function createUser(name: string, email: string) {
return await db.users.create({ data: { name, email } });
}

规则

  • 文件必须以 "use server"; 指令开头
  • 只有 命名的异步函数导出 会被转换
  • 推荐:使用 .server.ts 扩展名(例如 users.server.ts)或将它们放在 src/api/ 目录下,以帮助区分客户端代码。
  • 不支持默认导出 —— 只支持命名导出

查询模式

evjs 提供类型安全的 useQueryuseSuspenseQuery,可直接接受服务端函数。服务端函数桩还携带 .queryKey().fnId.fnName 属性,用于缓存失效和元信息获取。

直接使用(推荐)

import {
useQuery,
useSuspenseQuery,
useMutation,
useQueryClient,
getFnQueryKey,
getFnQueryOptions,
} from "@evjs/client";
import { getUsers, getUser, createUser } from "../api/users.server";

// 查询 —— 直接传入服务端函数,类型自动推导
const { data: users } = useQuery(getUsers); // data: User[]
const { data: user } = useQuery(getUser, userId); // data: User
const { data } = useSuspenseQuery(getUsers); // data: User[](保证有值)

// 变更 —— 直接传入服务端函数,与 useQuery 用法一致
const queryClient = useQueryClient();
const { mutate } = useMutation(createUser, {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: getFnQueryKey(getUsers) });
},
});

// 路由加载器 / 预取 —— 使用 getFnQueryOptions()
loader: ({ context }) =>
context.queryClient.ensureQueryData(getFnQueryOptions(getUsers));

服务端函数元信息

每个注册的服务端函数桩在运行时携带以下属性:

getFnQueryKey(getUsers)         // → ["<fnId>"]
getFnQueryKey(getUsers, someArg)// → ["<fnId>", someArg]
getUsers.fnId // → "<hash>"(稳定的 SHA-256)
getUsers.fnName // → "getUsers"
  • getFnQueryKey(fn, ...args) — 构建 TanStack Query key。用于 invalidateQueriessetQueryData 等。
  • .fnId — 稳定的内部函数 ID(只读)。
  • .fnName — 可读的导出名称(只读)。
  • getFnQueryOptions(fn, ...args) — 返回 { queryKey, queryFn },用于加载器、预取和 useInfiniteQuery

传输配置

HTTP(默认)

import { initTransport } from "@evjs/client";
initTransport({ endpoint: "/api/fn" });

自定义传输(如 WebSocket)

实现 ServerTransport 接口以使用自定义协议:

import { initTransport } from "@evjs/client";
import type { ServerTransport } from "@evjs/client";

const wsTransport: ServerTransport = {
call: async (fnId, args) => {
// 在这里实现你的 WebSocket 或自定义协议
},
};

initTransport({ transport: wsTransport });

错误处理

服务端

抛出带状态码和数据的结构化错误:

import { ServerError } from "@evjs/server";

export async function getUser(id: string) {
const user = await db.users.findById(id);
if (!user) {
throw new ServerError("用户未找到", {
status: 404,
data: { id },
});
}
return user;
}

客户端

捕获类型化错误:

import { ServerFunctionError } from "@evjs/client";

try {
const user = await getUser("123");
} catch (e) {
if (e instanceof ServerFunctionError) {
console.log(e.message); // "用户未找到"
console.log(e.status); // 404
console.log(e.data); // { id: "123" }
}
}

构建管道

在构建时,"use server" 指令触发两个独立的转换:

要点总结

模式用法
查询useQuery(fn, ...args)
Suspense 查询useSuspenseQuery(fn, ...args)
变更useMutation(fn)useMutation(fn, { onSuccess })
缓存失效getFnQueryKey(fn, ...args)
加载器 / 预取getFnQueryOptions(fn, ...args){ queryKey, queryFn }
函数元信息fn.fnIdfn.fnName
参数传递展开传入:useQuery(getUser, id) 而不是 useQuery(getUser, [id])
服务端错误服务端 ServerError → 客户端 ServerFunctionError