To-Do Calendar - Day8 Vuex 狀態管理(下)

接下來要實作對 url 進行攔截,當用戶未登入時,導回首頁並自動開啟登入 Modal 的部分。

Vuex 的運行原理(單向數據流)

  1. 在組件中用 dispatch 方法去觸發 action 事件
  2. 在 Actions 中執行同步/非同步的操作,並 commit 至 Mutations
  3. 在 Mutations 中更新 State 狀態
  4. State 再回應到組件中進行 Render
    image

Vuex 的核心概念

  1. State:儲存資料狀態的地方,如同全域的 data
  2. Actions:可以執行同步和非同步的操作(如 ajax、setTimeout..等),但不會直接更新資料狀態
  3. Getter:加工資料呈現,如同全域的 computed
  4. Mutations:實際更新資料狀態,只能執行同步的操作
  5. Modules:將 store 分割成模組
    Warning
    重點整理:
    更新 Vuex 的 store 中的資料狀態的唯一方法是提交 Mutations。
    Mutations 只能做同步的操作,如果有非同步的需求,則要使用到 Actions。
    Actions 和組件都不能直接修改 State,只有 Mutations 可以更改 State。


取得 store 中 state 資料的方法

  • 方法1:直接使用 $store 來取得 state 中的資料
    this.$store.state.isLogin;
    
  • 方法2:定義在 computed 中,並在 template 載入
    computed: {
      isLogin () {
            return this.$store.state.isLogin;
        }
    }
    
    {{ isLogin }}
    
  • 方法3:在組件載入 mapState,並在 computed 內使用 mapState 輔助函數
    import { mapState } from "vuex";
    
    computed: mapState(["isLogin","isLoginModalOpen","userId","userName"])
    
    或是
    computed: {
      ...mapState(["isLogin","isLoginModalOpen","userId","userName"])
    },
    
    mapState 其實就是 Vuex 的一個語法糖,相當於下面的程式碼,mapState 適合用在從 store 取多個 state 資料的情境,能使程式碼更簡潔(mapGetters、mapMutations、mapActions 同理,引入後就能直接調用)
    computed:{
      isLogin() {
        return this.$store.state.isLogin;
      },
     isLoginModalOpen() {
        return this.$store.state.isLoginModalOpen;
      },
     userId() {
        return this.$store.state.userId;
     },
     userName(){
        return this.$store.state.userName;
     },
      ...
    }
    
    Info
    es6 中的展開運算符…,是將 mapState 映射的計算屬性追加上去,而不是覆蓋原有的,所以可以和組件自定義的計算屬性一起使用。


更新 store 中 state 資料的方法

  • 方法1:在組件中直接修改 state (這是錯誤的做法!請避免這樣做)

    this.$store.state.isLogin = true;
    
    Warning
    從 Vue DevTools 工具的 Vuex 裡觀察,方法1並沒有更新 store 中 state 的資料。
  • 方法2:在組件中使用 store.commit(),提交 Mutations

    mutations: {
     isLoginModalOpen (state, status) {
      state.isLoginModalOpen = status;
    }
    
    openLoginModal() {
      this.$store.commit("isLoginModalOpen", true);
    }
    
  • 方法3:通過 $store.dispatch() 觸發 Actions,提交 Mutations

    actions: {
      updateLoginModalStatus(context, status) {
        context.commit("isLoginModalOpen", status);
      }
    },
    mutations: {
     isLoginModalOpen (state, status) {
      state.isLoginModalOpen = status;
     }
    
    openLoginModal() {
      this.$store.dispatch("updateLoginModalStatus", true);
    }
    

    Info
    重點整理:
    直接獲取 state 的資料,用 this.$store.state.值
    透過加工 state 得到新的資料,用 this.$store.getters.值
    非同步修改 state 的資料狀態,用 this.$store.dispatch(“action 方法”)
    同步修改 state 的資料狀態,用 this.$store.commit(“mutation 方法”)


實作前端登入驗證

因為很多地方都需要用到登入狀態和登入 Modal 狀態,所以將登入狀態(isLogin)和登入 Modal 狀態(isloginModalOpen)放進 Vuex 管理。
想法是將狀態和操作分開,把登入 Modal 的開關操作統一交給 watch 去控制,一旦 watch 偵測到 isloginModalOpen 狀態變更,就開啟登入 Modal 或關閉登入 Modal,其他組件只負責根據事件結果提交 isloginModalOpen 狀態。
直接來看程式碼吧!

src/store/index.js

  • 定義變數和配置 mutations 方法
    export default new Vuex.Store({
     state: {
       isLogin: false,
       isLoginModalOpen: false,
       ...
     },
     mutations: {
       isLogin(state, status) {
        state.isLogin = status;
      },
      isLoginModalOpen (state, status) {
        state.isLoginModalOpen = status;
      },
      ...
    });
    

src/pages/index.vue

  • 在 computed 讀取 isLoginModalOpen 的狀態,並在 watch 監聽與控制登入 Modal 操作
    computed: {
      ...mapState(["isLoginModalOpen"]),
    },
    watch: {
      isLoginModalOpen() {
        if (this.$store.state.isLoginModalOpen) {
          $("#loginModal").modal({ backdrop: "static", keyboard: false });
          console.log("是watch開的");
        } else {
          $("#loginModal").modal("hide");
          console.log("是watch關的");
        }
      }
    }
    

觸發更新登入 modal 狀態的事件

  1. 點首頁的 Start 按鈕,直接轉交給 vue-router 的導航守衛判斷要攔截或放行

    openLoginModal() {
     let self = this;
     self.$router.push("innerPage/todoLists");
     },
    
  2. 取消或按x關閉登入 Modal(modal 設定禁用空白處點擊關閉,避免有操作繞過觸發關閉事件)

     closeLoginModal() {
       let self = this;
       self.resetForm();
       self.$store.commit("isLoginModalOpen", false);
     }
    
  3. 點切換到註冊連結

     openRegisterModal() {
       let self = this;
       self.resetForm();
       self.$store.commit("isLoginModalOpen", false);
       $("#registerModal").modal({ backdrop: "static", keyboard: false });
     },
    
  4. 點登入按鈕,經過前端檢查輸入框後送登入 API,經後端驗證後返回登入結果,登入成功時 isLogin 才會更新為 true(API 待實作,這邊先模擬)

     sendUserLogin() {
       let self = this;
       let result = true;
       if (self.checkLoginInput()) {
         //send login API to server
         if (result) {
           self.$store.commit("isLogin", true);
           self.$store.commit("isLoginModalOpen", false);
           self.$store.commit("setUserId", "userId");
           self.$store.commit("setUserName", "userName");
           self.$store.commit("setToken", "token");
           setTimeout(() => {
             self.$router.push("innerPage/todoLists");
           }, 1000);
         } else {
           self.serverErrorMsg = "帳號或密碼錯誤";
         }
       }
     }
    
    Update
    只有關閉登入 Modal 和登入成功後會提交 isLoginModalOpen 為 false。
  5. 跳轉頁面時,若該頁面需要登入才能訪問,會檢查 isLogin 的值

     router.beforeEach((to, from, next) => {
       if (to.meta.requireAuth) {
         if (store.state.isLogin) {
           next();
         } else {
           alert("請重新登入");
           store.commit("isLoginModalOpen", true);
           next({
               path: "/",
           }
         }
       } else {
         next();
       }
     });
    

    Update
    只有被導航守衛攔截後會提交 isLoginModalOpen 為 true。

運行結果

image

從首頁點 Start 按鈕



image

從首頁輸入內頁網址



延伸閱讀

參考資料

comments

comments powered by Disqus