以前からbasolatoで何か作ってみたいなとは思っていたので今回、簡単な投票アプリを作ることにしました。
出来上がったものはこちらになります。
ログイン機能もつけていないのでめちゃくちゃシンプルです。
機能としては、
・セレクトボックスを選択
・投票するボタンを押すとアラートが表示
・ページ下部に合計の投票数が表示
また、セッションに値を持たせることで期限切れしない限りは一人のユーザーが連続して投票できない仕組みになっています。
ソースコードはこちらです。
ローカルで動かしてみる
後々READMEに記載予定ですがこちらにも記載いたします。
このアプリの起動にはDockerを使うことを推奨しています。
もしDockerのインストールがまだでしたら先にDockerのインストールを行うことを推奨いたします。
git clone https://github.com/Runacy/basolato-poll-app
cd basolato-poll-app
docker-compose up --build -d # コンテナのビルドとデーモンで起動
docker exec -it basolato-poll-app_app_1 bash # コンテナに入る
上記手順でコンテナに入ることができます。
開発用サーバーの起動
basolatoでは開発用のサーバーをducereコマンドで簡単に行うことができます。
# pwd
# /root/project
cd poll-app
ducere make config # コンフィグのs作成
ducere migrate # テーブルの作成
ducere serve # 開発用サーバーの起動
ducereコマンドの詳細が知りたい方はドキュメントに詳しく述べられていますのでこちらをお読みください!
ソースコードの簡単な説明
ここからは、今回のアプリのソースコードの説明をbasolatoの機能の説明を踏まえて行いたいと思います。
ルーティング
basolatoではルーティングはmain.nimに記載します。
var routes = Routes.new()
routes.middleware(re".*", auth_middleware.checkCsrfTokenMiddleware)
routes.middleware(re"/api/.*", cors_middleware.setCorsHeadersMiddleware)
routes.get("/", poll_controller.toppage)
routes.get("/poll", poll_controller.index)
routes.post("/poll", poll_controller.store)
serve(routes)
routes.getでGET,routes.postでPOSTのようにHTTP動詞を指定することができます。
middlewareもここで書くことができ、デフォルトでcsrf対策が取られたセキュアな仕組みとなっています。
コントローラー
コントローラーの作成もducereコマンドで行えます。
ducere make controller (コントローラーの名前)
コントローラーは(コントローラーの名前)_controller.nimファイルで作成されます。
メソッドを例に挙げると、以下のようなものになります。
proc index*(context:Context, params:Params):Future[Response] {.async.} =
let name = "Basolato " & BasolatoVersion
return render(welcomeView(name))
今回のアプリではセッションの扱いが鍵でした。
basolatoでセッションを扱いたい場合、contextから呼ぶことができます。
セッションに値をセットしたい場合、
await context.set("キー", "value")
セットされた値を呼びたいときは、
let a = await context.get("キー", "value")
のようにして呼ぶことができます。
今回の場合だと、post側でsetした値がredirect先でも同じ場合は同一ユーザーと見做して再度投票することができない仕組みになっています。
マイグレーション
マイグレーションファイルの作成もducereコマンドで行えます。
ducere make migration (migrationファイルの名前)
ducere migrateする際、これらのmigrationは非同期で行われますのでかなり効率的だと思います。
migrationのスキーマの定義は、basolato作者のmedy氏が開発したライブラリを用いて行います。
proc poll_select*() {.async.} =
rdb.schema(
table("poll_select", [
Column().string("user_id"),
Column().string("select_item")
])
)
Column().〇〇でカラムの型を指定できます。
指定できる型の一覧はこちらです。
https://itsumura-h.github.io/nim-allographer/schema_builder/column.html
今回のアプリでは、投票ボタンを押した後に、全集計結果が表示されます。
こちらは単純にリソースコントローラのindexメソッドで、group byを用いて集計したものをそのまま表示する形になっています。
allographerでgroup byの操作を行いたい場合、以下のようになります。
await rdb.table("poll_select")
.select("select_item", "count(select_item)")
.group_by("select_item")
.get()
group_by("カラム名")とすることで目的のカラムで集計することが可能になります。
ビュー
basolatoのviewはtemplateを通して幾つかのコンポーネントに分けて書くことができます。
また、Nimの関数をtemplate内で呼び出すことができます。
たとえば以下のようにして、呼び出すことができます。
proc disabled(status: bool): string =
if status:
return "disabled"
else:
return ""
proc impl(status: bool, data: seq[JsonNode]):string =
tmpli html"""
<input type="radio" name="select_item" value="ご飯派" $(disabled(status))>ご飯派<br>
"""
のように,$(disabled(status))と囲むことで 呼び出すことができます。
今回のアプリでは、すでに投票していた場合は選択できないようにdisabledされる機能を実装するために使いました。
このほか、直接for分回せたりするので何かと便利な場面は多いかと思います。
まとめポエム
アーキテクチャについて勉強不足な面があるので、ドメインモデルについては触れていません、、
しかし、ducereコマンドでルールに沿ったテンプレートが生成されるので、逸脱しないような設計で進めることができるのではないでしょうか。
個人的に結構面白いフレームワークだと思ったので1年くらい個人開発で運用してみるのもありかなあと思いました。