使用Rust的单页应用程序

2020-08-11 23:01:54

WebAssembly(Wasm)允许用JavaScript以外的语言编写的代码在浏览器上运行。如果你没有注意到,所有主流浏览器都支持wasm,全球超过90%的用户都有可以运行wasm的浏览器。

由于Rust编译为wasm,是否有可能完全用Rust构建SPA(单页应用程序),而无需编写任何JavaScript行?简短的回答是肯定的!请继续阅读了解更多信息,如果您无法抑制您的兴奋,请访问演示站点!

我们将建立一个简单的电子商务网站,名为“RustMart”,将有2个页面:

我使用此示例测试构建现代SPA所需的最小功能集:

$Cargo安装wasm-pack#将Rust编译为Wasm并生成JS互操作代码$Cargo Install Cargo-make#Task Runner$Cargo Install Simple-http-SERVER#SIMPLE SERVER为资产提供服务。

我们将使用Yew库来构建UI组件。让我们将此依赖项和wasm依赖项添加到Cargo.toml:

[tasks.build]command=";wasm-pack";args=[";build";,";--dev";,";--target";,";web";,";--out-name";,";wasm";,";--out-dir";,";/static";]Watch={Ignore_Pattern=";static/*";}[tasks.serve]command=";Simple-http-server";args=[";-i";,";,";-p";,";3000";,";--nocache";,";--try-file";,"。./static/index.html";]。

如果你是Rust的新手,我为初学者写了一些指南,它们会帮助你更好地阅读这篇文章。

//src/lib.rs将wasm_bindgen::prelude::*;use yew::prelude::*;struct Hello{}Impl组件用于Hello{type message=();type Properties=();fn create(_:self::properties,_:ComponentLink<;self>;)->;self{self{}}fn update(&;MUT Self,_:Self::Message)->;ShouldRender{true}FN Change(&;MUT Self,_:Self::Properties)->;ShouldRender{true}FN视图(&;Self)->;HTML{html!{<;span>;{";Hello World!";}<;/span>;}pub fn run_app(){App::<;Hello>;::new()。Mount_to_body();}。

发生了很多事情,但是您可以看到,我们正在创建一个名为“Hello”的新组件,该组件将<;span>;Hello World!/span>;呈现到DOM中。稍后我们将了解更多有关紫杉组件的信息。

它起作用了!!这只是“hello world”,但这是用铁锈写的。

通过组合组件并以单向方式传递数据来构建UI是前端世界中的一种范式转变。这是我们对UI推理方式的一个巨大改进,一旦您习惯了这一点,就很难回到命令式DOM操作上来。

收听生命周期事件,如“实例化”、“挂载在DOM中”等。

因此,我们不是在用户交互、网络请求等发生时强制更新UI,而是更新数据(Props、State、AppState),并基于此数据更新UI。当有人说“UI是状态的函数”时,这就是他们的意思。

确切的细节在不同的库中有所不同,但这应该会让您大致了解。如果您是新手,这种思维方式可能需要一段时间才能“点击”并习惯。

让我们先建立主页。我们将把主页构建为一个整体组件,然后将其分解成更小的可重用组件。

//src/ages/home.rs将yew::prelude::*;pub struct Home{}Impl组件用于Home{type message=();type properties=();fn create(_:self::properties,_:ComponentLink<;self>;)->;self{self{}}fn update(&;mut self,_:self::message)->;ShouldRender{true}FN Change(&;mut Self,_:Self::Properties)->;ShouldRender{true}FN视图(&;Self)->;HTML{html!{<;span>;{";Home Sweet Home!";}<;/span>;}。

//src/lib.rs+mod页面;+使用页面::home;使用wasm_bindgen::prelude::*;使用yew::prelude::*;-struct Hello{}-Iml组件for Hello{-type message=();-type Properties=();-fn create(_:self::properties,_:ComponentLink<;self>;)->;self{-self{}-}-fn。ShouldRender{-true-}-FN Change(&;mut self,_:self::properties)->;ShouldRender{-true-}-FN视图(&;self)->;HTML{-html!{<;span>;{";Hello World!";}<;/span>;}-}-}#[wasm_bindgen(Start)]pub FN run_。Hello>;::New().mount_to_body();+App::<;Home>;::new().mount_to_body();}。

现在,你应该看到“家,甜蜜的家!”而不是“你好,世界!”在您的浏览器中呈现。

然后,我们使用名为Products的字段创建一个新的struct State,以保存来自服务器的产品:

使用yew::prelude::*;+struct Product{+id:I32,+name:string,+description:string,+image:string,+Price:f64,+}+struct State{+products:VEC<;Product>;,+}-pub struct Home{}+pub struct Home{+state:state,+}Iml Component for Home{type message=();type Properties=();FN Create(_:self::properties,_。Product>;=vec![+Product{+id:1,+name:";Apple";.to_string(),+Description:";.to_string(),+image:";/products/apple.png";.to_string(),+product:3.65,+},+Product{+id:2,+name:";香蕉";.to_string(),+描述:";一片老香蕉叶曾经是嫩绿的";.to_string(),+image:";/products/banana.png";.to_string(),+Price:7.99,+},+];-self{}+self{+state:state{+products,+},+}}FN更新(&;mut self,_:self::message)->;应该。MUT Self,_:Self::properties)->;ShouldRender{true}FN视图(&;Self)->;HTML{+let Products:VEC<;HTML>;=Self+.State+.products+.iter()+.map(|product:&;Product|{+html!{+<;div>;+<;img src={&;Product.image}/>;Div>;{";$";}{&;product.price}<;/div>;+<;/div&>;+}+})+.Collect();++html!{<;span>;{Products}<;/span>;}-html!{<;span>;{";主页!}<;/span>;}。

创建组件时会调用创建生命周期方法,这是我们设置初始状态的位置。目前,我们已经创建了一个产品模拟列表,并将其作为初始值分配给该州内的产品。稍后,我们将使用网络请求获取此列表。

视图生命周期方法在呈现组件时调用。在这里,我们迭代了州内的产品以生成产品卡。如果您熟悉Reaction,这与Render方法和html是一样的!宏类似于JSX。

将一些随机图像保存为static/products/apple.png和static/products/banana.png,您将获得此UI:

我们在一个名为Cart_Products的新状态字段中跟踪添加到购物车中的所有产品。

添加逻辑以在单击“add to cart”按钮时更新CART_PRODUCTS状态。

使用yew::prelude::*;+#[派生(克隆)]struct Product{id:I32,Name:String,Description:String,image:String,Price:f64,}+struct CartProduct{+product:product,+Quantity:I32,+}struct State{products:VEC<;Product>;,+Cart_Products:VEC<;CartProduct>;,}pub struct Home{state:state,+link:,}+pub enum MSG{+AddToCart(I32),+}家庭实施组件{-type Message=();+type Message=MSg;type Properties=();-FN Create(_:Self::Properties,_:ComponentLink<;Self>;)->;Self{+FN Create(_:Self::Properties,Link:ComponentLink<;Self>;)->;Self{let Products。Apple";.to_string(),描述:";一天一个苹果远离医生";.to_string(),image:";/products/apple.png";.to_string(),价格:3.65,},产品{id:2,名称:";香蕉";.to_string(),描述:";.to_string(),/products/banana.png";.to_string(),价格:7.99,},];+let cart_products=vec![];self{state:state{products,+cart_products,},+link,}}-fn update(&;mut self,_:self::message)->;ShouldRender{+fn update(&;mut self,message:self::message)->;ShouldRender。{+let product=self+.state+.products+.iter()+.find(|p:&;&;Product|p.id==product_id)+.unwire();+let cart_product=self+.state+.cart_products+.iter_mut()+.find(|cp:&;&;mut CartProduct|cp.duct.id==product_id);++if let(Cp)=cart_product{+。+}Else{+self.state.cart_Products.ush(CartProduct{+product:Product.clone(),+Quantity:1,+})+}+true+}+}-true}fn change(&;mut self,_:self::properties)->;ShouldRender{true}fn视图(&;self)->;HTML{let products:VEC<;HTML>;=self.State.products.iter(。Html!{<;div&>;<;img源={&;Product.image}/>;<;div>;{&;Product.name}<;/div>;<;div>;{";$";}{&;product.price}<;/div>;+<;按钮onClick=self.link.callback(Move|_|msg::AddToCart(Product_Id))>;{";添加到购物车";}<;/button>;<;/div>;}}).Collect();+let cart_value=self+.state+.cart_products+.iter()+.fold(0.0,|acc,cp|acc+(cp.Quantity as f64*cp.products t.price));-html!{<;span>;{products}<;/span>;}+html!{+<;{format!(";Cart Value:{:.2}";,Cart_Value)}<;/span>;+<;span>;{products}<;/span>;+<;/div>;+}。

克隆-我们在Product struct中派生了Clone特征,因此每当用户将克隆的Product添加到购物车时,我们都可以将其保存到CartProduct中。

更新-此方法是更新组件状态或执行副作用(如网络请求)的逻辑所在的位置。它是使用包含组件支持的所有操作的消息枚举调用的。当我们从此方法返回true时,组件将重新呈现。在上面的代码中,当用户单击“add to cart”按钮时,我们将发送一条msg::AddToCart消息进行更新。在update内部,这要么将产品添加到cart_product(如果它不存在),要么增加数量。

链接-这允许我们注册可以触发我们的更新生命周期方法的回调。

如果你以前使用过Redux,更新类似于Reducer(用于状态更新)和Action Creator(用于副作用),Message类似于Action,LINK类似于Dispatch。

以下是UI的外观,尝试单击“添加到购物车”按钮,然后查看“购物车值”中的更改:

我们将产品数据从CREATE函数移动到static/products/products ts.json,并使用FETCH API进行查询。

[{";id";:1,";名称";:";苹果";,";说明";:";,";image";:";/products/apple.png";,";价格";:3.65},{";ID";:2,";名称";:";香蕉";,";描述";:";一片古老的香蕉叶曾经是嫩绿的";,";image";::";/products/banana.png";,";价格";::7.99}]。

Yew通过称为“服务”的东西公开了常见的浏览器API,如FETCH、LOCALSTORAGE等。我们可以使用FetchService发出网络请求。不管怎么说,它需要一些板条箱,我们来安装它们吧:

[Package]name=";rustmart";version=";0.1.0";Authors=[";sheshbabu<;[email protected]>;";]版本=";2018";[lib]crate-type=[";cdylib";,";rlib";][依赖项]yew=";+无论如何=";1.0.32";+serde={版本=";1.0";,功能=[";派生";]}。

让我们将Product和CartProduct解压缩到src/tyes.rs,以便可以跨多个文件共享它:

使用serde::{Desialize,Serialize};pub struct Product{pub id:I32,pub name:string,pub description:string,pub image:string,pub Price:F64,}pub struct CartProduct{pub product:product,pub Quantity:I32,}。

我们已经使结构和它们的字段都是公共的,并且已经派生了反序列化和序列化特征。

我们将使用API模块模式并创建一个名为src/api.rs的单独模块来保存我们的获取逻辑:

//src/api.rs Use crate::Types::Product;Use Anyhow::Error;Use Yew::Callback::Callback;Use Yew::Format::{json,Nothing};Use Yew::Services::Fetch::{FetchService,FetchTask,Request,Response};pub type FetchResponse<;T>;=Response<;Json<;Result<;T,Error&。FetchResponse<;T>;>;;pub FN get_products(回调:FetchCallback<;Vec<;Product>;)->;FetchTask{let req=request::get(";/products/products ts.json&34;)。正文(无)。UnWrap();FetchService::Fetch(req,callback)。展开()}。

FetchService API有点笨拙-它接受一个请求对象和回调作为参数,并返回一个称为“FetchTask”的东西。这里一个令人惊讶的问题是,如果这个“FetchTask”被删除,网络请求就会中止。所以我们返回它并将其存储在我们的组件中。

//src/lib.rs+mod api;+mod类型;mod页面;使用Pages::Home;使用wasm_bindgen::prelude::*;use yew::prelude::*;#[wasm_bindgen(Start)]pub FN run_app(){App::<;Home>;::new().mount_to_body();}。

+Use Crate::API;+Use Crate::Types::{CartProduct,Product};+Use Anyhow::Error;+Use Yew::Format::Json;+Use Yew::Services::Fetch::FetchTask;Use Yew::Prelude::*;-#[派生(克隆)]-struct Product{-id:I32,-Name:String,-Description:String,-image:String,-Price:f64,-}-struct CartProduct{-product:product,-Quantity:I32,-}struct State{products:VEC<;Product>;,Cart_Products:VEC<;CartProduct>;,+Get_Products_Error:Option<;Error>;,+Get_Products_Load:Bool,}pub struct Home{state:state,link:ComponentLink<;self&>;,+task:option<;FetchTask&>,}pub枚举消息{AddToCart(I32),+GetProducts,+GetProductsSuccess(VEC<;Product>;),+GetProductsError(Error),}Impl Home组件。Product>;=vec![-Product{-id:1,-name:";Apple";.to_string(),-description:";.to_string(),-image:";/products/apple.png";.to_string(),-Price:3.65,-},-Product{-id:2,-name:";香蕉";.to_string(),-description:";一片老香蕉叶曾经嫩绿";.to_string(),-image:";/products/banana.png";.to_string(),-price:7.99,-},-];+let products=vec![];let cart_products=vec![];+link.send_message(msg::GetProducts);Self{state:state{Products,Cart_Products,+Get_Products_Error:NONE,+GET_PRODUCTS_LOADED:FALSE,},LINK,+TASK:NONE,}}FN UPDATE(&;mut Self,Message:Self::Message)->;ShouldRender{Match Message{+msg::GetProducts=>;{+self.state.get_products_loaded=false;+let Handler=+self.link+.callback(Move|。|{+let(_,json(Data))=response.into_part();+匹配数据{+OK(Products)=>;msg::GetProductsSuccess(Products),+err(Err)=>;msg::GetProductsError(Err),+}+});+self.task=Some(API::Get_Products(Handler));+true+}+msg::GetProducts。+true+}+msg::GetProductsError(Error)=>;{+self.state.get_products_error=ome(Error);+self.state.get_products_loaded=true;+true+}msg::AddToCart(Product_Id)=>;{let product=sel.state.products.iter().find(|p:&;&;Product|p.id==product_id).unwork();让cart_product=sel.State.cart_products.iter_mut().find(|cp:&;&;amp;mut CartProduct|cp.duct.id==product_id);如果让Some(Cp)=cart_product{cp.Quantity+=1;}Else{self.state.cart_Products.ush(CartProduct{product:products t.clone(),Quantity:1,})}fn change(&;mut。Self)->;HTML{let products:VEC<;HTML>;=self.State.products.iter().map(|product:&;Product|{let product_id=duct.id;html!{<;div>;<;img src={&;products t.image}/>;<;div>;{&;products t.name}<;/div>;&。Products t.price}<;/div>;<;button onclick=self.link.callback(Move|_|msg::AddToCart(Product_Id))>;{";添加到购物车";}<;/button>;<;/div>;}).Collect();let cart_value=sel.state.cart_products.iter().fold(0.0,|。+if!self.state.get_products_loaded{+html!{+<;div>;{";正在加载...";}<;/div>;+}+}如果让某些(_)=self.state.get_products_error{+html!{+<;div>;+<;span<;{";}<;加载产品时出错!+}+}Else{html!{<;div>;<;span>;{format!(";cart value:{:.2}";,cart_value)}<;/span&>;<;<;{products}<;/span&>;<;/div>;}+}。

退出

在view方法中,我们使用条件呈现来根据组件的状态呈现加载视图、错误视图或产品视图。

让我们将“产品卡”组件提取到它自己的模块中,这样我们就可以在其他页面中重用它。

//src/Components/product_card.rs为ProductCard使用crate::type::product;use yew::prelude::*;pub struct ProductCard{props:props,}pub struct props{pub product:product,pub on_add_to_cart:callback<;()>;,}为ProductCard实施组件{type message=();type Properties=props;fn create(props:self:)->;Self{Self{props}}FN更新(&;mut Self,_msg:Self::Message)->;ShouldRender{true}FN更改(&;MUT Self,_Props:Self::Properties)->;ShouldRender{true}FN视图(&;Self)->;HTML{let onclick=self.props.on_add_to_cart。REGATE(|_|());html!{<;div>;<;img src={&;sel.props.product.image}/>;<;div>;{&;sel.props.product.name}<;/div>;<;div>;{";$&34;}{&;sel.props.product.price}<。<;button onclick=onclick>;{";添加到购物车";}<;/button>;<;/div>;}。

//src/ages/home.rs Use Crate::api;+Use Crate::Components::ProductCard;Use Crate::Types::{CartProduct,Product};Use Anyhow::Error;Use Yew::Format::Json;Use Yew::Prelude::*;Use Yew::Services::Fetch::FetchTask;//不更改主页的IMPL组件{//不更改FN视图(&;self)-&。=self.State.products.iter().map(|product:&;Product|{let product_id=duct.id;html!{-<;div。

.