使用 tide、handlebars、rhai、graphql 开发 Rust web 前端(1)- crate 选择及环境搭建

目前,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 + mongodbactix-web + async-graphql + rbatis + postgresql / mysql 开发了 GraphQL 服务后端。感兴趣的朋友可以参阅博文——

本次实践中,即是基于 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,可以访问如下仓库获取源码:

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;另外,serdeserde_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》作为入门资料,是个很不错的选择。另外,机械工业出版社的《Rust 编程- 入门、实战与进阶》,非大块头的厚书。讲解了 Rust 核心语法后,注重编码能力训练,并且以 LeetCode 面试真题作为示例。

对于 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> &nbsp; </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> &nbsp; </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.rsutil/common.rs 2 个文件,代码如下:

util/mod.rs

#![allow(unused)]
fn main() {
pub mod common;
}

util/common.rs

#![allow(unused)]
fn main() {
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 文件中,我们定义要在模板中展示的数据。代码内容如下:

#![allow(unused)]
fn main() {
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)结构体中。这点在 tideactix-web 中,概念是一致的,写法稍有差别。

Statetide 服务器的状态(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 buildcargo run 后,如果你未自定义端口,请在浏览器中打开 http://127.0.0.1:3000 。可以发现,handlebars 模板文件 templates/index.html 中的 HTML 元素:title、h1,以及 h3 的值来自路由处理函数 async fn index(_req: Request<State>)

至此,使用 handlebars 模板的 Rust web 前端开发环境已经搭建成功。

谢谢您的阅读!