使用 tide、handlebars、rhai、graphql 开发 Rust web 前端(2)- 获取并解析 GraphQL 数据

上一篇文章《crate 选择及环境搭建》中,我们对 HTTP 服务器端框架、模板引擎库、GraphQL 客户端等 crate 进行了选型,以及对开发环境进行了搭建和测试。另外,还完成了最基本的 handlebars 模板开发,这是 Rust web 开发的骨架工作。本篇文章中,我们请求 GraphQL 服务器后端提供的 API,获取 GraphQL 数据并进行解析,然后将其通过 handlebars 模板展示。

本次实践中,我们使用 surf 库做为 HTTP 客户端,用来发送 GraphQL 请求,以及接收响应数据。对于 GraphQL 客户端,目前成熟的 crate,并没有太多选择,可在生产环境中应用的,唯有 graphql_client。让我们直接将它们添加到依赖项,不需要做额外的特征启用方面的设定:

cargo add surf graphql_client

如果你想使用 reqwest 做为 HTTP 客户端,替换仅为一行代码(将发送 GraphQL 请求时的 surf 函数,修改为 reqwest 函数即可)。

现在,我们的 Cargo.toml 文件内容如下:

[package]
name = "frontend-handlebars"
version = "0.1.0"
authors = ["我是谁?"]
edition = "2018"

[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"

surf = "2.2.0"
graphql_client = "0.9.0"
handlebars = "4.0.0"

编写 GraphQL 数据查询描述

首先,我们需要从 GraphQL 服务后端下载 schema.graphql,放置到 frontend-handlebars/graphql 文件夹中。schema 是我们要描述的 GraphQL 查询的类型系统,包括可用字段,以及返回对象等。

然后,在 frontend-handlebars/graphql 文件夹中创建一个新的文件 all_projects.graphql,描述我们要查询的项目数据。项目数据查询很简单,我们查询所有项目,不需要传递参数:

query AllProjects {
  allProjects {
    id
    userId
    subject
    website
  }
}

最后,在 frontend-handlebars/graphql 文件夹中创建一个新的文件 all_users.graphql,描述我们要查询的用户数据。用户的查询,需要权限。也就是说,我们需要先进行用户认证,用户获取到自己在系统的令牌(token)后,才可以查看系统用户数据。每次查询及其它操作,用户都要将令牌(token)作为参数,传递给服务后端,以作验证。

query AllUsers($token: String!) {
  allUsers(
    token: $token
  ) {
    id
    email
    username
  }
}

用户需要签入系统,才能获取个人令牌(token)。此部分我们不做详述,请参阅文章《基于 tide + async-graphql + mongodb 构建异步 Rust GraphQL 服务》、《基于 actix-web + async-graphql + rbatis + postgresql / mysql 构建异步 Rust GraphQL 服务》,以及项目 zzy/tide-async-graphql-mongodb 进行了解。

使用 graphql_client 构建查询体(QueryBody)

在此,我们需要使用到上一节定义的 GraphQL 查询描述,通过 GraphQLQuery 派生属性注解,可以实现与查询描述文件(如 all_users.graphql)中查询同名的结构体。当然,Rust 文件中,结构体仍然需要我们定义,注意与查询描述文件中的查询同名。如,与 all_users.graphql 查询描述文件对应的代码为:

#![allow(unused)]
fn main() {
type ObjectId = String;

#[derive(GraphQLQuery)]
#[graphql(
    schema_path = "./graphql/schema.graphql",
    query_path = "./graphql/all_users.graphql",
    response_derives = "Debug"
)]
struct AllUsers;
}

type ObjectId = String; 表示我们直接从 MongoDB 的 ObjectId 中提取其 id 字符串。

接下来,我们构建 graphql_client 查询体(QueryBody),我们要将其转换为 Value 类型。项目列表查询没有参数,构造简单。我们以用户列表查询为例,传递我们使用 PBKDF2 对密码进行加密(salt)和散列(hash)运算后的令牌(token)。

本文实例中,为了演示,我们将令牌(token)获取后,作为字符串传送。实际应用代码中,当然是作为 cookie/session 参数来获取的,不会进行明文编码。

#![allow(unused)]
fn main() {
    // make data and render it
    let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJlbWFpbCI6ImlvazJAYnVkc2hvbWUuY29tIiwidXNlcm5hbWUiOiLmiJHmmK9vazIiLCJleHAiOjEwMDAwMDAwMDAwfQ.Gk98TjaFPpyW2Vdunn-pVqSPizP_zzTr89psBTE6zzfLQStUnBEXA2k0yVrS0CHBt9bHLLcFgmo4zYiioRBzBg";
    let build_query = AllUsers::build_query(all_users::Variables {
        token: token.to_string(),
    });
    let query = serde_json::json!(build_query);
}

使用 surf 发送 GraphQL 请求,并获取响应数据

相比于 frontend-yew 系列文章,本次 frontend-handlebars 实践中的 GraphQL 数据请求和响应,是比较简单易用的。

  • surf 库非常强大而易用,其提供的 post 函数,可以直接请求体,并返回泛型数据。
  • 因为在 hanlebars 模板中,可以直接接受并使用 json 数据,所以我们使用 recv_json() 方法接收响应数据,并指定其类型为 serde_json::Value
  • 在返回的数据响应体中,可以直接调用 Response<Data> 结构体中的 data 字段,这是 GraphQL 后端的完整应答数据。
#![allow(unused)]
fn main() {
    let gql_uri = "http://127.0.0.1:8000/graphql";
    let gql_post = surf::post(gql_uri).body(query);

    let resp_body: Response<serde_json::Value> = gql_post.recv_json().await.unwrap();
    let resp_data = resp_body.data.expect("missing response data");
}

let gql_uri = "http://127.0.0.1:8000/graphql"; 一行,实际项目中,通过配置环境变量来读取,是较好的体验。

数据的渲染

我们实现了数据获取、转换,以及部分解析。我们接收到的应答数据指定为 serde_json::Value 格式,我们可以直接将其发送给 handlebars 模板使用。因此,下述处理,直接转移到 handlebars 模板 —— html 文件中。是故,需要先创建 templates/users/index.html 以及 templates/projects/index.html 两个文件。

我们的数据内容为用户列表或者项目列表,很显然是一个迭代体,我们需要通过要给循环控制体来获取数据——handlebars 的模板语法我们不做详述(请参阅 handlebars 中文文档)。如,获取用户列表,使用 handlebars 模板的 #each 语法:

    <h1>all users</h1>

    <ul>
      {{#each allUsers as |u|}}
        <li><b>{{u.username}}</b></li>
        <ul>
          <li>{{ u.id }}</li>
          <li>{{ u.email }}</li>
        </ul>
      {{/each}}
    </ul>

基本上,技术点就是如上部分。现在,让我们看看,在上次实践《crate 选择及环境搭建》基础上新增、迭代的完整代码。

数据处理的完整代码

main.rs 文件,无需迭代。

routes/mod.rs 路由开发

增加用户列表、项目列表路由的设定。

#![allow(unused)]
fn main() {
use tide::{self, Server, Request};
use serde_json::json;

pub mod users;
pub mod projects;

use crate::{State, util::common::Tpl};
use crate::routes::{users::user_index, projects::project_index};

pub async fn push_res(app: &mut Server<State>) {
    app.at("/").get(index);
    app.at("users").get(user_index);
    app.at("projects").get(project_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": "我是谁?"});

    index.render(&data).await
}
}

routes/users.rs 用户列表处理函数

获取所有用户信息,需要传递令牌(token)参数。注意:为了演示,我们将令牌(token)获取后,作为字符串传送。实际应用代码中,是通过 cookie/session 参数来获取的,不会进行明文编码。

#![allow(unused)]
fn main() {
use graphql_client::{GraphQLQuery, Response};
use tide::Request;

use crate::{util::common::Tpl, State};

type ObjectId = String;

#[derive(GraphQLQuery)]
#[graphql(
    schema_path = "./graphql/schema.graphql",
    query_path = "./graphql/all_users.graphql",
    response_derives = "Debug"
)]
struct AllUsers;

pub async fn user_index(_req: Request<State>) -> tide::Result {
    let user_index: Tpl = Tpl::new("users/index").await;

    // make data and render it
    let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJlbWFpbCI6ImlvazJAYnVkc2hvbWUuY29tIiwidXNlcm5hbWUiOiLmiJHmmK9vazIiLCJleHAiOjEwMDAwMDAwMDAwfQ.Gk98TjaFPpyW2Vdunn-pVqSPizP_zzTr89psBTE6zzfLQStUnBEXA2k0yVrS0CHBt9bHLLcFgmo4zYiioRBzBg";
    let build_query = AllUsers::build_query(all_users::Variables {
        token: token.to_string(),
    });
    let query = serde_json::json!(build_query);

    let gql_uri = "http://127.0.0.1:8000/graphql";
    let gql_post = surf::post(gql_uri).body(query);

    let resp_body: Response<serde_json::Value> = gql_post.recv_json().await.unwrap();
    let resp_data = resp_body.data.expect("missing response data");

    user_index.render(&resp_data).await
}
}

routes/projects.rs 项目列表处理函数

项目列表的处理中,无需传递参数。

#![allow(unused)]
fn main() {
use graphql_client::{GraphQLQuery, Response};
use tide::Request;

use crate::{util::common::Tpl, State};

type ObjectId = String;

#[derive(GraphQLQuery)]
#[graphql(
    schema_path = "./graphql/schema.graphql",
    query_path = "./graphql/all_projects.graphql",
    response_derives = "Debug"
)]
struct AllProjects;

pub async fn project_index(_req: Request<State>) -> tide::Result {
    let project_index: Tpl = Tpl::new("projects/index").await;

    // make data and render it
    let build_query = AllProjects::build_query(all_projects::Variables {});
    let query = serde_json::json!(build_query);

    let gql_uri = "http://127.0.0.1:8000/graphql";
    let gql_post = surf::post(gql_uri).body(query);

    let resp_body: Response<serde_json::Value> = gql_post.recv_json().await.unwrap();
    let resp_data = resp_body.data.expect("missing response data");

    project_index.render(&resp_data).await
}
}

前端渲染的完整源码

templates/index.html 文件,无需迭代。

对于这部分代码,或许你会认为 headbody 部分,每次都要写,有些啰嗦。

实际上,这是模板引擎的一种思路。handlebars 模板认为:模板的继承或者包含,不足以实现模板重用。好的方法应该是使用组合的概念,如将模板分为 headheaderfooter,以及其它各自内容的部分,然后在父级页面中嵌入组合。

所以,实际应用中,这些不会显得啰嗦,反而会很简洁。本博客的 handlebars 前端源码 surfer/tree/main/frontend-handlebars 或许可以给你一点启发;至于具体使用方法,请参阅 handlebars 中文文档

templates/users/index.html 用户列表数据渲染

<!DOCTYPE html>
<html>

  <head>
    <title>all users</title>

    <link rel="icon" href="/static/favicon.ico">
    <link rel="shortcut icon" href="/static/favicon.ico">
  </head>

  <body>
    <a href="/"><strong>frontend-handlebars / tide-async-graphql-mongodb</strong></a>
    <h1>all users</h1>

    <ul>
      {{#each allUsers as |u|}}
        <li><b>{{u.username}}</b></li>
        <ul>
          <li>{{ u.id }}</li>
          <li>{{ u.email }}</li>
        </ul>
      {{/each}}
    </ul>

  </body>

</html>

templates/projects/index.html 项目列表数据渲染

<!DOCTYPE html>
<html>

  <head>
    <title>all projects</title>

    <link rel="icon" href="favicon.ico">
    <link rel="shortcut icon" href="favicon.ico">
  </head>

  <body>
    <a href="/"><strong>frontend-handlebars / tide-async-graphql-mongodb</strong></a>
    <h1>all projects</h1>

    <ul>
      {{#each allProjects as |p|}}
        <li><b>{{p.subject}}</b></li>
        <ul>
          <li>{{p.id}}</li>
          <li>{{p.userId}}</li>
          <li><a href="{{p.website}}" target="_blank">{{p.website}}</a></li>
        </ul>
      {{/each}}
    </ul>

  </body>

</html>

编译和运行

执行 cargo buildcargo run 后,如果你未自定义端口,请在浏览器中打开 http://127.0.0.1:3000 。

列表数据

至此,获取并解析 GraphQL 数据已经成功。

谢谢您的阅读!