尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

【一起学Rust 框架篇 Anansi框架】万字长文带你入门RustWeb开发

【一起学Rust 框架篇 Anansi框架】万字长文带你入门RustWeb开发 #### 文章目录* 前言* 一、安装* 二、入门案例* * 1.创建一个基础的网站 * 2.启动web服务器 * 3.创建一个应用 * 4.定义记录结构 * 5.迁移数据库 * 6.前端渲染记录 * 7. 使用组件 * 8. 组件级css * 9. 集成 Tailwind * 10. 添加缓存 * 11. 创建一个超级管理员 * 12. 添加剩余视图 * 13. 实现用户登陆 * 14. 实现创建帖子 * 15. 实现更新帖子功能 * 16. 超级管理员的功能 * 17. 实现删除帖子功能* *前言–新手学习一门编程语言总是很迷茫的除了枯燥的语法还要学习其庞大的生态如果你不了解生态那么掌握语法也是空。在Rust中也有着跟其他语言一样的Web开发生态但是基本上都是一言难尽。如果你对rust比较关注那么你至少应该了解过AxumRocketActix-web三座大山但是rust学习曲线陡峭我认为新手尤其是国内开发者并不适合使用这些本来语法就难还要被各种概念卷的晕头转向我更推荐Salvo赛风功能全文档也丰富更加适合中国开发者。本文介绍的是一个设计理念跟Django差不多的轻量化Web开发框架虽然小众但是五脏俱全可快速开发出简单的web应用。Anansi是一个使用Rust写的简单MVC的web框架在我的使用体验中我更加觉得这个框架就是Rust版的Django,如果你有Django开发经验那么将会非常轻而易举的上手开发安全又高性能的web应用如果你没有Django开发经验那么也不要慌跟着本文做完这个小demo,基本上可以用rust做出你想象中的那个应用。让我们现在就开始入门学习他吧。*一、安装----首先执行以下命令安装Anansi cargo install ananc 执行以后会自动将依赖等都安装好就是下图的样子。查看Anansi的版本如果返回版本号说明你已经安装成功运行正确的结果如下图所示 ananc --version二、入门案例------ 注意本段内容为官网原文翻译### 1.创建一个基础的网站确保你的版本与我一致然后创建一个项目 ananc new mini-forum 这个代码将会创建一个名字为mini-forum的项目其中包含以下必要文件http_errors/- http错误处理页面*main.rs- 应用和集合*project.rs- 项目设置*urls.rs- 管理路由 从以上文件可以看出这个web框架其实就是mvc的老三样。注意Anansi默认自带一个Sqlite数据库如果你想要使用PostgreSQL那么需要完成以下三步1. 在Cargo.toml中将sqlite改成postgres2. 在src/project.rs中修改database!(sqlite)为database!(postgres)3. 在settings.toml将[databases.default]部分修改成以下代码 name “mydatabase” user “myuser” password “mypassword” address “127.0.0.1:5432” ### 2.启动web服务器启动web服务器非常简单只需要cmd切换到mini-forum/目录然后运行以下命令即可 ananc run 如果运行没有任何错误直接访问http://127.0.0.1:9090/即可看到创建的网站了。 官方项目git: https://github.com/saru-tora/mini-forum### 3.创建一个应用这块内容用过Django的都熟悉应该能轻松理解你只需要知道一个web由多个应用组成就行了。 运行以下命令来创建一个应用以名字forum为例 ananc app forum 然后你就会看到创建了一个forum目录其中有以下文件 . ├── migrations ├── mod.rs ├── records.rs └── urls.rs 这些文件的功能你跟着教程走下去就知道是做什么的了。接下来在main.rs中包含启用这个应用 mod forum; apps! { auth, sessions, forum, } ### 4.定义记录结构在forum目录应该能看到文件records.rs这个就是记录也就是数据库中的记录首先定义记录结构 等同于Entity use anansi::records::{VarChar, DateTime, ForeignKey}; use anansi::util::auth::records::User; #[record] #[derive(Relate, FromParams)] pub struct Topic { pub title: VarChar200, pub user: ForeignKey, pub content: VarChar40000, pub date: DateTime, } #[record] #[derive(Relate, FromParams)] pub struct Comment { pub topic: ForeignKey, pub user: ForeignKey, pub content: VarChar40000, pub date: DateTime, } *#[record]会默认添加一个id字段并且像topic::date这种字段就可以使用order_by来查询数据库。*Relate表现的是实体之间的关系*FromParams将允许你从请求的参数中获取记录也就是反序列化*ForeignKey用来表示外键约束表示Topic和User有多对一的关系### 5.迁移数据库在定义好记录结构后即可生成迁移文件同步到数据库首先是第一步生成迁移文件 ananc make-migrations forum/ 如果你想要查看本次迁移的SQL,那么就执行以下命令 ananc sql-migrate forum 0001 输出将会根据你所选的数据库所生成SQL,如果你使用的是postgresql,那么就会看到以下代码 CREATE TABLE “forum_topic” ( “id” bigint NOT NULL PRIMARY KEY, “title” varchar(200) NOT NULL, “user” bigint NOT NULL REFERENCES “auth_user” (“id”) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, “content” varchar(40000) NOT NULL, “date” timestamp NOT NULL ); CREATE INDEX “forum_topic_user_index” ON “forum_topic” (“user”); --snip-- CREATE TABLE “forum_comment” ( “id” bigint NOT NULL PRIMARY KEY, “topic” bigint NOT NULL REFERENCES “forum_topic” (“id”) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, “user” bigint NOT NULL REFERENCES “auth_user” (“id”) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, “content” varchar(40000) NOT NULL, “date” timestamp NOT NULL ); CREATE INDEX “forum_comment_topic_index” ON “forum_comment” (“topic”); CREATE INDEX “forum_comment_user_index” ON “forum_comment” (“user”); --略-- 接下来应用迁移执行 ananc migrate ### 6.前端渲染记录首先可以先生成前端文件切到forum目录然后运行 ananc make-view topic 就会生成forum/topic/views.rs文件进行编辑 use super::super::records::{Topic, topic::date}; #[viewer] implR: Request TopicView { #[view(Site::is_visitor)] pub async fn index(req: mut R) - Result { let title “Latest Topics”; let topics Topic::order_by(date().desc()) .limit(25).query(req).await?; } }Site::is_visitor将会校验是否是游客如果是游客也就是访问时那么就会将Latest Topics赋值给变量title然后再将最新的25个topic赋值给变量topics接下来渲染这些变量到前端打开forum/topic/templates/index.rs.html进行如下改动 block title {title} block content {titlefor topic in topics {topic.title}} 可以看到这种一种简单的模板语法使用来格式化变量成字符串读者可以自行体会。然后添加路由打开urls.rs文件添加以下内容 use crate::forum::{self, topic::views::TopicView}; pub fn routesR: Request() - Router { Router::new() .route(“”, TopicView::index) .nest(“/topic”, forum::urls::routes()) // 添加 } 此时你可以打开http://127.0.0.1:9090/然而并不会看到任何topic,一个因为数据库本来就没有数据一个是因为你需要加访问的路径例如http://127.0.0.1:9090/topic### 7. 使用组件组件是用来做网页的交互的其中真义只有跟着做下去才知道。首先还是安装环境进入项目根目录也就是mini-forum/下运行命令 ananc init-components This will create the mini-forum-comps and mini-forum-wasm crates. In mini-forum-comps/Cargo.toml, add the following dependency:这个操作将会创建mini-forum-comps和mini-forum-wasm包在mini-forum-comps/Cargo.toml中添加以下依赖 gloo-net { version “0.2”, features [“http”, “json”] } 不建议随便更新版本因为会出现无法预料的问题rust就是这样的。然后创建mini-forum-comps/src/loader.rs并写入以下内容 use anansi_aux::prelude:; use gloo_net::http::Request; #[derive(Properties, Serialize, Deserialize)] pub struct LoaderProps { pub load_url: String, pub show_url: String, } #[derive(Serialize, Deserialize)] pub struct Data { pub id: String, pub title: String, } #[store] #[derive(Serialize, Deserialize)] pub struct Loader { visible: bool, page: u32, fetched: Vec, } #[component(Loader)] fn init(props: LoaderProps) - Rsx { let mut state Self::store(true, 1, vec![]); let (data_resource, handle_click) resource!(Vec, state, props, { *state.visible_mut() false; let request Request::get(props.load_url) .query([(“page”, state.page().to_string())]); }); rsx!(state, props, data_resource, { for data in state.fetched() {href props.show_url, data.id {data.title}} resource data_resource, state { Resource::Pending {Loading…} Resource::Rejected(_) { *state.visible_mut() true;Problem loading topics} Resource::Resolved(mut f) { if f.len() 25 *state.page() 3 {state.page_mut() 1;state.visible_mut() true; } state.fetched_mut().append(mut f); } } ifstate.visible() { button onclick(handle_click)Load more } } } 这段代码创建了一个按钮点击后将请求到topics.现在修改mini-forum-comps/src/lib.rs pub mod loader; anansi_aux::app_components! { loader::Loader, } 然后在src/main.rs中就可以使用这个包了 app_statics! { admin, wasm_statics!(“mini-forum-wasm”), } 然后你就可以在forum/topic/views.rs中添加一个load方法 use anansi::{url, check, http_404}; use mini_forum_comps::loader::{Loader, Data}; #[viewer] implR: Request TopicView { #[view(Site::is_visitor)] pub async fn index(req: mut R) - Result { let title “Latest Topics”; let topics Topic::order_by(date().desc()) .limit(25).query(req).await?; // Replace with url!(req, Self::show) later. let show_url “/topic”; let load_url url!(req, Self::load); } #[check(Site::is_visitor)] pub async fn load(req: mut R) - Result { let page: u32 req.params().get(“page”)?.parse()?; if page 3 { http_404!(); } let topics Topic::order_by(date().desc()) .limit(25).offset(25 * page).query(req).await?; let data: Vec topics.iter().map(|t| { Data {id: t.pk().to_string(), title: t.title.to_string()} }).collect(); Ok(Response::from_json(serde_json::to_string(data)?)) } } 然后添加load的路由文件位置forum/urls.rsuse super::topic::views::TopicView; pub fn routesR: Request() - Router { Router::new() .route(“load”, TopicView::load) } 接下来就可以在前端进行调用了文件位置forum/topic/templates/index.rs.htmlblock content { load components {titlefor topic in topics {topic.title} if topics.len() 25 { Loader show_url load_url / }} } 此时你再次运行web服务器 ananc run 当然你也并不会看到什么东西你进数据库添加几条内容就可以看到预期效果了### 8. 组件级css这部分内容搞前端的同学应该相当熟悉了要实现的功能就是css只应用于当前的组件。例如创建一个spinner组件文件mini-forum-comps/src/spinner.rsuse anansi_aux::prelude::; #[function_component(Spinner)] fn init() - Rsx { style! { div { display: inline-block; width: 25px; height: 25px; border: 3px solid #cfd0d1; border-radius: 50%; border-top-color: #1c87c9; animation: spin 1s ease-in-out infinite; -webkit-animation: spin 1s ease-in-out infinite; } keyframes spin { to { -webkit-transform: rotate(360deg); } } -webkit-keyframes spin { to { -webkit-transform: rotate(360deg); } } } rsx! {} } 这段代码通过function_component创建了一个组件因为他不需要状态管理然后css写在了style宏内现在将这些东西添加到mini-forum-comps/src/lib.rspub mod spinner; anansi_aux::comp_statics! { “spinner”, } anansi_aux::app_components! { loader::Loader, spinner::Spinner, } 注册这个组件文件位置src/main.rsapp_statics! { admin, wasm_statics!(“mini-forum-wasm”), eddit_comps, } 这样你就可以在loader中使用了文件位置mini-forum-comps/src/loader.rsuse crate::spinner::Spinner; // --省略-- #[component(Loader)] fn init(props: LoaderProps) - Rsx { // --省略-- rsx! { for data in state.fetched {href props.show_url, data.id {data.title}} resource data_resource { Resource::Pending { } // --省略-- } if state.visible { button onclick(handle_click)Load more } } } ### 9. 集成 Tailwind首先安装Tailwind ananc init-tailwind 然后在forum/topic/templates/base.rs.html中编辑block titleblock content 这样你就可以在项目中使用Tailwind类了。### 10. 添加缓存首先添加序列化的依赖,文件位置Cargo.tomlserde { version “1.0”, features [“derive”] } serde_json “1.0” 对于小型网站默认使用Moka就可以了如果是大型网站那么建议你使用redis,按照以下步骤 1. 需要在Cargo.toml中添加redis特性 2. 在src/project.rs中将app_cache!(local)换成app_cache!(redis) 3. 在settings.toml中的[caches.default]字段添加location redis://127.0.0.1/然后记录添加序列化相关特性文件位置forum/records.rsuse serde::{Serialize, Deserialize}; #[record] #[derive(Relate, FromParams, Serialize, Deserialize)] pub struct Topic { pub title: VarChar200, pub user: ForeignKey, pub content: VarChar40000, pub date: DateTime, } 然后你就可以在视图层使用你的缓存了,文件位置forum/topic/views.rsuse anansi::cache::prelude::; #[viewer] implR: Request TopicView { #[view(Site::is_visitor)] pub async fn index(req: mut R) - Result { let title “Latest Topics”; let topics cache!(req, Some(30), “topic_index”, { Topic::order_by(date().desc()) .limit(25).query(req).await? }); } } 缓存会每30s请求一次数据库到topic_index### 11. 创建一个超级管理员看到这里的伙伴应该一下就能想到django吧这块简直就是rust版的django,你只需运行下面命令输入账号密码即可 ananc admin 创建好超级管理员账户以后 运行web服务器然后打开http://127.0.0.1:9090/admin/login就可以登陆了自带了一个超级管理员后台管理后台主页然后你就可以添加用户了点击User右边的Add### 12. 添加剩余视图在forum/records.rs中添加 use anansi::db::OrderBy; use anansi::ToUrl; #[record] #[derive(Relate, FromParams, Serialize, Deserialize, ToUrl)] pub struct Topic { pub title: VarChar200, pub user: ForeignKey, pub content: VarChar40000, pub date: DateTime, } impl Topic { pub fn recent_comments(self) - OrderBy { Comment::by_topic(self).order_by(comment::date().desc()) } }ToUrl将会返回一个简写的id例如ixNr1-tGUe9by_topic是通过添加外键约束生成的用来筛选结果。然后添加路由文件位置forum/urls.rsuse super::topic::views::TopicView; pub fn routesR: Request() - Router { Router::new() // 匹配例如 /topic/ixNr1-tGUe9 .route(“{topic_id}”, TopicView::show) } {topic_id}将会匹配到请求url中的参数。然后更新forum/topic/views.rsuse anansi::get_or_404; use anansi::humanize::ago; #[viewer] implR: Request TopicView { // --省略-- #[view(Site::is_visitor)] pub async fn show(req: mut R) - Result { let topic get_or_404!(Topic, req); let title topic.title; let poster topic.user.get(req).await?.username; let comments topic.recent_comments().limit(25).query(req).await?; let users comments.parents(req, |c| c.user).await?; } } 在show中get_or_404! 宏将会通过topic_id来筛选指定的topic否则返回404错误用户用comments来检索parents。现在添加前端的模板forum/topic/templates/show.rs.htmlblock title {title} block content {titlePosted by poster ago(topic.date)topic.contentfor (comment, user) in comments.iter().zip(users.iter()) {Posted by user.username ago(comment.date)comment.content} } 然后在Index中添加指向show的连接,文件位置forum/topic/templates/index.rsblock content {titlefor topic in topics {link req, Self::show, topic {topic.title}}}link等同于 topic.title ### 13. 实现用户登陆本例子直接复用了管理员登陆的页面当然你也可以自己写一个页面文件位置forum/topic/views.rsuse anansi::handle; use anansi::forms::ToRecord; use anansi::util::auth::forms::UserLogin; #[viewer] implR: Request TopicView { // --省略-- #[view(Site::is_visitor)] pub async fn login(req: mut R) - Result { let title “Log in”; let button “Log in”; let form handle!(UserLogin, ToRecord, req, user, { req.auth(user).await?; req.session().set_and_redirect(req, Self::index) })?; } }handle!宏会创建一个新的form,如果请求是GET的话。否则将提交表单登陆如果失败则返回结果你可以在forum/topic/templates/login.rs.html中找到他 block title {title} block content {titlebuild form { unescape form.errors() for field in form.fields() { unescape field.label_tag()unescape field unescape field.errors()} unescape form.submit(button) }} 使用模板来构建表单要比纯html实现安全的多。错误就使用无序列表显示了Problem with username or password.字段中的错误将会被field-errors所代替然后添加路由,文件位置urls.rspub fn routesR: Request() - Router { Router::new() .route(“/”, TopicView::index) .nest(“/topic”, forum::urls::routes()) .route(“/login”, TopicView::login) } 现在就可以用超管页面登陆用户账号了。### 14. 实现创建帖子在forum/forms.rs中添加一个表单 use crate::prelude::; use anansi::records::{DateTime, ForeignKey}; use anansi::forms::{VarChar, ToRecord}; use super::records::Topic; #[form(Topic)] pub struct TopicForm { pub title: VarChar200, pub content: VarChar40000, } #[async_trait] implR: Request ToRecord for TopicForm { async fn on_post(mut self, data: TopicFormData, req: R) - Result { Topic::new() .title(data.title) .user(ForeignKey::from_data(req.user().pk())?) .content(data.content) .date(DateTime::now()) .saved(req) .await .or(form_error!(“Problem adding topic”)) } }#[form(Topic)]生成了一个TopicFormData结构将会保持表单中的数据并且与Topic关联.on_post将会尝试将表单数据转化成一个记录。form_error将会简单的创建一个FormError它将会与表单的errors关联。然后在forum/mod.rs添加 pub mod forms; 然后在forum/topic/views.rs中使用他 use anansi::{check, extend}; use crate::forum::forms::TopicForm; #[viewer] implR: Request TopicView { // --snip-- #[check(Site::is_auth)] pub async fn new(req: mut R) - Result { let title “New Topic”; let button “Create”; let form handle!(TopicForm, ToRecord, req, |topic| { Ok(redirect!(req, Self::show, topic)) })?; extend!(req, base, “login”) } }Site::is_auth实现了如果用户未登陆则重定向到访客页面。由于重定向不是异步的所以给handle!宏传递一个闭包。至于模板你可以简单的复用login.rs.html通过使用check和extend宏如果是实际要用的网站你就需要自定义了。如果用户已经登陆了也可以添加一个连接到index.rs.html的链接。 block content {titleif req.user().is_auth() { link req, Self::new {New Topic} }for topic in topics {link req, Self::show, topic {topic.title}}} 然后添加路由, 文件位置urls.rspub fn routesR: Request() - Router { Router::new() .route(“new”, TopicView::new) .route(“{topic_id}”, TopicView::show) } ### 15. 实现更新帖子功能首先需要给记录添加特性文件位置forum/forms.rsuse anansi::{GetData, ToEdit}; #[form(Topic)] #[derive(GetData, ToEdit)] pub struct TopicForm { pub title: VarChar200, pub content: VarChar40000, } 然后添加一个视图 use anansi::handle_or_404; use anansi::forms::ToEdit; #[viewer] implR: Request TopicView { // --snip-- #[check(Topic::owner)] pub async fn edit(req: mut R) - Result { let title “Update Topic”; let button “Update”; let form handle_or_404!(TopicForm, ToEdit, req, |topic| { Ok(redirect!(req, Self::show, topic)) })?; extend!(req, base, “login”) } }Topic::owner将会检查文章是否是自己的。handle_or_404!宏与handle!类似但是如果记录找不到则返回404.其余的跟之前一样继续复用login.rs.html如果文章是自己的那么你也可以在show.rs.html添加一个链接如果这篇文章的作者就是当前用户。 block content {titlePosted by poster ago(topic.date)topic.contentif topic.user.pk() req.user().pk() { link req, Self::edit, topic {Edit} } for (comment, user) in comments.iter().zip(users.iter()) {Posted by user.username ago(comment.created)comment.content} } 接着更新路由 pub fn routesR: Request() - Router { Router::new() .route(“new”, TopicView::new) .route(“{topic_id}”, TopicView::show) .route(“{topic_id}/edit”, TopicView::edit) } ### 16. 超级管理员的功能将Topic添加到超管后台 use anansi::{init_admin, register, record_admin}; use super::records::Topic; init_admin! { register!(Topic), } record_admin! {Topic, // You can specify which fields (if any) should be searchable search_fields: [title, content, date], } 然后在forum/mod.rs中添加 pub mod admin; 然后在main.rs中添加 app_admins! { auth, forum, }Topic将会在/admin/forum/topic中显示。### 17. 实现删除帖子功能首先视图添加一个方法,文件位置forum/topic/views.rs#[viewer] implR: Request TopicView { // --省略-- #[view(Topic::owner)] pub async fn destroy(req: mut R) - Result { let title “Delete topic”; let topic get_or_404!(Topic, req); let form handle!(req, R, { topic.delete(req).await?; Ok(redirect!(req, Self::index)) })?; } }forum/topic/templates/destroy.rs.html中代码非常简单 block title {title} block content {titleAre you sure you want to delete the topic “topic.title”? build form { unescape form.submit(“Confirm”) } } 然后在forum/topic/templates/show.rs.html中添加一个链接 block content {titlePosted by poster ago(topic.date)topic.contentif topic.user.pk() req.user().pk() { link req, Self::edit, topic {Edit} link req, Self::destroy, topic {Delete} } for (comment, user) in comments.iter().zip(users.iter()) {Posted by user.username ago(comment.created)comment.content} } 最后再添加路由 pub fn routesR: Request() - Router { Router::new() .route(“new”, TopicView::new) .route(“{topic_id}”, TopicView::new) .route(“{topic_id}/edit”, TopicView::edit) .route(“{topic_id}/destroy”, TopicView::destroy) } 到现在为止本项目就算完成了虽然功能不多但耗时也挺久的这些内容已经足够你开始做出自己的网站了再深入的东西就没有不然为什么叫轻量级web框架呢后续的内容就需要自己探索了但是光从这篇帖子就可以知道这个web框架做一些简单的应用还是很方便的但是由于这个框架使用的是rust,学习难度高但是框架不是很完善所以性价比就比较差了暂且可以当个玩具耍耍。
返回列表