概要
nuxtから各種のAPIを叩いてリソースを操作する際、vuex storeや、component側の fetch
computed
辺りに、似たような処理を何度も何度も書く必要があり面倒ですよね。
この共通処理を非常にシンプルに書くため、nuxt-resource-based-apiというnpm packageを公開しました。この記事はこのライブラリの紹介記事になります。
(多分みんな同じようなことを独自でやっているんだろうなあ、と思ったのでパッケージにした。)
例
TODOアプリを例に説明します。APIサーバーは既に完成しているものとします。
以下のようにシンプルな記法で、いい感じのVuexストアを作成できます。
store/index.js
import Napi from 'nuxt-resource-based-api'
import axios from 'axios'
const axiosInstance = axios.create({
baseURL: process.env.API_URL,
})
Napi.setConfig({
axios: axiosInstance
})
store/task.js
import Napi from 'nuxt-resource-based-api'
export const { state, mutations, actions } = Napi.createStore(
'task',
['index', 'show', 'new', 'edit', 'destroy'],
)
Vuexストアだけでなく、component側のfetchやcomputedも楽に書きたい場合は、以下のように作成します。
lib/create_component.js
import Vue from 'vue'
import Napi from 'nuxt-resource-based-api'
export default function createComponent(resources) {
return Vue.extend({
fetch: Napi.generateFetch(resources),
computed: Napi.generateComputed(resources),
methods: Napi.generateMethods(resources)
})
}
pages/index.vue
<script>
import createComponent from '@/lib/create_component'
export default createComponent([
{ resource: 'task', action: 'index' },
]).extend({ // createComponentの返り値はVue Componentなので、extendを使って拡張することができます。
methods: {
foo() { return 1 }
}
})
</script>
<template>
<div>
<div class="task" v-for="task in tasks">
{{ task.name }}
</div>
</div>
</template>
非常にシンプルにコンポーネントを実装できました!上記は以下の糖衣構文になっています。
// こんな感じのハンドラを初期化時に設定できます(ここについては省略)
const createHeaders = (context) => { return {} }
const errorHandler = (e, context) => { throw e }
export default Vue.extend({
fetch(context) {
const headers = createHeaders(context)
const { store } = context
try {
await store.dispatch('task/fetchTasks', { headers }) // リクエストの送信(GET https://api.awesome-task-manager.com/tasks)を行い、レスポンスを `task/tasks` に格納するアクション
} catch (e) {
errorHandler(e, context)
}
},
computed: {
tasks() {
return this.$store.state.task.tasks
}
},
methods: {
fetchTasks(force = false) {
const headers = createHeaders(this)
try {
await store.dispatch('task/fetchTasks', { headers, force })
} catch(e) {
errorHandler(e, this)
}
},
foo() { return 1 }
}
})
上記はタスク一覧ページの実装でしたが、タスク作成ページを作りたい場合は以下のように書けます。
pages/tasks/new.vue
<script>
import createComponent from '@/lib/create_component'
export default createComponent([
{ resource: 'task', action: 'create' },
]).extend({
data() {
return {
title: '',
body: '',
}
},
methods: {
async create() {
await this.createTask({
title: this.title,
body: this.body
})
}
}
})
</script>
<template>
<!-- ... -->
</template>
何が嬉しいか
実装がシンプルで楽なのはもちろんですが、コードがシンプルで短く、後から非常に読みやすいのも大きなメリットだと思います。
また、いちいちvuexのテストを書く必要がないのも良いですね。上の例ではでていませんが、「更新画面であるタスクを更新すると、タスク一覧画面のそのタスクも自動的に更新される」のような不整合がでないアクションの実装になっています。
向いていないユースケース
ルーティングについて
ここまで完全に無視していましたが「APIはリソースベースのルーティングで統一されていなければならない」という強い制約があります。(Railsにおける resource
を使った際のルーティング)
例えば、 task
というエンティティに対するAPIは以下のようになっている必要があります。
METHOD | path | action |
---|---|---|
GET | /tasks | index |
GET | /tasks/:id | show |
GET | /tasks/new | new |
POST | /tasks | create |
GET | /tasks/:id/edit | edit |
PUT | /tasks/:id | update |
DELETE | /tasks/:id | destroy |
これ以外のルーティングに対する処理は、自分でactionを書いてね、という思想のライブラリになります。
なので、上記に合わないAPIが既にある中でNuxtの開発をする、というケースではこのライブラリは向いていません。
逆に、APIとフロントエンドを同時に開発していくような、新規開発時には真価を発揮します。(実際にこのライブラリを半年使って開発していますが、非常に効率的に開発できています)
また、このライブラリを積極的に使おうとすると、APIサーバー側のコントローラーはRESTアクション以外のメソッドを実装できないため、API側の実装にも統一感が生まれます。
これはRailsの作者であるDHHも推奨している設計です
コントローラが元々持っているRESTアクションやデフォルトの5つの機能にはないメソッドを付け加えたいと思ったら、いつだって新しいコントローラを作る。それだけでいいのです。
リソースごとの細かい制御
「様々なリソースの統一的に取り扱う」という思想のライブラリなので、リソースごとに細かくクエリを指定してリクエストしたりしたい場合は不向きです。
大規模になるともちろんこれは避けられないと思うので、小中規模のアプリ向けと言えると思います。
(とはいえ、このライブラリをベースに拡張していくことは十分可能で、かつ意義があることだと思っています)
Typescript
動的にVuex storeやcomponentを作成するため、残念ながら型のサポートが難しいです。
ジェネリクスとas
を使って、無理やり型を入れ込むことは可能にしているのですが、もっといい解決方があればぜひご教授ください…
終わりに
ということでライブラリの紹介記事でした。
この辺の処理、みなさんどうやって効率的に実装しているのか興味があるので、色々意見もらえると嬉しいです!