Show HN: Frond – 一款管理应用依赖图的前端运行时

Hacker News Top 工具

摘要

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 Blog

Hugging Face 推出 `gradio.Server`,一款全新工具,让开发者能够将 Gradio 的后端基础设施(队列、托管)与使用 React、Svelte 或纯 HTML/CSS/JS 构建的自定义前端结合使用。

Show HN: Freenet,一个面向去中心化应用的点对点平台

Hacker News Top

Freenet 是一个点对点平台,用于构建无需服务器运行的去中心化应用,它采用按环上位置组织的小世界网络。开发者可以使用 Rust 和 TypeScript 等熟悉工具部署应用,用户则通过浏览器访问,无需担心被追踪或被下架风险。

Show HN: Ant – 一个 JavaScript 运行时与生态

Hacker News Top

Ant 是一个 JavaScript 运行时与生态系统,包含引擎、包管理器、注册中心 (ants.land)、部署平台以及桌面应用框架 (Ant Desktop),旨在作为现有 JavaScript 技术栈的统一替代方案。