vuex
是一個狀態管理模式,通過用戶的actions
觸發事件,然后通過mutations
去更改數據(你也可以說狀態啦 -> state),最后通過getters
對狀態進行獲取,更改頁面展示的內容。哈哈 ?? ,詳細的內容請接著往下看,如有不妥請文末留言啊。原創文章,轉載請注明出處。
原文請戳傳送門
注意 ?? 文章中涉及到項目代碼是使用Vue
官方提供的腳手架vue-cli
進行搭建的,如果看者感興趣,可以自行用vue-cli
搭建項目,并進行代碼的驗證。
Vuex是什么
官網介紹:Vuex是一個專門為Vuejs應用程序開發的狀態管理模式
。(類似react的redux)。Vuex
采用集中式存儲管理應用的所有組件的狀態,并以相應的規則保證狀態以一種可預測的方式發生變化。Vuex在構建中大型的應用比較適用,小型的應用用組件之間的通信就可以了,小型應用用上Vuex
就顯得比較臃腫了。
Vuex的安裝
因為自己是使用npm
來輔助開發的,所以我也只說下通過npm
安裝Vuex
的方法。其他的安裝方法,請戳傳送門。
進入你項目的根目錄,然后執行:
$ npm install vuex --save
或
$ npm install vuex --save-dev
然后在store
主入口的javascript文件,一般是store/index.js
中通過use
進行引用,前提是你已經安裝了vue
:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
為了方便在各個組件中使用store
,需要在程序的根組件中將其注入
到每個子組件。我們需要在實例化Vue
的時候將store
引入(實例化Vue
的文件一般是main.js主入口文件)。
import Vue from 'vue'
import store from '/path/to/store/index.js'
const initApp =new Vue({
store: store
}).$mount('#app')
核心概念
在使用Vuex
進行開發的過程中,你可以理解核心的概念只有State
、Action
和Mutation
三個,就像本文章開篇給出的截圖流程那樣簡單明了。但是,我們使用Vuex
開發一個應用,肯定是想要方便管理等等。這里自己按照五個核心概念來談談,五個核心概念也是官網推薦使用的。Vuex
的五個核心概念除了上面指出的三個之外,還包括Getter
和Module
兩個。先一句話來概括下它們 :
State : 數據源的存放地
Getter : store的計算屬性
Mutation : 處理數據邏輯,使得數據和視圖分離(同步)
Action : 類似Mutation(異步),改變狀態的話,還得觸發Mutation
Module : 將store分解成模塊
下面來詳細講解各個核心概念咯 ??
State
Vuex
是使用單一狀態樹,一個對象就包含了全部的應用層級狀態。這也就表明,一個應用僅僅包含一個store
的實例。
狀態State
對應于Vue
對象中的data,因為兩者是對應的關系,所以在這里可以稱狀態==數據
的。如下代碼指出:
<script>
export default {
name: '',
data() { // state對應的地方
return {
...
}
}
}
</script>
State
里面存放的數據是響應式的,Vue
組件從store
中讀取數據,若是store
中的數據發生改變,依賴這個數據的組件也會發生更新。也就是說數據和視圖是同步的。
局部狀態
雖然說Vuex
的Store
倉庫讓我們統一管理數據變得更加方便,但是代碼一多也會變得冗長和不直觀。有些組件的數據是自己嚴格使用,我們可以將state
放在組件自身,作為局部數據,專供此組件使用。比如現在只想在一個組件中使用emotion: happiness
,那就不必要在store
的state
中進行定義了,只在本組件初始化就行了:
data () {
return {
emotion: 'happiness'
}
}
獲取狀態
在Vue
組件中獲取store
中的數據(狀態),最直接的就是通過計算屬性獲取。因為在上面我將store
注冊到根組件上了,所以在這里直接通過this.$store
就可以調用了。比如我獲取狀態(state)中的count: 100
:
computed: {
count: function (){
return this.$store.state.count;
}
}
mapState輔助函數
mapState
輔助函數把全局的State
映射到當前組件computed
計算屬性中,即是幫助我們生成計算屬性。簡化我們的代碼操作,不需要使用this.$store.state
獲取了。以上面狀態(state)中的count: 100
為例子 :
import { mapState } from 'vuex' // 注意別漏了引入
export default {
computed:
mapState({
count: state => state.count
}),
}
Getter
上面的state
中我們了解到,在store
倉庫里,state
是用來存儲數據的。在多個組件中要進行使用同一種狀態的話,對數據進行簡單操作,我們可以通過在組件的computed中進行獲取this.$store.state.theDataName
。簡單操作沒問題,但是,我們進行其他的操作,比如過濾操作
,我們就得寫一堆的代碼 :
computed: {
filterData: function () {
this.$store.state.theDataName.filter(function(item){
// do something ...
})
}
}
然后在每個組件中復制這一大堆的代碼,或者你單獨新建一個文件把代碼寫進入,每個組件都引入(如果你不覺得很麻煩的話)。
而Getter
可以把組件中共享狀態抽取出來,這也是Getter
存在的意義。我們可以認為,Getter
是Store
的計算屬性。
如何使用Getter
為了方便管理,需要一個單獨的getters.js
的文件,假如已經有對數據進行過濾的函數了:
export default {
filterDatas (state,getter,rootState) {
// do something ...
}
}
那么只要在相關的組件的computed
中引入就可以了,是不是很方便啊 :
computed: {
filterItems: function () {
return this.$store.getters.filterDatas;
}
}
mapGetters輔助函數
mapGetters
輔助函數僅僅是將store
中的getter
映射到局部計算屬性,看情況使用,類似mapState
。下面使用mapGetter
改寫上面的filterItems
:
import { mapGetters } from 'vuex' // 記得引入
export default {
computed:
mapGetters({
filterItems: 'filterDatas'
})
}
Mutation
Vuex的中文官網中明確指出更改Vuex的store中的狀態(state)的唯一的方法是提交mutation
。
Mutation
可以理解為:在Mutation
里面裝著一些改變數據方法的集合。即把處理數據邏輯方法全部放在Mutation
里面,使得數據和視圖分離。
使用Mutation
Mutation
的結構:每個mutation都有一個字符串的事件類型(type)
和一個回調函數(handler)
也可以理解為{type:handler()}
,這和訂閱發布有點類似。先是注冊事件,當觸發響應類型的時候調用handle()
,調用type
的時候需要用到store.commit('typeName')
方法。比如我想在要觸發mutations.js
中的INCREASE
處理函數:
// mutations.js
const INCREASE = 'INCREASE'; // 不能漏
export default {
[INCREASE](state,data){
// change the state ...
}
}
因為我注冊了store
到根組件,那么在.vue
組件中就可以通過this.$store.commit('INCREASE')
觸發這個改變相關狀態的處理函數了。如果在actions.js
中調用,直接使用提供的commit
參數進行commit('INCREASE')
觸發處理函數。
提交載荷(Payload)
可以向store.commit
傳入額外的參數,參數一般為object
類型。我這里接著上面的示例,組件觸發的時候傳入一個100
的數字到data
里面 :
methods:{
increase: function (){
this.$store.commit('INCREASE',100);
}
}
使用mutation-types.js
使用mutation-types.js
(名稱可根據愛好隨便取)是為了方便管理項目mutation
的類型。我在知乎上也回答過為什么要使用mutation-types.js,當然你完全沒必要使用它,不過我自己喜歡使用它。將使用mutation
內容中的mutations.js
代碼拆分為兩部分,一部分是mutation-types.js
,另一部分是mutations.js
,示范如下 :
// mutation-types.js
export const INCREASE = 'INCREASE';
// mutations.js
import {INCREASE} from '/path/to/mutation-type.js'
export default {
[INCREASE](state,data){
// change the state ...
}
}
mapMutations輔助函數
為了簡化你的代碼量,使得代碼看起來逼格更高點,你可以使用mapMutations
輔助函數將組件中的methods
映射為store.commit
調用(需要在根節點注入store哦)。demo來映射上面的increase
:
import {mapMutations} from 'vuex' // 不能漏哦
export default {
methods: {
...mapMutations([
'INCREASE'
])
}
}
Action
Action 類似于 Mutation,不同點是 :
Action提交的是 mutation,而不是直接變更狀態
Action是異步的,而Mutation是同步的
詳細的相似點可以回滾看Mutation
的啦,或者直接戳vue官網Store
組件內分發Action
因為我在全局組件中掛載了store
,所以引用就可以這樣寫 -> this.$store.dispatch('dispatchEvent')
,當然你可以傳參過去啦。比如:this.$store.dispatch('dispatchEvent',param)
,param一般是obj類型的。
mapActions輔助
為了簡化操作,Action像Mutaion一樣有一個映射的函數mapActions
。使用方法也類似Mutation,demo如下 :
import {mapActions} from 'vuex' // 不能漏哦
export default {
methods: {
...mapActions([
'INCREASE'
])
或
...mapActions([
increase: 'INCREASE'
])
}
}
Module
由于vue中使用單一的狀態樹,當管理的項目中大型的時候,所有的狀態都集中在一個對象中會變得比較復雜,難以管理,顯得項目比較臃腫。為了解決這些問題,我們可以使用vuex提供的Module功能
,將store分割成模塊。每個模塊都有自己的state、mutation、action、getter。現在假設你的應用的功能包括登錄和音樂
兩個功能模塊頁面,那么store的結構可以這樣寫:
- module
- music
actions.js
getters.js
index.js // music module 的入口文件
mutations.js
state.js
- user
actions.js
getters.js
index.js // user module的入口文件
mutations.js
state.js
actions.js
index.js // store 的入口文件
mutation-types.js // 管理所有的mutations
mutations.js
state.js
模塊的局部狀態
對于模塊內部的mutation,接收的第一個參數是state
,也就是接收本模塊的局部狀態,比如上面的music模塊,我在其state.js中寫上 :
export default {
music: {
list: [],
total: 100
}
}
我在同級的mutations.js
中有 :
import * as types from '../../mutation-types'
export default {
[types.UPDATE_MUSIC](state,data){
console.log(state.music.total); // 打印出100
...other handle
}
}
命名空間
默認情況下,模塊內部
的action、mutation 和 getter是注冊在全局命名空間的 -> 這樣使得多個模塊能夠對mutation和action作出響應。
如果看者希望你寫的模塊具有更高的封裝度和復用性,你可以通過添加namespaced:true
的方式使其成為命名空間模塊。當模塊被注冊后,它的所有 getter、action 及 mutation 都會自動根據模塊注冊的路徑調整命名。比如上面的music模塊 :
import state from './state' //state
import getters from './getters' //getters
import * as actions from './actions' //actions
import mutations from './mutations' //mutations
//modules
export default {
namespaced: true, // 添加命名空間
state,
getters,
actions,
mutations
}
詳細的情況請戳vuex官網modules
store結構
vuex的官網談項目結構,我這里談store結構
,因為我覺得每個人的項目的結構布局有所不同,但是vuex
可以是一個模版化的使用。當然,這模版化的使用遵循了官網所定的規則:
應用層級的狀態應該集中在單個 store對象中
提交mutation是更改狀態(state)的唯一方法,并且這個過程是同步的
異步邏輯都應該封裝到action里面
整理的store結構如下:
.
├── ...
│
└── store
├── actions.js // 根級別的 action
├── index.js // 我們組裝模塊并導出 store 的地方
├── mutation-types.js // store所有mutations管理
├── mutations.js // 根級別的 mutation
├── state.js // 根級別的 state
└── modules
├── moduleA
├── moduleB
└── moduleC
├── actions.js // moduleC 的 action
├── getters.js // moduleC 的 getter
├── index.js // moduleC 的 入口
├── mutations.js // moduleC 的 mutation
└── state.js // moduleC 的 state
上面的結構比較通用,模版化,我在接下來的完整小項目
中就是使用上面的store結構
來管理啦 ??
完整小項目
自己在上面講了一大推的<del>廢話</del>,嗯哈,為了證明那不是<del>廢話</del>,下面就結合上面講的知識點來一個綜合的min-demo
吧,歡迎指正啊! @~@
是什么項目呢
思來想去,自己還是覺得做一個簡單版本的todo
項目好點,理由如下:
個人時間精力郵箱(main reason)
todo項目 -> 麻雀雖小,五臟俱全
項目包含一個簡單的登錄頁面,然后跳轉到todo小項目
的頁面。如圖所示:
在登錄頁面,會要求你填寫非空的內容進入,我這里填了自己的名字啦。在todo頁面
,你就需要在輸入框輸入你要做的事情啦,事情的添加默認是未做的狀態。當然,允許進行時間的狀態進行設置和事件的刪除啦。成品可查看下面最終的效果gif動效
,就醬 @~@
項目的初始化
?? 本項目在mac系統上使用vue-cli
的基礎上搭建(搭建日期2018.01.14)
的小項目,其完整的覆蓋了vue的全家桶了 -> 使用的vue版本是^2.5.2
,vuex的版本是^3.0.1
,vue-router的版本也是^3.0.1
。如果你使用低版本,請參考低版本的相關說明。
# 全局安裝 vue-cli
$ npm install --global vue-cli
# 進入桌面
$ cd desktop
# 初始化項目min-demo
$ vue init webpack min-demo
? Project name min-demo # 項目名稱
? Project description A Vue.js project # 項目描述
? Author reng99 # 項目作者
? Vue build standalone
? Install vue-router? Yes # 是否使用路由
? Use ESLint to lint your code? No # 是否啟動語法檢查
? Set up unit tests No # 是否配置單元測試
? Setup e2e tests with Nightwatch? No # 是否配置集成測試
? Should we run `npm install` for you after the project has been created? (recom
mended) npm # 選擇那種包管理工具進行安裝依賴,共三種選擇:npm,yarn,no thanks 我選擇了npm
vue-cli · Generated "min-demo".
# 等待安裝依賴的完成
...
# 進入項目
$ cd min-demo
# 啟動項目
$ npm run dev
# 如果一切正常,就會在瀏覽器的http://localhost:8080的地址頁面有相關的vue界面展示出來
當然,使用腳手架搭建的項目,沒有自動集成vuex
,這就需要你進入項目的根目錄,執行npm install vuex --save
命令來安裝啦。
項目的實現
嗯嗯,下面我將改寫在vue-cli
搭建的項目,以符合我自己期望。改寫的代碼就不全給出來了啊,關鍵的項目代碼還是會貼一下的。??
這個項目的結構如下:
.
├── build/ #webpack 的配置項
│ └── ...
├── config/
│ ├── index.js # 項目的主要配置
│ └── ...
├── node_modules/ # 相關依賴
│ └── ...
├── src/
│ ├── main.js # 應用的主入口
│ ├── App.vue # 引用的根組件
│ ├── components/
│ │ ├── Login.vue # 登錄組件
│ │ └── Todo.vue # todo組件
│ ├── store/
│ │ ├── modules/ # todo組件
│ │ │ └── todo
│ │ │ ├── actions.js # todo的actions
│ │ │ ├── getters.js # todo的getters
│ │ │ ├── index.js # todo的入口
│ │ │ ├── mutations.js # todo的mutations
│ │ │ └── state.js # todo的狀態
│ │ ├── actions.js # 根actions
│ │ ├── index.js # store入口文件
│ │ ├── mutation-types.js # 整個store中的mutation的管理
│ │ ├── mutations.js # 根mutations
│ │ └── state.js # 根的狀態
│ ├── router/
│ │ └── index.js # 路由文件
│ └── assets/ # 模塊的資源
│ └── ...
├── static/ # 靜態資源存放的地方
│ └── ...
├── .babelrc # 語法轉換babel的相關配置
├── .editorconfig # 編輯器IDE的相關配置
├── .gitignore # 提交到github忽略的內容配置
├── .postcssrc.js # css的處理配置postcssrc
├── index.html # index html模版
├── package.json # 相關的執行命令和依賴配置
└── README.md # 項目的說明文件
?? 項目重點在src
文件夾內
在/src/components/Login.vue
中
<template>
<div id="login">
<div class='login'>
<div class='login-title'>簡單模擬登錄</div>
<div class='login-body'>
<div class='hint' v-show='hintFlag'>輸入的文字不能為空</div>
<input placeholder='請輸入任意文字...' type='text' v-model='loginTxt'/>
<div class="btn" @click='login'>登錄</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Login',
data () {
return {
hintFlag: false,
loginTxt: ''
}
},
methods: {
login () {
var vm = this;
if(vm.loginTxt.trim()==''){
vm.hintFlag = true;
}else{
// 進入todo的頁面
vm.hintFlag = false;
// 觸發獲取登錄名
vm.$store.dispatch('createUsername',vm.loginTxt);
vm.$router.push('/todo');
}
}
},
watch:{
loginTxt(curVal){
var vm = this;
if(curVal.trim()==''){
vm.hintFlag = true;
}else{
vm.hintFlag = false;
}
}
}
}
</script>
<style scoped lang='less'>
#login{
margin-top: 100px;
.login{
width: 400px;
margin: 0 auto;
&-title{
color: #999;
font-size: 22px;
text-align: center;
margin-bottom: 20px;
}
&-body{
width: 360px;
padding: 40px 20px 60px 20px;
background: #ededed;
input{
width: 100%;
display: block;
height: 40px;
text-indent: 10px;
}
.btn{
width: 100%;
text-align: center;
height: 40px;
line-height: 40px;
background: #09c7d1;
color: #fff;
margin-top: 20px;
cursor: pointer;
}
.hint{
color: red;
font-size: 12px;
text-align: center;
padding-bottom: 10px;
}
}
}
}
</style>
在上面的組件中,自己原封不動的將里面的代碼復制過來了,你應該可以看出,這個.vue
文件中結合了三塊的東西,分別是html
的模版、javascript
代碼和運用less預處理器編寫的css
代碼。
在/src/components/Todo.vue
組件的結構依舊是這樣:
<template>
<div id="todo">
<div class='username'>歡迎您!<span>{{username}}</span></div>
<div class="main">
<div class="input">
<input placeholder='請輸入要做的事情...' type='text' v-model='eventTxt'/>
<button @click="addEvent">增加</button>
</div>
...
</div>
</div>
</template>
<script>
export default {
name: 'ToDo',
data () {
return {
noDataFlag: true,
...
}
},
created(){
var vm = this;
if(vm.username == ''){
vm.$router.push('/');
}
},
computed: {
username(){
return this.$store.getters.username;
},
...
},
methods: {
delEvent (id) {
this.$store.dispatch('delEvent',id);
},
...
},
watch:{
...
}
}
</script>
<style scoped lang='less'>
#todo{
margin-top: 100px;
...
}
</style>
在路由的文件中,因為知識涉及了兩個頁面的路由跳轉,這里也全貼出來吧 --
import Vue from 'vue'
import Router from 'vue-router'
import Login from '@/components/Login'
import ToDo from '@/components/ToDo'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Login',
component: Login
},
{
path: '/todo',
name: 'ToDo',
component: ToDo
}
]
})
關于store
,這是一個重點,我打算詳細說啦。首先當然得從整個store
的入口文件講起啦。在store/index.js
中,我是這樣引用的 :
import Vue from 'vue' // 引入vue依賴
import Vuex from 'Vuex' // 引入vuex依賴
import state from './state' // 引入根狀態
import * as actions from './actions' // 引入根actions
import mutations from './mutations' // 引入根mutations
import todo from './modules/todo/index' // 引入todo模塊
Vue.use(Vuex) // 引入vuex
// 初始化store
export default new Vuex.Store({
state,
actions,
mutations,
modules:{
todo
}
})
在根的store
的mutation-types.js
文件中,管理著整個項目的狀態管理函數 --> 包括創建用戶名、添加要做的事情、刪除創建的事情、顯示事件的狀態(全部,已經做,沒有做)和標記事件(已經做的事件標記為未做,未做的事件標記為已經做)。代碼展示如下 :
export const CREATE_USERNAME = 'CREATE_USERNAME' // 創建用戶名
export const ADD_EVENT = 'ADD_EVENT' // 添加事件
export const DEL_EVENT = 'DEL_EVENT' // 刪除事件
export const ALL_EVENT = 'ALL_EVENT' // 全部事件
export const UNDO_EVENT = 'UNDO_EVENT' // 沒做事件
export const DONE_EVENT = 'DONE_EVENT' // 已做事件
export const MARK_UNDONE = 'MARK_UNDONE' // 標記為未做
export const MARK_DONE = 'MARK_DONE' // 標記為已做
store/state.js
的作用在你聽完store/todo/state.js
的講解后你應該會明白。在模塊todo
的state中,自己定義了此模塊的相關的數據結構,如下:
export default {
// 事件列表
list:[
// {
// id: 0, 相關的id
// content:'', // 事件的內容
// flag: 1 // 是否完成,1是完成,0是未完成
// }
],
allList:[],
increase: 0,
total: 0,
done: 0
}
定義的這些數據結構,你可以說是狀態吧,是為了給mutation和getters
進行操作。對了,你也許注意到了store
根目錄中沒有getters.js
文件。因為,這是分散模塊管理項目,為什么還需要呢,如果你想保留,你可以自己新建一個,按照自己的習慣進行管理項目唄。
上個段落以及前面某部分內容已經談及了mutations
的作用,本項目中使用mutation就是為了改變自己在todo/state.js
定義的狀態,比如改變allList:[]
:
import * as types from '../../mutation-types'
export default {
// 添加事件
[types.ADD_EVENT] (state,data){
var obj = {
id: state.increase++,
content: data,
flag: 0
}
state.allList.push(obj);
state.list = state.allList;
state.total = state.allList.length;
},
...
}
而todo/getter.js
就是為了將vuex
中的狀態獲取,方便顯示在頁面的啦,在本項目中,自己超級簡單的使用了下:
export default {
list (state,getters,rootState) {
return state.list;
},
username (state,getters,rootState) {
return rootState.username;
},
...
}
最后一個是關于todo/actions.js
,這是頁面中的用戶的事件去發送事件,使得產生mutations
去改變狀態(state.js),最終使得頁面展示的內容(getters)發生改變。這里以一個派遣添加事件為例子 :
import * as types from '../../mutation-types'
export const addEvent = ({commit,state,rootState},query) => {
commit(types.ADD_EVENT,query);
}
嗯,整篇文章都說整個store是掛載在根組件上的,那么是在哪里呢?答案就是src/main.js
文件啦,文件內的代碼如下 :
import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store'
Vue.config.productionTip = false
new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>'
})
最終的效果
好吧,自己利用了一個下午搭建項目并簡單思考了相關的邏輯,簡單實現項目,其最終的效果如下gif動圖
啦 :
嗯,項目是不是很簡單,所以就不放源碼上去了 ?? 。其實自己覺得源碼實現不夠嚴謹啦,畢竟只是花了短短一個下午和晚上從設計到實現... 逃:)
參考內容
( 完 @~@ )