Rust言語をWebAssembly(WASM、ブラウザ上で高速に動くバイナリ形式)にコンパイルしてフロントエンドを組む手法に関心があるエンジニアに向けて、SVG(拡張性のあるベクター画像形式)コンポーネントの実装パターンを整理します。Yew・Dioxus・Leptosという3つのRust製Webフレームワークにまたがる実装例が題材で、フレームワーク横断でUIコンポーネントをどう共有するかという実務的な問いに答える内容です。
題材は「Sushi RS」という、寿司の絵をSVGで描画するRust製クレート(ライブラリの単位)です。ネタとしては軽いものの、内部の実装はフロントエンド開発者が直面する「同じUI部品を複数フレームワークで動かすにはどうするか」という課題をそのまま体現しています。
何が新しいのか: フレームワーク非依存のSVG文字列エンジン
このクレートの核心は、src/svg.rsに置かれた「フレームワークに依存しないSVG文字列生成エンジン」です。
シェイプ(図形)ごとのレンダラー関数が、Rustの標準文字列型であるStringとしてSVGマークアップを返す構造になっています。
つまりYewでもDioxusでもLeptosでも、コンポーネント側は「文字列としてのSVG」を受け取るだけです。
フレームワーク固有のコンポーネントツリーやシグナル(状態管理の仕組み)を経由せず、生成済みのマークアップをそのまま画面に流し込みます。
この「そのまま流し込む」部分が技術的に一番おもしろいところです。
各フレームワークにはHTMLを生文字列として挿入する「エスケープハッチ(安全機構を意図的に迂回する手段)」が用意されており、Sushi RSはこれを横断的に利用しています。
| フレームワーク | 生HTML挿入手段 | 備考 |
|---|---|---|
| Yew | Html::from_html_unchecked(...) | 関数名に unchecked が明記される |
| Dioxus | dangerous_inner_html: "{svg}" | 属性名に dangerous が明記される |
| Leptos | inner_html={svg} | 属性名はシンプルだが挙動は同じ |
3つとも関数名・属性名に「unchecked」「dangerous」といった警告的な語が含まれています。
これは偶然ではなく、ReactのdangerouslySetInnerHTMLと同じ設計思想を踏襲した結果です。
Reactを触ったことがある方なら、この命名の意図はすぐ理解できるはずです。
フレームワークが提供する仮想DOM(実DOMを直接操作せず差分計算する内部表現)のサニタイズ(無害化処理)を素通りさせる操作だからです。
なぜこの設計になるのか: WASMとSVG生成の相性問題
RustでSVGを組み立てる際、素直に書くとformat!()マクロで文字列を組み立てたくなります。
しかしここで実装上の落とし穴に触れています。
SVGの色指定でよく使う16進数カラーコード(例: #3a6a20のような海苔の緑色)を、Rustの生文字列リテラルr#"..."#の中に埋め込もうとすると構文エラーになります。#が生文字列の終端記号と衝突するためです。
この回避策として、実装ではシングルクォートによる文字列テンプレートを使い、最後に.replace('\'', '"')でダブルクォートに置換する処理を挟んでいます。
いかにもRustらしい、コンパイラの型・構文チェック(借用チェッカーを含む静的検証機構)と格闘した跡が残る箇所です。
こうした細かい制約は、TypeScriptでテンプレートリテラルを使ってSVGを組み立てる場合には基本的に発生しません。
JavaScript系の文字列リテラルはRustほど記号の予約に厳格ではないため、この種のエスケープ処理を明示的に書く必要が薄いという違いがあります。
関連技術との比較: JSXのSVGと何が違うか
ReactやVueのエコシステムでSVGコンポーネントを扱う場合、多くはSVGをJSX・テンプレート構文としてそのままマークアップに埋め込みます。
ビルド時にSVGファイルをReactコンポーネントへ変換するSVGRのようなツールも広く使われており、DOMツリーの一部として型安全に扱えるのが一般的です。
一方、Sushi RSの方式は「文字列としてSVGを生成し、生HTML挿入で流し込む」やり方です。
これは型安全性の一部を手放す代わりに、フレームワーク間でロジックを完全に共有できるという利点があります。
この設計判断は、WASM単体で動くRust製フロントエンドが抱える構造的な事情を反映しています。
Yew・Dioxus・Leptosはいずれも独自のコンポーネントモデルを持ち、相互のコンポーネント資産をそのまま流用できません。
そのため「共通のロジック層を文字列生成に落とし込み、各フレームワークの薄い接着コードで包む」という戦略が、実務上の妥協点として選ばれています。
マイクロフロントエンド(複数フレームワークを1画面に共存させる構成)を検討したことがある方なら、この発想自体は馴染み深いはずです。
アクセシビリティ(支援技術への対応)の面では、生成されるSVGにARIA属性(スクリーンリーダー向けの意味づけ)を付与する実装も含まれています。
装飾的なグラフィックであっても、読み上げソフトが内容を説明できるようにする配慮です。
読者への影響: 今日確認できること
Rust製WASMフロントエンドの採用を検討している、あるいはすでにYew・Dioxus・Leptosのいずれかを触っている方は、以下の観点で自分のプロジェクトへの影響を確認できます。
- 生HTML挿入を使っている箇所があるか: 各フレームワークで
from_html_unchecked・dangerous_inner_html・inner_htmlのいずれかを検索する - 挿入する文字列の出所: ユーザー入力を含まず、自前生成のSVGなど信頼できる文字列に限定されているか確認する
- サニタイズの有無: XSS(悪意あるスクリプト注入)対策として、外部由来の文字列であれば
ammoniaのようなRust製サニタイズクレートを通しているか点検する - 対応バージョンの確認: 利用中のYew・Dioxus・Leptosのバージョンで該当APIの仕様変更がないか、各プロジェクトのCHANGELOGを見る
実際に手元で試すなら、Cargo.tomlに該当クレートを追加してビルドが通るか確認するのが手早い方法です。
cargo add sushi-rs --features yew
cargo build --target wasm32-unknown-unknownWASMターゲットへのビルドが初めての場合は、wasm-packやtrunkといったビルドツールの導入状況も合わせて確認してください。
これらはYew・Leptosのエコシステムで標準的に使われるツールチェーンです。
まとめ
Sushi RSという題材そのものは軽い読み物ですが、内部実装は次の3点でフロントエンド技術者にとって参考になります。
- フレームワーク横断のUI共有は「文字列生成+生HTML挿入」という現実的な妥協で成立させられる
- YewのHtml::from_html_unchecked、Dioxusのdangerous_inner_html、Leptosのinner_htmlは同じ思想の異なる表現である
- RustでSVGを文字列として組み立てる際は、生文字列リテラルと
#記号の衝突に注意が必要
自分のプロジェクトでこれらのAPIを使っている場合は、まず該当箇所を検索し、入力元が信頼できる文字列に限定されているかを点検してみてください。
そのうえで、外部入力を扱う可能性がある箇所には別途サニタイズ処理を挟む判断が必要です。