🔥

Elmとaxumでポートフォリオサイト作ってみた

2022/01/11に公開

はじめに

私は駆け出してすらいない人間ですが、1~2年後に駆け出してみようかなという選択肢も浮かんでいるので、勉強のためにポートフォリオサイトを作ってみようと思いました。

技術選定

以下の技術要素で開発を行いました

  • フロントエンド
    • Elm
  • バックエンド
    • Rust
      • axum (Webフレームワーク)
      • sqlx (データベースを使うためのクレート)
      • tracing (ロギングをするクレート)
      • and more

フロントエンドを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-httpcorsモジュールを利用しました。

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などの激むず言語にチャレンジするというルートを辿るひとがもっといてもいいのになと思います。

GitHubで編集を提案

Discussion