それではフロント側と API を連携させて、家計簿を完成させていきます!
まずは、axios
というライブラリをプロジェクトに追加します。
さまざまなプロジェクトで API にアクセスする際よく利用されています。
> yarn add axios
Vuex の中で axios を使って API にアクセスします。
この図の Actions <---> Backend API
の部分を実装します。
API クライアントをつくる
src の中に新しく api ディレクトリを作成し、
その中に gasApi.js
を作成します。
このリクエストを送れるようにします。
{
method: 'GET or POST or PUT or DELETE',
authToken: '認証情報',
params: {
// 任意の処理の引数となるデータ
}
}
import axios from 'axios'
// 共通のヘッダーを設定したaxiosのインスタンス作成
const gasApi = axios.create({
headers: { 'content-type': 'application/x-www-form-urlencoded' }
})
// response共通処理
// errorが含まれていたらrejectする
gasApi.interceptors.response.use(res => {
if (res.data.error) {
return Promise.reject(res.data.error)
}
return Promise.resolve(res)
}, err => {
return Promise.reject(err)
})
/**
* APIのURLを設定します
* @param {String} url
*/
const setUrl = url => {
gasApi.defaults.baseURL = url
}
/**
* authTokenを設定します
* @param {String} token
*/
let authToken = ''
const setAuthToken = token => {
authToken = token
}
/**
* 指定年月のデータを取得します
* @param {String} yearMonth
* @returns {Promise}
*/
const fetch = yearMonth => {
return gasApi.post('', {
method: 'GET',
authToken,
params: {
yearMonth
}
})
}
/**
* データを追加します
* @param {Object} item
* @returns {Promise}
*/
const add = item => {
return gasApi.post('', {
method: 'POST',
authToken,
params: {
item
}
})
}
/**
* 指定年月&idのデータを削除します
* @param {String} yearMonth
* @param {String} id
* @returns {Promise}
*/
const $delete = (yearMonth, id) => {
return gasApi.post('', {
method: 'DELETE',
authToken,
params: {
yearMonth,
id
}
})
}
/**
* データを更新します
* @param {String} beforeYM
* @param {Object} item
* @returns {Promise}
*/
const update = (beforeYM, item) => {
return gasApi.post('', {
method: 'PUT',
authToken,
params: {
beforeYM,
item
}
})
}
export default {
setUrl,
setAuthToken,
fetch,
add,
delete: $delete,
update
}
最初に共通の設定をしたインスタンスを作成します。あとからデフォルト設定を上書きもできます。
// 共通のヘッダーを設定したaxiosのインスタンス作成
const gasApi = axios.create({
headers: { 'content-type': 'application/x-www-form-urlencoded' }
})
// リクエスト先のURLを変更する
gasApi.defaults.baseURL = 'https://xxxxx.com'
インスタンスを作成すると get
, post
, put
, delete
などのメソッドが使えます。
このメソッドで各リクエストを送信できます。今回は API の仕様上すべて post
を使います。
gasApi.post(url, data)
また、interceptors
を利用するとリクエスト時、レスポンス時の共通処理を設定できます。
今回はレスポンスの内容に error
が含まれていた場合、reject してエラーにします。
// response共通処理
// errorが含まれていたらrejectする
gasApi.interceptors.response.use(res => {
if (res.data.error) {
return Promise.reject(res.data.error)
}
return Promise.resolve(res)
}, err => {
return Promise.reject(err)
})
API からデータを取得する
それでは、作成した API クライアントを使用して実際に通信してみます。
import Vue from 'vue'
import Vuex from 'vuex'
import gasApi from '../api/gasApi'
Vue.use(Vuex)
/**
* State
* Vuexの状態
*/
const state = {
/** 家計簿データ */
abData: {},
/** ローディング状態 */
loading: {
fetch: false,
add: false,
update: false,
delete: false
},
/** エラーメッセージ */
errorMessage: '',
/** 設定 */
settings: {
/** ~ 省略 ~ */
}
}
/**
* Mutations
* ActionsからStateを更新するときに呼ばれます
*/
const mutations = {
/** ~ 省略 ~ */
/** ローディング状態をセットします */
setLoading (state, { type, v }) {
state.loading[type] = v
},
/** エラーメッセージをセットします */
setErrorMessage (state, { message }) {
state.errorMessage = message
},
/** 設定を保存します */
saveSettings (state, { settings }) {
state.settings = { ...settings }
const { appName, apiUrl, authToken } = state.settings
document.title = appName
gasApi.setUrl(apiUrl)
gasApi.setAuthToken(authToken)
// 家計簿データを初期化
state.abData = {}
localStorage.setItem('settings', JSON.stringify(settings))
},
/** 設定を読み込みます */
loadSettings (state) {
const settings = JSON.parse(localStorage.getItem('settings'))
if (settings) {
state.settings = Object.assign(state.settings, settings)
}
const { appName, apiUrl, authToken } = state.settings
document.title = appName
gasApi.setUrl(apiUrl)
gasApi.setAuthToken(authToken)
}
}
/**
* Actions
* 画面から呼ばれ、Mutationをコミットします
*/
const actions = {
/** 指定年月の家計簿データを取得します */
async fetchAbData ({ commit }, { yearMonth }) {
const type = 'fetch'
commit('setLoading', { type, v: true })
try {
const res = await gasApi.fetch(yearMonth)
commit('setAbData', { yearMonth, list: res.data })
} catch (e) {
commit('setErrorMessage', { message: e })
commit('setAbData', { yearMonth, list: [] })
} finally {
commit('setLoading', { type, v: false })
}
},
/** ~ 省略 ~ */
}
/** ~ 省略 ~ */
import で作成したクライアントを使えるようにして、
state にローディング状態とエラーメッセージを追加します。
import gasApi from '../api/gasApi'
/** ローディング状態 */
loading: {
fetch: false,
add: false,
update: false,
delete: false
},
/** エラーメッセージ */
errorMessage: '',
saveSettings, loadSettings 内でアプリ設定の apiUrl, authToken を gasApi に反映させます。
const { appName, apiUrl, authToken } = state.settings
document.title = appName
gasApi.setUrl(apiUrl)
gasApi.setAuthToken(authToken)
Actions の中でクライアントを使ってリクエストを送信します。
/** 指定年月の家計簿データを取得します */
async fetchAbData ({ commit }, { yearMonth }) {
const type = 'fetch'
// 取得の前にローディングをtrueにする
commit('setLoading', { type, v: true })
try {
// APIにリクエスト送信
const res = await gasApi.fetch(yearMonth)
// 取得できたらabDataにセットする
commit('setAbData', { yearMonth, list: res.data })
} catch (e) {
// エラーが起きたらメッセージをセット
commit('setErrorMessage', { message: e })
// 空の配列をabDataにセット
commit('setAbData', { yearMonth, list: [] })
} finally {
// 最後に成功/失敗関係なくローディングをfalseにする
commit('setLoading', { type, v: false })
}
}
ホーム画面で fetchAdData を呼んでいた箇所も変更が必要なので、対応させます。
/** ~ 省略 ~ */
export default {
name: 'Home',
/** ~ 省略 ~ */
data () {
const today = new Date()
const year = today.getFullYear()
const month = ('0' + (today.getMonth() + 1)).slice(-2)
return {
/** 月選択メニューの状態 */
menu: false,
/** 検索文字 */
search: '',
/** 選択年月 */
yearMonth: `${year}-${month}`,
/** テーブルに表示させるデータ */
tableData: []
}
},
computed: {
...mapState({
/** 家計簿データ */
abData: state => state.abData,
/** ローディング状態 */
loading: state => state.loading.fetch,
}),
/** ~ 省略 ~ */
},
methods: {
/** ~ 省略 ~ */
/** 表示させるデータを更新します */
async updateTable () {
const yearMonth = this.yearMonth
const list = this.abData[yearMonth]
if (list) {
this.tableData = list
} else {
await this.fetchAbData({ yearMonth })
this.tableData = this.abData[yearMonth]
}
},
/** ~ 省略 ~ */
}
}
data の中で持っていた loading は消して、State の loading を使うようにします。
computed: {
...mapState({
/** 家計簿データ */
abData: state => state.abData,
/** ローディング状態 */
loading: state => state.loading.fetch,
}),
/** ~ 省略 ~ */
},
fetchAbData は Promise を返すようにしたので async/await
に直します。
async updateTable () {
/** ~ 省略 ~ */
await this.fetchAbData({ yearMonth })
/** ~ 省略 ~ */
},
このままだと通信でエラーが起きたときにメッセージが表示されないので、
App.vue にエラーメッセージを表示させるようにします。
<template>
<v-app>
<!-- ~ 省略 ~ -->
<v-main>
<!-- ~ 省略 ~ -->
</v-main>
<!-- スナックバー -->
<v-snackbar v-model="snackbar" color="error">{{ errorMessage }}</v-snackbar>
</v-app>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'App',
data () {
return {
snackbar: false
}
},
computed: mapState({
appName: state => state.settings.appName,
errorMessage: state => state.errorMessage
}),
watch: {
errorMessage () {
this.snackbar = true
}
},
/** ~ 省略 ~ */
}
</script>
スナックバーは画面下に表示される、通知のようなものです
watch
で errorMessage を監視して、変更のあったタイミングでスナックバーを表示させます。
スナックバーは一定時間経過すると自動で消えます。
watch: {
// errorMessageに変更があったら
errorMessage () {
// スナックバーを表示
this.snackbar = true
}
},
API との疎通確認をしてみます!
家計簿アプリの設定を開き、「API URL」と「Auth Token」を入力して、「保存」ボタンをクリック。
※ authToken を設定してない方は空のままでOKです。
ホーム画面に戻ってスプレッドシートのデータが表示されるか確認してみてください!
API で追加/更新できるようにする
次に、ItemDialog から API を使って追加/更新できるようにします。
さきほどと同じように Actions との内容を書き換えます。
/** ~ 省略 ~ */
const actions = {
/** 指定年月の家計簿データを取得します */
async fetchAbData ({ commit }, { yearMonth }) {
/** ~ 省略 ~ */
},
/** データを追加します */
async addAbData ({ commit }, { item }) {
const type = 'add'
commit('setLoading', { type, v: true })
try {
const res = await gasApi.add(item)
commit('addAbData', { item: res.data })
} catch (e) {
commit('setErrorMessage', { message: e })
} finally {
commit('setLoading', { type, v: false })
}
},
/** データを更新します */
async updateAbData ({ commit }, { beforeYM, item }) {
const type = 'update'
const yearMonth = item.date.slice(0, 7)
commit('setLoading', { type, v: true })
try {
const res = await gasApi.update(beforeYM, item)
if (yearMonth === beforeYM) {
commit('updateAbData', { yearMonth, item })
return
}
const id = item.id
commit('deleteAbData', { yearMonth: beforeYM, id })
commit('addAbData', { item: res.data })
} catch (e) {
commit('setErrorMessage', { message: e })
} finally {
commit('setLoading', { type, v: false })
}
},
/** ~ 省略 ~ */
}
/** ~ 省略 ~ */
ItemDialog も async/await
に対応させます。
/** ~ 省略 ~ */
import { mapActions, mapGetters, mapState } from 'vuex'
export default {
name: 'ItemDialog',
data () {
return {
/** ダイアログの表示状態 */
show: false,
/** 入力したデータが有効かどうか */
valid: false,
/** 日付選択メニューの表示状態 */
menu: false,
/** 操作タイプ 'add' or 'edit' */
actionType: 'add',
/** ~ 省略 ~ */
}
},
computed: {
/** ~ 省略 ~ */
...mapState({
/** ローディング状態 */
loading: state => state.loading.add || state.loading.update
}),
/** ~ 省略 ~ */
},
methods: {
/** ~ 省略 ~ */
/** 追加/更新がクリックされたとき */
async onClickAction () {
const item = {
date: this.date,
title: this.title,
category: this.category,
tags: this.tags.join(','),
memo: this.memo,
income: null,
outgo: null
}
item[this.inout] = this.amount || 0
if (this.actionType === 'add') {
await this.addAbData({ item })
} else {
item.id = this.id
await this.updateAbData({ beforeYM: this.beforeYM, item })
}
this.show = false
},
/** ~ 省略 ~ */
}
}
追加も編集も同じコンポーネントで行っているので、
どちらかが実行中であれば loading が true となるようにします。
...mapState({
/** ローディング状態 */
loading: state => state.loading.add || state.loading.update
}),
追加/編集がダイアログから実行できるか確認してみます!
どちらも実行できればOKです!スプレッドシートも確認してみてください。
API で削除できるようにする
最後に、DeleteDialog から API を使って削除できるようにします。
/** ~ 省略 ~ */
const actions = {
/** 指定年月の家計簿データを取得します */
async fetchAbData ({ commit }, { yearMonth }) {
/** ~ 省略 ~ */
},
/** データを追加します */
async addAbData ({ commit }, { item }) {
/** ~ 省略 ~ */
},
/** データを更新します */
async updateAbData ({ commit }, { beforeYM, item }) {
/** ~ 省略 ~ */
},
/** データを削除します */
async deleteAbData ({ commit }, { item }) {
const type = 'delete'
const yearMonth = item.date.slice(0, 7)
const id = item.id
commit('setLoading', { type, v: true })
try {
await gasApi.delete(yearMonth, id)
commit('deleteAbData', { yearMonth, id })
} catch (e) {
commit('setErrorMessage', { message: e })
} finally {
commit('setLoading', { type, v: false })
}
},
/** ~ 省略 ~ */
}
/** ~ 省略 ~ */
/** ~ 省略 ~ */
import { mapActions, mapState } from 'vuex'
export default {
name: 'DeleteDialog',
data () {
return {
/** ダイアログの表示状態 */
show: false,
/** 受け取ったデータ */
item: {}
}
},
computed: mapState({
/** ローディング状態 */
loading: state => state.loading.delete
}),
methods: {
/** ~ 省略 ~ */
/** 削除がクリックされたとき */
async onClickDelete () {
await this.deleteAbData({ item: this.item })
this.show = false
}
}
}
削除がダイアログから実行できるか確認してみます!
実行できればOKです!スプレッドシートも確認してみてください。
ハンズオンは以上になります。お疲れ様でした!🎉 🍻
ホーム画面で収支の総計を確認できるようにしたり、毎月1日に先月の収入を自動で繰り越す GAS プログラムを追加したり…。
フロントに限らず、GAS 側も自分好みにしてみてください!
ハンズオン完成時点のソースコード一覧はこちらから確認できます!