2020 年最後の学期で Web 演習の授業を取りました。授業概要として、5 人くらいでチームを組み、役割分担しつつクライエント(指導教員)から新規サイト作成の要望を聞き、デザインからデプロイまでを行う授業です。
普段やらないことをやってみたいという考えのもと、チームメンバーの都合で、私はバックエンド・インフラ・イベント画面・管理画面を担当して、偉大なる yama 先生はトップページのコーディングをやってくれました。
Repository
=> https://github.com/yue4u/tsukiyo
技術選定のプロセス
技術スタックやインフラの選択も提案のうち自由ということなので、最近試したかったけど試せなかったものを積み合わせました。
いろんなものを試すことが目的なので、最終的な技術スタックはかなりマニアックの結果になりました。
フロントエンド
yama 先生もVue3
+vite
の構成でやりたかったらしいので、フロントエンドの framework としてそれらを採用しました。css の pre processor としてscss
を入れましたが、途中でtailwind
も試したくなったので、実際プロジェクト内でscss
はほとんど使われていません。また、フロントエンド開発のベースラインとして TypeScript
を使用しました。
バックエンド
私は最初からrust
で書きたいのと、sql
データベース上でgraphql
レイヤーを作るという縛りでやりたかったので、ORM
としてのdiesel
と graphql ライブラリーのjuniper
を採用しました。全体の web framework はactix
に載せました。
開発環境
複数人同時開発なので、docker-compose
で構築しました。
インフラ
インフラ選択の初期にVPS
を借りてやるか、cloud vendor
を利用するのかについて結構悩みました。クライアントから記事の画像を貰ってアップロードし反映したいのと、できればコストを抑えたいという要望がありました。その上大量のアクセスが来ないという想定なので、Pay as you go
のcloud vendor
なら無料枠で済ませるという算段でした。
私達のチームはrust
という相対的にマイナーな言語を選択したので、デプロイするにはCloud Run
が一番手軽です。さらに、CI/CD
の構成や自動 revision はVPS
より楽です。以上を踏まえ、インフラはGCP
を選択しました。
- Cloud Storage フロントエンド hosting 用
- Cloud Run
また、Cloud Run
のためにContainer Registry
も合わせて使用しました。管理画面から backend を経由せず直 接アプロードできるという利点があるので、Identity Platform
を authentication の手段として採用しました。
CI/CD
の pipeline はgithub actions
を採用しました。最初にCloud Build
を使ってみましたが、一番安いランタイムのパフォーマンスが非常に悪く、当時の build 用の Dockerfile に問題もあったけれど、10 分のビルドタイムですらアウトしたので、github actions
に移行しました。
遭遇した問題と振り返り
フロントエンド
フロントエンドは計 11 ページで、サイトマップは以下のような感じです。
path | ページ | 実装した機能 | |
---|---|---|---|
1 | / | LP | デザイン通りの coding |
2 | /contact | Contact | contact を作成する |
3 | /contact/success | ContactSuccess | contact 送信成功の表示 |
4 | /event | Event | 公開している event 一覧 |
5 | /event/:id_or_slug | EventItem | id か slug からイベントを表示する |
6 | /admin | Admin | admin トップページ |
7 | /admin/login | Login | login 機能 |
8 | /admin/contact-list | ContactList | 送られた contact 一覧 |
9 | /admin/event-editor | EventEditor | - 新規イベントの作成 - 既存イベントの更新 |
10 | /admin/event-list | EventList | すべての event 一覧 |
11 | /* | 404 |
vue3
、<script setup/>
とTypeScript
今回の制作にあたって、まだRFCであるscript setup
を採用しました。svelte
と似たような感じで<script/>
の内容を直接 expose してくれるため、boilerplate
が減り、見通しを良くすることができます。
tooling
がまだ追いついてないため、vetur
の設定などに最初は少し苦労しました。その後、Volar
(johnsoncodehk/volar)という vscode-extension の存在を知りました。これはscript setup
をサポートしている上、<template/>
の中でも型チェック出来るようになっていて自動補完も効くので、開発効率が爆速になりました。
urql
とgraphql-codegen
Backend の graphql API と通信するため、@urql/vue
を入れました。使ってみた印象は良く、もし次があればまた使いたいと思いました。<script setup/>
と合わせて、非常に clean なコンポーネントが書けます。
vue
<template><div v-if="!data?.events?.length">There is no events</div><div v-else>There is {{ data.events.length }} events</div><div v-if="fetching">Loading...</div><div v-else-if="error">Oh no... {{ error }}</div><div v-else><ul v-if="data"><liv-for="event in data.events":key="event.id"class="event"@click="deleteEvent({ id: event.id })">{{ event.id }} ) {{ event.title }}</li></ul></div></template><script setup lang="ts">import { useQuery, useMutation } from "@urql/vue"import { Event } from "@/type/gql"const { fetching, data, error } = useQuery<{events: Pick<Event, "id" | "title">[]}>({query: `{events {idtitle}}`,requestPolicy: "network-only",})const { executeMutation: deleteEvent } = useMutation(`mutation ($id: Int!) {deleteEvent(id: $id) {id}}`)</script><style lang="scss" scoped>.event {font-size: 2rem;cursor: pointer;&:hover {&::after {margin-left: 1rem;content: "x";color: red;}}}</style>
型生成にはgraphql-codegen
を使いました。urql
用の pluginもありますが、ざっくり見た感じそれらはreact
向けです。@urql/vue
とうまくいくかわからなかったのでそれらは使いませんでした。graphql-codegen
も buggy でうまく行かないことが多かったため、いい印象はないです。今回はchange-case
を手動で入れてtypeNames: change-case#pascalCase
を指定することで回避しました。
overwrite: true schema: "http://localhost:4000/graphql" generates: type/gql.ts: plugins: - "typescript" config: skipTypename: true useTypeImports: true namingConvention: typeNames: "change-case#pascalCase"
vite
とesm
vite
はwebpack
と違ってcommonjs
とesm
(node と browser)の export の区別に対して厳しいため、firebase
などの一部esm
にサポートしていないようです。そのため package の export が怪しいモジュールに対しては特殊の処理が必要です。例えば以下のようにvite.config.ts
のoptimizeDeps
に入れる必要があります。
ts
export default defineConfig({plugins: [vue()],optimizeDeps: {include: ["firebase/app", "firebase/auth", "firebase/storage", "uuid"],},resolve: {alias: {"@": path.resolve(__dirname, "."),},},})
state 管理
今回 state 管理が必要なのは event の作成と更新ページだけなので、vuex
などの state 管理ライブラリーを使わずにcomposition api
で全部やりました。
画像の最適化
当初から Pull Request のテストとしてpreactjs/compressed-size-action
を入れていたので、予期せぬサイズ変更をすぐに検知できました。普通ならカメラで撮った画像そのままサイトに載せることはないですが、誤って commit した画像に対してわかりやすく warning を出すことができました。もちろん SSG にする時は画像の最適化を組み込むのが一番ですが、raw 画像はそもそも commit するべきではないため検知する意味はあったと思います。
ちゃんとリサイズと圧縮すれば、14MB
-> 155KB
まで変わるので、無視できないプロセスだと再認識しました。