上一篇文章《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
查询描述文件对应的代码为:
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)。
代码语言:javascript复制本文实例中,为了演示,我们将令牌(token)获取后,作为字符串传送。实际应用代码中,当然是作为 cookie/session 参数来获取的,不会进行明文编码。
// 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 后端的完整应答数据。
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
路由开发
增加用户列表、项目列表路由的设定。
代码语言:javascript复制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 参数来获取的,不会进行明文编码。
代码语言:javascript复制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 项目列表处理函数
项目列表的处理中,无需传递参数。
代码语言:javascript复制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
文件,无需迭代。
对于这部分代码,或许你会认为
head
、body
部分,每次都要写,有些啰嗦。 实际上,这是模板引擎的一种思路。handlebars 模板认为:模板的继承
或者包含
,不足以实现模板重用。好的方法应该是使用组合的概念,如将模板分为head
、header
、footer
,以及其它各自内容的部分,然后在父级页面中嵌入
组合。 所以,实际应用中,这些不会显得啰嗦,反而会很简洁。本博客的 handlebars 前端源码 surfer/tree/main/frontend-handlebars 或许可以给你一点启发;至于具体使用方法,请参阅 handlebars 中文文档。
templates/users/index.html 用户列表数据渲染
代码语言:javascript复制<!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 项目列表数据渲染
代码语言:javascript复制<!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 build
、cargo run
后,如果你未自定义端口,请在浏览器中打开 http://127.0.0.1:3000 。
至此,获取并解析 GraphQL 数据已经成功。
谢谢您的阅读,欢迎交流。