[vue]使用vuex來管理元件間的狀態

[vue]使用vuex來管理元件間的狀態

前言

這一篇想討論的範例是從vuerx那一篇同樣的例子,整個翻成vuex的範例,如果想要比較一下用rxjs來管理狀態和vuex有何不同的話,可以前往筆者之前的文章(https://dotblogs.com.tw/kinanson/2017/07/06/080727)做比較,不過這篇並不是針對vuex的入門教學,所以如果實在看不懂的話,代表了你對redux或vuex還不夠理解,可以看一下筆者以前針對vuex寫的文章,或者直接參訪一下官網的文章。

導覽

  1. 情境模擬
  2. 沒有狀態管理的做法
  3. 使用vuex來做管理狀態
  4. 結論

情境模擬

先來看一下最後完成的例子會長什麼樣子。

接著來看一下整個畫面拆元件的示意圖

沒有狀態管理的做法

先來看一下沒有使用vuex的原始碼樣子

bookMarketService.js

let state = {
  member: {
    name: 'kinanson',
    totalPrice: 0,
    totalNumber: 0
  },
  books: [{
    id: 1,
    title: 'TensorFlow+Keras 深度學習人工智慧實務應用 ',
    context: `人工智慧時代來臨,必須學習的新技術
輕鬆學會「深度學習」:先學Keras再學TensorFlow

★成長最快領域:深度學習與類神經網路,是人工智慧成長最快的領域,讓電腦更接近人類的思考。
★應用深入生活:手機語音助理、人臉識別、影像辨識、手寫辨識、醫學診斷、自然語言處理。
★實作快速上手:只需Python基礎,依照本書Step by Step學習,就可以輕鬆學會深度學習概念與應用。

TensorFlow功能強大、執行效率高、支援各種平台,然而TensorFlow是低階的深度學習程式庫,學習門檻高。所以本書先介紹Keras,Keras是高階的深度學習程式庫(以TensorFlow作為後端引擎),對初學者學習門檻低,可以很容易地建立深度學習模型,並且進行訓練、預測。等讀者熟悉深度學習模型概念與應用後,再來學習TensorFlow就很輕鬆了。`,
    added: false,
    price: 450
  }, {
    id: 2,
    title: `資料結構--使用Python `,
    context: `資料結構(Data Structures)是資訊學科中的核心課程之一,也是基礎和必修的科目。本書確實闡述資料結構的重要主題,並以圖文並茂的方式表達,最能達到教學與學習事半功倍的效果。

內容共分十三章,分別為第一章演算法分析、第二章陣列、第三章堆疊與佇列、第四章鏈結串列、第五章遞迴、第六章樹狀結構、第七章Heap結構、第八章高度平衡二元搜尋樹、第九章2-3 Tree及2-3-4 Tree、第十章m-way 搜尋樹與B-Tree、第十一章圖形結構、第十二章排序,以及第十三章搜尋。`,
    added: false,
    price: 400
  }, {
    id: 3,
    title: `ffective SQL 中文版 | 寫出良好SQL的61個具體做法 `,
    context: `“與其瞎忙或四處尋找答案,請幫自己一個忙:直接買這本書吧!”
-Dave Stokes,MySQL社群經理,Oracle Corporation`,
    added: false,
    price: 400
  }, {
    id: 4,
    title: `無瑕的程式碼 ── 敏捷完整篇 ── 物件導向原則、設計模式與C#實踐 (Agile principles, patterns, and practices in C#) `,
    context: `這本書是《無瑕的程式碼》系列書的第三冊,也是《名家名著》系列書的第三冊。主題是「敏捷開發」,而重點仍舊是回歸到「如何撰寫出好的程式碼」。

什麼是「敏捷開發(Agile Development)」呢?簡單來說,它是軟體開發的一套方法,特點是只要透過這套方法,就能使你的專案更敏捷。

我們為何非得要讓專案變得敏捷呢?原因無他,就是因為我們有慣老闆、還有慣客戶。也就是說,對於現今的市場環境而言,專案不夠敏捷是不行的。這一點,相信所有的軟體工程師都無法否認吧!`,
    added: false,
    price: 500
  }]
}

const service = {
  get() {
    return state
  }
}

export default service

Hello.vue

<template>
  <div class="hello" v-if="data.member">
    <nav class="navbar fixed-top navbar-inverse bg-primary">
      <div class="navbar-brand">
        <price-count :total-price="data.member.totalPrice"></price-count>
        <span>
          <confirm @on-confirm="confirmPay"></confirm>
        </span>
        <span class="navbar-toggler-right">{{data.member.name}} 歡迎您</span>
      </div>
    </nav>
    <div class="container">
      <div class="row">
        <div class="col-md-12" v-for="book in data.books">
          <detail :book="book" @on-add="add"></detail>
        </div>
      </div>
    </div>
    <div class="fixed-area card">
      <book-count :total-number="data.member.totalNumber"></book-count>
    </div>
  </div>
</template>

<script>
import bookMarketService from '../bookMarketService'
import BookCount from './BookCount.vue'
import Detail from './Detail.vue'
import Confirm from './Confirm.vue'
import PriceCount from './PriceCount.vue'
export default {
  name: 'hello',
  components: {
    BookCount,
    Detail,
    Confirm,
    PriceCount
  },
  data () {
    return {
      data: {},
      goodTotal: 0
    }
  },
  methods: {
    get () {
      this.data = bookMarketService.get()
    },
    add (book) {
      book.added = !book.added
      this.data.member.totalPrice += book.added ? +book.price : -book.price
      this.data.member.totalNumber += book.added ? +1 : -1
    },
    confirmPay () {
      this.data.books.forEach(item => item.added = false)
      this.data.member.totalPrice = 0
      this.data.member.totalNumber = 0
    }
  },
  mounted () {
    this.get()
  }
}
</script>

BookCount.vue

<template>
  <div class="card-block">
    您已購入{{totalNumber}}本書
  </div>
</template>

<script>
export default {
  name: 'bookCount',
  props: ['totalNumber']
}
</script>

Confirm.vue

<template>
  <div>
    <button class="btn btn-primary" @click="$emit('on-confirm')">確定結帳</button>
  </div>
</template>

<script>
export default {
  name: 'confirm'
}
</script>

Detail.vue

<template>
  <div class="card">
    <div class="card-header">
      <span class="float-left">書名:{{book.title}}</span>
      <span class="float-right">價格:{{book.price}}</span>
    </div>
    <div class="card-block">
      <div class="float-left">
        <button class="btn btn-info" @click="$emit('on-add',book)">
          <span v-show="!book.added">加入購物車</span>
          <span v-show="book.added">已購買</span>
        </button>
      </div>
      <div class="clearfix"></div>
      <h4 class="card-title">簡介:</h4>
      <p>
        {{book.context}}
      </p>
    </div>
  </div>
</template>

<script>
export default {
  name: 'detail',
  props: ['book']
}
</script>

PriceCount.vue

<template>
  <div>
    <span class="navbar-toggler-left">結帳金額為:{{totalPrice}}</span>
  </div>
</template>

<script>
export default {
  name: 'priceCount',
  props: ['totalPrice']
}
</script>

使用vuex來做管理狀態

首先新增一個stores資料夾,然後新增一支index.js,因為目前只有一支,所以為求方便好理解我就全部寫在一起了

import Vue from 'vue'
import Vuex from 'vuex'
import bookMarketService from '../bookMarketService'

Vue.use(Vuex)

export default new Vuex.Store({
  state: bookMarketService.get(),
  actions: {
    addToCar({ commit }, book) {
      commit('addToCar', book)
    },
    resetCar({ commit }) {
      commit('resetCar')
    }
  },
  mutations: {
    addToCar(state, payload) {
      let book = {}
      state.books.forEach(item => {
        if (item === payload) {
          item.added = !item.added
          book = item
        }
      })

      state.member.totalPrice += book.added ? +book.price : -book.price
      state.member.totalNumber += book.added ? +1 : -1
    },
    resetCar(state, payload) {
      state.member.totalPrice = 0
      state.member.totalNumber = 0
      state.books.forEach(book => book.added = false)
    }
  },
  getters: {
    books: state => state.books,
    member: state => state.member
  },
  strict: process.env.NODE_ENV === 'development'
})

main.js

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import '../node_modules/bootstrap/dist/css/bootstrap.min.css'
import Vue from 'vue'
import App from './App'
import store from './stores'

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  template: '<App/>',
  store,
  components: { App }
})

Hello.vue

原本在父元件定義的props或者evens,都交給vuex的getter和actions處理了,所有溝通的管道都交給vuex處理了。

<template>
  <div class="hello">
    <nav class="navbar fixed-top navbar-inverse bg-primary">
      <div class="navbar-brand">
        <price-count></price-count>
        <span>
          <confirm></confirm>
        </span>
        <span class="navbar-toggler-right">{{member.name}} 歡迎您</span>
      </div>
    </nav>
    <div class="container">
      <div class="row">
        <div class="col-md-12" v-for="book in books">
          <detail :book="book"></detail>
        </div>
      </div>
    </div>
    <div class="fixed-area card">
      <book-count></book-count>
    </div>
  </div>
</template>

<script>
import BookCount from './BookCount.vue'
import Detail from './Detail.vue'
import Confirm from './Confirm.vue'
import PriceCount from './PriceCount.vue'
import { mapGetters } from 'vuex'

export default {
  name: 'hello',
  components: {
    BookCount,
    Detail,
    Confirm,
    PriceCount
  },
  computed: {
    ...mapGetters([
      'books',
      'member'
    ])
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.btn-info {
  color: #fff;
  background-color: #0275D8;
  border-color: #0275D8;
}

.navbar-brand {
  display: inline-block;
  padding-top: .25rem;
  padding-bottom: .25rem;
  margin-right: 1rem;
  font-size: 1.25rem;
  line-height: inherit;
  white-space: nowrap;
  height: 35px;
}

.fixed-area {
  position: fixed;
  top: 650px;
  left: 5px;
}
</style>

Detail.vue

<template>
  <div class="card">
    <div class="card-header">
      <span class="float-left">書名:{{book.title}}</span>
      <span class="float-right">價格:{{book.price}}</span>
    </div>
    <div class="card-block">
      <div class="float-left">
        <button class="btn btn-info" @click="$store.dispatch('addToCar',book)">
          <span v-show="!book.added">加入購物車</span>
          <span v-show="book.added">已購買</span>
        </button>
      </div>
      <div class="clearfix"></div>
      <h4 class="card-title">簡介:</h4>
      <p>
        {{book.context}}
      </p>
    </div>
  </div>
</template>

<script>
export default {
  name: 'detail',
  props: ['book']
}
</script>

<style>

</style>

PriceCount.vue

<template>
  <div>
    <span class="navbar-toggler-left">結帳金額為:{{member.totalPrice}}</span>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  name: 'priceCount',
  computed: {
    ...mapGetters([
      'member'
    ])
  }
}
</script>

<style>

</style>

Confirm.vue

<template>
  <div>
    <button class="btn btn-primary" @click="$store.dispatch('resetCar')">確定結帳</button>
  </div>
</template>

<script>

export default {
  name: 'confirm'
}
</script>

<style>

</style>

BookCount.vue

<template>
  <div class="card-block">
    您已購入{{member.totalNumber}}本書
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
export default {
  name: 'bookCount',
  computed: {
    ...mapGetters([
      'member'
    ])
  }
}
</script>

<style>

</style>

結論

其實這篇是整個跟vue-rx做一樣的範例,所以大家可以比較一下vuex和vue-rx來管理狀態的程式碼有何不同(https://dotblogs.com.tw/kinanson/2017/07/06/080727),接著來說一下我對於vuex和vue-rx使用的心得吧,基本上vuex只是在做管理狀態的事情而已,不過我們使用vuex可以享受devtool的好處,而且我們也可以設定嚴格模式,限制只能透過action去呼叫mutation來改變state的資料,所以單就管理狀態來說確實是vuex做得會比較好,而且devtool也沒有針對vuerx來做任何支援,不過vue-rx其實能做的不是只有管理狀態,如果我們用了rxjs,其實有很多方面可以使用rxjs來處理,比如多條ajax或比較複雜的dom操作,或者一些非同步處理,所以其實在一個專案裡面同時引用vuex和rxjs來做各自專長的事情,也是一項不錯的選擇,如果有任何想法或意見,再請告知筆者囉。