差點就要開天窗了...,今天整理之前的介紹,實際用 Laravel 和 Nuxt 做了最雛形的網站包含了「註冊」、「登入」和「登出」功能,有興趣的鐵人大大可以到 GitLab 看完整的 code ! 今天主要是 Nuxt 部分的介紹。
一開始我們先準備好下列的 page components:
| page component | 預期功能 | 
|---|
index.vue | 除了做為系統官方首頁,同時對於登入者而言,也用來導向正確的角色 (管理者/一般用戶) 系統首頁register.vue | 註冊一般用戶頁面,如果目前已經是登入狀態則導向首頁login.vue | 登入頁面,如果目前已經是登入狀態則導向首頁
在 register.vue 和 login.vue 當中,除了昨天介紹過的表單寫法之外,主要的重點在 asyncData 的部份,當頁面載入的時候要先檢查是否已經存有 token (即登入的狀態),若已經登入就要跳轉至首頁:
    async asyncData({ store, redirect }) {
        const hasToken = store.getters['hasToken'];
        if (hasToken) {
            redirect('/');
        }
        // register 和 login 註冊的表單
    },
在 login.vue 的 asyncData 裡面,用來判斷受否已經登入以及登入者的角色並用來跳轉頁面:
export default {
  name: 'index',
  async asyncData({ app, store, redirect }) {
    const hasToken = store.getters.userToken;
    if (hasToken) {
      const isManager = store.getters['user/isManager'];
      const isUser = store.getters['user/isUser'];
      if (isManager) {
        redirect('/manager');
      }
      if (isUser) {
        redirect('/user');
      }
    }
    return {};
  }
}
剛剛上面有用到 Vuex store module 的資料,所以建立下面的 store module:
| module | 資料類型 | 
|---|
index.js | 存放需要記錄在 cookie 的資料user.js | 存放登入者的使用資訊
為了方便控管,同時也是藉由 nuxtServerInit 方法儲存 cookie 相關資料。在今天的例子只有 userToken 一個資料:
const state = () => {
    return {
        userToken: undefined,
    };
};
const actions = {
    nuxtServerInit({ commit }, { req }) {
        const hasCookieInReq = !!req.headers.cookie;
        if (hasCookieInReq) {
            try {
                const allCookies = cookieparser.parse(req.headers.cookie);
                const token = allCookies['auth-token'];
                commit('SET_USER_TOKEN', token);
            } catch (error) {
                // ...
            }
        }
    },
    // ...
在 user.js 的 getters 中,除了取得儲存的 user 同時也特別增加了 hasUser,這是用來確認目前的 store 裡面有沒有資料,假若頁面可以取得 token 但卻沒有 user 資料,代表 Vuex 的資料揮發了,需要透過 API 重新取得資料。
const state = () => {
    return {
        user: {
            name: undefined,
            email: undefined,
            role: undefined,
        },
    };
};
// ... 
const getters = {
    hasUser(state) {
        return !!state.user.role;
    },
    // ...
};
由於任一個 page component 都可能受到 Vuex 資料揮發的影響,因此我們在 middleware 底下加入 auth.js,並且將它掛在 nuxt.config.js 的 router 屬性下,這樣載入任何一頁都會先檢查是否存在 「有 userToken 但是沒有 user」 的情況:
import { validators } from '@/configs/api/user';
export default async ({ app, store }) => {
    const hasToken = store.getters.hasToken;
    const hasUser = store.getters['user/hasUser'];
    if (hasToken) {
        if (!hasUser) {
            try {
                const { data } = await app.$api(validators.AccessUserRequest, { userId: null });
                await store.dispatch('user/setUser', data);
            } catch (error) {
                await store.dispatch('removeUserToken');
            }
        }
    }
};
順帶一提,因為 JWT 是有時效性的,所以假若 token 已經過期,無法取得 user 資料時,我們會將 userToken 清除,讓使用者可以再次登入。
至目前為止「註冊」和「登入」功能完成了,但「路由依角色跳轉」功能只完成一半 (首頁的部份),我們很常會直接打上特定網址瀏覽頁面,所以另一半是要撰寫在 pages/manager 底下的頁只有管理者可以瀏覽,pages/user則相反,所以我們增加下列資檔案:
| neast page component | 功能 | 
|---|
manager.vue | 將所有 pages/manager 底下的 page components 共同需要的設定即畫面設定於此user.vue | 將所有 pages/user 底下的 page components 共同需要的設定即畫面設定於此
目前上述兩者只需要設定 middleware 即可。
| middleware | 功能 | 
|---|
managerCheck.js | 檢查 Vuex store user module 裡面儲存的角色是否為管理者userCheck.js | 檢查 Vuex store user module 裡面儲存的角色是否為一般使用者
在上述兩個 middlewares 中,我們都只檢驗個別角色而已,同時只要有不符合的情況一律轉到 pages/index.vue,這樣假設未來系統角色變多了,實際判斷要跳轉的角色首頁都在 pages/index.vue 做就可以了。
export default async ({ store, redirect }) => {
    const hasToken = store.getters.hasToken;
    const isUser = store.getters['user/isUser'];
    if (!(hasToken && isUser)) {
        redirect('/');
    }
};
最後我們來補上「登出」功能就可以收工啦! 因為不論是管理者或是一般使用者都要有登出功能,在 pages/manager.vue 和 pages/user.vue 各寫一次也可以,但同樣的假設未來角色變多就很麻煩,所以我們可以把「登出」寫在 layouts,這樣在 neast page components 要修改的幅度就少很多了!
下面是 layout/baseLayout.vue
<template>
    <Row class="base-layout">
        <Row class="head">
            <!-- ... -->
            <button class="logout-btn" @click="logoutAction">登出</button>
        </Row>
        <Row class="body">
            <nuxt />
        </Row>
    </Row>
</template>
<script>
import { mapActions } from 'vuex';
import { validators } from '@/configs/api/user';
export default {
    name: 'BaseLayout',
    data() {
        return {
            storeUser: undefined,
        };
    },
    methods: {
        ...mapActions(['removeUserToken']),
        async logoutAction() {
            try {
                const { status, code } = await this.$api(validators.LogoutRequest, {});
                const successLogout = status && code === 200;
                if (successLogout) {
                    this.removeUserToken();
                    this.redirectToIndex();
                }
            } catch (error) {
            }
        },
        redirectToIndex() {
            this.$router.push({ name: 'index' });
        }
    },
    async asyncData({ store }) {
        const storeUser = store.getters['user/storeUser'];
        return {
            storeUser,
        }
    }
}
</script>
pages/manager.vue 和 pages/user.vue 各加入 layouts 的設定:
    // ...
    layout: 'baseLayout',
}
以上就是「註冊」、「登入」和「登出」搭配多角色的權限控管以跳轉範例,大家可以玩玩看。 今天的範例畫面都醜到炸,明天介紹 SCSS 的基本應用跟小弟個人的小經驗,至少有一些基礎之後不會被說工程師都不懂美感 XD