お問い合わせ

ブログ

これまでに経験してきたプロジェクトで気になる技術の情報を紹介していきます。

Laravel mix vue No.4 - Vuex - Vuexで状態管理

okuda Okuda 3 years
Laravel mix vue No.4 - Vuex - Vuexで状態管理

こんにちは、あすかのkoheiです。

今回は前回作成したApiをUIからリクエストして、Login、Register、Logout機能を完成させます。
認証状態の保持、メッセージなどにVuexを使います。

連載記事

Vuex - Vuexで状態管理

サンプル


フォルダ構成


└─ server
   └─ resources
      └─ js
         ├─ components
         |  └─ Header.vue
         ├─ pages
         |  ├─ errors
         │  │  ├─ NotFound.veu
         │  │  └─ SystemError.vue
         │  ├─ Home.vue
         │  └─ Login.vue
+        ├─ store
+        │  ├─ index.js
+        │  ├─ auth.js
+        │  ├─ error.js
+        │  └─ message.js
         ├─ app.js
         ├─ App.vue
         ├─ bootstrap.js
+        ├─ const.js
         └─ router.js

dockerスタートとnpmモジュール追加

gitからクローンした場合は.envの作成と設定を忘れないように!

# コンテナスタート
docker-compose start

# コンテナに入る
docker-compose exec php bash

# composerをインストール(前回からの続きで行う場合はいらない)
composer install

# npmをインストール(前回からの続きで行う場合はいらない)
npm i

# encryption keyを作成(前回からの続きで行う場合はいらない)
php artisan key:generate

# 使用するモジュールを追加
npm i js-cookie

# ホットリリード開始
npm run watch

使用するnpmモジュール

js-cookie


bootstrap.jsでaxiosApi通信の設定をする

bootstrap.jsの修正

ApiでもCSRF対策をするためにAjax通信で使う「Axios」のヘッダーに「CSRFトークン」をクッキーから取り出して添付する

server\resources\js\bootstrap.jsを以下と差し替える

    // クッキーを簡単に扱えるモジュールをインポート
    import Cookies from "js-cookie";

    /*
     * lodash
     * あると便利のなのでそのままおいておく
     */
    window._ = require("lodash");

    /*
     * axios
     * Ajax通信にはこれを使う
     */
    window.axios = require("axios");

    // Ajaxリクエストであることを示すヘッダーを付与する
    window.axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";

+   // requestの設定
+   window.axios.interceptors.request.use(config => {
+       // クッキーからトークンを取り出す
+       const xsrfToken = Cookies.get("XSRF-TOKEN");
+       // ヘッダーに添付する
+       config.headers["X-XSRF-TOKEN"] = xsrfToken;
+       return config;
+   });

+   // responseの設定
+   // API通信の成功、失敗でresponseの形が変わるので、どちらとも response にレスポンスオブジェクトを代入
+   window.axios.interceptors.response.use(
+       // 成功時の処理
+       response => response,
+       // 失敗時の処理
+       error => error.response || error
+   );

constの作成

server\resources\js\const.jsを作成して使用するステータスコードを定義

    /*
     * status cord
     */

    // response OK
    export const OK = 200; 
    // data created
    export const CREATED = 201; 
    // not found
    export const NOT_FOUND = 404; 
    // 認証切れの場合のレスポンスコード
    export const UNAUTHORIZED = 419; 
    // バリデーションエラー
    export const UNPROCESSABLE_ENTITY = 422; 
    // 認証回数制限
    export const TOO_MANY_REQUESTS = 429; 
    // システムエラー
    export const INTERNAL_SERVER_ERROR = 500; 

ストアを作成

auth storeの作成

server\resources\js\store\auth.jsAUTHストアを作成

    /*
     * status cord
     */
    import { OK, CREATED, UNPROCESSABLE_ENTITY, TOO_MANY_REQUESTS } from "../util";

    /*
     * ステート(データの入れ物)
     */
    const state = {
        // ログイン済みユーザーを保持
        user: null,
        // Api通信の成功、失敗の保持
        apiStatus: null,
        // ログインのエラーメッセージの保持
        loginErrorMessages: null,
        // 登録のエラーメッセージの保持
        registerErrorMessages: null,
    }

    /*
     * ゲッター(ステートから算出される値)
     */
    const getters = {
        // ログイン済みかどうかのチェック
        check: state => !!state.user,
        // ユーザネームの取得
        username: state => (state.user ? state.user.name : "")
    }

    /*
     * ミューテーション(同期処理)
     */
    const mutations = {
        // userステートの更新
        setUser(state, user) {
            state.user = user
        },
        // Api通信の成功、失敗の更新
        setApiStatus(state, status) {
            state.apiStatus = status;
        },
        // ログインのエラーメッセージの更新
        setLoginErrorMessages(state, messages) {
            state.loginErrorMessages = messages;
        },
        // 登録のエラーメッセージの更新
        setRegisterErrorMessages(state, messages) {
            state.registerErrorMessages = messages;
        },
    }

    /*
     * アクション(非同期処理)
     */
    const actions = {
        /*
         * loginのアクション
         */
        async login(context, data) {

            // apiStatusのクリア
            context.commit("setApiStatus", null)

            // Apiリクエスト
            const response = await axios.post("/api/login", data)

            // 通信成功の場合 200
            if (response.status === OK) {

                // apiStatus を true に更新
                context.commit("setApiStatus", true);
                // user にデータを登録
                context.commit("setUser", response.data);
                // ここで終了
                return false;
            }

            // 通信失敗の場合
            // apiStatus を false に更新
            context.commit("setApiStatus", false);

            // 通信失敗のステータスが 422(バリデーションエラー)の場合
            // または、認証回数制限429;(認証回数制限)の場合
            if (response.status === UNPROCESSABLE_ENTITY || response.status === TOO_MANY_REQUESTS) {

                // loginErrorMessages にエラーメッセージを登録
                context.commit("setLoginErrorMessages", response.data.errors);
            }
            // 通信失敗のステータスがその他の場合
            else {

                // エラーストアの code にステータスコードを登録
                // 別ストアのミューテーションする場合は第三引数に { root: true } を追加
                context.commit("error/setCode", response.status, {
                    root: true
                });
            }
        },
        /*
         * registerのアクション
         */
        async register(context, data) {

            // apiStatusのクリア
            context.commit("setApiStatus", null)

            // Apiリクエスト
            const response = await axios.post("/api/register", data)

            // 通信成功の場合 201
            if (response.status === CREATED) {

                // apiStatus を true に更新
                context.commit("setApiStatus", true);
                // user にデータを登録
                context.commit("setUser", response.data);
                // ここで終了
                return false;
            }

            // 通信失敗の場合
            // apiStatus を false に更新
            context.commit("setApiStatus", false);

            // 通信失敗のステータスが 422(バリデーションエラー)の場合
            if (response.status === UNPROCESSABLE_ENTITY) {

                // registerErrorMessages にエラーメッセージを登録
                context.commit("setRegisterErrorMessages", response.data.errors);
            }
            // 通信失敗のステータスがその他の場合
            else {

                // エラーストアの code にステータスコードを登録
                // 別ストアのミューテーションする場合は第三引数に { root: true } を追加
                context.commit("error/setCode", response.status, {
                    root: true
                });
            }
        },
        /*
         * logoutのアクション
         */
        async logout(context) {

            // apiStatusのクリア
            context.commit("setApiStatus", null);

            // Apiリクエスト
            const response = await axios.post("/api/logout");

            // 通信成功の場合 200
            if (response.status === OK) {

                // apiStatus を true に更新
                context.commit("setApiStatus", true);
                // user にデータをクリア
                context.commit("setUser", null);
                // ここで終了
                return false;
            }

            // 通信失敗のステータスがその他の場合
            // apiStatus を false に更新
            context.commit("setApiStatus", false);

            // エラーストアの code にステータスコードを登録
            // 別ストアのミューテーションする場合は第三引数に { root: true } を追加
            context.commit("error/setCode", response.status, {
                root: true
            });
        },
        /*
         * カレントユーザのアクション
         */
        async currentUser(context) {

            // apiStatusのクリア
            context.commit("setApiStatus", null);

            // Apiリクエスト
            const response = await axios.get("/api/user");

            // ユーザをレスポンスから取得、なければnull
            const user = response.data || null;

            // 通信成功の場合 200
            if (response.status === OK) {

                // apiStatus を true に更新
                context.commit("setApiStatus", true);
                // user に取得したuserを登録
                context.commit("setUser", user);
                // ここで終了
                return false;
            }

            // 通信失敗の場合
            // apiStatus を false に更新
            context.commit("setApiStatus", false);
            // エラーストアの code にステータスコードを登録
            // 別ストアのミューテーションする場合は第三引数に { root: true } を追加
            context.commit("error/setCode", response.status, {
                root: true
            });
        }
    }

    /*
     * エクスポート
     */
    export default {
        // 名前をストア別に区別できるようにネームスペースを使う
        namespaced: true,
        state,
        getters,
        mutations,
        actions
    }

error storeの作成

server\resources\js\store\error.jsERRORストアを作成

    /*
     * ステート(データの入れ物)
     */
    const state = {
        // エラーコードの保持
        code: null
    };

    /*
     * ミューテーション(同期処理)
     */
    const mutations = {
        // エラーコードの更新
        setCode(state, code) {
            state.code = code;
        }
    };

    /*
     * エクスポート
     */
    export default {
        // 名前をストア別に区別できるようにネームスペースを使う
        namespaced: true,
        state,
        mutations
    };

message storeの作成

server\resources\js\store\message.jsMESSAGEストアを作成

    /*
     * ステート(データの入れ物)
     */
    const state = {
        // メッセージの保持
        content: ""
    };

    /*
     * ミューテーション(同期処理)
     */
    const mutations = {
        // メッセージの更新
        setContent(state, { content, timeout }) {
            state.content = content;

            // タイムアウトでステートをクリア
            if (typeof timeout === "undefined") {
                timeout = 3000;
            }

            setTimeout(() => (state.content = ""), timeout);
        }
    };

    /*
     * エクスポート
     */
    export default {
        // 名前をストア別に区別できるようにネームスペースを使う
        namespaced: true,
        state,
        mutations
    };

store indexの作成

server\resources\js\store\index.jsを作成

これですべてのストアをひとつにする

    import Vue from 'vue'
    import Vuex from 'vuex'

    // 各ストアのインポート
    import auth from './auth'
    import error from './error'
    import message from './message'

    Vue.use(Vuex)

    const store = new Vuex. Store({

        modules: {
            // 各ストアと登録
            auth,
            error,
            message
        }
    })

    export default store

ストアをページへ実装

app.jsの修正

server\resources\js\app.jsでストアを読み込む

    import "./bootstrap";
    import Vue from "vue";
    import App from "./App.vue";
    import router from "./router";
    // ストアをインポート
+   import store from './store'

// 非同期通信でAUTHストアのcurrentUserアクションを実行するので
// asyncメソッドにして、awaitで通信をまつ
+ const createApp = async () => {

    // AUTHストアのcurrentUserアクションでユーザの認証状態をチェック
+   await store.dispatch("auth/currentUser");

    new Vue({
        el: "#app",
        router,
        // ストアを登録
+       store,
        components: {App},
        template: "<App />"
    });
+ };

+ // createAppを実行
+ createApp();

ログインページを修正

server\resources\js\pages\Login.vueを以下のように編集する

    <template>
    ...

    <!-- login -->
    <section
      class="login"
      v-show="tab === 1"
    >
      <h2>Login</h2>

+      <!-- errors -->
+       <div v-if="loginErrors" class="errors">
+           <ul v-if="loginErrors.email">
+               <li v-for="msg in loginErrors.email" :key="msg">{{ msg }}</li>
+           </ul>
+           <ul v-if="loginErrors.password">
+               <li v-for="msg in loginErrors.password" :key="msg">{{ msg }}</li>
+           </ul>
+       </div>
+     <!--/ errors -->

      <!-- @submitで login method を呼び出し -->
      <!-- @submitイベントリスナに prevent をつけるとsubmitイベントによってページがリロードさない -->
      <form @submit.prevent="login">
        <div>Email</div>
        <div>
          <!-- v-modelでdataをバインド -->
          <input type="email" v-model="loginForm.email" />
        </div>
        <div>Password</div>
        <div>
          <input type="password" v-model="loginForm.password" />
        </div>
        <div>
          <button type="submit">login</button>
        </div>
      </form>
    </section>
    <!-- /login -->

    <!-- register -->
    <section class="register" v-show="tab === 2">
      <h2>register</h2>
+     <!-- errors -->
+     <div v-if="registerErrors" class="errors">
+         <ul v-if="registerErrors.name">
+             <li v-for="msg in registerErrors.name" :key="msg">{{ msg }}</li>
+         </ul>
+         <ul v-if="registerErrors.email">
+             <li v-for="msg in registerErrors.email" :key="msg">{{ msg }}</li>
+         </ul>
+         <ul v-if="registerErrors.password">
+             <li v-for="msg in registerErrors.password" :key="msg">{{ msg }}</li>
+         </ul>
+     </div>
+     <!--/ errors -->
      <form @submit.prevent="register">
        <div>Name</div>
        <div>
          <input type="text" v-model="registerForm.name" />
        </div>
        <div>Email</div>
        <div>
          <input type="email" v-model="registerForm.email" />
        </div>
        <div>Password</div>
        <div>
          <input type="password" v-model="registerForm.password" />
        </div>
        <div>Password confirmation</div>
        <div>
          <input type="password" v-model="registerForm.password_confirmation" />
        </div>
        <div>
          <button type="submit">register</button>
        </div>
      </form>
    </section>
    <!-- /register -->

    <!-- forgot -->
    <section class="forgot" v-show="tab === 3">
      <h2>forgot</h2>
      <form @submit.prevent="forgot">
        <div>Email</div>
        <div>
          <input type="email" v-model="forgotForm.email" />
        </div>
        <div>
          <button type="submit">send</button>
        </div>
     </form>
    </section>
    <!-- /forgot -->

    ...
    </template>

    <script>
    export default {
        data() {
            return {
                tab: 1,
                loginForm: {
                    email: "",
                    password: "",
                    remember: true
                },
                registerForm: {
                    name: "",
                    email: "",
                    password: "",
                    password_confirmation: "",
                },
                forgotForm: {
                    email: "",
                }
            };
        },
        // 算出プロパティでストアのステートを参照
+       computed: {
+           // authストアのapiStatus
+           apiStatus () {
+               return this.$store.state.auth.apiStatus
+           },
+           // authストアのloginErrorMessages
+           loginErrors () {
+               return this.$store.state.auth.loginErrorMessages
+           },
+           // authストアのregisterErrorMessages
+           registerErrors () {
+               return this.$store.state.auth.registerErrorMessages
+           },
+       },
        methods: {
            /*
            * login
            */
            async login() {
-               alert("login");
-               this.clearForm();
                // authストアのloginアクションを呼び出す
+               await this.$store.dispatch("auth/login", this.loginForm);
                // 通信成功
+               if (this.apiStatus) {
+                   // トップページに移動
+                   this.$router.push("/");
+               }
            },
            /*
             * register
             */
            async register() {
-               alert("register");
                // authストアのregisterアクションを呼び出す
+               await this.$store.dispatch(
+                   "auth/register",
+                   this.registerForm
+               );
                // 通信成功
+               if (this.apiStatus) {
+                   // メッセージストアで表示
+                   this.$store.commit("message/setContent", {
+                       content: "登録しました。",
+                       timeout: 10000
+                   });
+                   // AUTHストアのエラーメッセージをクリア
+                   this.clearError();
+                   // フォームをクリア
+                   this.clearForm();
+               }
            },
            /*
             * forgot
             */
            async forgot() {
                alert("forgot");
                this.clearForm();
            },
+           /*
+            * clear error messages
+            */
+           clearError() {
+               // AUTHストアのすべてのエラーメッセージをクリア
+               this.$store.commit("auth/setLoginErrorMessages", null);
+               this.$store.commit("auth/setRegisterErrorMessages", null);
+               this.$store.commit("auth/setForgotErrorMessages", null);
+           },

            /*
             * clearForm
             */
            clearForm() {
                // login form
                this.loginForm.email = "";
                this.loginForm.password = "";
                // register form
                this.registerForm.name = "";
                this.registerForm.email = "";
                this.registerForm.password = "";
                this.registerForm.password_confirmation = "";
                // forgot form
                this.forgot.email = "";
            }
        },
        created() {
            // clear error messages
            this.clearError();
        }
    };
    </script>

    <style scoped>
    ...
    </style>

ヘッダーコンポネントを修正

  • ログアウトの実装
  • ログイン時に名前の表示と、ボタンの切り替えを入れる

上記のためにserver\resources\js\components\Header.vueを以下のように編集する


    <template>
      <header>
          <!-- リンクを設定 -->
          <RouterLink to="/">home</RouterLink>
-         <RouterLink to="/login">login</RouterLink>
+         <RouterLink v-if="!isLogin" to="/login">login</RouterLink>
          <!-- ログインしている場合はusernameを表示 -->
+         <span v-if="isLogin">{{username}}</span>
          <!-- クリックイベントにlogoutメソッドを登録 -->
-         <span @click="logout">logout</span>
+         <span v-if="isLogin" @click="logout">logout</span>
      </header>
    </template>

    <script>
    export default {
    // 算出プロパティでストアのステートを参照
    computed: {
        // authストアのステートUserを参照
        isLogin() {
            return this.$store.getters["auth/check"];
        },
        // authストアのステートUserをusername
        username() {
            return this.$store.getters["auth/username"];
        }
    },
    methods: {
        // ログアウトメソッド
        async logout() {
            // authストアのlogoutアクションを呼び出す
            await this.$store.dispatch("auth/logout");

            // ログインに移動
-           this.$router.push("/login");
+           if (this.apiStatus) {
+               this.$router.push("/login");
+           }
        }
    };
    </script>

# グローバルメッセージを表示

グローバルなメッセージを出せるメッセージコンポネントを作成

メッセージコンポネントを作成

メッセージを表示するコンポネントserver\resources\js\components\Message.vueを作成


<template>
    <div class="message" v-show="message">{{ message }}</div>
</template>

<script>
export default {
    computed: {
        // Messageストアのcontentステートを取得
        message() {
            return this.$store.state.message.content;
        },
    }
};
</script>

Appコンポネントに組み込む

  • INTERNAL_SERVER_ERROR(500)の場合に500にSystemErrorぺージに移動
  • UNAUTHORIZED(419)の場合にトークンをリフレッシュ、ストアのuserをクリア、ログイン画面へ移動
  • NOT_FOUND(404)404へNotFound移動
  • エラーメッセージを表示するためにERRORストアのステートを監視

上記のためにserver\resources\js\App.vueを修正


    <template>
      <div>

        <!-- Headerコンポーネント -->
        <Header />

        <main>
          <div class="container">
            <!-- message -->
+           <Message />

            <!-- ルートビューコンポーネント -->
            <RouterView />
          </div>
        </main>

      </div>
    </template>

    <script>
    // Headerコンポーネントをインポート
    import Header from "./components/Header.vue";
+   import Message from "./components/Message.vue";
    // ステータスコードをインポート
+   import { NOT_FOUND, UNAUTHORIZED, INTERNAL_SERVER_ERROR } from "./const";

    export default {
      // 使用するコンポーネントを宣言
      components: {
-       Header
+       Header,
+       Message
      }, 
+     computed: {
        // ERRORストアのcodeステートを取得
+       errorCode() {
+           return this.$store.state.error.code;
+       }
      },
+     watch: {
+       // errorCodeを監視
+       errorCode: {
+           async handler(val) {
+               // 500
+               if (val === INTERNAL_SERVER_ERROR) {
+                   // 500に移動
+                   this.$router.push("/500");
+               }
+               // 419
+               else if (val === UNAUTHORIZED) {
+                   // トークンをリフレッシュ
+                   await axios.get("/api/refresh-token");
+                   // ストアのuserをクリア
+                   this.$store.commit("auth/setUser", null);
+                   // ログイン画面へ移動
+                   this.$router.push("/login");
+               }
+               // 404
+               else if (val === NOT_FOUND) {
+                   // 404へ移動
+                   this.$router.push("/not-found");
+               }
+           },
+           // createdでも呼び出すときはこれだけでOK
+           immediate: true
+       },
+       // 同じrouteの異なるパラメータで画面遷移しても、vue-routerは画面を再描画しないのでwatchで監視
+       $route() {
+           this.$store.commit("error/setCode", null);
+       }
+     }
    };
    </script>

ルーターを修正

ログインした状態ではログインページ、登録ページに行けないようにserver\resources\js\router.jsを修正

 
  import Vue from 'vue'
  // ルーターをインポート
  import VueRouter from 'vue-router'
  // ストアをインポート
+ import store from "./store";

  // ページをインポート
  import Home from './pages/Home.vue'
  import Login from './pages/Login.vue'
  import SystemError from './pages/errors/SystemError.vue'
  import NotFound from './pages/errors/NotFound.vue'

  // VueRouterをVueで使う
  // これによって<RouterView />コンポーネントなどを使うことができる
  Vue.use(VueRouter)

  // パスとページの設定
  const routes = [
      // home
      {
          // urlのパス
          path: '/',
          // インポートしたページ
          component: Home
      },
      // login
      {
          // urlのパス
          path: '/login',
          // インポートしたページ
          component: Login,
          // ページコンポーネントが切り替わる直前に呼び出される関数
          // to はアクセスされようとしているルートのルートオブジェクト
          // from はアクセス元のルート
          // next はページの移動先
+         beforeEnter(to, from, next) {
              // AUTHストアでログインしているかチェック
+             if (store.getters["auth/check"]) {
                  // してる場合はホームへ
+                 next("/");
+             } else {
                  // してない場合はそのまま
+                 next();
+             }
+         }
      },
      // システムエラー
      {
          path: "/500",
          component: SystemError
      },
      // not found
      {
          // 定義されたルート以外のパスでのアクセスは <NotFound> が表示
          path: "*",
          component: NotFound
      }
  ]

...

これで、Register、Logiin、Logoutできるようになりました。

次は今回作ったものをメール認証に対応するように変更します。

Laravel mix vue No.5 - Api Email Verification - メール認証に変更

Laravel mix vue No.4 - Vuex - Vuexで状態管理 2021-08-18 05:52:39

コメントはありません。

4979

お気軽に
お問い合わせください。

お問い合わせ
gomibako@aska-ltd.jp