
Yew Agent 实战指南在 Rust / Wasm 中利用 Web Worker 构建多线程应用【免费下载链接】yewRust / Wasm framework for creating reliable and efficient web applications项目地址: https://gitcode.com/gh_mirrors/ye/yew导读yew-agent是 Yew 生态中的 Web Worker 实现模块它把浏览器 Web Worker 封装成一套贴近组件开发习惯的Agent代理模型你只需用宏定义 Worker 逻辑、用 Provider 组件提供上下文、再用 Hook 建立桥接即可在后台线程中执行计算密集任务同时保持 UI 线程流畅。读完本文你将掌握 Oneshot、Reactor、Worker 三类 Agent 的适用场景理解Reach可达性对 Agent 实例生命周期的影响并能基于 Bridge / Subscription / Runner 三种通信方式写出可运行的 Yew 多线程应用。一、什么是 yew-agentYew 的 Web Worker 封装yew-agent是 Yew 官方工作区中的一个独立 crateCargo.toml其定位在 README.md 中写得很明确This module contains Yews web worker implementation该模块包含 Yew 的 Web Worker 实现。它依赖gloo-worker版本 0.6启用futuresfeature作为底层 Worker 通信实现并将 Worker 的生成、注册、桥接等底层细节向上抽象为三类 Agent 类型。在架构上yew-agent由四个核心子模块组成见 lib.rsoneshot一次性任务 Agent一次输入对应一次输出reactor基于异步函数的响应式 Agent单桥多输入多输出worker底层 actor 模型 Worker一对多桥接reach定义 Agent 的Reach可达性枚举。同时yew-agent提供了prelude模块lib.rs集中 re-export 了常用的 Hook 与句柄类型pub use crate::oneshot::{UseOneshotRunnerHandle, oneshot, use_oneshot_runner}; pub use crate::reach::Reach; pub use crate::reactor::{ ReactorEvent, ReactorScope, UseReactorBridgeHandle, UseReactorSubscriptionHandle, reactor, use_reactor_bridge, use_reactor_subscription, }; pub use crate::worker::{ UseWorkerBridgeHandle, UseWorkerSubscriptionHandle, WorkerScope, use_worker_bridge, use_worker_subscription, }; pub use crate::{Registrable, Spawnable};开发者通常只需use yew_agent::prelude::*;即可获得绝大多数常用类型。二、三类 AgentOneshot、Reactor 与 WorkerREADME 将 Agent 划分为三种类型它们在输入/输出数量与抽象层级上各有侧重。2.1 Oneshot一次输入一次输出A kind of agent that for each input, a single output is returned.一种针对每个输入返回单个输出的 Agent。Oneshot 是最简单的任务型 Agent适合提交一个参数等一个结果的场景典型如耗时计算、文件哈希、图像处理等。在源码层面Oneshottrait 是gloo_worker::oneshot::Oneshot其要求Input与Output都实现Serialize Deserialize见 oneshot/mod.rs。一个 Oneshot Agent 的本质是一个async fn配合过程宏#[oneshot]使用。仓库中的 web_worker_fib 示例就是标准模板定义一个FibonacciTask其run方法接收输入u32返回计算后的u32整个过程在独立 Worker 线程中执行UI 线程不会阻塞。2.2 Reactor单桥多输入多输出A kind of agent that can send many inputs and receive many outputs over a single bridge.一种可以在单条桥接上发送多个输入、接收多个输出的 Agent。Reactor 是事件流型 Agent它本质上是一个异步函数接收一个ReactorScopeInput, Output参数。reactor/mod.rs 的文档说明得非常清楚ReactorScope既是一个输入流Stream从桥接中持续产出输入又是一个输出接收器Sink提供额外的send方法向桥接发送输出当桥接断开时输入流与输出接收器都会关闭。use futures::sink::SinkExt; use futures::stream::StreamExt; use yew_agent::reactor::{ReactorScope, reactor}; #[reactor(MyReactor)] pub async fn my_reactor(mut scope: ReactorScopeReactorInput, ReactorOutput) { while let Some(input) scope.next().await { // 处理每个输入 let output ReactorOutput { /* ... */ }; // 发送输出若失败说明桥接已断开 if scope.send(output).await.is_err() { break; } } }这种模型非常适合持续连接、持续收发的场景例如一个维护会话状态的后台服务、一个持续推送数据的订阅源。2.3 Worker底层 actor 模型The low-level implementation of agents that provides an actor model and communicates with multiple bridges.Agent 的底层实现提供 actor 模型并可与多个桥接通信。Worker 是最底层的 Agent 类型采用经典的actor 模型worker/mod.rs。你需要实现yew_agent::worker::Workertrait通常只需实现四个关联成员关联成员说明type Input组件向 Worker 发送的消息类型须可序列化type OutputWorker 返回给组件的消息类型须可序列化type MessageWorker 内部的异步消息类型fn create(scope: WorkerScopeSelf) - Self创建 Worker 实例fn update(mut self, scope, msg: Self::Message)处理内部消息fn received(mut self, scope, msg: Self::Input, id: HandlerId)收到组件消息时回调可通过scope.respond(id, output)回复use yew_agent::worker::{HandlerId, WorkerScope}; pub struct MyWorker {} impl yew_agent::worker::Worker for MyWorker { type Input (); type Output WorkerResponseType; type Message (); fn create(scope: WorkerScopeSelf) - Self { MyWorker {} } fn update(mut self, scope: WorkerScopeSelf, _msg: Self::Message) { // 处理内部消息 } fn received(mut self, scope: WorkerScopeSelf, _msg: Self::Input, id: HandlerId) { // 回复指定 HandlerId 的桥接 scope.respond(id, WorkerResponseType::IncrementCounter); } }由于一个 Worker 实例可同时与多条桥接通信它天然适合单 Worker 服务多个组件的共享后台任务场景。仓库中的 web_worker_prime 示例展示了质数计算这一类共享型任务如何用 Worker Agent 承载。三、Reach可达性决定 Agent 实例的共享粒度README 强调When an agent is spawned, each agent is associated with a reachability.Agent 生成时都会关联一个可达性。在源码中这是一个派生自Debug, Clone, PartialEq, Eq, Hash, Copy的简单枚举reach.rspub enum Reach { /// 公共可达性 Public, /// 私有可达性 Private, }3.1 Private每次桥接一个独立实例Each time a bridge is created, a new instance of agent is spawned. This allows parallel computing between agents.每次创建桥接时都会生成一个新的 Agent 实例从而允许 Agent 之间并行计算。Private 模式下每个调用 Hook 的组件都会得到独立生成的 Agent 实例。这在源码中有明确体现WorkerProviderState::create_bridge在Reach::Private分支直接执行(self.spawn_bridge_fn)()生成全新桥接worker/provider.rs。由于实例互不共享多个组件可以并行执行各自的任务互不干扰。适合任务相互独立、追求并行度的场景。3.2 Public同一 Provider 下共享单实例Public agents are shared among all children of a provider. Only 1 instance will be spawned for each public agents provider.Public Agent 在 Provider 的所有子组件间共享每个 Public Agent Provider 只会生成 1 个实例。Public 模式下WorkerProvider会持有一个驻留桥接held bridge所有 Hook 都通过fork派生自己的桥接但底层 Worker 进程只有一份。看 worker/provider.rs 的实现fn get_held_bridge(self) - RcWorkerBridgeW { let mut held_bridge self.held_bridge.borrow_mut(); match held_bridge.as_mut() { Some(m) m.clone(), None { let bridge Rc::new((self.spawn_bridge_fn)()); *held_bridge Some(bridge.clone()); bridge } } } pub fn create_bridge(self, cb: CallbackW::Output) - WorkerBridgeW { match self.reach { Reach::Public { let held_bridge self.get_held_bridge(); held_bridge.fork(Some(move |m| cb.emit(m))) } Reach::Private (self.spawn_bridge_fn)(), } }即首个请求触发 Worker 生成之后的请求复用同一底层 Worker。同样Oneshot 的OneshotProviderState在 Public 模式下也会复用驻留桥接并forkoneshot/provider.rs。适合多个组件共享同一份状态/同一份计算结果的场景能显著节省资源。3.3 Provider 与 lazy 属性的配合Each Agent requires a provider to provide communications and maintain bridges. All hooks must be called within a provider.每个 Agent 都需要一个 Provider 来提供通信并维护桥接所有 Hook 必须在 Provider 内部调用。这是使用yew-agent的硬性约束Hook 必须在对应的 Provider 组件子树内调用否则会触发 panic。以use_oneshot_runner为例其实现直接调用use_context::OneshotProviderStateT().expect(failed to find worker context)oneshot/hooks.rs找不到上下文即 panic。use_worker_bridge同样如此use_context::RcWorkerProviderStateT().expect_throw(cannot find a provider for current agent.)worker/hooks.rs。WorkerProvider的完整属性worker/provider.rs属性类型默认值说明pathAttrValue必填Agent 脚本Worker的路径reachReachReach::PublicAgent 的可达性moduleboolfalse是否以typemodule方式创建 WorkerES Modulelazybooltrue是否懒生成首次有 Hook 请求桥接时才生成 Agent不影响 Private 模式childrenHtml默认Provider 的子节点其中lazy的语义在源码中可见一斑当reach Public !lazy时Provider 在挂载时就会立刻触发get_held_bridge()预生成 Agentworker/provider.rs若lazy true则等到第一个 Hook 请求时才生成。OneshotProvider也有完全相同的逻辑oneshot/provider.rs。四、与 Agent 通信Bridge、Subscription 与 RunnerREADME 指出Hooks provides means to communicate with agent instances.Hook 提供了与 Agent 实例通信的手段。yew-agent提供三种通信方式A bridge takes a callback to receive outputs from agents and provides a handle to send inputs to agents.桥接通过回调接收 Agent 的输出并提供句柄向 Agent 发送输入。4.1 Bridge回调式双向通信Bridge 是回调推送模型你向 Hook 传入一个回调闭包Agent 每次输出都会触发该回调同时你拿到的句柄可以随时send输入。对应两个 Hookuse_worker_bridgeworker/hooks.rs返回UseWorkerBridgeHandleWuse_reactor_bridge返回UseReactorBridgeHandleR。以use_worker_bridge的官方示例为例worker/mod.rsuse serde::{Deserialize, Serialize}; use yew::prelude::*; use yew_agent::worker::{use_worker_bridge, UseWorkerBridgeHandle}; #[derive(Serialize, Deserialize)] pub enum WorkerResponseType { IncrementCounter, } #[component(UseWorkerBridge)] fn bridge() - Html { let counter use_state(|| 0); { let counter counter.clone(); // 回调接收 Worker 输出类型为 MyWorker::Output let bridge: UseWorkerBridgeHandleMyWorker use_worker_bridge(move |response| match response { WorkerResponseType::IncrementCounter { counter.set(*counter 1); } }); } html! { div{*counter}/div } }UseWorkerBridgeHandleW提供两个方法worker/hooks.rssend(self, msg: W::Input)向 Worker 发送一条输入reset(self)断开旧桥接并重新建立新桥接内部通过use_reducer的 dispatcher 触发重建实现见 worker/hooks.rs。值得注意的实现细节use_worker_bridge的回调会在每次渲染时刷新通过use_mut_ref保存最新闭包确保闭包中捕获的状态始终是最新的且桥接在整个组件生命周期内只建立一次use_memo依据 provider state 与 reducer 状态缓存。4.2 Subscription切片式批量收集输出Similar to bridges, a subscription produces a handle to send inputs to agents. However, instead of notifying the receiver with a callback, it collect all outputs into a slice.与桥接类似订阅也提供发送输入的句柄但区别在于它不通过回调通知接收者而是把全部输出收集到一个切片中。当你不关心每个输出到达的实时时刻、只想要当前所有已收到的输出时Subscription 更合适。对应两个 Hookuse_worker_subscriptionworker/hooks.rs返回UseWorkerSubscriptionHandleWuse_reactor_subscription返回UseReactorSubscriptionHandleR。UseWorkerSubscriptionHandleW实现了DerefTarget [RcW::Output]worker/hooks.rs因此你可以像操作[RcW::Output]一样直接遍历所有输出。其内部原理是用use_reducer维护一个OutputsState每次桥接收到输出时 dispatch 一个OutputsAction::Push把输出压入切片并在桥接重建时通过 effect 清空旧输出worker/hooks.rs。它的reset()会在断开旧桥接的同时清空已收集的输出。4.3 RunnerOneshot 的按需执行器Unlike other agents, oneshot bridges provide ause_oneshot_runnerhook to execute oneshot agents on demand.与其他 Agent 不同Oneshot 桥接提供use_oneshot_runnerHook 来按需执行 Oneshot Agent。由于 Oneshot 是一次一答它没有常驻桥接的必要而是提供use_oneshot_runnerT()返回UseOneshotRunnerHandleToneshot/hooks.rs。该句柄的核心方法是pub async fn run(self, input: T::Input) - T::Output { self.state.create_bridge().run(input).await }调用run(input).await即可在后台线程执行任务并等待结果。UseOneshotRunnerHandle实现了Clone与PartialEq可以安全地在闭包间克隆共享。仓库中的 web_worker_fib 是 Runner 的完整实战示例点击按钮后spawn_local一个异步任务在后台 Worker 中计算斐波那契数期间主线程上的计数器依然可以点击累加直观展示耗时计算不阻塞 UI的效果let fib_agent fib_task.clone(); spawn_local(async move { // 在后台 Worker 中执行不阻塞主线程 let output_value fib_agent.run(input_value).await; output.set(format!(Fibonacci value: {output_value})); });对应的 Provider 挂载方式html! { OneshotProviderFibonacciTask, Postcard path/worker.js Main / /OneshotProviderFibonacciTask, Postcard }注意这里展示了两个关键点其一OneshotProviderT, C的第二个泛型参数是Codec编解码器默认是Bincode示例中使用了Postcard其二path指向编译产出的 Worker 脚本如/worker.js这正是 Provider 中path属性的作用。五、Provider 泛型与 Codec 说明三类 Provider 在实现上高度同构均通过ContextProvider把内部状态注入组件树worker/provider.rs、oneshot/provider.rs并由一个spawn_bridge_fn闭包封装如何生成桥接以便在上下文中擦除 Codec 泛型let spawn_bridge_fn: Rcdyn Fn() - WorkerBridgeW { let path path.clone(); Rc::new(move || W::spawner().as_module(module).encoding::C().spawn(path)) };见 worker/provider.rs。这段代码揭示了 Provider 泛型参数的底层逻辑W或TAgent 类型要求其Input/Output实现Serialize Deserialize且static见 worker/provider.rsCCodec编解码器类型默认Bincodepub fn WorkerProviderW, C Bincode底层通过spawner().encoding::C()指定module属性对应.as_module(module)决定 Worker 是否按 ES Module 方式加载path对应.spawn(path)决定 Worker 脚本的 URL。Registrable、Spawnable、Bincode、Codec这些底层 trait 也由yew-agent在 lib.rs 中公开 re-export方便开发者直接使用。六、实战总结如何选择 Agent 类型与通信方式综合 README 的骨架与源码实现可给出如下选型建议需求场景推荐类型通信方式一次调用、一次返回如斐波那契、哈希、图像处理oneshotuse_oneshot_runner长连接、持续多输入多输出如聊天、推送、会话服务reactoruse_reactor_bridge/use_reactor_subscription底层 actor 模型、单实例服务多桥接如全局共享任务队列workeruse_worker_bridge/use_worker_subscription实时关注每个输出需要回调触发副作用任意Bridge只需展示全部已收集输出不关心实时到达任意Subscription多组件需并行执行独立任务任意reach Reach::Private多组件共享同一 Worker 与状态任意reach Reach::Public无论选择哪条路线都需要牢记三条使用铁律所有 Hook 必须在对应的 Provider 组件内调用否则use_context会直接 panic这是 oneshot/hooks.rs 与 worker/hooks.rs 中显式expect的结果Agent 的Input/Output必须可序列化Serialize Deserialize因为跨线程/跨 Worker 通信本质上依赖序列化path属性必须指向构建产物中的 Worker 脚本路径并视构建工具如 Trunk配置决定是否设置module true。从 README 到源码yew-agent的设计始终围绕把 Web Worker 的复杂性收进 Provider Hook这一思路类型系统Oneshot/Reactor/Worker定义任务形态Reach定义共享粒度Provider 维护桥接生命周期Hook 提供声明式通信接口。理解这四层之后你便可以在 Yew 应用中放心地把重计算、轮询、消息处理等任务移出主线程写出更流畅的 Web 应用。【免费下载链接】yewRust / Wasm framework for creating reliable and efficient web applications项目地址: https://gitcode.com/gh_mirrors/ye/yew创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考