Elmとaxumでポートフォリオサイト作ってみた
はじめに
私は駆け出してすらいない人間ですが、1~2年後に駆け出してみようかなという選択肢も浮かんでいるので、勉強のためにポートフォリオサイトを作ってみようと思いました。
技術選定
以下の技術要素で開発を行いました
- フロントエンド
- Elm
- バックエンド
- Rust
- axum (Webフレームワーク)
- sqlx (データベースを使うためのクレート)
- tracing (ロギングをするクレート)
- and more
- Rust
フロントエンドをElmにした理由はJavaScriptが難しすぎて私の実力では全く書けないからで、バックエンドをRustにしたのはその他の言語をよく知らないからです。
制作物
こんな感じで制作しました。(CSSが全く分からないのでクソみたいな見た目です。)
デプロイするメリットがとくにないのと、DDoSなどで高額請求がきたら嫌なのでデプロイはしてません。
バックエンドでAPIサーバーを建てる必要が全くないほどシンプルなのですが、勉強のためにバックエンドからデータを取得するようにしています。
エラー処理を何もしていないなど実装は不備がたくさんあります。
TOPページ
Aboutページ
Careerページ
Skillページ
GitHubのURLは以下です
フロントエンド
フロントエンドはTOPページ、Aboutページ、Careerページ、SkillページからなるSPAにしました。
SPAにする必要は全くないのですが、練習のためにSPAにしました。
SPA
ElmでSPAを作るならelm-spaを使ったほうが良いんだろうなと思いましたが、私は勉強のために素のElmで実装しました。
とはいえほとんどElm in Actionに載っていたSPAのコードの構成に習っています。
以下ざっくりポイントを説明します。
まず1ページ1ページを普通のElmのアプリケーションだと思って作ります。そこからどうSPA化するかという話です。
Main.elm
(名前は何でもいい)というファイルを作ってそこに記述していきます。
ページ
ページという型を導入し、それぞれにそのページのModelをもたせます。
type Page
= TopPage Top.Model
| AboutPage About.Model
| CareerPage Career.Model
| SkillPage Skill.Model
| NotFound
モデル
モデルはページとNavigation.Keyを持っています。Navigation.KeyはURLを変更するNavigationコマンドで必要とされるものです。
これにより、pageの値を適切にコントロールすることで、各ページは自身のModelにアクセスできるようになります。
type alias Model =
{ page : Page
, key : Nav.Key
}
ビュー
ビューはmodelのpageに応じておのおのpageのview関数を呼び出します。
それぞれのページのviewが返すMsgはMainのMsgではないので調整が必要です。
view model =
let
content =
case model.page of
TopPage top ->
Top.view top
|> Html.map GotTopMsg
AboutPage about ->
About.view about
|> Html.map GotAboutMsg
CareerPage career ->
Career.view career
|> Html.map GotCareerMsg
SkillPage skill ->
Skill.view skill
|> Html.map GotSkillMsg
NotFound ->
text "Not Found"
in
{ title = "Hayao's portfolio"
, body =
[ div [ class "container" ]
[ viewHeader model.page
, content
, viewFooter
]
]
}
アップデート
他のURLに変更しようとしたときとURLが変わったときにメッセージがくるので、その対処をします
updateUrl url model
がCmd Got~Msgを出すようにしているのでGot~Msgが来たらそれぞれのページのupdate関数を呼びます。
それぞれのページのupdateが返すMsgはMainのMsgではないので調整が必要です。
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
-- URL変更リクエストがきたとき ここの記述は定型文っぽい
UrlRequest urlRequest ->
case urlRequest of
Browser.External href ->
( model, Nav.load href )
Browser.Internal url ->
( model, Nav.pushUrl model.key (Url.toString url) )
-- URLが変更されたとき、URLをパースして適切なMsgを送る
ChangedUrl url ->
updateUrl url model
GotTopMsg topMsg ->
case model.page of
TopPage top ->
toTop model (Top.update topMsg top)
_ ->
( model, Cmd.none )
GotAboutMsg aboutMsg ->
case model.page of
AboutPage about ->
toAbout model (About.update aboutMsg about)
_ ->
( model, Cmd.none )
GotCareerMsg careerMsg ->
case model.page of
CareerPage career ->
toCareer model (Career.update careerMsg career)
_ ->
( model, Cmd.none )
GotSkillMsg skillMsg ->
case model.page of
SkillPage skill ->
toSkill model (Skill.update skillMsg skill)
_ ->
( model, Cmd.none )
Elmの勉強法
私はElm in Actionを読みましたが、とても良い本でした。ただ英語なので、Elm GuideとプログラミングElmを読んだほうが良いかもしれません。
バックエンド
バックエンドはただフロントエンドで表示するデータをJSONで返すだけです。
CORS
フロントエンドとバックエンドを違うOriginで走らせていたので、CORSの設定が必要でした。
なのでtower-httpをcorsモジュールを利用しました。
use tower_http::cors::{CorsLayer, Origin};
let app = Router::new()
.route("/skills", get(skills))
.route("/about", get(about))
.route("/careers", get(careers))
.layer(
CorsLayer::new()
.allow_origin(Origin::exact("http://localhost:8000".parse().unwrap()))
.allow_methods(vec![Method::GET]),
)
.layer(AddExtensionLayer::new(connection_pool));
Rustの勉強法
Rustは二年くらいまえから知っていたので、どう勉強したのかは忘れました。
RustのWebプログラミングについてはzero to production in rustという本が出色の出来で私はかなり参考にしています。無料で記事も読めるみたいです。
axumについてはGitHubのexampleフォルダが充実しているのでそこを見ると勉強になります。
sqlxとtracingはGitHubのREADMEを読むと雰囲気はわかるのですが、ベストプラクティスなど全くわからないのでだれか詳しい解説記事書いてほしいです。
おわりに
ElmとRustの練習のために駆け出しエンジニアがよく作るポートフォリオサイトを作ってみました。(私は駆け出せてすらいませんが)
Elmはとても書きやすいです。Rustは言語の習得難易度は高いですが、axumはとても使いやすく感じました。JavaScriptやRuby、Pythonなどの激むず言語で作っているひとが多いですが、腕のあるプログラマーですら苦戦することが多い言語を駆け出しプログラマーが使うのはとても大変だと思います。少なくとも私はうまく使えません。
実務で広く使われているのでJavaScriptやPythonをいずれは使わなければならないとしても、最初はElmやRustでプログラミングに慣れてから、JavaScriptなどの激むず言語にチャレンジするというルートを辿るひとがもっといてもいいのになと思います。
Discussion