Vuejs Axiosで共通的な例外をあつかう

課題

Vuejs利用時に、HTTPレスポンスの例外処理を共通化したい。 各画面の各リクエストごとに、例外処理を実装するのは避けたい。

解決方法

HTTPクライアントライブラリのAxiosの、interceptorsの仕組みを利用する。

検証環境

package.json

  "dependencies": {
    "axios": "^0.17.1",
    "vue": "^2.5.2",
    "vue-router": "^3.0.1",
    "vue-toasted": "^1.1.24"
  },

モックサーバを用意する

検証用に、エラーを返すモックサーバを用意する。

$ npm install -D json-server

モック定義のファイル(mock.js)を用意する。

const jsonServer = require('json-server')
const server = jsonServer.create()
const middlewares = jsonServer.defaults()

server.use(middlewares)
server.listen(3000, () => {
  console.log('JSON Server is running')
})
server.get('/unauthorized', (req, res) => {
  res.status(401).jsonp({
    message: "unauthorized"
  })
})

server.get('/systemerror', (req, res) => {
  res.status(500).jsonp({
    message: "something wrong"
  })
})

モックサーバを起動する。

$ node mock.js
JSON Server is running

システムエラーの場合は、500を返す。

$ curl -i localhost:3000/systemerror
HTTP/1.1 500 Internal Server Error
X-Powered-By: Express
Vary: Origin, Accept-Encoding
Access-Control-Allow-Credentials: true
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
X-Content-Type-Options: nosniff
Content-Type: application/json; charset=utf-8
Content-Length: 32
ETag: W/"20-fhnUB5BwaOsQsXyG8exFr0MEGzY"
Date: Wed, 17 Jan 2018 13:16:01 GMT
Connection: keep-alive

{
  "message": "something wrong"
}

認証エラーの場合は、401を返す。

$ curl -i localhost:3000/unauthorized
HTTP/1.1 401 Unauthorized
X-Powered-By: Express
Vary: Origin, Accept-Encoding
Access-Control-Allow-Credentials: true
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
X-Content-Type-Options: nosniff
Content-Type: application/json; charset=utf-8
Content-Length: 29
ETag: W/"1d-1AQxoXvOBStoEV/A43KaFU1XEOg"
Date: Wed, 17 Jan 2018 13:16:42 GMT
Connection: keep-alive

{
  "message": "unauthorized"
}

エラーメッセージの表示方法

今回はエラーメッセージの表示に、vue-toastedというライブラリを利用する。 vue-toastedは、以下のようなトースト表示ができるライブラリ。 vue toasted demo

Axiosでinterceptorsを実装する

interceptorsによって、HTTPレスポンスの共通処理を作成していく。

import Vue from 'vue'
import Axios from 'axios'

const http = Axios.create({
  // for cors
  withCredentials: true
})
http.interceptors.response.use(function (response) {
}, function (error) {
  // 認証エラー時の処理
  if (error.response.status === 401) {
    Vue.toasted.clear()
    Vue.toasted.error(error.response.data.message)
  // システムエラー時の処理
  } else if (error.response.status === 500) {
    Vue.toasted.clear()
    Vue.toasted.error(error.response.data.message)
  }
  return Promise.reject(error)
})

export default http

Axiosをimportし、Vueのprototypeに設定することで、共通処理が設定済みのAxiosを$httpで利用できるようになる。(グローバルな空間にオブジェクトをつっこむのが嫌であれば、各画面でimportしても良い)

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
import Axios from './axios'
import Toasted from 'vue-toasted'

Vue.prototype.$http = Axios
Vue.use(Toasted)
Vue.config.productionTip = false

/* eslint-disable no-new */

new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})

APIアクセス時には、$httpを利用する。

<template>
  <div id="app">
    <button type="button" @click="callUnauthorized">unauthorized</button>
    <button type="button" @click="callSystemError">system error</button>
  </div>
</template>

<script>
export default {
  name: 'App',
  methods: {
    callUnauthorized: function () {
      this.$http.get('http://localhost:3000/unauthorized')
    },
    callSystemError: function () {
      this.$http.get('http://localhost:3000/systemerror')
    }
  }
}
</script>

実行結果

HTTPレスポンスの共通処理が実行されて、トーストが表示される。

f:id:kimulla:20191201193833g:plain

注意点

今回の例だと1コンポーネントしかないので、トーストを表示しっぱなしにしている。 もしコンポーネントが切り替わったときにトーストを消したい場合は、vue-router のナビゲーションガードやコンポーネントのdestroyedフック内で Vue.toasted.clear()を呼び出すといい。
参考: ナビゲーションガードライフサイクルフック

サンプルアプリケーション

githubに置きました。
参考: Axios Sample