💳

Nuxt 3 RC/beta でオンラインコワーキングの寄付募集サイトを構築した話

2022/04/29に公開

2022年4月21日(日本時間) Nuxt 3 の rc1 が公開されました。

Nuxt 3 は Vue.js 3 に対応しただけでなく Nitro(ナイトロ)により Serverless 環境で簡単にサイトを公開できたり、Vite や ES Modules, TypeScript 等を活用した開発体験を(設定無しで)得られたりします。

2021年10月のベータ公開から Release Candidate 公開まで半年かかっていますが、その間 Nitro は Nuxt から切り離されたスタンドアロンな JavaScript Server になり、また Vite によるテストスイート Vitest を標準のテストツールに採用したりと、より広範なエコシステムを構築する方向で進んできています。

僕はこれまでベータリリース当初より Nuxt 3 を試してきました。
まだまだ意図した動作が得られないことも多いのですが、それでも TypeScript や Vite を使用した開発体験や、設定無しで必要な機能が使える便利さもあり、もう新しいプロジェクトは Nuxt 3 で進めたいと強く思っています。

今回、オンラインコワーキングスペース みんコワ の寄付募集サイトを Nuxt 3 で構築しましたので、技術的な内容を含め記事を書きました。
ぜひ開発の参考にしていただければ嬉しいです。

寄付募集サイトを Nuxt 3 で

僕はコワーキングスペース茅場町 Co-Edoというリアルのコワーキングスペースの運営者ですが、コロナ禍に入り オンラインのコワーキングスペース を複数のコワーキングスペース運営者(や利用者の方)と共同で運営しています。

https://note.com/coworking/n/na6160c6712fa

2周年を迎えるタイミングで、みんコワの寄付募集サイトを作ることになりました。
当初はノーコードで作ろうかなどと考えましたが、ボランティアによる運営でリソースが限られるなか、自分ひとりで作ることになったため利用技術や仕様をリソースにあわせて検討した結果 Nuxt 3 で構築することにします(ベータでしたが、経験の浅いノーコードよりは工数も読めたのです)

Co-Edoの運営やシステム開発事業もあり、自分自身のとれる時間も限られますので、空き時間の範囲内で最低限「寄付したいひとが寄付できること」を目指し構築しました(想定工数1人月程度)

https://support.mincowa.com/

公開スケジュール

2022-01-21 Nuxt 3 beta で開発開始(最初のコミット)
2022-02-23 決済テスト用サイト公開
2022-04-13 本リリース (みんコワ2周年パーティにて)
2022-04-26 Nuxt 3 RC1 対応完了

利用技術等

Authプロパイダ と データベース等

慣れているので Firebase Authentication と Firestore を使用します。
新しくなった v9 SDK を使用し、本番環境のビルドサイズを小さくすることで、より Nitro の良さを活かせそうです。

決済関連

決済サービスは API や Webhook が使いやすく、利用者側のダッシュボードもあって開発工数も少なくてすむ Stripe を使用しています。
Firebase 環境との連携は公式の Firebase Extension を使用、Nuxt からも Firestore Stripe Payments Web SDK でアクセスします。

今回は公式の拡張機能で提供されている機能の範囲内で収める想定で選択しています。
Connect 等使う必要がありそうな仕様はばっさりと落としました。

サーバー

当初は Firebase Hosting でと考えていましたが、現状の Nitro が標準のプリセットでは us-central1 にデプロイし、また Firebase Functions と干渉するため Nitro ともっとも相性の良い Netlify を使用しています。

Nuxt 3 は Netlify で特別な設定なしに SSR することができます。

Nitro は unjs/h3 というミニマルな http server を使用していて CDN 環境の function でも軽快に動作します。

デザイン

まだ Vuetify 3 が正式公開されていないので Tailwind CSS を利用して、簡易的なデザインにしています。
実際は Vite と相性の良い Windi CSS で開発し、その後 UnoCSS に切り替えています。

どちらも Vite により Variant Groups などの簡潔な記述が可能です
例: <div class="hover:(bg-gray-400 font-medium) bg-white font-light"/>

サイトの特徴・機能

どんなサイトにするか検討する際は GitHub Sponsors を参考にしました。

誰でも無理のない範囲で気軽に寄付ができる

みんコワは有志が自発的に運営しています。
寄付も同じく「寄付したい」と思ったひとが、強制されることなく、寄付をするプレッシャーを感じることもなく、気持ちにあった金額を支援できる環境を用意したいと準備チームで決めました。

  • 自分の決めた金額を寄付できる
  • 目安となる寄附金額が提示されていて、そのうえで金額を変更できる
  • 毎月の継続寄付を基本とし、1回のみの寄付も選択できる
  • 継続寄付はいつでも自由に停止・再開・金額変更ができる
  • 支援者は決済手数料を含めた金額を寄付する(500円の寄付は519円を決済する)
    • 決済手数料がいくらか即時でわかる

そのほか実装した機能

自分たちでは持たないもの・しないこと

  • カード情報等(すべて決済事業者側で管理)
  • サーバーのメンテナンス(いわゆる Serverless な環境で運用)
  • 領収書の発行等(Stripeのダッシュボードを利用してもらう)

Nuxt 3 で開発するポイント

Nuxt は Vue.js を基盤に、ルーティングやレイアウトの機能などを利用し、フルフルのWebサイトを構築するフレームワークです。

Nuxt 2 と Nuxt 3 では Vue.js のバージョンの違いにとどまらず、より柔軟なルーティングが可能になったりいくつかの機能追加・仕様変更が行われています。

機能追加やそれに伴う仕様変更等

設定なしで TypeScript による開発が可能

ゼロコンフィグを謳う Nuxt 3 は、初期状態で ES Modules および TypeScript を使用した開発が可能です。
設定ファイルは nuxt.config.ts となりました。
初期状態のままでも大抵のことはできます。

nuxt.config.ts
import { defineNuxtConfig } from 'nuxt'

export default defineNuxtConfig({
})

TypeScript の設定ファイル tsconfig.json も次のとおりで Nuxt が用意してくれています。
初期状態のままで充分です。

tsconfig.json
{
  "extends": "./.nuxt/tsconfig.json"
}

「TypeScript を使わず JavaScript でも開発が可能か?」という質問がたまにありますが、もちろん可能です。
ファイルの拡張子も変更する必要はありません。

.vue ファイルも TypeScript で書くことが可能です。

pages/index.vue
<script setup lang="ts">
const name = ref<string | number>('tanaka')
</script>

<template>
  <div>
    わたしは {{ name }} です
  </div>
</template>

<script setup> は Vue.js 3.2 で導入された記法です
参考: Nuxt 3 における script setup の基本的な使い方と FAQ

また Vite による ES Modules サポートに伴い require() してた箇所などは import 等に書き換える必要があります

強化された Auto Import

Nuxt 3 は components, plugins, middleware のほか Composable ファンクションを入れる composables も自動的に読み込み import 不要で使用可能です。
たとえば次のように簡潔に使えます。

composables/useCounter.ts
export const useCounter = () => {
  const count = ref(0)
  return { count }
}
pages/index.vue
<script setup lang="ts">
const { count } = useCounter()
onMounted(() => count.value++)
</script>

<template>
  <div>
    {{ count }}
  </div>
</template>

必要なデータは Component 側で取得する

Nuxt には Layout Component, Page Component および通常の Component があります。
Nuxt 2 ではページコンポーネントで asyncData() 等を使って、非同期データの取得とそれに伴うレンダリングの制御をしていました。

Nuxt 3 ではより汎用的な useFetch() が使えるようになり、通常のコンポーネントで使用した場合であっても、その上位のコンポーネントは(Promiseが解決されるまで)描画を停止します。

みんコワ寄付サイトではマイページで過去の決済を <SubscriptionsList/> として一覧表示していますが、データの取得は pages/mypage.vue ではなく components/SubscriptionsList.vue 内で行っています。

components/SubscriptionsList.vue
<script setup lang="ts">
// サブスクリプションの一覧を取得する Composable Function
const { customerSubscriptions } = useStripeCustomer()
</script>

<template>
  <div class="mt-6">
    <div class="py-4">
      <SubscriptionsListItem
        v-for="{ id } in customerSubscriptions"
        :key="id"
        :sid="id"
      />
    </div>
  </div>
</template>

/server/api でサーバー側で動作する機能を Server Middleware として実装する

たとえば Stripe の API を利用するためには API キー が必要です。
非公開な情報ですのでクライアント側で使用し、ブラウザから見れてしまってはいきません。
そんなときに使用するのが Server Middleware ですが Nuxt 3 では /server/api フォルダ以下を自動的に /api/~~ にルーティングします。

Component 内で await useFetch('/api/cancel') とすれば /server/api/cancel.ts が使用され、そのなかで(外部APIなどから)取得したデータを返せば API キー等を知られることなく利用できます。
Component(ブラウザ) ↔ API endpoint: /api/*(サーバー) ↔ 何らかの外部API等(外部サイト)
という流れです。

API キーなどを .env で渡す場合は useRuntimeConfig() が便利です。

nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    stripeApiKey: '',
    public: {
      fooBar: '',
    },
  },
})
.env
NUXT_STRIPE_API_KEY = rk_test_******
NUXT_PUBLIC_FOO_BAR = baz
types/index.d.ts
declare module '@nuxt/schema' {
  interface RuntimeConfig {
    stripeApiKey: string
    // PublicRuntimeConfig
    public: {
      fooBar: string
    }
  }
}
server/api/foo.ts
import Stripe from 'stripe'

export default defineEventHandler((event) => {
  const config = useRuntimeConfig()
  const stripe = new Stripe(config.stripeApiKey, { apiVersion: '2020-08-27' })
  // 以下略
})

runtimeConfig.public 以外はクライアント側には渡りません。
SSR によるサーバー側でのレンダリング時など、サーバー側で動作する場合のみ呼び出し可能です。

RCリリース直前のベータにて仕様変更があり privateRuntimeConfig は無くなりました。
また publicRuntimeConfigruntimeConfig.public に変更されています。
ドキュメントには型を自動的に生成する記述がありますが、現状まだできませんでした。上記 types/index.d.ts の記述も変更(不要)になるかもしれません。

セキュリティ上の注意

何も対処をしないと /api/~~~ への直接アクセスが可能です。
提供している機能によっては、悪意をもったユーザーが利用することができないように対処する必要があります。
ここでは詳解しませんが、呼び出し側で location.origin 等をヘッダーとして付与し、それが一致するかをチェックするなどが考えられます。

// Stripe API を使用し寄附金額を変更する
const method = 'POST'
const headers = { 'X-From': location.origin }
const body: ApiUpdateDonationParams = {
  subscriptionID,
  quantity,
}
await $fetch('/api/update-donation', { method, headers, body })
server/api/update-donation.post.ts
export default defineEventHandler(async (event) => {
  assertMethod(event, 'POST', true) // .post.ts は POST しか受け取らないため基本的に不要だけど念のため記述してます
  const headers = event.req.headers
  if (headers['x-from'] !== headers.origin) {
    return createError({
      statusCode: 405,
      statusMessage: 'Not Allowed',
    })
  }
  const body = await useBody(event)
  // 以下略
})

より柔軟なルーティングとレイアウト

従来は Layout Component > Page Component > Component(0~複数) という階層構造でしたが app.vue が加わりました。

app.vue > Layout Component > Page Component > Component という関係になります。
app.vue に「すべてのレイアウトに共通の記述」や「初回の(リクエスト時の)レンダリングの際にのみ実行されるような記述」が可能です。

ルーティングの記述方法が _id 方式から [id] 方式に変更になりました。
これによって user-[id].vue のようなルーティングもできるようになっています。

みんコワ寄付サイトでは pages/donate-monthly-[amount].vue というファイル名で https://support.mincowa.com/donate-monthly-1000 のようなURLにしています。

もともと pages/donate-[type]-[amount].vue のようにし typeamount のふたつを取得できるようにしていましたが変更しました

pages/donate-monthly-[amount].vue
<script setup lang="ts">
const route = useRoute()
const { amount: _amount } = route.params
const amount = parseInt(`${_amount}`)
if (!amount) {
  navigateTo('/')
}
</script>

Nuxt 3 では外部リンクか内部リンクかに関わらずすべてのリンクは組み込みの <NuxtLink> Component を使用します。
また useRouter().push() の代わりに navigateTo() でサイト内遷移を行います。

本リリースに向けて、今後さらに期待

Nuxt 3 は Vite や unjs の各種ツール等のエコシステムのうえに成り立っています。
複数のソフトウェアが絡むことで、何か予期しない動作が起きたときに、今は慣れるまで原因究明に時間を要します。
また Nuxt Content や Nuxt Image などの公式 Modules も多くはこれから対応です。

まだまだ ES Modules をサポートしていない外部ライブラリも存在し nuxt.config.tsbuild.transpile に記述するなどのいくつかの対処を行う必要もあるかもしれません。
https://v3.nuxtjs.org/guide/going-further/esm#troubleshooting-esm-issues

また Nitro も発展途上であり Hybrid Rendering というページやコンポーネントごとに SSG や SWR 等を設定可能になるのは現時点では達成されていません。
https://github.com/nuxt/framework/discussions/560

ですが、ぜひ Nuxt 3 を体験してほしいと思っています。
これから対応されていくものを含め、間違いなく今後の開発に無くてはならない存在になっていくはずで、僕自身はぜひ早いうちから Nuxt 3 の開発経験を増やしておきたいと思いました。

この開発体験を知ると、なかなか以前の環境には戻れません。
1日も早く本リリースされることを願って、できることをしていきたいと思います。

https://support.mincowa.com/

Discussion