news 2026/9/19 19:29:05

在 Yew 应用中使用 yew-agent 与 Web Worker 执行后台计算:web_worker_fib 示例深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
在 Yew 应用中使用 yew-agent 与 Web Worker 执行后台计算:web_worker_fib 示例深度解析

在 Yew 应用中使用 yew-agent 与 Web Worker 执行后台计算:web_worker_fib 示例深度解析

【免费下载链接】yewRust / Wasm framework for creating reliable and efficient web applications项目地址: https://gitcode.com/gh_mirrors/ye/yew

本文以仓库中的 examples/web_worker_fib 示例为主体,结合yew-agent的源码实现,讲解如何在 Yew 应用中把 CPU 密集型任务(如斐波那契数列计算)交给 Web Worker 线程执行,避免阻塞主线程、保持界面流畅。读完本文,你将掌握基于yew-agent的 oneshot 任务代理(Oneshot Agent)的完整链路:定义任务、自定义消息编解码、注册 Worker、挂载 Provider 以及通过 Hook 在组件中调用,并能照搬到自己的项目中。

一、示例要解决的问题:计算密集任务与主线程卡顿

浏览器的主线程负责 DOM 更新、事件处理和 JavaScript/Wasm 执行,任何长时间运行的同步计算都会阻塞用户交互。斐波那契数列的递归实现是指数级复杂度,当输入 n 较大时(示例中默认输入44),在主线程直接计算会造成界面长时间无响应——按钮点击、输入框键入都会失去反馈。

web_worker_fib示例演示的正是这一场景的解决方案:把计算任务发送到 Web Worker 线程执行,主线程只负责发起请求、等待结果、更新界面。示例 UI 中刻意安排了一个"主线程计数器"按钮,用于直观验证:提交大数计算后,Worker 在后台忙计算,主线程的计数器仍然可以即时累加,证明界面没有被阻塞。

该示例本身是一个公开 Demo(见 README 中的 demo 徽章),核心思路来自社区贡献者 insou22,其中关于"如何在 wasm 场景下编译 Web Worker"的实践参考了yvt/img2text项目(见 examples/web_worker_fib/README.md)。

二、运行示例

示例采用 Trunk 作为开发构建工具,README 给出的运行方式为:

trunk serve --open

在 examples/web_worker_fib 目录下执行该命令即可启动开发服务器并自动打开浏览器。页面交互流程为:

  1. 在数字输入框中输入一个数值(max="50",默认44);
  2. 点击submit按钮,任务被发送到 Worker 线程;
  3. Output区域显示计算结果Fibonacci value: N
  4. 点击Main thread value区域的click!按钮,计数器应保持即时响应——即使前一个计算尚未完成。

Trunk.toml中仅声明了wasm_opt = "version_129"(见 examples/web_worker_fib/Trunk.toml),用于在构建时对 Wasm 产物做体积优化。

三、核心概念:yew-agent 的三类代理与四种要素

本示例的技术核心是yew-agentcrate(当前仓库中版本为 0.5.0,见 packages/yew-agent/Cargo.toml),它是 Yew 的 Web Worker 实现模块。根据 packages/yew-agent/README.md,Agent(代理)分为三种类型:

类型通信模型适用场景
Oneshot每次输入对应一次输出,use_oneshot_runner按需执行一次性计算任务,如本例的斐波那契
Reactor通过单条 bridge 发送多个输入、接收多个输出需要多轮问答的会话式任务
Worker底层 actor 模型,支持多 bridge 通信需要持续维护状态、多客户端订阅的服务

四种贯穿始终的要素:

  • Reachability(可达性):Agent 被生成时带有一个可达性属性。Private表示每次创建 bridge 都会生成一个全新的 Agent 实例,可并行计算;Public表示 Provider 的所有子组件共享同一个实例,整个 Provider 只生成一个 Agent。
  • Provider(提供者):每个 Agent 都需要一个 Provider 组件来提供通信能力并维护 bridge,所有相关 Hook 必须在 Provider 内部调用。
  • Hook(通信钩子):组件与 Agent 实例通信的入口。Worker/Reactor 提供use_worker_bridgeuse_reactor_bridge(回调接收输出)与 subscription 变体(把输出收集进切片);Oneshot 提供use_oneshot_runner按需执行。
  • Codec(编解码器):负责 Agent 输入输出消息在 JS 边界两侧的序列化/反序列化,默认是Bincode,可自定义。

四、代码详解:从任务定义到界面调用的完整链路

4.1 定义 Oneshot 任务:agent.rs

任务定义位于 examples/web_worker_fib/src/agent.rs。#[oneshot]属性宏把一个async fn转换为可运行在 Worker 中的 Oneshot Agent:

#[oneshot] pub async fn FibonacciTask(n: u32) -> u32 { fn fib(n: u32) -> u32 { if n <= 1 { 1 } else { fib(n - 1) + fib(n - 2) } } fib(n) }

这里使用递归斐波那契实现是刻意的:随着n增大,调用次数呈指数增长,从而制造一个真实的"长时间计算"来检验 Worker 是否真的把主线程解放了出来。

从源码结构看(见 packages/yew-agent-macro/src/oneshot.rs),#[oneshot]宏会展开生成:

  • 一个与函数同名的结构体(如FibonacciTask),内部持有Pin<Box<dyn Future<Output = u32>>>
  • 实现yew_agent::oneshot::Oneshottrait(type Input = u32create方法把函数调用封装成 future);
  • 实现std::future::Future(转发 poll 到内部 future);
  • 实现Registrable(注册到 Worker)与Spawnable(提供 Spawner)trait。

也就是说,你只需要写一个普通的async fn,宏会负责把它"变成"一个可注册、可派生的 Worker Agent。

4.2 自定义消息编解码:Postcard Codec

同一文件中还定义了一个自定义 Codec:

pub struct Postcard; impl Codec for Postcard { fn encode<I>(input: I) -> JsValue where I: Serialize, { let buf = postcard::to_vec::<_, 32>(&input).expect("can't serialize a worker message"); Uint8Array::from(buf.as_slice()).into() } fn decode<O>(input: JsValue) -> O where O: for<'de> Deserialize<'de>, { let data = Uint8Array::from(input).to_vec(); postcard::from_bytes(&data).expect("can't deserialize a worker message") } }

要点:

  • Codectrait 定义了两个关联方法:encode把实现Serialize的输入消息转成JsValuedecodeJsValue还原为实现Deserialize的输出消息;
  • 这里使用postcard(一种紧凑的 no-std 序列化格式,见 examples/web_worker_fib/Cargo.toml 中的postcard = "1.0.10"依赖),编码结果写入js_sys::Uint8Array再转换为JsValue穿过 JS 边界;
  • 从 packages/yew-agent/src/oneshot/provider.rs 的泛型签名pub fn OneshotProvider<T, C = Bincode>可以看到,默认编解码器是Bincode,自定义 Codec 通过泛型参数C注入。示例选用 Postcard 是为了展示自定义编码方案的能力——凡是输入/输出类型满足Serialize + Deserialize的消息格式(JSON、MessagePack 等)都可以按同样模式接入。

4.3 Worker 侧注册入口:src/bin/worker.rs

Agent 定义好后,需要一个独立的二进制入口把该 Agent 注册进 Worker 运行时:

use yew_agent::Registrable; use yew_worker_fib::agent::{FibonacciTask, Postcard}; fn main() { FibonacciTask::registrar().encoding::<Postcard>().register(); }

Registrable::registrar()返回宏生成的OneshotRegistrar<Self>.encoding::<Postcard>()声明使用自定义编解码器,.register()完成注册(见 examples/web_worker_fib/src/bin/worker.rs)。

4.4 应用侧入口:src/bin/app.rs

主应用入口保持极简,仅渲染根组件:

fn main() { yew::Renderer::<yew_worker_fib::App>::new().render(); }

(见 examples/web_worker_fib/src/bin/app.rs)

4.5 双二进制构建配置:index.html

本示例需要同时产出"主应用"和"Worker"两份 Wasm,二者通过 examples/web_worker_fib/index.html 中的 Trunk 指令区分:

<link>#[function_component] pub fn App() -> Html { html! { <OneshotProvider<FibonacciTask, Postcard> path="/worker.js"> <Main /> </OneshotProvider<FibonacciTask, Postcard>> } }
  • 泛型参数一:Agent 类型FibonacciTask;泛型参数二:编解码器Postcard
  • path="/worker.js":Worker 脚本的 URL 路径,由 Trunk 按data-type="worker"生成的产物命名。

在子组件Main中,通过 Hook 获取任务执行器:

let fib_task = use_oneshot_runner::<FibonacciTask>();

点击提交按钮时,把计算包装进spawn_local异步任务:

let calculate = { let input_value = *input_value; let output = output.clone(); move |_e: MouseEvent| { let fib_agent = fib_task.clone(); let output = output.clone(); spawn_local(async move { let output_value = fib_agent.run(input_value).await; output.set(format!("Fibonacci value: {output_value}")); }); } };

关键细节:

  • use_oneshot_runner::<T>()返回UseOneshotRunnerHandle<T>,其内部通过use_context::<OneshotProviderState<T>>()从组件树中查找对应 Provider 的状态(见 packages/yew-agent/src/oneshot/hooks.rs),因此Hook 必须在OneshotProvider内部调用,否则会 panic(提示failed to find worker context);
  • handle.run(input)是一个async方法,内部调用state.create_bridge().run(input).await:先创建(或复用)bridge,再把输入序列化发送给 Worker,等待结果返回;
  • 使用yew::platform::spawn_local(见 examples/web_worker_fib/src/lib.rs 的导入)在主线程调度这个异步等待,await期间主线程不会被占用;
  • use_state_equse_state分别管理输入框数值与输出文本,oninput事件里把输入字符串parseu32

界面中"主线程计数器"(clicker_value)与计算按钮并存,正是为了验证:计算任务真正运行在 Worker 线程,主线程事件循环保持畅通

五、底层原理:Provider 如何维护 Worker 生命周期

结合 packages/yew-agent/src/oneshot/provider.rs 的源码,可以看清 Provider 的内部机制:

  • OneshotProvider组件在挂载时构造OneshotProviderState,其中保存一个spawn_bridge_fn闭包,闭包内容等价于:

    OneshotSpawner::<T>::new() .as_module(module) .encoding::<C>() .spawn(&path)

    即真正负责new Worker(path)并配置编解码器的动作被封装成可复用的工厂函数。

  • 状态通过use_memo((path, lazy, reach), ...)记忆化,并把状态放入ContextProvider,从而对子树可见。

  • create_bridge区分可达性:

    • Reach::Public:复用一个held_bridge(首个 bridge 被持有,后续请求通过fork派生子 bridge),整个 Provider 只启动一个 Worker 实例;
    • Reach::Private:每次调用都通过spawn_bridge_fn新建 bridge 与 Worker,可并行。
  • lazy属性(默认true)控制是否延迟生成:为falsereach == Public时,Provider 挂载即预生成 bridge;为true时,Worker 在第一次有 Hook 请求 bridge 时才被创建。

Worker 侧的共享属性定义在 packages/yew-agent/src/worker/provider.rs 的WorkerProviderProps中,OneshotProvider复用了同一套属性:path(必填,Worker 脚本路径)、reach(默认Public)、module(是否以 ES Module 类型创建,默认false)、lazy(默认true)、children。这些配置完全适用于自定义场景,例如把reach改为Private即可让每个调用方拥有独立 Worker 实现并行计算。

六、在自己项目中复用此模式的清单

web_worker_fib的模式迁移到自己的 Yew 项目,需要完成以下步骤:

  1. 添加依赖:在Cargo.toml中加入yew-agent(以及可选的postcard或你选择的序列化库),参考 examples/web_worker_fib/Cargo.toml;
  2. 定义任务:用#[oneshot] pub async fn YourTask(input: InputType) -> OutputType声明任务,输入输出类型需实现Serialize + Deserialize
  3. (可选)自定义 Codec:实现Codectrait 的encode/decode,不定义则使用默认Bincode
  4. 编写 Worker 入口:创建src/bin/worker.rs,调用YourTask::registrar().encoding::<C>().register()
  5. 配置 Trunk:在index.html添加data-type="worker"的 rust 资源链接,保证 Worker 以独立 wasm 产物输出;
  6. 挂载 Provider:在组件树顶层使用<OneshotProvider<YourTask, C> path="/worker.js">包裹;
  7. 调用任务:在子组件中用use_oneshot_runner::<YourTask>()获取句柄,配合spawn_local在异步上下文中run(input).await获取结果。

七、小结

web_worker_fib虽小,却完整展示了 Yew 生态中yew-agent的 oneshot 代理从"任务定义 → 注册 → 构建 → 调用 → 回写界面"的整条链路,并用一个"主线程计数器"把"Worker 计算不阻塞主线程"这一核心收益直观呈现出来。理解这个示例后,你可以将任意 CPU 密集型或需异步化处理的逻辑(图片处理、数据聚合、解析大文件等)以同样的方式卸载到 Worker 线程,同时借助reachlazy、自定义Codec等配置按需调整并发与消息格式。进一步探索可阅读 packages/yew-agent/README.md 以及yew-agent源码中的 oneshot 与 worker 模块。

【免费下载链接】yewRust / Wasm framework for creating reliable and efficient web applications项目地址: https://gitcode.com/gh_mirrors/ye/yew

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 19:27:27

树莓派实现Modbus TCP到声光语音终端字节帧协议转换

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 19:26:02

网盘直链解析全攻略:从原理到实战,告别龟速下载

1. 分享链接为什么下载不爽&#xff1a;先搞懂直链的价值1.1 网盘下载的常规路径到底绕在哪先说个经常遇到的场景&#xff1a;你想从一个网盘分享链接里拿一个大文件&#xff0c;点开分享页&#xff0c;页面做得很干净&#xff0c;伸手就能看到“下载”按钮。但真等你去点&…

作者头像 李华
网站建设 2026/9/19 19:25:29

从数据闭环到智能决策:数字化转型的落地路线图

简介&#xff1a;《一本书读懂数字化转型》读书笔记以118页PPT形式呈现&#xff0c;面向企业管理者、数字化转型项目负责人以及对数字化逻辑感兴趣的读者。内容围绕“取势、明道、优术”三部分&#xff0c;系统讲解数字化与数字化转型的概念、转型改变的本质&#xff0c;以及战…

作者头像 李华
网站建设 2026/9/19 19:21:51

嵌入式工程师的边缘AI转型:从驱动开发到系统架构

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 19:21:47

AD9361 Fast Lock脚本:Python自动生成Profile寄存器配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华