[vue]使用vuex來管理元件間的狀態
前言
這一篇想討論的範例是從vuerx那一篇同樣的例子,整個翻成vuex的範例,如果想要比較一下用rxjs來管理狀態和vuex有何不同的話,可以前往筆者之前的文章(https://dotblogs.com.tw/kinanson/2017/07/06/080727)做比較,不過這篇並不是針對vuex的入門教學,所以如果實在看不懂的話,代表了你對redux或vuex還不夠理解,可以看一下筆者以前針對vuex寫的文章,或者直接參訪一下官網的文章。
導覽
先來看一下最後完成的例子會長什麼樣子。
接著來看一下整個畫面拆元件的示意圖
先來看一下沒有使用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>
首先新增一個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來做各自專長的事情,也是一項不錯的選擇,如果有任何想法或意見,再請告知筆者囉。