ここ数ヶ月でNext.jsで作成していたブログを思い切ってRust製のReact likeなフレームワークであるYewにリプレイスしてみました

ブログをNext.jsからwasm(Rust)に移行してみた

その際にSSR化や諸々の修正が残っており、その残作業について、Rustで実装する過程の情報を記事としてまとめてみましたので、同じような実装をしている・試みている方の参考となれば幸いです🙏

やっぱりSSR化大変だった

情報が少ない

「情報が少ない」状態で実装するのは結構大変でした

参考となる公式の情報↓

公式から提供されているコードはあるので簡単に実装できると考えていたのですが、Rustの所有権の問題がSSRする時点で発生したりと想定以上のエラーに悩まされかなりの時間を浪費してしまいました

当初は全部WASMでEdgeで動かすという方針だったため、CloudflareのWorkersで稼働させる予定でしたが、

  • 公式サンプルの情報不足
  • 私のRustの経験不足

等の理由でCloud Runに切り替えています

YewのSSRは実験的機能

SSRは機能としては提供されていますが、実験的な機能であり、まだまだ制約があります

特に大変だったポイントがWeb APIを利用することができない、という点です

こちらが公式のメッセージ

Web APIs such as web_sys are not available when your component is rendering on the server side. Your application will panic if you try to use them. You should isolate logics that need Web APIs in use_effect or use_effect_with as effects are not executed during server-side rendering.

https://yew.rs/ja/docs/next/advanced-topics/server-side-rendering

Web API利用できないということは、「DOMの操作ができない」ということです

SSR計画時点では下記のcrateを利用して、headのmetaタグを変更したり、bodyの中身を変更してSEO対策するつもりでいました

web_sysを利用すると簡単にDOM操作が行えるのでSSRモード以前はSPAのブログとしてuse_effectの中でDOM操作でSEO対策を行っていました

しかしSSRモードで実行するとエラーが大量に発生し、web_sysの利用箇所を削除、に至ります

※現時点2023/06/25時点では利用できないのであって、バージョンが変わるタイミングで利用可能になる可能性はあります

headのmetaタグはSSR時に注入する方針

SSR化の動機はSEO対策だったので、割と早期に問題にぶち当たりました

前セクションで記述の通り、Web APIが利用できないためバックエンドから取得したデータをHTMLに反映することができない状態で(できるけどheadやbodyにデータが反映される前にresponseが完了してしまう)、SEO的にベストとは言い難い実装です

さらにプラスで問題となったのが、Yewではindex.htmlの存在があり、こちらがエントリーポイントとなっています

NextやRailsのようにLayoutをテンプレート化してheadをある程度機動的に触ることができないという制約が課せられています

従って、bodyタグにバックエンドからのデータを反映させるだけでなく、headに関してもmetaタグを反映させる、ということも必要となります

公式のサンプルで具体的な方法を提示されていなかったため

  • SPAのままでweb_sysを利用する
  • SSRレンダリング時にheadにデータを注入する
  • headを諦める

という選択肢がありましたが、下記のリポジトリの実装を見て、2点目の「SSRレンダリング時にheadにデータを注入する」という方法で実現できそうだったのでこちらで実装しました

blog-romira-dev

※コードの公開と解説感謝です🙏

サーバー実装

※ここの部分はメモしていなかったため記憶で書いています。流し読みでお願いします🙏

基本的な実装は公式サンプルの通りなので特に問題なかったですが、Yewアプリで定義したAPI通信のコードをサーバーで利用することでハマってしまいました

下記のコードは修正後なので問題なく動きますが、Yewアプリで定義したinfrastructureのコードをimportして利用するとスレッド間共有に関する問題が発生してしまいました(確かスレッド間でしたが保証できません🙏)

pub async fn fetch_data_from_api(slug: &str) -> Result<PostFromApi, reqwest::Error> {
    let url = format!("https://api.masahiro.me/api/posts/{}", slug);
    let client = Client::new();
    let response = client.get(&url).send().await?;
    let body = response.json::<PostFromApi>().await?;
    Ok(body)
}

let meta_future = tokio::spawn(async move {
    let api_response = fetch_data_from_api(&slug).await;
    let post_from_api = match api_response {
        Ok(body) => body,
        Err(err) => panic!("error: {}", err),
    };
}

https://github.com/masahiro04/masahiro-me/blob/70459059e644adb6e85feef7b42b92b1cf8a05a9/ssr_server/src/bin/simple_ssr_server.rs#L45-L78

非同期を実行する際はRustはマルチスレッドで実行することを考慮するので、スレッド間でデータを参照する場合は冪等であることを求めます

従ってYewアプリのinfrastructureで定義したコードを利用する際も、データの冪等性を求められるのですが、外部の実行コードであるYewアプリの中のinfrastructureを実行する際は、それを特定すべき?のようなエラーが発生していました

個人レベルのブログで採算を気にしないで良い環境のため、綺麗にコードをまとめたかったのですが、なかなか面倒そうな問題だったため、reqwestを利用したメソッドを定義して対応することで問題を回避しました

CDN

WASMをEdgeで動かす、という制約を自分自身に設けていましたが、サーバーをWorkersで動かすのも大変そうだったのでCloud Runに載せてCloudflareのCDN経由で配信に変更しました

Cloud RunのカスタムドメインマッピングでCloudflareを使う

DX(Developer experience)

SSRを実装する際は

  • Yewアプリをbuild
  • cargo runで実行

という流れになるので、Yewの修正を行う際は必ずbuildしてからSSRのサーバーを起動する必要があります

なので、開発環境では dev用のMakefileを実行して、デプロイの際はSSR buildを利用することで開発速度を落とさないように工夫しました

※通常のSPAサイトであれば trunk serveでhot reloadがかかるので特に問題ありません

dev:
	cd app && \
	trunk serve
ssr_build:
	cd app && \
	cd ../ssr_server && \
	trunk build --release -d ./dist && \
	cp robots.txt ./dist/robots.txt && \
	cd ../ssr_server && \
	cargo build --release --features=ssr --bin simple_ssr_server --

ssr_run:
	cd ssr_server && \
	cargo run --release --features=ssr --bin simple_ssr_server -- --dir dist

Build時間長い

Rustのbuildが思ったよりも遅かったです

遅いこと自体は同僚に聞いていたので心構えしていたのですが、想像以上に遅く、特にDockerのimageを作成する際は長すぎて仕事で利用する際はかなり注意する必要があると感じました

個人用途で、デプロイ頻度は低いので問題にはなりませんが、いつか時間を作って改善してみたいです

所感

「TypeScriptやNext.jsは偉大である」と改めて感じました

今後はRustもフロント領域に殴り込みをかけていくことになるかと思いますが、ほとんどの箇所ではTypeScriptでとにかく速度を求める部分にのみWASMを適用がやはり吉だと感じます

とはいえ、「Rustでフロント開発しています!」というかなりパンチの効いた採用フレーズを使えるタイミングではあるとも思うので、戦略的には割とアリな気もしています

今後のチャレンジ

今後は

  • Firebase Authentication
  • FIrebase Firestore
  • Cloudflare Workersにリプレイス(やっぱりWASMとEdgeにこだわりたい)

なども利用できるようにチャレンジしてみようと思います🦀🚀

コメントをどうぞ

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA