Google が提供している Online Boutique (microservices-demo) というデモアプリケーションがあります。
私は、この中の一部をRustで書き直し、microservices-demo-rust という名前で GitHub に公開しました。
Online Boutique は、ユーザーが商品を閲覧したり、カートに追加したり、購入したりできる Webベースの E コマース アプリケーションです。Kubernetes クラスター上で動作し、マイクロサービスが gRPC で連携します。開発言語には、Go、C#、JavaScript、Java、Python が使われています。
11 個のマイクロサービスで構成されています。私は、この中の 4 個のサービスを Rust で書き直しました。
次の表で、書き直し
欄に "Rust" と書かれているサービスを Rust で書き直しました。それ以外のサービスはオリジナルのままです。
サービス | オリジナル言語 | 書き直し | 説明 |
---|---|---|---|
frontend | Go | Rust | ウェブサイトを提供するための HTTP サーバを公開します。サインアップ/ログインは不要で、すべてのユーザーに対して自動的にセッション ID を生成します。 |
cartservice | C# | Rust | ユーザーのショッピングカート内の商品を Redis に保存して、それを取得します。 |
productcatalogservice | Go | Rust | JSON ファイルから商品の一覧を提供し、商品を検索して個々の商品を取得する機能を提供します。 |
currencyservice | JavaScript | ある通貨の金額を別の通貨に変換します。ヨーロッパ中央銀行から取得した実際の値を使用します。これは最も高い QPS(1 秒あたりのクエリ数)のサービスです。 | |
paymentservice | JavaScript | 与えられたクレジットカード情報(モック)と金額で課金し、トランザクション ID を返します。 | |
shippingservice | Go | ショッピングカートに基づいて送料の見積もりを提供します。アイテムを指定された住所に発送します。(モック) | |
emailservice | Python | ユーザーに注文確認のメールを送信します。(モック) | |
checkoutservice | Go | ユーザーのカートを取得し、注文を準備し、支払い、配送、およびメール通知を調整します。 | |
recommendationservice | Python | カートに入っている商品に基づいて、他の商品をお勧めします。 | |
adservice | Java | Rust | ランダムなテキスト広告を提供します。 |
目的
私がこの書き直しを行った目的は、Kubernetes クラスタで動作する Web システムを Rust で作るとどうなるのか試してみたかったからです。microservices-demo は、学習と検証をするには、ちょうど良い規模のプロジェクトでした。
Rust の実装では、フロントエンドには Web フレームワークである axum を、 gRPC のライブラリには tonic を使いました。
フロントエンドでは、画面をコンポーネントに分割して、コンポーネントが HTML を生成するという方法を採りました。これは、React に触発されたものです。
サーバーサイドで HTML を生成していながらも、React のようなコンポーネント指向であるというプログラムにしました。
この記事では、フロントエンドの実装について、概要をご紹介させていただきます。
フロントエンド
フロントエンドを Rust で書き直すに際して、以下の方針で実装しました。
- 画面をコンポーネントに分割し、各コンポーネントが HTML のタグを出力する。
- HTML テンプレートは使用せず、String のバッファーに HTML タグを追記していく。
ソースコードのフォルダ構成を以下のようにしました。
フォルダ | 役割 |
---|---|
components | 画面の構成要素に応じた部品。HTML タグの出力を行う。 |
components/body | 画面の body の出力を行うコンポーネント。 |
components/body/parts | 画面の body の中で部品として使われるコンポーネント。 |
handlers | 画面の制御を行う。 |
model | データの取得と保持を行う。 |
pages | 画面のコンポーネントを保持する。 |
rpc | gRPC により、他のサービスと連携してデータの取得・登録・更新を行う。 |
MVC で言うと、Component が View、Model が Model、Handler が Controller に該当します。
コンポーネント
すべてのコンポーネントは、Component
トレイトを実装します。
Component
トレイトの write
関数は、画面のプロパティへの不変参照 props: &Props
と、HTML出力用バッファへの可変参照 buf: &mut String
を引数として受け取ります。
pub trait Component {
fn write(&self, props: &Props, buf: &mut String);
}
Component
トレイトを実装している1つの例として、HomeBody
は次のようになっています。
use super::super::Component;
use super::parts::{footer::Footer, header::BodyHeader};
use crate::Props;
pub struct HomeBody {
pub body_header: Box<dyn Component + Send>,
pub footer: Box<dyn Component + Send>,
}
impl HomeBody {
pub fn new() -> Self {
let body_header = BodyHeader::new();
let footer = Footer {};
HomeBody {
body_header: Box::new(body_header),
footer: Box::new(footer),
}
}
}
impl Component for HomeBody { // (1)
fn write(&self, props: &Props, buf: &mut String) { // (2)
*buf += r#"<body>"#;
{
self.body_header.write(props, buf);
*buf += r#"<main role="main" class="home">"#;
{
*buf += r#"<div class="home-mobile-hero-banner d-lg-none"></div>"#;
*buf += r#"<div class="container-fluid">"#;
{
*buf += r#"<div class="row">"#;
{
*buf += r#"<div class="col-4 d-none d-lg-block home-desktop-left-image"></div>"#;
*buf += r#"<div class="col-12 col-lg-8">"#;
{
*buf += r#"<div class="row hot-products-row px-xl-6">"#;
{
*buf += r#"<div class="col-12">"#;
{
*buf += r#"<h3>Hot Products</h3>"#;
}
*buf += r#"</div>"#;
if let Some(hot_products) = &props.hot_products {
hot_products.write(props, buf);
}
}
*buf += r#"</div>"#;
*buf +=
r#"<div class="row d-none d-lg-block home-desktop-footer-row">"#;
{
*buf += r#"<div class="col-12 p-0">"#;
{
self.footer.write(props, buf);
}
*buf += r#"</div>"#;
}
*buf += r#"</div>"#;
}
*buf += r#"</div>"#;
}
*buf += r#"</div>"#;
}
*buf += r#"</div>"#;
}
*buf += r#"</main>"#;
}
*buf += r#"</body>"#;
}
}
行(1)の impl Component for HomeBody {
で、 Component
トレイトをインプリメントしています。
行(2) fn write(&self, props: &Props, buf: &mut String) {
から、 write
関数を実装しています。buf
に +=
で HTML タグの内容を追記しています。{ ・・・ } で囲まれたコードブロックが HTML タグの階層構造になっています。
このプログラムは、次のような、改行や空白のない HTML を出力します。
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>Online Boutique</title><link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous"><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet"><link rel="stylesheet" type="text/css" href="/static/styles/styles.css"><link rel="stylesheet" type="text/css" href="/static/styles/cart.css"><link rel="stylesheet" type="text/css" href="/static/styles/order.css"><link rel="icon" type="image/x-icon" href="/static/favicon.ico"></head><body><header><div class="navbar sub-navbar"><div class="container d-flex justify-content-between"><a href="/" class="navbar-brand d-flex align-items-center"><img src="/static/icons/Hipster_NavLogo.svg" alt="" class="top-left-logo" /></a><div class="controls">・・・
改行や空白があると、人間にはわかりやすいですが、ブラウザにとっては意味がありません。HTML のサイズが大きくなり、トラフィックが増大します。その分、わずかにパフォーマンスが悪くなるはずです。
このため、コードブロックでインデントすることで、人間にとっての見やすさを確保しながら、ブラウザには不要な改行や空白をなくすという実装をすることにしました。
コンポーネント構造
ホーム画面のコンポーネントの構造は、以下のようになっています。
この図の中の、model::hot_product::HotProducts
と rpc::hipstershop::Product
は、モデルや RPC の構造体ですが、コンポーネントとしても使用されています。
これらの構造体が Component
トレイトを実装しているので、コンポーネントとしても使用できます。
use crate::{components::Component, model, rpc, Props};
impl Component for model::hot_product::HotProducts { // (1)
fn write(&self, props: &Props, buf: &mut String) {
for product in &self.products {
product.write(props, buf);
}
}
}
impl Component for rpc::hipstershop::Product { // (2)
fn write(&self, _props: &Props, buf: &mut String) {
let money = self.price_usd.as_ref().unwrap();
*buf += r#"<div class="col-md-4 hot-product-card">"#;
{
*buf += r#"<a href="/product/"#;
*buf += &self.id;
*buf += r#"">"#;
{
*buf += r#"<img alt="" src=""#;
*buf += &self.picture;
*buf += r#"">"#;
*buf += r#"<div class="hot-product-card-img-overlay"></div>"#;
}
*buf += r#"</a>"#;
*buf += r#"<div>"#;
{
*buf += r#"<div class="hot-product-card-name">"#;
*buf += &self.name;
*buf += r#"</div>"#;
*buf += r#"<div class="hot-product-card-price">"#;
*buf += &money.money_for_display();
*buf += r#"</div>"#;
}
*buf += r#"</div>"#;
}
*buf += r#"</div>"#;
}
}
行(1) impl Component for model::hot_product::HotProducts {
と 行(2) impl Component for rpc::hipstershop::Product {
で、それぞれの構造体が Component
トレイトをインプリメントしており、write
関数が実装されています。このため、コンポーネントとして HTML の出力を行うことができます。
上記以外については、プロジェクトのドキュメントをご覧ください。日本語ドキュメントもあります。
ご興味がありましたら、動かしてみていただければと思います。今回は axum を使いましたが、Actix など、他の Webフレームワークでも、同じような実装を行うことができるはずです。
改善できる点がまだ多いだろうと思います。改善点やご意見がありましたら、ぜひお寄せください。