[Rust] 使用 tide、handlebars、rhai、graphql 开发 Rust web 前端(1)- crate 选择及环境搭建
💥 内容涉及著作权,均归属作者本人。若非作者注明,默认欢迎转载:请注明出处,及相关链接。
Summary: 《使用 tide、handlebars、rhai、graphql 开发 Rust web 前端》第一部分:HTTP 服务器端框架、模板引擎库,GraphQL 客户端等 crate 的选择,以及开发环境的搭建和测试。
Topics: rust tide rust-web handlebars rhai graphql-client
目前,web 前端开发方面,通常有两种技术组合:一种是使用模板引擎,主要在服务器端渲染,这种方式对 seo 有较高要求的应用有利;同时,在后续优化方面,也较有优势。另一种则是前端框架,如 yew、react、vue、seed 一类,采用声明式设计;在保证性能下限的前提下,高效且灵活地进行快速开发。
另外,具体到 yew、react、vue、seed 来说,也有所不同:yew、seed 则是 WebAssembly 框架。WebAssembly 是 W3C 组织于 2019 年 12 月下旬才发布的新标准,还处于发展初期。前景或许更广阔一些,但目前落地的应用场景还比较罕见。
前时的文章《Rust 和 Wasm 的融合,使用 yew 构建 WebAssembly 标准的 web 前端》,即是对 Rust 生态中 WebAssembly 框架的实践。放眼整个 web 前端开发,都可以说是比较新颖的技术。但是对于生产环境,其小规模使用,或许都是一个挑战。如果你想使用 Rust 技术栈开发 web 应用,目前还是采用模板引擎的组合,较为稳妥一些。
实践目标
在以前的构建 Rust 异步 GraphQL 服务系列中,分别采用 tide + async-graphql + mongodb
和 actix-web + async-graphql + rbatis + postgresql / mysql
开发了 GraphQL 服务后端。感兴趣的朋友可以参阅博文——
- 构建 Rust 异步 GraphQL 服务:基于 tide + async-graphql + mongodb,共计 4 篇。
- 基于 actix-web + async-graphql + rbatis + postgresql / mysql 构建异步 Rust GraphQL 服务,也是共计 4 篇。
本次实践中,即是基于 Rust 技术生态,采用模板引擎,来实现 Rust web 前端的开发。实践过程中,我们通过 GraphQL 服务后端 API,获取 GraphQL 数据并解析。然后,在页面中,对用户列表、项目列表做以展示。
crate 的选择
Rust 生态中,成熟的模板引擎库非常多。askama 模板引擎的开发者,对下述出现较早的模板库进行了极其简单的测评,有兴趣可以参考 djc/template-benchmarks-rs:
- write!:基于标准库
write!
宏实现 - handlebars:handlebars 模板的 Rust 实现
- tera:基于 jinja2/django 模板规范
- liquid:liquid 模板的 Rust 实现
- askama:类型安全的、类 jinja 的编译型模板
- horrorshow:使用 Rust 宏实现的模板
- ructe:高效、类型安全的编译型模板
- fomat:使用类
print/write/format
宏实现的小型模板 - markup:快速、类型安全的模板
- maud:Rust 实现的编译时 HTML 模板引擎
- sailfish:简单、小型、快速的模板引擎
上述列表所提及模板,仅为开发较早,askama 模板引擎的开发者对其测评。另外,比较成熟的 Rust 模板引擎还有 mustache 规范的 rustache、rust-mustache,以及微型而极快的 tinytemplate 等等。
本系列文章,笔者选择了 handlebars-rust 模板引擎。评测中,其基准测试结果并不出众,但评测都有其局限性,仅可参考。而 handlebars-rust 对 rhai
(Rust 的嵌入式脚本引擎)的支持方面,笔者非常感兴趣,是故选择。
HTTP 服务器框架,笔者选择了轻型的 tide
(中文文档)。但是如果你对 actix-web 或者其它服务器端框架更感兴趣,或者想替换也是非常容易的,因为 cookie、GraphQL 客户端等代码都是通用的。
HTTP 客户端框架,笔者选择了 surf
。如果你想使用 reqwest
,替换仅为一行代码(将发送 GraphQL 请求时的 surf
函数,修改为 reqwest
函数即可)。
项目中,rhai
(Rust 的嵌入式脚本引擎),主要用于开发页面脚本,作为 JavaScript 的一个替代方案。
嗯,本次实践用到的主要 crate,大概就是这些。
工程的创建
我们从零开始,进行本次实践。
在我们的实践项目根目录 tide-async-graphql-mongodb 或者 actix-web-async-graphql-rbatis 中,创建新的新的工程 frontend-handlebars。
GraphQL 服务后端,开源在 github,可以访问如下仓库获取源码:
- tide-async-graphql-mongodb(本博客即在此仓库基础上扩展实现)
- actix-web-async-graphql-rbatis
cd tide-async-graphql-mongodb # 或 actix-web-async-graphql-rbatis
cargo new frontend-handlebars --vcs none
同时,需要在根目录的 Cargo.toml
(不是 frontend-handlebars 目录中的 Cargo.toml
)将 frontend-handlebars 项目添加到 workspace
部分:
[workspace]
members = [
"./backend",
"./frontend-handlebars",
"./frontend-yew"
]
开发环境的配置
本文中,我们先进行开发环境的基础配置,整合各个 crate,并运行展示一个包含 handlebars 模板语法的 HTML 文件即可。因此,目前需要的主要 crate 仅为 tide、async-std,以及 handlebars-rust;另外,serde
和 serde_json
crate 也是需要的。其中,async-std
需要启用特性 attributes
,而 serde
需要启用特性 derive
。我们使用 cargo-edit 工具,将它们加入到 frontend-handlebars 工程中。
cargo add async-std tide serde serde_json handlebars
此时,frontend-handlebars 项目中的 Cargo.toml
文件内容如下:
[package]
name = "frontend-handlebars"
version = "0.1.0"
authors = ["我是谁?"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-std = { version = "1.9.0", features = ["attributes"] }
tide = "0.16.0"
serde = { version = "1.0.126", features = ["derive"] }
serde_json = "1.0.64"
handlebars = "4.0.0"
代码开发
本文直接进入 Rust web 的开发演练,对于 Rust 的基础不做提及。
如果你没有 Rust 基础,《通过例子学 Rust》作为入门资料,是个很不错的选择。
对于 handlebars 模板语法,我们也不做提及,官网资料很丰富,或者访问国内同步更新站点。
虽然仅是演练,但笔者不建议将代码一股脑写入 main.rs
中。我们划分模块,分层实现。
handlebars
模板
在 frontend-handlebars 目录下,创建放置模板文件的目录:
cd frontend-handlebars
mkdir templates
touch templates/index.html
templates/index.html
是包含 handlebars 语法的模板文件:
<!doctype html>
<html lang="zh">
<head>
<title>{{ app_name }}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content="zzy, https://github.com/zzy/tide-async-graphql-mongodb">
</head>
<body>
<center>
<h1>{{ app_name }} </h1>
<h3 style="padding-left: 20%;">-- {{ author }}</h3>
<ul>
<h2> </h2>
<h2>
<li><a href="/users">all users with token from graphql data</a></li>
</h2>
<h2>
<li><a href="/projects">all projects from graphql data</a></li>
</h2>
<h2> </h2>
<h2>
<li><a href="http://127.0.0.1:8000/graphiql">graphql API</a></li>
</h2>
</ul>
</center>
</body>
</html>
模板渲染
对模板渲染,很显然是一个通用的处理过程。因此,我们将其抽象,放在通用类模块中。
cd frontend-handlebars/src
mkdir util
touch util/mod.rs util/common.rs
模板的渲染抽象,主要是实现:规范模板路径、注册模板,以及对模板压入渲染数据。util/mod.rs
和 util/common.rs
2 个文件,代码如下:
util/mod.rs
pub mod common;
util/common.rs
use handlebars::Handlebars;
use serde::Serialize;
use tide::{http::mime::HTML, Body, Response, StatusCode};
pub struct Tpl<'tpl> {
pub name: String,
pub reg: Handlebars<'tpl>,
}
impl<'tpl> Tpl<'tpl> {
pub async fn new(rel_path: &str) -> Tpl<'tpl> {
let tpl_name = &rel_path.replace("/", "_");
let abs_path = format!("./templates/{}.html", rel_path);
// create the handlebars registry
let mut hbs_reg = Handlebars::new();
hbs_reg.register_template_file(tpl_name, abs_path).unwrap();
Tpl {
name: tpl_name.to_string(),
reg: hbs_reg,
}
}
pub async fn render<T>(&self, data: &T) -> tide::Result
where
T: Serialize,
{
let mut resp = Response::new(StatusCode::Ok);
resp.set_content_type(HTML);
resp.set_body(Body::from_string(
self.reg.render(&self.name, data).unwrap(),
));
Ok(resp.into())
}
}
路由开发
路由,其定义就放在专门的路由模块中:
cd frontend-handlebars/src
mkdir routes
touch routes/mod.rs
也可以再定义一个
home.rs
或者index.rs
,然后将其引入mod.rs
。
目前,仅一个页面,所以仅需定义一个路由处理函数,配置一个路由路径即可。所以我们直接将 index
路由处理函数放在 mod.rs
文件中。但是,后续的用户列表、项目列表路由处理,我们会放在各自的模块中。
handlebars 语法规则,可以直接接收 json
格式的数据并解析展示。因此,routes/mod.rs
文件中,我们定义要在模板中展示的数据。代码内容如下:
use tide::{self, Server, Request};
use serde_json::json;
use crate::{State, util::common::Tpl};
pub async fn push_res(app: &mut Server<State>) {
app.at("/").get(index);
}
async fn index(_req: Request<State>) -> tide::Result {
let index: Tpl = Tpl::new("index").await;
// make data and render it
let data = json!({"app_name": "frontend-handlebars - tide-async-graphql-mongodb", "author": "zzy"});
index.render(&data).await
}
应用入口
main.rs
作为 web 应用的入口,需要读取路由模块的配置,并将其压入到服务器(Serve)结构体中。这点在 tide
和 actix-web
中,概念是一致的,写法稍有差别。
State
是tide
服务器的状态(State)结构体,用于存放一些和服务器具有相同生命周期
的对象或值。actix-web 中,概念同样一致。笔者此书仅为示例,表示tide
有此特性。
mod routes;
mod util;
#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
// tide logger
tide::log::start();
// Initialize the application with state.
// Something in Tide State
let app_state = State {};
let mut app = tide::with_state(app_state);
// app = push_res(app).await;
routes::push_res(&mut app).await;
app.listen(format!("{}:{}", "127.0.0.1", "3000")).await?;
Ok(())
}
// Tide application scope state.
#[derive(Clone)]
pub struct State {}
编译和运行
执行 cargo build
、cargo run
后,如果你未自定义端口,请在浏览器中打开 http://127.0.0.1:3000 。可以发现,handlebars 模板文件 templates/index.html
中的 HTML 元素:title、h1,以及 h3 的值来自路由处理函数 async fn index(_req: Request<State>)
。
至此,使用 handlebars 模板的 Rust web 前端开发环境已经搭建成功。
谢谢您的阅读!