LoginSignup
25
18

More than 1 year has passed since last update.

Rails APIモード + devise_token_auth + Vue.js 3 で認証機能付きのSPAを作る(Vue.js編 その1)

Last updated at Posted at 2021-01-24

はじめに

本記事はAPIをRailsのAPIモードで開発し、フロント側をVue.js 3で開発して、認証基盤にdevise_token_authを用いてトークンベースの認証機能付きのSPAを作るチュートリアルのVue.js編の記事(その1)になります。

Rails側のチュートリアルを終わらせてからこちらのチュートリアルに取り組まれることを推奨します。

前回: Rails編

次回: Vue.js編(その2)

環境

Vue.js 3.0.5
Vue CLI 4.5.9
npm 6.14.8
node 14.15.0
TypeScript 3.9.7

Vue.jsのボイラープレートを作る

インストール

まずはvue/cliをインストールします。
yarnでもOKですが、今回はnpmでインストールを行います。

既にvue/cliをインストールされている方は、

$ vue --version

でVue CLIのバージョンを確認し、4.5.0よりバージョンが低い場合は、4.5.0以上にアップグレードを行うようにお願いします。
この時、既に作成されているVueアプリの開発に影響が出ることがあるので、
作業ディレクトリ内にのみ最新版のVue CLI を入れるようにしましょう。

初めてVue CLIをインストールする場合

$ npm install -g @vue/cli

4.5.0よりバージョンが低いVue CLIをインストールしたことがある方

$ mkdir 任意のディレクトリ
$ cd 任意のディレクトリ
任意のディレクトリ $ npm install @vue/cli

vue createの実行

無事Vue CLIのインストールに成功したら、vue createコマンドを実行してアプリを作成します。
プロジェクト名はなんでもいいですが、今回はspa-frontとします。

$ vue create spa-front

初期設定は以下のようにしました。(LintやCSS、パッケージ管理ツールの設定はお好みで)

? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, TS, Router, CSS Pre-processors, Linter
? Choose a version of Vue.js that you want to start the project with 3.x (Preview)
? Use class-style component syntax? No
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with dart-sass)
? Pick a linter / formatter config: Standard
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No
? Pick the package manager to use when installing dependencies: NPM

作成終了したら、以下のコマンドを実行しましょう。

$ cd spa-front

$ npm run serve

 DONE  Compiled successfully in 1831ms                                                                                         12:34:08


  App running at:
  - Local:   http://localhost:8080/
  - Network: http://192.168.11.9:8080/

  Note that the development build is not optimized.
  To create a production build, run npm run build.

Issues checking in progress...
No issues found.

http://localhost:8080にアクセスして、
以下のように表示されればボイラープレートの作成は成功です。

init.png

axiosを用いてAPIを叩く準備

今回はPromiseベースのHTTPクライアントであるaxiosを用いてRailsで作ったAPIを叩きます。
https://github.com/axios/axios

インストールおよびコード作成

$ npm i axios

API関連のコードはsrc/apiにまとめます。

コンポーネント内に直接APIを叩くコードを書くと、コンポーネントの責務が増えてしまうため、
別ディレクトリにまとめるようにします。

$ mkdir src/api

$ touch src/api/client.ts

client.tsには以下のコードを記述してください。

import axios from 'axios'

export default axios.create({
  baseURL: process.env.VUE_APP_API_BASE
})

process.env.VUE_APP_API_BASEは、環境変数からデータを取得する記述方法です。
今回の場合、 http://localhost:3000 がベースのURLになります。

今回は.envファイルで環境変数を管理します。
参考: https://qiita.com/go6887/items/2e254d31b5a4af42f813

以下のファイルをルートディレクトリに作成します。

$ touch .env.development

そして、.env.developmentのファイルに以下を記述します。

VUE_APP_API_BASE=http://localhost:3000

これで、「process.env.VUE_APP_API_BASE」が開発環境だと http://localhost:3000 を返すようになります。

試しにsrc/views/Home.vueでconsole.logを使って、process.env.VUE_APP_API_BASEの値を確認してみましょう。

<script lang="ts">
import { defineComponent } from 'vue'
import HelloWorld from '@/components/HelloWorld.vue' // @ is an alias to /src

export default defineComponent({
  name: 'Home',
  components: {
    HelloWorld
  }
})
console.log(process.env.VUE_APP_API_BASE)
</script>

一度control + c でサーバーを停止させて、再度npm run serve します。(.env系のファイルはサーバー起動時に読み込まれるため)

chromeの検証ツールを開いて、consoleのタブを開くと、添付画像のようにURLが表示されているかと思います。

スクリーンショット 2021-01-24 18.59.21.png

問題なく.env.developmentに設定した環境変数を取得できることが確認できました。

これで、他のファイルで以下のようにES6のimport構文で読み込むと、

import Client from '@/api/client'

読み込んだファイル内で以下のようにAPIをコールできます。

Client.post('/auth_sign_in', {...})

今回はaxiosをラップするように書きましたが、仮にaxiosではなく別の(kyとかjavascript標準のfetch APIとか)APIクライアントを使うことになっても、ラップしておけば修正範囲を最小限に留めることができます。

ログイン画面を作成する

簡単なログイン画面を構築してみます。

Login.vueの作成

src/views/Login.vueを作成します。

$ touch src/views/Login.vue
<template>
  <div>
    <label for="email">
      Email
    </label>
    <input v-model="email" id="Email" type="text" placeholder="Email">
  </div>
  <div>
    <label for="password">
      Password
    </label>
    <input v-model="password" id="password" type="password" placeholder="******************">
  </div>
  <button @click="handleLogin()">
    Sign In
  </button>
</template>

<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue'
import { login } from '@/api/auth'

export default defineComponent({
  name: 'Home',
  setup () {
    const formData = reactive({
      email: '',
      password: ''
    })

    return {
      ...toRefs(formData),
      handleLogin: async () => {
        await login(formData.email, formData.password)
          .then((res) => {
            if (res?.status === 200) {
              console.log(res)
            } else {
              alert('メールアドレスかパスワードが間違っています。')
            }
          })
          .catch(() => {
            alert('ログインに失敗しました。')
          })
      }
    }
  }
})
</script>

ルーティングの設定

src/router/index.tsに/loginのルーティングを定義します。

import Home from '@/views/Home.vue' // @記法に修正
import Login from '@/views/Login.vue' // 追加

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {                  // 追加
    path: '/login',  // 追加
    name: 'Login',   // 追加
    component: Login // 追加
  },                 // 追加
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

ログイン・ログアウトAPIを叩く準備

次はログイン・ログアウトのAPIを叩けるようにします。

認証tokenを管理する必要があるのですが、ここではLocalStorageにtokenを保存する実装を採用します。
LocalStorageに認証用のtokenを保存することの危険性については各所で指摘されていますが、
管理するデータの機微性に応じて、保存場所を決めるべきかと思います。

LocalStorageにTokenを保存する

まずは認証tokenをLocalStorageに保存する実装を行います。

$ mkdir src/utils

$ mkdir src/types

$ touch src/utils/auth-data.ts

$ touch src/types/auth.ts
// src/utils/auth-data.ts

import { AuthHeaders } from '@/types/auth'

export const getAuthDataFromStorage = (): AuthHeaders => {
  return {
    'access-token': localStorage.getItem('access-token'),
    'client': localStorage.getItem('client'),
    'expiry': localStorage.getItem('expiry'),
    'uid': localStorage.getItem('uid'),
    'Content-Type': 'application/json'
  }
}

export const setAuthDataFromResponse = (authData: AuthHeaders): void => {
  if (authData['access-token'] && authData['client'] && authData['uid'] && authData['expiry']) {
    localStorage.setItem('access-token', authData['access-token'])
    localStorage.setItem('client', authData['client'])
    localStorage.setItem('uid', authData['uid'])
    localStorage.setItem('expiry', authData['expiry'])
  }
}

export const removeAuthDataFromStorage = (): void => {
  localStorage.removeItem('access-token')
  localStorage.removeItem('client')
  localStorage.removeItem('uid')
  localStorage.removeItem('expiry')
}
// src/types/auth.ts

export type AuthHeaders = {
  'access-token': string | null;
  'uid': string | null;
  'client': string | null;
  'expiry': string | null;
  'Content-Type': string;
}

上記の関数は

「LocalStorageからtokenを取得してオブジェクトとして返す」
「引数のtokenをLocalStorageに格納する」
「LocalStorageからtokenを削除する」

を行っています。この各種関数をsrc/api/auth.tsで呼び出すようにします。

ログイン・ログアウトのAPIを叩く関数を実装

src/api/auth.tsファイルを作成し、以下のコードを記述します。

$ touch src/api/auth.ts
// src/api/auth.ts

import Client from '@/api/client'
import { User } from '@/types/user'
import {
  getAuthDataFromStorage,
  removeAuthDataFromStorage,
  setAuthDataFromResponse
} from '@/utils/auth-data'
import { AxiosResponse, AxiosError } from 'axios'

export const login = async (email: string, password: string) => {
  return await Client.post<User>('/auth/sign_in', { email, password })
    .then((res: AxiosResponse<User>) => {
      setAuthDataFromResponse(res.headers)
      return res
    })
    .catch((err: AxiosError) => {
      return err.response
    })
}

export const logout = async () => {
  return await Client.delete('/auth/sign_out', { headers: getAuthDataFromStorage() })
    .then(() => {
      removeAuthDataFromStorage()
    })
}

返却される値はUser型として定義したいので、以下のファイルを作成します。

$ touch src/types/user.ts
// src/types/user.ts

export type User = {
  allow_password_change: boolean;
  email: string;
  id: string;
  image: string | null;
  nickname: string;
  provider: string;
  uid: string;
}

この状態で npm run serve を実行した後に http://localhost:8080/login にアクセスし、添付画像のような表示がされていればOKです。
スクリーンショット 2021-01-24 17.29.58.png

ここまで実装できたら、画面からEmailとPasswordを入力して認証ができるかどうかをテストしてみましょう。

画面からログインの動作を確認

http://localhost:8080/login にアクセスして、検証ツールを開いて、Consoleにタブを合わせた状態でログインをしてみます。

Rails編で作成したユーザーのメールアドレス(test-user+1@example.com)とパスワード(password)を入力し、Sign In ボタンをクリックしてください。

ログインに成功すると、Consoleに添付画像のようなデータが表示されるかと思います。
スクリーンショット 2021-01-24 18.41.09.png

この値が、login関数の返り値になっています。

ApplicationタブのLocal Storageの中身を見てみましょう。
スクリーンショット 2021-01-24 18.41.55.png

setAuthDataFromResponse関数の呼び出しが正しく行われて、LocalStorageに認証に必要な情報が格納できています。

画面からログアウトの動作を確認

簡易的なログアウトボタンを実装してみます。
Home.vueを以下のように編集してください。

<template>
  <button @click="handleLogout()">Logout</button>
</template>

<script lang='ts'>
import { defineComponent } from 'vue'
import { logout } from '@/api/auth'
import router from '@/router'

export default defineComponent({
  name: 'Home',
  setup () {
    const handleLogout = () => {
      logout().then(() => {
        router.push('/about')
      })
    }
    return {
      handleLogout
    }
  }
})
</script>

logout関数をimportし、buttonのclickイベントが発生した際にlogout関数を呼び出し、
aboutの画面に遷移するようにしています。

http://localhost:8080/ にアクセスするとログアウトボタンが表示されていると思いますので、ボタンを押してください。
ApplicationタブのLocal Storageからaccess-token等が削除されれば、ログアウト成功です。
スクリーンショット 2021-01-24 18.48.13.png

おわりに

その2へ続く予定です。
次回は「投稿機能」を実装していきます。

25
18
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
18