小工具知识网

当前位置: 首页 > 行业知识 > rust前端web开发框架yew使用

行业知识

rust前端web开发框架yew使用

2026-11-15 04:57:43 来源:互联网转载

构建完整基于 rust 的 web 应用,使用yew框架

trunk 构建、打包、发布 wasm web 应用

安装后会作为一个系统命令,默认有两个特性开启

  • rustls - 客户端与服务端通信的 tls 库
  • update_check - 用于应用启动时启动更新检查,应用有更新时提示用户更新。
  • native-tls 需要指定开启,使用系统原生的 tls 用于客户端;使用 openssl 用于服务端
    $> cargo install --locked trunk

    创建一个 rust web 项目cargo new rust-yew-web,项目根目录下创建index.html

    没有任何内容,尝试一下trunk build,可以看到dist目录

    $> trunk buil

    安装yew开始 web 程序

    用于创建使用 webAssembly 的多线程 web 应用框架。

    $> cargo add yew --features "csr"

    yew不会默认指定特性,我们做的是 web 端开发,所以指定开启csr。其他的还包括 ssr - 服务端渲染;hydration - 混合开发,支持客户端、服务端渲染。

    在src/main.rs定义方法app渲染 html,html!宏可以定义类似 jsx 语法的视图结构。

    use yew::prelude::*;#[function_component(App)]fn app() -> Html {    html! {        

    {"hello, trunk/yew!"}

    }}fn main() { yew::Renderer::::new().render();}

    #[function_component(App)] 属性宏使一个普通的 rust 函数变成了一个函数组件,这个组件必须返回 html.#[function_component]接受一个组件名称,这里我们定义的是App

    函数组件可以接受一个参数props: &Props用于组件之间传递数据。它通常是一个没有状态的静态组件。

    启动,通过trunk serve --open启动一个服务,直接打开浏览器

    $> trunk serve --open

    可以看到浏览器输出,修改内容会重新编译,实时刷新页面。

    可以在Trunk.toml中配置启动服务的地址、端口

    # The address to serve on LAN.address = "127.0.0.1"# The address to serve on WAN.# address = "0.0.0.0"# The port to serve on.port = 8000

    组件语法,使用注意项

    可以注意到组件中

    {"hello, trunk/yew!"}

    文本字符展示需要使用{}括起来。
    • 组件只能有一个根节点,节点必须是闭合的。如果不需要渲染根节点,可以使用,使用block也行

          html! {                    

      {"hello, trunk/yew!"}

      {"good!"}

      }
    • 循环渲染,渲染的每一个节点业务需要返回html!。最后通过collect消费掉。

         // 需要渲染的字段  let names = ["admin", "test", "hboot"];  // 渲染片段  let names = names      .iter()      .map(|name| {          html! {              

      {format!("{name}")}

      } }) .collect::();

      在组件的html返回中使用,通过{},可以看到浏览器中的输出。

        html! {                

      {"hello, trunk/yew!"}

      {"good!"}

      {names} }

      也可以直接在html! {}中直接使用循环渲染。这里可以使用另一种语法{for ...}来替代消费.collect()

         // 渲染片段  let names = names.iter().map(|name| {      html! {          

      {format!("{name}")}

      } }); // .collect::(); html! { {for names} }
    • 属性绑定,给节点绑定动态的 class,通过{}。这里只演示了变量绑定,动态的则需要hook声明。

        let active = "active";  html! {                

      {"good!"}

      }
    • 条件判断,通过if判断

         let bool = true;   html! {                {if bool{              html!{                  {"yes"}              }          }else{              html!{                  {"no"}              }          }}        }

      在未来更新后,内部的html!可能就不需要了,现在仍需要加上。表明类型是 html。

      关于组件 - Componenttrait

      上面我们实现一个组件App,通过#[function_component]属性宏转变 rust 函数为一个组件。也可以通实现Componenttrait,来实现组件的功能。

      重新创建一个模块src/user.rs,创建一写关于个人信息的组件

      use yew::prelude::*;// 定义用户结构体pub struct User {    name: String,}impl Component for User {    type Message = ();    type Properties = ();    fn create(ctx: &Context) -> Self {        Self {            name: "hboot".to_string(),        }    }    fn view(&self, ctx: &Context) -> Html {        html! {                            

      {self.name.clone()}

      } }}

      Component作为一个 trait,只要实现了就可以作为一个函数组件渲染到视图中。必须要实现的方法create和view,两个必须要声明的类型Message\Properties。还有一些其他的方法

      • create 在组件创建后调用,用于初始化。

      • view 定义组件视图,语法类似于 jsx。 create方法跟随 view 方法的调用;view 方法不总是跟随update和changed方法,内部做了一些渲染优化。

      • type Message: 'static 用来声明消息类型,使得组件变成动态组件、可交互。它通过枚举来定义消息类型。

      • type Properties: Properties 定义组件的属性,它接受来自上文context的消息,不一定是父组件。触发组件的重新渲染。

      • update 可选,交互式消息触发时的钩子函数,在这里处理逻辑。返回bool来定义是否触发组件更新。

      • changed 可选,定义属性变更时的钩子函数。返回bool来定义是否触发组件更新。

      • rendered 可选,在组件渲染完成,还未更新到页面时调用。在view之后

      • destroy 可选,在组件销毁卸载之前调用。

      • prepare_state 可选,在服务端渲染后,组件被渲染前调用。

        在main.rs中导入使用,可以看到页面上已经出现了展示内容。

        mod user;// 使用组件use user::User;fn app() -> Html {    // ...    html! {                                }}

        添加一个事件,通过点击 button 来改变当前的用户名。就需要实现update方法来处理交互消息

        // 定义消息类型pub enum Msg {    UpdateName,}// ...impl Component for User {    // ...    fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool {        match msg {            Msg::UpdateName => {                self.name = "admin".to_string();                true            }        }    }    fn view(&self, ctx: &Context) -> Html {        html! {                            {"更新"}                

        {self.name.clone()}

        } }}

        在update方法中,通过接收到的msg消息类型来匹配需要执行的逻辑。当然可以将处理逻辑抽离提供一个方法进行调用。在view中添加了一个 button 作为按钮触发点击事件,通过onclick监听点击事件,ctx.link().callback()来发起一个事件,这有点像 react 的 redux。

        数据传递,单项数据流

        首先要接收来自父组件的数据,我们定义一个Props类型,props 需要过程宏derive来实现Properties \ PartialEq,然后定义type Properties = Props

        #[derive(Properties, PartialEq)]pub struct Props {    pub age: i32,}impl Component for User {    // ...    type Properties = Props;    //...    fn view(&self, ctx: &Context) -> Html {        html! {                            

        {format!("姓名:{}",self.name.to_string())}

        {format!("年龄:{}",ctx.props().age)}

        } }}

        定义完Properties,就需要在用到组件的地方增加传参。默认是必传的

         html! {                 }

        通过属性宏来设置属性的状态,这样就可以不必传:

        • #[prop_or_default] 默认初始化值。
        • #[prop_or(value)] 使用默认值value指定默认值。
        • #[prop_or_else(fn)] 指定初始化值函数,没有传值时会调用。函数签名FnMut()-> T
          #[derive(Properties, PartialEq)]pub struct Props {    #[prop_or(28)]    pub age: i32,}

          这样就可以不必传了。我们可以在父组件使用props!宏来定义子组件的 props,然后传给子组件,为了标识 props,我们把 user 中的 props 改名为UserProps

          在main.rs:

          use yew::props;// ...use user::{User, UserProps};fn app() -> Html {    // ...    let user_props = props! {        UserProps {            age:30        }    };    html! {                                }}

          可以看到props!宏可以接受多个 props。定义完之后通过..user_props绑定到子组件上。注意是两个点..。

          两个定义 props 字段类型的建议:

          1. 不要使用String类型,而是使用&str。因为 String 类型的复制消耗大
          2. 不要使用内部可变性的智能指针,这会导致组件不知掉什么时候需要更新。

          子组件更新父组件状态

          子组件通过事件回调的方式更新父组件的状态。我们定义父组件更新age的方法,然后在子组件触发调用更新

          在main.rs,因为需要更新 age,所以需要声明 age 为父组件的一个状态数据,在函数组件中使用use_state定义。然后声明更新方法update_age提供给子组件调用。

          let age = use_state(|| 30);// 点击更新年龄let update_age: Callback = {    let age = age.clone();    Callback::from(move |_| age.set(26))};html! {            // ...            }

          因为 props 的不可变性,我们使用props!创建的传参改为传统方式。

          *age 解引用获取指向的值。通过age.set()方法更新值。子组件 props 增加接受回调函数,我们这个回调不接受参数,所以给一个()

          在user.rs中:

          pub enum Msg {    // ...    UpdateAge,}#[derive(Properties, PartialEq)]pub struct UserProps {    // ...    pub on_update_age: Callback,}impl Component for User {    // ...    fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool {        match msg {            // ...            Msg::UpdateAge => {                ctx.props().on_update_age.emit(());                false            }        }    }    fn view(&self, ctx: &Context) -> Html {        html! {                            // ...                {"更新age"}                

          {format!("年龄:{}",ctx.props().age)}

          } }}

          首先增加了一个 msgUpdateAge,有 button 触发点击回调Msg::UpdateAge,在update中匹配消息类型,调用来自父组件的回调方法ctx.props().on_update_age.emit(()),不是直接调用哦,而是通过.emit()可以传值,因为我们不需要,所以给一个元组。

          组件内部嵌套html

          上面子组件更新父组件的状态感觉很费劲,既然状态在父组件的我们可以通过在子组件调用时嵌套 html 的方式增加子组件视图。

          在main.rs中

          let update_age = {    let age = age.clone();    Callback::from(move |_| age.set(26))};html! {                        {"更新age"}            

          {format!("年龄:{}",*age)}

          }

          在父组件渲染就没有那么多的弯弯绕绕。

          修改子组件user.rs,移除掉之前的回调函数的定义。增加 props 类型children:Html,就可以直接在渲染时访问。

          #[derive(Properties, PartialEq)]pub struct UserProps {    // ...    pub children: Html,}impl Component for User {    fn view(&self, ctx: &Context) -> Html {        html! {                            // ...                {ctx.props().children.clone()}                    }    }}

          组件hooks

          上面已经使用了一个 hookuse_state用于管理数据状态。这样类比 react,不就是函数组件和类组件的区别吗,感觉就容易上手很多。

          可以使用已经定义好的 hook,还可以自定义 hook。

          yew hook 文档

          • use_state 管理函数组件的数据状态,只要设置新的值,就会触发组件重新渲染。

          • use_state_eq 设置新值,需要比较旧值是否相等。不相等才出发组件渲染,定义的数据结构需要实现PartialEqtrait

          • use_effect 副作用钩子函数,在组件渲染完成后调用。

              use log::{info, Level};  #[function_component(App)]  fn app() -> Html {      // ...      use_effect(move || {          // 渲染完成执行          info!("render!")      });      // 点击更新年龄      let update_age = {          let age = age.clone();          Callback::from(move |_| age.set(30))      };      // ...  }

            我们点击视图中的更新年龄,会一直调用update_age,虽然值没有发生变化,但是组件仍会重新渲染。

            为了防止不必要的重复渲染,可以声明变量使用use_state_eq

              // let age = use_state(|| 30);  let age = use_state_eq(|| 30);

            我们再次测试点击更新,发现没有执行副作用函数use_effect的逻辑。还有另一种方式就是use_effect_with只有它接收的依赖变量发生变化时才触发调用。

          • use_effect_with 同use_effect,接受依赖,依赖变更时,才触发。

            let age = use_state(|| 30);//...let with_age = age.clone();use_effect_with(with_age, move |_| {    // 渲染完成执行    info!("dep render!")});

            点击更新时,没有触发use_effect_with钩子输出,初始时触发。

          • use_force_update 手动强制重新渲染组件

          • use_memo 优化计算,只有在依赖项发生变更时才会重新执行计算。

          • use_callback 优化渲染,使得子组件不受父组件数据状态变化而重新渲染。

          • use_context 捕获上下文的值,跨组件共享数据。

          • use_mut_ref 获取值的可变引用,但不会引起组件重新渲染。

          • use_node_ref 用来访问 DOM 元素,ref绑定

          • use_reducer 共享计算逻辑,可以在不同的组件内通过触发action来调用处理函数

            和 react 的 redux 的类似,定义数据结构必须实现Reducibletrait

            定一个UserInfo结构,存储用户个人信息,定一个 actionUserInfoAction触发 age 的值更新。实现Reducibletrait,定义 type Action = UserInfoAction.

              pub struct UserInfo {      pub age: i32,  }  // 定义操作的 action  pub enum UserInfoAction {      UpdateAge(i32),  }  impl Default for UserInfo {      fn default() -> Self {          Self { age: 28 }      }  }  // action reducer  impl Reducible for UserInfo {      type Action = UserInfoAction;      fn reduce(self: Rc, action: Self::Action) -> Rc {          match action {              UserInfoAction::UpdateAge(age) => {                  info!("update age --- {age}");                  Self { age }.into()              }          }      }  }

            通过reduce方法接收分发过来的 action,并增加处理逻辑。这里使用了Rc引用计数,维护多引用值的可变性。

            然后在main.rs引入使用,使用use_reducer,UserInfo::default初始化状态.dispatch方法分发事件。

            use std::rc::Rc;mod userInfo;use userInfo::{UserInfo, UserInfoAction};#[function_component(App)]fn app() -> Html {  // ...  let age = use_state(|| 30);  let user_info = use_reducer(UserInfo::default);  // 点击更新年龄  let update_age = {      let age = age.clone();      let user_info = user_info.clone();      let cb1 = Rc::new(Callback::from(move |_| age.set(30)));      let cb2 = Rc::new(Callback::from(move |_| {          user_info.dispatch(UserInfoAction::UpdateAge(30))      }));      //      Callback::from(move |_| {          cb1.emit(());          cb2.emit(());      })  };  // ...}

            在一个方法中同时处理多个事件时,通过Rc引用计数确保Callback::from闭包调用的所有权。在第三个Callback::from通过emit()方法同时触发多个事件。

          • use_reducer_eq 设置值时,比较新旧值是否相等。

            其他

            • rust 本身不支持解析 css 样式,只能通过外部样式来调整。给节点增加类或 id。

                 html! {      

              {"hello, trunk/yew!"}

              }

              再利用trunk的功能,加载外部样式。

              也可以直接通过style属性,你要是直接写一串字符串 css 样式,绑定到 style 也行,就是不好维护

                html! {      

              {"hello, trunk/yew!"}

              }

              需要动态的 class 设置,只能通过classes!()宏管理,动态添加、移除、切换,参数可以是 list、字符串、实现了Into的类型

              新增一个is_active状态,通过 button 点击事件更新值,再通过条件语句判断增加 class。Classess用于声明和管理类属性。

              let mut class = Classes::from("info");if *is_active {  class.push("active")}; html! {        {"active"}      

              {"hello, trunk/yew!"}

              }
            • html!宏那边定义的节点元素是小写的,如果需要使用大写,则可以这样html! { },也可以用于动态 tag 标签设置

            • 动态设置属性的,通过Some(None)定义字段,如果设置为None,这个属性则不会被设置。

            • 阻止事件冒泡,通过事件的回调参数event,可以调用event.prevent_default().stopPropagation()阻止默认事件及事件冒泡。

              相关的开发crate

              开发时,需要有一些工具库帮助我们进行开发,方便我们调试、验证逻辑;

              1. console_error_panic_hook 上一篇文章已经讲过了,可以帮助我们在浏览器控制台输出错误的具体信息

              2. console_log 在浏览器控制台输出信息

                安装依赖:

              $> cargo add log console_log

              在main.rs

              use log::{info, Level};fn main() {    let _ = console_log::init_with_level(Level::Debug);    info!("render web page");    yew::Renderer::::new().render();}
              1. wasm-bindgen 用于前端与 js 交互的桥梁。上一篇文章里写过。

              2. js-sys 基于wasm-bindgen提供的 js 全局 Api 对象,及属性绑定。例如 Array、Object、Reflect 等

                依赖安装:

                 cargo add js-sys

                新建一个数组,添加一个值,并取值

                 use js_sys::Array; use wasm_bindgen::prelude::*; fn main() {     // ...     let arr = Array::new();     arr.push(&JsValue::from(10));     info!("{:?}", arr.get(0).as_f64().unwrap()); }

                js 所有 api 调用传入的数据类型,都需要是JsValue,它定义在wasm_bindgen中;

              3. web-sys 基于wasm-bindgen 提供的原始浏览器提供的接口,例如 DOM、WebGL、CSSOM 等。

                安装依赖:

                $> cargo add web-sys

                为了不影响打包速度,我们将需要用到的 api 特性列在依赖特性中.这里看有哪些可以导入使用的对象

                [dependencies.web-sys]version = "0.3.69"features = ["Window"]

                这样我们就可以使用到Window对象下的所有方法、属性。使用Window.alert()

                 use wasm_bindgen::prelude::*;#[wasm_bindgen]pub fn alert() { let window = web_sys::window().unwrap(); window.alert_with_message("alert!!").unwrap();}

                然后在事件回调函数中调用alert(),想要弹出自定义消息,需要调用alert_with_message,具体 API 有哪些可以用的方法,查看文档。

              • gloo 是一个工具包,上面的js-sys和web-sys都是底层的 api 封装,理解和使用都很困难,而gloo正是为了解决这个困难,提供简单易用的 api。

                简单介绍了trunk打包工具,以及 web 库yew的概念知识,基本使用。下一篇则构建一个可以用于开发的脚手架。

上一篇:鲲鹏服务器现状怎么样?

下一篇:PigCMS使用心得与体验评价分享