Let's write β

プログラミング中にできたことか、思ったこととか

RustでWebプログラミング No.3 ~ HandlebarでHTMLテンプレート~

振り返り

前回 poketo7878-dev.hatenablog.com

までにRouterで複数URLに対応したHTTPサーバーを建てられるようになりました。 しかし、出力されるのは常に文字列でHTMLのページではありませんでした。

今回はHandlebarというテンプレートエンジンを利用してHTMLページをユーザーに表示できるようにしました。

handlebar-ironをCargo.tomlに追加

前回までと同様にCargo.tomlに以下のdependencyを追加します。

handlebars = "0.20.5"

また、今回はHTMLページでformを表示し、POSTしてみたいと思うので、 formやURLなどのパラメータをパース及び取得するためのcrateもdependencyに追加しておきます。

params = "0.4.1"

全体

まずはコードの全体です

extern crate iron;
extern crate router;
extern crate handlebars_iron as hbs;
extern crate params;

use std::collections::HashMap;
use std::error::Error;
use iron::prelude::*;
use iron::status;
use router::{Router, url_for};
use hbs::{Template, HandlebarsEngine, DirectorySource};

fn main() {

    fn top_handler(req: &mut Request) -> IronResult<Response> {
        let mut resp = Response::new();
        let mut data = HashMap::new();
        data.insert(String::from("greeting_path"),
                    format!("{}", url_for(req, "greeting", HashMap::new())));
        resp.set_mut(Template::new("index", data)).set_mut(status::Ok);
        return Ok(resp);
    }

    fn greet_handler(req: &mut Request) -> IronResult<Response> {
        use params::{Params, Value};
        let map = req.get_ref::<Params>().unwrap();
        return match map.find(&["name"]) {
            Some(&Value::String(ref name)) => {
                Ok(Response::with(
                    (status::Ok,
                     format!("Hello {}", name).as_str())))
            },
            _ => Ok(Response::with((status::Ok, "Hello world")))
        }
    }

    //Create Router
    let mut router = Router::new();
    router.get("/", top_handler, "index");
    router.post("/greet", greet_handler, "greeting");
    
    //Create Chain
    let mut chain = Chain::new(router);
    // Add HandlerbarsEngine to middleware Chain
    let mut hbse = HandlebarsEngine::new();
    hbse.add(Box::new(
        DirectorySource::new("./src/templates/", ".hbs")));
    if let Err(r) = hbse.reload() {
        panic!("{}", r.description());
    }
    chain.link_after(hbse);
    
    println!("Listen on localhost:3000");
    Iron::new(chain).http("localhost:3000").unwrap();
}

細かく見ていきましょう

Router

    //Create Router
    let mut router = Router::new();
    router.get("/", top_handler, "index");
    router.post("/greet", greet_handler, "greeting");

前回同様Routerを作成しています。今回はgreetの名前はurlの一部ではなくpostのパラメータとして送信することにしました。

Chain

let mut chain = Chain::new(router);

前回は作成したRouterを直接HandlerとしてIronに渡していましたが、今回はChainというオブジェクトを作成しています。 これはMiddlewareの連鎖を表現したオブジェクトで、ここにミドルウェアをつなげていくことができます。

HandlebarEngine

    // Add HandlerbarsEngine to middleware Chain
    let mut hbse = HandlebarsEngine::new();
    hbse.add(Box::new(
        DirectorySource::new("./src/templates/", ".hbs")));
    if let Err(r) = hbse.reload() {
        panic!("{}", r.description());
    }
    chain.link_after(hbse);

ここではHandlebarsEngineを作成し、 Handlebarのテンプレートファイルを設置するディレクトリのルートディレクトリ、 Handlebarのファイルとして扱うファイルの拡張子を指定しています

今回の場合は./src/templates/をルートディレクトリとして指定しています、また拡張子は.hbsとしています。

このようにディレクトリを指定したら

    if let Err(r) = hbse.reload() {
        panic!("{}", r.description());
    }

ディレクトリ中のファイルをロードしています。

そして最後に先ほどのChainの末尾にHandlebarsEngineを追加しています。

chain.link_after(hbse);

テンプレートの描画

    fn top_handler(req: &mut Request) -> IronResult<Response> {
        let mut resp = Response::new();
        let mut data = HashMap::new();
        data.insert(String::from("greeting_path"),
                    format!("{}", url_for(req, "greeting", HashMap::new())));
        resp.set_mut(Template::new("index", data)).set_mut(status::Ok);
        return Ok(resp);
    }

indexページのハンドラ中では、レスポンスのボディとしてTemplate::new("index", data)を指定しています。 このようにすることで、さきほどしたルートディレクトリからの相対パスで指定したhandlebarファイルを指定できます。

ファイル中で利用する変数のデータはJSONに変換可能なデータ構造、今回の場合はHashMap等で渡すことができます。 参考URL

トップページのテンプレートファイルは

<h1>Hello, world</h1>

<form action={{greeting_path}} method="POST">
      <input type="text" name="name"/>
      <input type="submit" value="Hello!"/>
</form>

このようにformのaction URLをgreetingのページのURLに設定しています。

このようにすると

https://gyazo.com/894654c0137504b7a682cfe47449c0a6

のようにformの表示されたHTMLページが描画されます。

POSTのパラメータの処理

さて、先ほどのページのformに値をいれてsubmitするとその値はnameというパラメータ名でPOSTされてきます。

この値をハンドラ中で利用するには

        let map = req.get_ref::<Params>().unwrap();
        return match map.find(&["name"]) {
            Some(&Value::String(ref name)) => {
                Ok(Response::with(
                    (status::Ok,
                     format!("Hello {}", name).as_str())))
            },
            _ => Ok(Response::with((status::Ok, "Hello world")))
        }

このように、まずリクエスト中からParams型の値を取り出し、 Paramsに対してfindメソッドで値を検索した結果のOptionalをmatchすることで利用できます。

今回の場合は取り出してきたnameを使ってHello 名前という形で表示しています。 https://gyazo.com/7464d8053c1c13b7262281b9c23c44fd

まとめ

今回は前回までテキストのみの表示だったページをHandlebarのテンプレートエンジンを用いることでHTMLの表示を可能にしました。 また、POST等のパラメータを取得するためのparams crateを利用しパラメータをハンドラ中で取得できるようになりました。

次回はHTMLだけでなくJSONなどのデータ型をユーザーからのリクエストに応じて返せるようにしたいと思います。