Show HN: Frond – 一款管理应用依赖图的前端运行时
摘要
Frond 是一款前端运行时,可管理 React 应用的依赖图,并自动处理服务的生命周期与清理工作。
暂无内容
查看缓存全文
缓存时间: 2026/07/01 14:00
# Frond — React 应用的前端运行时图 来源: https://frondruntime.dev/
## React 不是你的运行时。Frond 才是。
你的应用已经有运行时了。它分散在 provider、effect 和清理脚本中。
Frond 把它变成一张图。
Effect 运行它。
React 仍然是渲染器。
每个不断成长的前端应用都会遇到同样的问题:服务之间的依赖关系,以及当前用户变化时该清理什么。
**大多数不断成长的前端应用都会碰到相同的形状。实现方式是一份你手动维护的清单。**
### 没有 Frond 的日子 / 登出清单
```typescript
async function signOut() {
await session.end();
// ↓ 手动列出每个用户作用域的东西。
localStorage.removeItem("token");
queryClient.clear(); // 缓存的查询
abortInFlightRequests(); // 正在进行的请求
presenceChannel.leave(); // 实时 presence
socket.disconnect(); // 实时传输
billingStore.reset(); // 领域状态存储
navigate("/login");
// 添加了一个新的用户作用域服务?
// 记得在这里也加一行。
}
```
手动记忆。每个新的用户作用域服务都会增加一行需要记住的代码。漏掉一个,旧用户就可能通过存储、socket、分析身份、过期更新或正在运行的请求泄漏出去。
### 使用 Frond
#### frond / 认证动作
```typescript
type SessionSpec = Frond.NodeSpec<{
readonly args: Frond.Args.None;
readonly key: Frond.Key.Singleton;
readonly result: Session;
}>;
export class SessionNode extends Frond.NodeBase {
static readonly spec = Frond.serviceSpec({
tag: Frond.tag("app/session"),
key: () => Frond.Key.singleton(),
driver: Frond.Driver.Async({
acquire: Frond.Driver.Acquire(({ signal }) => restoreSession(signal)),
}),
});
}
// 一次调用 —— 所有依赖节点都会被驱逐、中断并释放。
function useSignOut() {
const controls = FrondReact.useNodeControls(SessionNode, {});
return () => controls.evict("selfAndDependents", "sign-out");
}
```
#### frond / 用户作用域资源
```typescript
type PresenceSpec = Frond.NodeSpec<{
readonly args: Frond.Args.None;
readonly key: Frond.Key.Singleton;
readonly deps: {
readonly socket: Frond.Dep;
readonly session: Frond.Dep;
};
readonly result: PresenceChannel;
}>;
export class PresenceNode extends Frond.NodeBase {
static readonly spec = Frond.resourceSpec({
tag: Frond.tag("app/presence"),
key: () => Frond.Key.singleton(),
dependencies: Frond.dependencies(() => ({
socket: Frond.dep(SocketNode, Frond.Args.none),
session: Frond.dep(SessionNode, Frond.Args.none),
})),
driver: Frond.Driver.Async({
// 在 acquire 时加入用户的 presence 频道 ——
// socket 按自己的节奏心跳。
acquire: Frond.Driver.Acquire(({ deps }) =>
deps.socket.result.join("presence", {
userId: deps.session.result.userId,
heartbeat: 5_000,
})
),
// release 与 acquire 配对 ——
// signOut() 永远不需要知道 presence。
release: Frond.Driver.Release(({ node }) =>
node.result.leave({ reason: "sign-out" })
),
}),
});
}
```
运行时边界。清理属于 acquire 资源的节点。驱逐会运行 release、取消进行中的工作、清除就绪状态,并拒绝被驱逐图记录的过期提交。
[阅读关于驱逐和 release 的信息](https://frondruntime.dev/docs/model/eviction-and-release)
### 状态工具回答
#### 值在哪里?
- **Redux / Zustand**:值、修改、选择器
- **React Query**:服务端缓存、失效、重试
- **MobX**:可观察的领域状态
- **Context**:通过 React 进行值连接
仍然在模型之外。
#### 谁拥有生命周期?
- 这个值加载之前,什么必须就绪?
- 这个状态附加到哪个键标识(keyed identity)?
- 当依赖变化时,谁取消进行中的工作?
- 驱逐之后,谁拒绝过时的提交?
- release、telemetry 和 reset 在哪里?
### Frond 回答
#### 状态何时允许存在?
- 标识(identity)
- 可观察状态(observable state)
- 依赖(dependencies)
- 就绪(readiness)
- 动作(actions)
- 作用域(scope)
- 释放(release)
- 驱逐(eviction)
可见状态。缓存结果、可观察字段和计算 getter 是可见的。Frond 保留了这些易用性,然后将它们附加到图标识、就绪、取消、释放和驱逐上。
运行时生命周期。React 读取节点。MobX 使其可观察。Effect 执行工作。Frond 掌控节点何时处于 alive、ready、stale、released 或 dead 状态。
[运行时及其图](https://frondruntime.dev/docs/model/runtime-and-graph)
1. 后端 schema
2. driver 返回
3. node.result
4. deps.x.result
5. useNode()
### 定义 / 类型化 driver
```typescript
type ProfileSpec = Frond.NodeSpec<{
readonly args: Frond.Args.None;
readonly key: Frond.Key.Singleton;
readonly deps: {
readonly auth: Frond.Dep;
readonly api: Frond.Dep;
};
readonly result: Profile;
}>;
export class ProfileNode extends Frond.NodeBase {
static readonly spec = Frond.resourceSpec({
tag: Frond.tag("app/profile"),
key: () => Frond.Key.singleton(),
dependencies: Frond.dependencies(() => ({
auth: Frond.dep(AuthNode, Frond.Args.none),
api: Frond.dep(ApiNode, Frond.Args.none),
})),
driver: Frond.Driver.Async({
acquire: Frond.Driver.Acquire(async (ctx) => {
// ctx.deps.auth.result → AuthState
// ctx.deps.api.result → ApiClient
return await ctx.deps.api.result.user.profile.query({
userId: ctx.deps.auth.result.userId,
signal: ctx.signal,
});
}),
}),
});
}
// Profile 从 driver 返回中推断 —— 无需注解。
```
### 依赖 / 类型传播
```typescript
type BillingSpec = Frond.NodeSpec<{
readonly args: Frond.Args.None;
readonly key: Frond.Key.Singleton;
readonly deps: {
readonly profile: Frond.Dep;
readonly api: Frond.Dep;
};
readonly result: Billing;
}>;
export class BillingNode extends Frond.NodeBase {
static readonly spec = Frond.resourceSpec({
tag: Frond.tag("app/billing"),
key: () => Frond.Key.singleton(),
dependencies: Frond.dependencies(() => ({
profile: Frond.dep(ProfileNode, Frond.Args.none),
api: Frond.dep(ApiNode, Frond.Args.none),
})),
driver: billingDriver,
});
// 无需注解 —— 从 dep(ProfileNode) 推断。
get plan() {
return this.deps.profile.result.plan;
// ^? Plan
}
}
```
### 消费 / 零注解
```typescript
function BillingPage() {
// 运行时提供一个就绪的 BillingNode ——
// 没有 isLoading,没有 fallback,没有守卫。
const node = FrondReact.useNode(BillingNode, {});
// node.plan 被推断为 Plan
// 通过 dep(ProfileNode) 链。
return <div>{node.plan.name}</div>;
}
```
没有消费者类型转换,没有手动依赖连线。图就是类型系统。`dep(ProfileNode)` 知道结果类型。依赖项继承它。React 读取它。如果 driver 改变形状,编译器会捕获每个消费者。
[Spec 和 class —— 类型化节点的工作方式](https://frondruntime.dev/docs/authoring/spec-and-class)
### 结构化失败
失败携带 `kind`、`tag`、`retryable` 和原因链。没有 `e: unknown`,不需要猜测 `null` 的含义。
### 走过的
运行时沿着链构建一个可序列化的报告 —— 指纹、标签、上下文、依赖聚合、运行时事件元数据。你不需要编写投影。
### 连接的
将 sink 一次性接入运行时。每个失败都会以图感知的分组路由到你的跟踪器。没有每个组件的 `try/catch`,不需要记得进行捕获。
#### 今天 / 捕获并重建上下文
```typescript
// 分散在每个 fetch、hook、边界中 ——
// 每个 catch 都手动构建它的 Sentry 上下文。
async function loadProfile(userId: string) {
try {
return await api.getProfile(userId);
} catch (e) {
Sentry.captureException(e, {
tags: { feature: "profile" },
// 它是就绪问题?认证问题?
// 一个扁平的 DependencyFailed?
// 我们只有 `e: unknown`。
// 没有链(三次 try/catch 之前丢失了)
// 没有 retryable 标志
// 没有一致的指纹
});
throw e;
}
}
// 在 billing.ts、feed.ts、dashboard.ts…… 重复
```
#### Frond / 一个 sink,走过的链
```typescript
// 一个 sink。每个节点的每个失败
// 都会以图感知的分组流向 Sentry。
// (也可以是任何跟踪器 —— 报告形状是通用的。)
const sentrySink = Frond.Diagnostics.createRuntimeReportSink({
name: "sentry",
handleReport: ({ report }) => {
Sentry.captureException(report.error, {
fingerprint: [...report.fingerprint],
// ["frond", kind, rootTag, nodeTag]
tags: report.tags,
// { "frond.kind", "frond.retryable",
// "frond.root_tag", "frond.node_tag" }
contexts: report.contexts,
// { frond, causeChain, dependencyFailures,
// runtimeEvent }
extra: report.extra,
});
},
});
const runtime = Frond.createRuntime({
sinks: [sentrySink],
});
```
错误是模型的一部分。运行时分类,走过原因链,并为 Sentry 风格的跟踪器构建报告 —— 指纹按图拓扑分组,标签携带 `kind` 和 `retryable`,上下文携带完整链。一次接线即可。
[错误如何在图中流动](https://frondruntime.dev/docs/authoring/errors)
### 取消
#### 信号无处不在
每个 acquire 和 refresh 都收到一个连接到其作用域的 `signal`。当一个节点被驱逐时,进行中的工作会被中断 —— 请求中止、定时器清除、流关闭。
### 作用域资源
#### 清理按逆序运行
Socket、订阅、interval —— 用 `disposers.add(...)` 注册它们。Release 按逆序运行它们,在运行时路径上。
### 可组合的失败
#### 抛出、传播、结构化
driver 抛出。运行时捕获,分类,附加原因链,并通知每个依赖。运行时使用上面显示的相同原因链报告。
### 可选加入
#### 编写你本来就会写的编排。将 `Frond.Driver.Async` 换成 `Frond.Driver.Effect`,你就能获得重试、有界并发、超时和声明式失败分类 —— 组合而成,而非手动拼凑。
- **重试**:`Schedule.exponential` vs 你自己的退避循环。
- **并发**:`Effect.all({ concurrency })` vs 你自己的 Promise 门控。
- **分类**:`while: (e) => ...` vs catch 中的嵌套 `if/else`。
#### Frond / effect 模式 driver,重试 + 并发 + 分类
```typescript
// DashboardSpec: facade, api dep, three-panel result.
export class DashboardNode extends Frond.NodeBase {
static readonly spec = Frond.facadeSpec({
tag: Frond.tag("app/dashboard"),
key: () => Frond.Key.singleton(),
dependencies: Frond.dependencies(() => ({
api: Frond.dep(ApiNode, Frond.Args.none),
})),
driver: Frond.Driver.Effect({
acquire: Frond.Driver.Acquire((ctx) =>
Effect.gen(function* () {
const fetchPanel = (panel: PanelId) =>
ctx
.tryPromise((signal) =>
ctx.deps.api.result.dashboard.panel(panel, signal)
)
.pipe(
// 指数退避,认证失败时快速失败。
Effect.retry({
schedule: Schedule.exponential("100 millis"),
times: 3,
while: (e) => e._tag !== "AuthError",
}),
Effect.timeout("5 seconds")
);
// 三个面板并行,同时最多两个在飞行。
const [activity, billing, feed] = yield* Effect.all(
[fetchPanel("activity"), fetchPanel("billing"), fetchPanel("feed")],
{ concurrency: 2 }
);
return { activity, billing, feed };
})
),
}),
});
}
```
Effect 是引擎,不是 API。你可以在不写一行 `Effect.gen` 的情况下获得取消、作用域和结构化失败。如果你想用,逃生口就在那里。
[Driver 和 Effect 模式](https://frondruntime.dev/docs/authoring/drivers)
### 可能不适合
- 你的应用主要渲染独立的屏幕。
- 数据加载仅限页面内。
- 登出清除一个 token 和一个缓存。
- React Query 解释了大部分异步状态。
- 你没有长期运行的前端服务。
### 可能适合
- 启动时有真正的就绪门控。
- 服务依赖于其他服务。
- 用户标识使应用一半失效。
- Socket、SDK、分析和传输需要清理。
- 屏幕聚合了许多资源。
- 你需要知道为什么某样东西没有就绪。
[编写你的第一个节点](https://frondruntime.dev/docs/start/first-node)
相似文章
使用 Gradio 后端构建任意自定义前端
Hugging Face 推出 `gradio.Server`,一款全新工具,让开发者能够将 Gradio 的后端基础设施(队列、托管)与使用 React、Svelte 或纯 HTML/CSS/JS 构建的自定义前端结合使用。
Show HN: Freenet,一个面向去中心化应用的点对点平台
Freenet 是一个点对点平台,用于构建无需服务器运行的去中心化应用,它采用按环上位置组织的小世界网络。开发者可以使用 Rust 和 TypeScript 等熟悉工具部署应用,用户则通过浏览器访问,无需担心被追踪或被下架风险。
Show HN: Nectar——一个类 Rust 的 React,编译为 WebAssembly
Nectar 是一个新型 Web 框架,将类 Rust 代码编译为 WebAssembly,通过 O(1) 信号更新和零依赖构建消除了 JavaScript 依赖。
Show HN: Ant – 一个 JavaScript 运行时与生态
Ant 是一个 JavaScript 运行时与生态系统,包含引擎、包管理器、注册中心 (ants.land)、部署平台以及桌面应用框架 (Ant Desktop),旨在作为现有 JavaScript 技术栈的统一替代方案。
Show HN: Nub – 类似 Bun 的用于 Node.js 的一体化工具包
Nub 是一个快速的一体化工具包,用于 Node.js,提供类似 Bun 的开发者体验,包括运行 TypeScript 文件、管理依赖项和 Node 版本,所有这些都集中在一个用 Rust 编写的 CLI 工具中。