Vue.jsでVuex+axiosを利用してAPIを叩く

## 概要

Vue.jsでVuex+axiosを利用してみました。
コードをシンプルにすると理解が進んで良いです^^

GitHubにコードをUPしましたので、よろしければご参考ください。
https://github.com/kai-kou/vue-js-typescript-vuex-axios


## 環境構築

Dockerを利用して、APIのモックサービスも立ち上げられるようにdrakovを利用しています。

drakovについては下記をご参考ください。

api blueprintとdrakovを利用してAPIモックサーバを立ち上げる
https://qiita.com/kai_kou/items/bdbb6c3f8d1ac655595d


```sh
> mkdir 任意のディレクトリ
> cd 任意のディレクトリ
> vi Dockerfile
> vi docker-compose.yml
```

```txt:Dockerfile
FROM node:10.8.0-stretch

RUN npm install --global @vue/cli
RUN npm install -g drakov

WORKDIR /projects
```

```yaml:docker-compose.yml
version: '3'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    volumes:
      - ".:/projects"
    tty: true
  drakov:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - "./docs:/projects"
    tty: true
    command: drakov -f "**/*.md" --public --watch --p 3000
```

API用のmdファイルを用意しておきます。

```sh
> mkdir docs
> touch docs/sample.md
```

```md:docs/sample.md
# GET /
+ Response 200 (text/plain)

        Hello World!
```

コンテナを起動して、Vue.jsのプロジェクトを作成するコンテナに入ります。

```sh
> docker-compose up -d
> docker-compose exec app bash
```

```sh:コンテナ内
> vue create app

Vue CLI v3.0.1
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, TS, Vuex, Linter, Unit
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript for auto-detected polyfills? Yes
? Pick a linter / formatter config: TSLint
? Pick additional lint features: Lint on save
? Pick a unit testing solution: Mocha
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No
? Pick the package manager to use when installing dependencies: (Use arrow keys)
❯ Use Yarn
  Use NPM
```

プロジェクトが作成されたらVue.jsサービスを起動します。

```sh:コンテナ内
> cd app
> yarn serve
```

src/store.tsに実装を追加します。
Stateに```counter``` と```message``` を用意して```counter``` をインクリメントするアクションと、APIからテキストを取得して```message``` に保存するアクションを追加しました。

```ts:src/store.ts
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';

Vue.use(Vuex);

interface State {
  counter: number;
  message: string;
}

export default new Vuex.Store({
  state: {
    counter: 0,
    message: '',
  } as State,
  getters: {
    getCounter: (state, getters) => () => {
      return state.counter;
    },
    getMessage: (state, getters) => () => {
      return state.message;
    },
  },
  mutations: {
    increment(state, payload) {
      state.counter+= 1;
    },
    setMessage(state, payload) {
      state.message = payload.message;
    },
  },
  actions: {
    incrementAction(context) {
      context.commit('increment');
    },
    async getMessageAction(context) {
      const msg = await axios.get('http://localhost:3000').then(
         (res) => res.data,
         () => ''
      ); 
      const payload = {
        message: msg,
      };
      context.commit('setMessage', payload);
    },
  },
});

```

src/main.tsはそのままです。

```ts:src/main.ts
import Vue from 'vue';
import App from './App.vue';
import store from './store';

Vue.config.productionTip = false;

new Vue({
  store,
  render: (h) => h(App),
}).$mount('#app');

```

src/App.vueでStoreが利用できるようにします。
画像をクリックと```counter``` のインクリメントとAPIアクセスがされるようにしています。

```vue:src/App.vue
<template>
  <div id="app">
    <p>{{ message }}</p>
    <img alt="Vue logo" src="./assets/logo.png" @click="increment">
    <HelloWorld :msg="`Welcome to Your Vue.js + TypeScript App ${this.counter}`"/>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import HelloWorld from './components/HelloWorld.vue';

@Component({
  components: {
    HelloWorld,
  },
})
export default class App extends Vue {
  private get counter(): number {
    return this.$store.getters.getCounter();
  }
  private get message(): string {
    return this.$store.getters.getMessage();
  }

  private increment(): void {
    this.$store.dispatch('incrementAction');
    this.$store.dispatch('getMessageAction');
  }
}
</script>
(略)
```

ブラウザで確認してみます。

```sh
> open http://localhost:8080/
```

<img width="1130" alt="スクリーンショット 2018-09-04 14.25.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/48549/2600090a-e213-3add-bad2-4d9a136862ea.png">

はい。
うまくAPIにアクセスできてますね。

ポイントとしてはsrc/store.tsでAPIアクセス時に`async` と`await` を利用するところでしょうか？利用しなくても以下のように書けますが、フラット化を意識していないと、行き着く先に、きっとコールバック地獄が待っていると思います^^

```ts:src/store.ts(抜粋)
    getMessageAction(context) {
      axios.get('http://localhost:3000').then(
        (res) => res.data,
        () => ''
      ).then(msg => {
        const payload = {
          message: msg,
        };
        context.commit('setMessage', payload);
      });
    },

```

## 追記(2020/04/06)

ネストを回避する手段について、@masmatsum さんからコメントを頂きました。

https://qiita.com/kai_kou/items/c4e449964df59d5a5fb0#comment-f0d2e268581786771291

コメントにあるサンプルコードや記事のように、ネストを回避する手段は`async` と`await` を利用する他にもありますので、ケースに合わせた実装方法をご検討ください。

- - -


GitHubの方に、vuex-type-helperというライブラリを利用してstoreをモジュール化する実装も置いてますので、ご参考ください。

vuex-type-helperについては別記事にまとめています。

VuexをTypeScriptで利用するのに悩んだ
https://qiita.com/kai_kou/items/fdd8ecd07995571da8a1


それでは、良きVuex+axiosを利用したVue.js開発ライフを^^


Vue.js+TypeScriptで開発するときの参考記事まとめ
https://qiita.com/kai_kou/items/19b494a41023d84bacc7