长乐网站建设,进广州最新政策,北京城乡住房建设官方网站,wordpress免费响应式构建完整基于 rust 的 web 应用,使用yew框架
trunk 构建、打包、发布 wasm web 应用
安装后会作为一个系统命令#xff0c;默认有两个特性开启
rustls - 客户端与服务端通信的 tls 库update_check - 用于应用启动时启动更新检查#xff0c;应用有更新时提示用户更新。nati…构建完整基于 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 csryew不会默认指定特性我们做的是 web 端开发所以指定开启csr。其他的还包括 ssr - 服务端渲染hydration - 混合开发支持客户端、服务端渲染。
在src/main.rs定义方法app渲染 htmlhtml!宏可以定义类似 jsx 语法的视图结构。
use yew::prelude::*;#[function_component(App)]
fn app() - Html {html! {h1{hello, trunk/yew!}/h1}
}fn main() {yew::Renderer::App::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组件语法使用注意项
可以注意到组件中h1{hello, trunk/yew!}/h1 文本字符展示需要使用{}括起来。 组件只能有一个根节点节点必须是闭合的。如果不需要渲染根节点可以使用/,使用block也行 html! {h1{hello, trunk/yew!}/h1h2{good!}/h2/}循环渲染,渲染的每一个节点业务需要返回html!。最后通过collect消费掉。 // 需要渲染的字段let names [admin, test, hboot];// 渲染片段let names names.iter().map(|name| {html! {p{format!({name})}/p}}).collect::Html();在组件的html返回中使用通过{},可以看到浏览器中的输出。 html! {h1{hello, trunk/yew!}/h1h2{good!}/h2{names}/}也可以直接在html! {}中直接使用循环渲染。这里可以使用另一种语法{for ...}来替代消费.collect() // 渲染片段let names names.iter().map(|name| {html! {p{format!({name})}/p}});// .collect::Html();html! {{for names}/}属性绑定给节点绑定动态的 class通过{}。这里只演示了变量绑定动态的则需要hook声明。 let active active;html! {h2 class{active}{good!}/h2/}条件判断通过if判断 let bool true;html! {{if bool{html!{span{yes}/span}}else{html!{span{no}/span}}}/}在未来更新后内部的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: ContextSelf) - Self {Self {name: hboot.to_string(),}}fn view(self, ctx: ContextSelf) - Html {html! {div classuser-centerh3{self.name.clone()}/h3/div}}
}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! {User //}
}添加一个事件通过点击 button 来改变当前的用户名。就需要实现update方法来处理交互消息
// 定义消息类型
pub enum Msg {UpdateName,
}
// ...
impl Component for User {// ...fn update(mut self, _ctx: ContextSelf, msg: Self::Message) - bool {match msg {Msg::UpdateName {self.name admin.to_string();true}}}fn view(self, ctx: ContextSelf) - Html {html! {div classuser-centerbutton οnclick{ctx.link().callback(|_| Msg::UpdateName )}{更新}/buttonh3{self.name.clone()}/h3/div}}
}在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: ContextSelf) - Html {html! {div classuser-centerh3{format!(姓名{},self.name.to_string())}/h3h4{format!(年龄{},ctx.props().age)}/h4/div}}
}定义完Properties,就需要在用到组件的地方增加传参。默认是必传的 html! {User age{30} //}通过属性宏来设置属性的状态这样就可以不必传
#[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! {User ..user_props //}
}可以看到props!宏可以接受多个 props。定义完之后通过..user_props绑定到子组件上。注意是两个点..。
两个定义 props 字段类型的建议
不要使用String类型而是使用str。因为 String 类型的复制消耗大不要使用内部可变性的智能指针这会导致组件不知掉什么时候需要更新。
子组件更新父组件状态
子组件通过事件回调的方式更新父组件的状态。我们定义父组件更新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! {// ...User age{*age} on_update_age{update_age} //
}因为 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: ContextSelf, msg: Self::Message) - bool {match msg {// ...Msg::UpdateAge {ctx.props().on_update_age.emit(());false}}}fn view(self, ctx: ContextSelf) - Html {html! {div classuser-center// ...button οnclick{ctx.link().callback(|_| Msg::UpdateAge )}{更新age}/buttonh4{format!(年龄{},ctx.props().age)}/h4/div}}
}首先增加了一个 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! {Userbutton οnclick{update_age}{更新age}/buttonh4{format!(年龄{},*age)}/h4/User/
}在父组件渲染就没有那么多的弯弯绕绕。
修改子组件user.rs,移除掉之前的回调函数的定义。增加 props 类型children:Html,就可以直接在渲染时访问。
#[derive(Properties, PartialEq)]
pub struct UserProps {// ...pub children: Html,
}impl Component for User {fn view(self, ctx: ContextSelf) - Html {html! {div classuser-center// ...{ctx.props().children.clone()}/div}}
}组件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,}// 定义操作的 actionpub enum UserInfoAction {UpdateAge(i32),}impl Default for UserInfo {fn default() - Self {Self { age: 28 }}}// action reducerimpl Reducible for UserInfo {type Action UserInfoAction;fn reduce(self: RcSelf, action: Self::Action) - RcSelf {match action {UserInfoAction::UpdateAge(age) {info!(update age --- {age});Self { age }.into()}}}}通过reduce方法接收分发过来的 action并增加处理逻辑。这里使用了Rc引用计数维护多引用值的可变性。 然后在main.rs引入使用,使用use_reducerUserInfo::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(());})};// ...
}在一个方法中同时处理多个事件时通过RcT引用计数确保Callback::from闭包调用的所有权。在第三个Callback::from通过emit()方法同时触发多个事件。 use_reducer_eq 设置值时比较新旧值是否相等。
其他 rust 本身不支持解析 css 样式只能通过外部样式来调整。给节点增加类或 id。 html! {h1 classinfo{hello, trunk/yew!}/h1}再利用trunk的功能加载外部样式。 也可以直接通过style属性,你要是直接写一串字符串 css 样式绑定到 style 也行就是不好维护 html! {h1 classinfo stylecolor:red;{hello, trunk/yew!}/h1}需要动态的 class 设置只能通过classes!()宏管理动态添加、移除、切换参数可以是 list、字符串、实现了IntoClasses的类型 新增一个is_active状态通过 button 点击事件更新值再通过条件语句判断增加 class。Classess用于声明和管理类属性。 let mut class Classes::from(info);
if *is_active {class.push(active)
};html! {button οnclick{change_active}{active}/buttonh1 class{class} stylecolor:red;{hello, trunk/yew!}/h1/}html!宏那边定义的节点元素是小写的如果需要使用大写则可以这样html! { {myBook} / },也可以用于动态 tag 标签设置 动态设置属性的通过Some(None)定义字段如果设置为None,这个属性则不会被设置。 阻止事件冒泡通过事件的回调参数event,可以调用event.prevent_default().stopPropagation()阻止默认事件及事件冒泡。
相关的开发crate
开发时需要有一些工具库帮助我们进行开发方便我们调试、验证逻辑 console_error_panic_hook 上一篇文章已经讲过了可以帮助我们在浏览器控制台输出错误的具体信息 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::App::new().render();
}wasm-bindgen 用于前端与 js 交互的桥梁。上一篇文章里写过。 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中 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的概念知识基本使用。下一篇则构建一个可以用于开发的脚手架。