1.vue应用的核心是store(仓库)
stor是个容器,包含应用中的大部分状态
vuex与全局对象的不同
1.vuex的状态存储是响应式,vue组件从store中读取状态时,若store的状态发生变化,组件也会得到更新 2.改变store状态唯一途径是现实的提交commit,可以方便跟踪抓柜台变化,能实现工具
创建store:提供初始state对象和mutation
import{} from 'vue'
import {} from 'vuex'
const store=createStore({
state(){
return{
count:0
}
},mutatons:{
increment(state){
state.count++
}
}
})
const app =createApp({根组件});
app.use(store);//把store实例作为插件安装
通过store.state()获取状态对象,store.commit()触发状态变更
vue组件中,this.$store访问store,可以在组件的方法中提交变更
methods:{
increament(){
this.$store.commit('increament');
console.log(this.$store.state.count);
}
}
通过在methods提交muation而非直接改变store.state.count,因为想更明确追踪状态变化,这样使意图更明显,能记录每次状态改变
2.State
单一状态树
vuex用一个对象包含全部应用层及状态,作为唯一数据源,每个应用只有一个store实例,能直接定位任意特定状态片段,调试过程能轻易取得应用状态快照。vuex中数据和vue实例的data规则相同,状态对象必须纯粹(plain)
vue组件获得vuex状态
vuex状态存储响应式,从store中读取状态最简单的就是计算书心中返回某个状态。
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return store.state.count
}
}
}
count变化时,都会重新求取计算属性(computed),触发更新相关的DOM,vuex通过vue插件把store实例从根组件注入所有子组件,子组件通过this.$store访问
return store.state.count
mapState辅助函数
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'
export default {
// ...
computed: mapState({
// 箭头函数可使代码更简练
count: state => state.count,
// 传字符串参数 'count' 等同于 `state => state.count`
countAlias: 'count',
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}
mapState函数返回一个对象,可以使用对象展开运算符简化写法:
computed: {
localComputed () { /* ... */ },
// 使用对象展开运算符将此对象混入到外部对象中
...mapState({
// ...
})
}
有些状态严格属于单个组件,还是作为组建的局部状态比较好,不需要放入vuex
3.Getter
有时需要从state中派生一些状态,比如对列表过滤并计数:
return this.$store.state.todos.filter(todo => todo.done).length
getter可以认为是store的计算属性,接受state作为第一个参数:
const store = createStore({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos: (state) => {
return state.todos.filter(todo => todo.done)
}
}
})
通过属性访问
通过属性访问时,是作为vue的响应式系统的一部分缓存其中的 可以以属性访问Getter: store.getters.doneTodos 可以接受其他getter作为第二个参数:
getters: {
// ...
doneTodosCount (state, getters) {
return getters.doneTodos.length
}
}
使用时:
computed: {
doneTodosCount () {
return this.$store.getters.doneTodosCount
}
}
通过方法访问
可以让getter返回一个函数实现给getter传参,getter通过方法访问每次都会调用,不会缓存结果
getters: {
// ...
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
mapGetters辅助函数
把store的getter映射到局部计算属性:
computed: {
// 使用对象展开运算符将 getter 混入 computed 对象中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
把getter属性取一个别的名字,可以用对象形式:
...mapGetters({
// 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
doneCount: 'doneTodosCount'
})
4.Mutation
mutation类似于事件,每个mutation有一个字符串的事件类型type和回调函数handler,回调函数时进行状态更改的地方,会接受state作为第一个参数。不能直接调用mutation处理函数,要唤醒一个mutation处理函数,需要以相应的type调用store.commit。
store.commit('increment)
提交载荷
可以传入额外参数作为载荷,通常hi一个对象,这样含多个字段并记录的mutation更易读
对象风格的提交方式
直接使用含type属性的对象,此时整个对象作为载荷传给mutation,处理函数不变
store.commit({
type: 'increment',
amount: 10
})
常量替代mutation事件类型
// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
// store.js
import { createStore } from 'vuex'
import { SOME_MUTATION } from './mutation-types'
const store = createStore({
state: { ... },
mutations: {
// 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名
[SOME_MUTATION] (state) {
// 修改 state
}
}
})
mutation必须是同步函数
因为在mutation中异步函数的回调使状态改变不可追踪
在组件中提交
了一使用this.$store.commit(),或者mapMutation辅助函数把methods映射成store.commit调用,需要在根节点注入store
5.Action
Action可以包含任意异步操作,提交的是mutation,不是直接变更状态。
actions: {
increment (context) {
context.commit('increment')
}
action接受了和store实例有相同方法和属性的context对象,所以调用context.commit提交mutation。可以这样简化:
actions: {
increment ({ commit }) {
commit('increment')
}
}
分发action
action内部可以执行异步操作
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
actions支持在和方式和对象方式进行分发
store.dipach('',{amount: })//载荷
store.dispatch({type:'' ,amount:})
进行一系列一部蔡总,并通过提交mutation记录action副作用(状态变更)
组件中分发action
在组件中使用 this.$store.dispatch('xxx') 分发 action,或者使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store)
组合Action,promise
store.dispatch可以处理被触发的action的处理函数返回的Promise,并且store.dispatch仍然返回promise,store.dispatch在不同模块中可以触发多个action函数,只有所有触发函数完成后,返回的promise才会执行。 利用asnyc/await,可以如下组合action:
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待 actionA 完成
commit('gotOtherData', await getOtherData())
}
}
6.Module
为了解决单一状态树下,应用所有状态集中到较大对象,当应用变得非常负责,store对象就可能变得臃肿,vuex允许将store分割成模块(module),每个模块有自己的state,mutation,action,getter,嵌套子模块:
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = createStore({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
模块的局部状态
模块内部mutation和getter,接受的第一个参数使模块的局部状态对象(也是state,但是是本模块的),模块内的action,局部状态通过context.state暴露出来,根节点为context.rootState,对于模块内部的getter,根节点状态会作为点第三个参数暴露出来。
命名空间
模块内部的action和mutation仍然是注册在全局命名空间,这样使得多个模块能够对同一个action或mutation作出响应。Getter默认注册在全局命名空间,但是目前这并非出于功能上的目的(只是维持现状避免非兼容性变更)。必须注意,不要在不同的、无命名空间的模块中定义两个相同的getter从而导致错误。可以添加namespaced:true使其成为带命名空间的模块。当模块被注册后,它的所有getter\action及mutation都会自动根据模块注册的路径调整命名。启用了命名空间的getter和Action会受到局部化的getter,dispatch和commit,在使用模块内容时不需要在同一模块内额外添加空间名前缀。更改namespace属性后不需要修改模块内的代码。
带命名空间的模块内访问全局内容
使用全局state和getter,rootState和rootGetters会作为第三和第四参数传入getter,也会通过context对象的属性传入action。 若需要在全局命名空间内分发action或提交mutation,将{root:true}作为第三参数传给dispatch或commit即可。
在带命名空间的模块注册全局action
若需要在带命名空间的模块注册全局action,你可添加root:true,并将这个action的定义放在函数handler
带命名空间的绑定函数
当使用mapState,mapGetters,mapActions和mapMutations,这些辅助函数来绑定带命名空间的模块时,写起来可能比较繁琐:
computed: {
...mapState({
a: state => state.some.nested.module.a,
b: state => state.some.nested.module.b
})
},
methods: {
...mapActions([ 'some/nested/module/foo', // -> this['some/nested/module/foo']()
'some/nested/module/bar' // -> this['some/nested/module/bar']()
])
}
可以把模块的空间名称字符串作为第一个参数传送给上述函数,这样所有绑定都会自动将该模块作为上下文
computed: {
...mapState('some/nested/module', {
a: state => state.a,
b: state => state.b
})
},
methods: {
...mapActions('some/nested/module', [
'foo', // -> this.foo()
'bar' // -> this.bar()
])
}
使用createNamespacedHelpers创建基于某个命名空间辅助函数,返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数。
import { createNamespacedHelpers } from 'vuex'
const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')
export default {
computed: {
// 在 `some/nested/module` 中查找
...mapState({
a: state => state.a,
b: state => state.b
})
},
methods: {
// 在 `some/nested/module` 中查找
...mapActions([
'foo',
'bar'
])
}
}
插件开发注意事项
Plugin(插件)提供了模块并循序用户将其添加到Vuex store,需要考虑模块的空间名称问题,可以通过插件的参数对象来允许用户指定空间名称
const namespace = options.namespace || ''
store.dispatch(namespace + 'pluginAction')
模块动态注册
在store创建之后,你可以使用store.registerModule方法注册模块
import { createStore } from 'vuex'
const store = createStore({ /* 选项 */ })
// 注册模块 `myModule`
store.registerModule('myModule', {
// ...
})
// 注册嵌套模块 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
// ...
})
通过store.state.myModule和store.state.nested.myModule访问模块的状态。 模块动态注册功能使其他vue插件可以通过在store中附加新模块来使用vuex管理状态,vuex-router-sync插件就是通过动态注册模块把vueRouter和Vuex结合,实现应用的路由管理。 store.hasModule(moduleName)方法检查模块是否已被注册到store,嵌套模块以数组形式传给registerModule和hasModule,不是以路径字符串形式传递给module
保留state
在注册一个新module时,你很有可能想保留过去的state,例如从一个服务端渲染的应用保留state.可以通过preserveState选项将其归档:store.registerModule('a', module, { preserveState: true }),设置后该模块会被注册,action,mutation,getter被添加到store中,state不会
模块重用
需要创建一个模块的多个实例,使用纯对象来声明模块装填,状态对象会通过引用被共享,修改时store或模块间数据会相互污染,和vue组件的data时同样的问题,可以使用函数来声明模块状态(2.3.0+支持)
const MyReusableModule = {
state: () => ({
foo: 'bar'
}),
// mutation、action 和 getter 等等...
}
7.项目结构
1.应用层级状态集中到单个store对象中
2.提交mutation时更改状态的唯一方法,且过程同步
3.一部逻辑都封装到action
8.组合式API
通过调用useStore函数,在setup钩子函数中访问store,这和组件中用选项式apo访问this.$store等效
import { useStore } from 'vuex'
export default {
setup () {
const store = useStore()
}
}
访问State和Getter
访问state,getter,需要创建computer引用保留响应性,与选项式api中计算属性等效
import { computed } from 'vue'
import { useStore } from 'vuex'
export default {
setup () {
const store = useStore()
return {
// 在 computed 函数中访问 state
count: computed(() => store.state.count),
// 在 computed 函数中访问 getter
double: computed(() => store.getters.double)
}
}
}
访问Mutaion和Action
Setup钩子函数中调用commit和dispatch函数
setup () {
const store = useStore()
return {
// 使用 mutation
increment: () => store.commit('increment'),
// 使用 action
asyncIncrement: () => store.dispatch('asyncIncrement')
}
9.插件
Vuex的Store接受plugins选项,暴露出每次mutation的狗子,vuex插件是一个函数,接收store作为唯一参数:
const myPlugin = (store) => {
// 当 store 初始化后调用
store.subscribe((mutation, state) => {
// 每次 mutation 之后调用
// mutation 的格式为 { type, payload }
})
}
使用方法:
const store = createStore({
// ...
plugins: [myPlugin]
})
插件中提交Mutation
,提交mutation,插件可以同步数据源到store,比如同步websocket数据源到store
export default function createWebSocketPlugin (socket) {
return (store) => {
socket.on('data', data => {
store.commit('receiveData', data)
})
store.subscribe(mutation => {
if (mutation.type === 'UPDATE_DATA') {
socket.emit('update', mutation.payload)
}
})
}
}
const plugin = createWebSocketPlugin(socket)
const store = createStore({
state,
mutations,
plugins: [plugin]
})
生成State快照
插件要获得状态的快照,比较改变前后状态,需要对状态对象进行深拷贝:
const myPluginWithSnapshot = (store) => {
let prevState = _.cloneDeep(store.state)
store.subscribe((mutation, state) => {
let nextState = _.cloneDeep(state)
// 比较 prevState 和 nextState...
// 保存状态,用于下一次 mutation
prevState = nextState
})
}
生成状态快照的插件只在开发阶段使用,使用Webpack,让构建工具帮我们处理:
onst store = createStore({
// ...
plugins: process.env.NODE_ENV !== 'production'
? [myPluginWithSnapshot]
: []
})
发布阶段,需要使用webpack的DefinePlugin使process.env.NODE_ENV !=='production'为false
内置Logger插件
vuex自带日记插件用于一般的调试,createLogger有几个配置项。日志插件可以通过
10.严格模式
开启严格模式,无论何时发生状态变更且不失mutation引起,就会抛出错误,创建store时传入strict:true即可,发布环境不要启用,会深度监测状态树,关闭以避免性能损失,类似于插件,可以用构建工具处理这种情况:stict:process.env.NODE_ENV !='production'. 严格模式在state上用v-model,会修改obj,抛出错误,所以可以在中绑定value,侦听input或change事件,在事件回调中调用方法
input :value="message" @input="updateMessage">
// ...
computed: {
...mapState({
message: state => state.obj.message
})
},
methods: {
updateMessage (e) {
this.$store.commit('updateMessage', e.target.value)
}
}
mutations: {
updateMessage (state, message) {
state.obj.message = message
}
}
双向绑定的计算属性
可以用带setter的双向绑定计算属性:
<input v-model="message">
// ...
computed: {
message: {
get () {
return this.$store.state.obj.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}
}
#11.测试
Mutation
使用ES2015,将mutation定义在store.js,除了模块的默认到处,还应该把mutation进行命名导出:export const mutations={...} Getter类似
Action
需要增加mocking服务层 ,把api调用抽象成服务,然后测试文件用mock服务回应API调用。解决mock以来,可以利用webpack和inject-loader打包测试文件
执行测试
mutation和action编写正确,经过mocking处理后,这些测试不依赖任何浏览器api,可以直接用webpack打包测试文件在Node中执行:
// webpack.config.js
module.exports = {
entry: './test.js',
output: {
path: __dirname,
filename: 'test-bundle.js'
},
module: {
loaders: [
{
test: /.js$/,
loader: 'babel-loader',
exclude: /node_modules/
}
]
}
}
命令行:
webpack
mocha test-bundle.js
12.热重载
用Webpack的Hot Module Replacement API,vuex支持在开发过程中热重载mutation,getter,action,getter,mutation和module,用store.hotUpdate().仅使用模块可以用require.context动态加载或热重载所有模块