[VueJS] Vuex 2.0 關於 plugins 的事情

身為一個專業農夫,持續耕耘一些地雷也是一件天經地義的事。不過這也不算是地雷,只是因為需求問題,所以需要一些比較奇技淫巧的處理方法。

不過也沒有很奇技淫巧啦其實。


Vuex Plugins

首先,這裡需要對於 Vuex 有一定的認識,比較基本的就不贅述,請去看文件,有簡體中文可以勉強看看,不然你看英文的也可以。

我們要提的是 Plugins 這一塊,

https://vuex.vuejs.org/en/plugins.html

前情提要

我們用 Vuex 當然是希望我們在整個應用程式內,資料盡量都是統一的,但是,如果遇上了非同步傳輸的資料,例如 AJAX 跟後面拿東西(更甚是 WebSocket 拿回來的),你在你的元件內就不一定會照你的方式呈現。

以下都以 Vue 2.0 的作法,請留意。

我們先來一個 A 元件,

<template>  
  <section>
    <header><h1>Hello World</h1></header>
    <body-component></body-component>

    <aside>
      <section>
        <h2>{{ getUserData.name }}</h2>
      </section>
    </aside>
  </section>
</template>

<script>  
import { mapGetter } from 'vuex'  
import bodyComponent from './bodyComponent.vue'

export default {  
  components: {
    'body-component': bodyComponent
  },
  computed: mapGetter([
    'getUserData'
  ]),
  data () {
   return {}
  },
  created () {
  }
}
</script>  

以上會遇到一個問題,

TypeError: Cannot read property 'name' of undefined  

然後,想說好,那我先把東西設定好,總可以了吧?

<script>  
import { mapGetters, mapActions } from 'vuex'  
import bodyComponent from './bodyComponent.vue'

export default {  
  components: {
    'body-component': bodyComponent
  },
  computed: mapGetters([
    'getUserData'
  ]),
  methods: mapActions([
    'fetchUserData'
  ]),
  data () {
    this.fetchUserData()
    return {}
  },
  created () {
  }
}
</script>  

以上會遇到一個問題,

TypeError: Cannot read property 'name' of undefined  

原因是,fetchUserData() 如果是 AJAX 的話,由於樣版會先渲染出來,所以一樣會告訴你上述的錯誤。那,再設定一次總可以了吧?

<template>  
  <section>
    <header><h1>Hello World</h1></header>
    <body-component></body-component>

    <aside>
      <section>
        <h2>{{ userData.name }}</h2>
      </section>
    </aside>
  </section>
</template>

<script>  
import { mapGetters, mapActions } from 'vuex'  
import bodyComponent from './bodyComponent.vue'

export default {  
  components: {
    'body-component': bodyComponent
  },
  computed: mapGetters([
    'getUserData'
  ]),
  methods: mapActions([
    'fetchUserData'
  ]),
  data () {
    return {
      userData: {
        name: ''
      }
    }
  },
  created () {
    this.fetchUserData().then(() => {
      this.userData.name = this.getUserData.name
    })
  }
}
</script>  

這樣每次都要 fetchUserData() 不就很麻煩?那改用 watch 好了,

<script>  
import { mapGetters, mapActions } from 'vuex'  
import bodyComponent from './bodyComponent.vue'

export default {  
  components: {
    'body-component': bodyComponent
  },
  computed: mapGetters([
    'getUserData'
  ]),
  methods: mapActions([
    'fetchUserData'
  ]),
  data () {
    return {
      userData: {
        name: ''
      }
    }
  },
  created () {
    this.fetchUserData()
  },
  watch: {
    getUserData (data) {
      this.userData.name = data.name
    }
  }
}
</script>  

好啦,那這樣 A 元件好像就沒什麼問題了。接著我們來看 bodyComponent 可能會出什麼狀況,

<template>  
  <article>
    <h2>{{ title }}</h2>
    <h4>{{ getUserData.name }}</h4>
    <p v-html="content"></p>
  </article>
</template>

<script>  
import { mapGetters } from 'vuex'

export default {  
  computed: mapGetters([
    'getUserData'
  ]),
  data () {
    return {
      title: '我是標題',
      content: '我是內容'
    }
  }
}
</script>  

這個還是會有這個問題,

TypeError: Cannot read property 'name' of undefined  

可是我在 A 元件拿過了啊?

但是因為他是非同步傳輸所以不知道什麼時候會拿回來給你。

好吧那我只好再 watch 一次,

<template>  
  <article>
    <h2>{{ title }}</h2>
    <h4>{{ userData.name }}</h4>
    <p v-html="content"></p>
  </article>
</template>

<script>  
import { mapGetters } from 'vuex'

export default {  
  computed: mapGetters([
    'getUserData'
  ]),
  data () {
    return {
      title: '我是標題',
      content: '我是內容',
      userData: {
        name: ''
      }
    }
  },
  watch: {
    getUserData (data) {
      this.userData.name = data.name
    }
  }
}
</script>  

這樣好像就沒什麼狀況。但是,如果每一個元件都這樣搞,這樣不就搞死人了。而且 Vuex 既然是放資料的,既然他資料都統一存放,我要隨時提取,應該就是拿到我要的資料才對,除非你沒給。

所以,這個時候就應該是 Plugins 派上用場的時機(在 Vuex 1.0 的時候,他叫做 middleware,是後來 2.0 才改名叫做 plugins,用法類似。

操作方式與元件生命週期

元件生命週期請先看一下官方解說,

https://vuejs.org/v2/guide/instance.html

我先不提 Plugins 怎麼操作,我們看一下元件常用的地方,

export default {  
  computed: mapGetters(['getUserData']),
  data () {
    console.log(this.getUserData.name)
    return {}
  },
  beforeCreate () {
    console.log(this.getUserData.name)
  },
  created () {
    console.log(this.getUserData.name)
  },
  mounted () {
    console.log(this.getUserData.name)
  }
}

以上會印出,

TypeError: Cannot read property 'name' of undefined  
TypeError: Cannot read property 'name' of undefined  
"hinablue"
"hinablue"

好,現在你知道 Vuex 在哪些地方不會有效果了,接著來看 Plugins 怎麼寫。首先,我們需要寫一個簡易的 Plugin 來處理拿資料的部分,非同步處理的部分。

export default function fetchUserData () {  
  return store => {
    store.subscribe((mutation, state) => {
      if (mutation.type === 'router/ROUTE_CHANGED') {
        // 由於 Vue-Router 會觸發 ROUTE_CHANGED
        // 所以我們只在這個時候作一次,避免重複被觸發
        store.dispatch('fetchUserData')
      }
    })
  }
}

就這樣,把他加入你的 Vuex.store

import fetchUserData from './plugins/fetchUserData'

const store = new Vuex.Store({  
  modules: {
    ...
  },
  strict: process.env.NODE_ENV !== 'production',
  plugins: [fetchUserData()]
})

然後,我們回到 A 元件,就可以回到最初,不需要 watch 也不用額外去抓資料回來,

<template>  
  <section>
    <header><h1>Hello World</h1></header>
    <body-component></body-component>

    <aside>
      <section>
        <h2>{{ getUserData.name }}</h2>
      </section>
    </aside>
  </section>
</template>

<script>  
import { mapGetter } from 'vuex'  
import bodyComponent from './bodyComponent.vue'

export default {  
  components: {
    'body-component': bodyComponent
  },
  computed: mapGetter([
    'getUserData'
  ]),
  data () {
   return {}
  },
  created () {
  }
}
</script>  

然後 bodyComponent 就可以改回這樣,

<template>  
  <article>
    <h2>{{ title }}</h2>
    <h4>{{ getUserData.name }}</h4>
    <p v-html="content"></p>
  </article>
</template>

<script>  
import { mapGetters } from 'vuex'

export default {  
  computed: mapGetters([
    'getUserData'
  ]),
  data () {
    return {
      title: '我是標題',
      content: '我是內容'
    }
  }
}
</script>  

然後,還有一種例外,就是當你的 Plugin 拿不到資料的時候,一樣會出現這個錯誤,

TypeError: Cannot read property 'name' of undefined  

至於這個問題要怎麼解?

  1. 只能在 Plugin 中先給 預設值
  2. 在取資料失敗的時候給 預設值
  3. dispatch 或是 commit 拿到失敗的資料給 預設值
  4. 不管怎樣你就是要給 預設值

Plugin 的執行順序會比元件要早,但是並不是 最早的,剛剛上述的元件生命週期,我列出來給你們看了。雖然我在 Plugin 中先賦予值(即便我不是非同步拿的資料),在元件當中,還是得在 created() 之後才拿得到。

預設值在這裡是必須的,不然,在開發模式都會強制被中斷(正式機會不會壞?你可以試試看 XD

所以,倘若你是在 data() 當中去處理,就會拿不到東西。但是,如果你有使用 Vue-Router 的話,在 beforeRouteEnter 是拿得到的,因為他是在 router/ROUTE_CHANGED 之後才觸發。

所以,這樣是可以的,

beforeRouterEnter (to, from, next) {  
  next(vm => {
    console.log(vm.getUserData.name)
  })
},
data () {  
  console.log(this.getUserData.name)
  return {}
}

這樣是可以拿到東西的,反之 data() 是無法拿到的,

"hinablue"
TypeError: Cannot read property 'name' of undefined  

例外

如果是在 App 的最上層,以上的作法會出現例外。

由於 Vuex 在最上層並不會進入 router/ROUTE_CHANGED 這個狀態,所以上述的東西就不會被執行。這個時候必須要把某些東西,移出 store.subscribe 到外層去,但是這樣會沒有正確的 mutation, state 可以用(特別是你有跟 Router 同步的時候,這個時候會拿不到 router 的數值)。

小結

API 文件寫得好,預設值沒煩惱。

但是我好像寫得不是很好(已哭

Hina Chen

偏執與強迫症的患者,算不上是無可救藥,只是我已經遇上我的良醫了。