いまさらNuxt3(その2)
はじめに
1回目に続いて、Nuxt3 の入門記事です。
作ってみよう
app.vue と layouts と pages と
ま、以下を参照してください。
で、app.vue。ま、main というかエントリポイントになってます。
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
これで、layouts を使うよ。とか、ルーティングで pages から刺してね。みたいな
layouts を使うと、(付け替えもできるけど)全部に被さるので、Vuetify のお約束の v-app に入れといてね❤を強制できます。今回は、Vuetify 感を出すために、v-app-bar を少しモリましたが、一か所で共通レイアウトとかコンポーネントとか実現できていい感じです。
<template>
<v-app>
<v-app-bar>
<template #prepend>
<v-app-bar-nav-icon />
</template>
<v-app-bar-title>Title</v-app-bar-title>
<template #append>
<v-btn icon="mdi-dots-vertical" />
</template>
</v-app-bar>
<v-main>
<slot />
</v-main>
</v-app>
</template>
そして、pages も折角なんで、Vuetify の Grid と Style を使ったものにしました。
<template>
<v-container>
<v-row>
<v-spacer />
<v-col
align="center"
class="text-h1"
>
Top Page
</v-col>
<v-spacer />
</v-row>
</v-container>
</template>
で、pnpm dev して、動かして、ブラウザ上の Nuxt DevTools で見ると構造が見えて、頭の中も整理できるかもしれません。
お題
さて、お題です。RESTful な WebAPI のバックエンドに対するフロントエンドを作ろうというものです。このブログでは、かつて RESTful な API を作ろうシリーズをやってまして、そのお題と同じものをバックエンドとして想定したいと思います。
まず、Nuxt の中を流れるデータ型を定義します。Nuxt3 では、オートインポートの対象にならないのですが、shared/types に作ります。
export interface Customer {
customerId: number
name?: string
address?: string
}
export interface OrderHeader {
orderId: number
customerId?: number
orderDate?: string
orderDetail?: OrderDetail[]
}
export interface OrderDetail {
rowNum: number
productId?: number
quantity?: number
pricePerUnit?: number
}
export interface Product {
productId: number
name?: string
pricePerUnit?: number
}
で、これらの NestJS が自動生成するところの create、findAll、findOne、update、remove を作っていきます。
create の画面
新規登録画面です。Vuetify に任せて、あまりゴリゴリ書かなくても、いい感じにできます。
フォームは、Vuetify の Grid でレスポンシブ対応しています。v-text-field に number で入出力したり、右端の矢印を消したり、右寄せにしたりしましたので、必要なところをお使いください。
また、v-text-field 単位のバリデーションを設定してあり、アクティブになっていないチェックも「作成」ボタンの押下時に検査されて、検査に通ってからサーバー呼び出しをします。
<script setup lang="ts">
import type { Customer } from '~/shared/types/customer'
const customer = ref<Partial<Customer>>({})
const formRef = useTemplateRef('form')
const create = async () => {
const validResult = await formRef.value?.validate()
if (validResult?.valid) {
console.log('validなのでサーバを呼びます!')
}
}
</script>
<template>
<v-form ref="form">
<v-container>
<v-row class="pt-6">
<div class="text-subtitle-1">
顧客マスタ
</div>
</v-row>
<v-row class="py-2">
<v-col
cols="12"
sm="6"
md="4"
ls="3"
>
<v-text-field
v-model.number="customer.customerId"
name="customerId"
label="顧客ID"
hint="顧客の識別子"
type="number"
:hide-spin-buttons="true"
class="my-input-number"
:rules="[(value) => (value ? true : '必須項目です')]"
/>
</v-col>
<v-col
cols="12"
sm="6"
md="4"
ls="3"
>
<v-text-field
v-model="customer.name"
name="name"
label="氏名"
hint="顧客の氏名"
type="text"
/>
</v-col>
<v-col
cols="12"
sm="6"
md="4"
ls="3"
>
<v-text-field
v-model="customer.address"
name="address"
label="住所"
hint="顧客の住所"
type="text"
/>
</v-col>
</v-row>
<v-row class="py-2">
<v-spacer />
<v-col
cols="12"
sm="6"
md="4"
ls="3"
>
<v-btn
block
text="作成"
color="primary"
@click="create"
/>
</v-col>
</v-row>
</v-container>
</v-form>
</template>
<style scoped>
.my-input-number :deep(input) {
text-align: end
}
</style>
サーバー呼び出しをしていないので、ここまでで画面は動きます。
create のサーバー
上の画面から直接バックエンドの API を呼び出しても良いのですが、今回のお題としては、バックエンドの API は、この Nuxt のフロントエンドからだけ呼び出せる(=外部にさらさない)というものなので、Nuxt 自身でも API を作って、リレーします。
また、後出しなのですが、UI 用の URL を API 用の URL に近いものにしたいという要件もあり、そうすると、Nuxt で実装する API と同じになってしまいます。/api は、以下の用途で封じられているので、今回は、/my の下に入れておきます。
import type { Customer } from '~/shared/types/customer'
export default defineEventHandler(
async (event): Promise<Customer> => {
try {
const body = await readBody(event)
// return await $fetch<Customer>(`https://xxxxx/customer/`, {
// method: 'POST',
// body: body,
// })
return body
}
catch (e) {
throw createError(e as Error)
}
},
)
ま、張りぼてなんですが。これを呼ぶように、new.vue も書き換えて
@@ -7,7 +7,10 @@ const formRef = useTemplateRef('form')
const create = async () => {
const validResult = await formRef.value?.validate()
if (validResult?.valid) {
- console.log('validなのでサーバを呼びます!')
+ customer.value = await $fetch<Customer>('/my/customer', {
+ method: 'POST',
+ body: JSON.stringify(customer.value),
+ })
}
}
composables なるもので snackbar なるものを
サーバー呼び出しをしてみたが、これだと終わったのか終わってないのかよくわからないので、alert みたいにダサくない方法で、成功しましたしたいし、エラーの場合ももちろん通知したいし、それって、この画面だけじゃないよね。ということで、
を
を使って、コンポーネント間の状態を共有しつつ、部品化してみます。
const show = ref(false)
const message = ref('')
const color = ref('info')
export const useSnackbar = () => {
const clear = () => {
show.value = false
message.value = ''
color.value = 'info'
}
const error = (msg: string) => {
show.value = true
message.value = msg
color.value = 'error'
}
const info = (msg: string) => {
show.value = true
message.value = msg
color.value = 'info'
}
const success = (msg: string) => {
show.value = true
message.value = msg
color.value = 'success'
}
const warning = (msg: string) => {
show.value = true
message.value = msg
color.value = 'warning'
}
return {
show,
message,
color,
clear,
error,
info,
success,
warning,
}
}
ここで定義した状態を使った UI を layouts に埋め込みます。
@@ -1,3 +1,7 @@
+<script setup lang="ts">
+const { show, message, color, clear } = useSnackbar()
+</script>
+
<template>
<v-app>
<v-app-bar>
@@ -12,5 +16,19 @@
<v-main>
<slot />
</v-main>
+ <v-snackbar
+ v-model="show"
+ :color="color"
+ location="top center"
+ timeout="4000"
+ >
+ {{ message }}
+ <template #actions>
+ <v-btn
+ icon="mdi-close-circle"
+ @click="clear"
+ />
+ </template>
+ </v-snackbar>
</v-app>
</template>
こうしておけば、使う側の各画面には、以下のように組み込めます。
@@ -4,13 +4,22 @@ import type { Customer } from '~/shared/types/customer'
const customer = ref<Partial<Customer>>({})
const formRef = useTemplateRef('form')
+const { error, success } = useSnackbar()
+
const create = async () => {
const validResult = await formRef.value?.validate()
if (validResult?.valid) {
- customer.value = await $fetch<Customer>('/my/customer', {
- method: 'POST',
- body: JSON.stringify(customer.value),
- })
+ try {
+ customer.value = await $fetch<Customer>('/my/customer', {
+ method: 'POST',
+ body: JSON.stringify(customer.value),
+ })
+ success('登録しました')
+ }
+ catch (e) {
+ console.error(e)
+ error('失敗しました')
+ }
}
}
と、ここまでやると、こんな感じで、まぁまぁな感じのアプリケーションになります。
おわりに
いまさら Nuxt3?と思って始めたのですが、なかなか実践的なノウハウが公開されていなくて、1か月ぐらい試行錯誤したので、その一部を公開していきます。次回からは認証の予定です。
Discussion
当局は現在 nuxt4 を準備中です
早く出ると良いですね。周辺のサポートも含めてですが。