# Vuex
# 组件之间传值
目标:熟悉组件之间传值的各种情况(关注非父子之间传值)
- 父组件向子组件传值 props
- 子组件向父组件传值 $emit
- 非父子组件之间传值 : 爷孙;兄弟,其他任何关系
- 发布订阅模式

- 基于Vue实现发布订阅模式
// 相当于中介
const eventBus = new Vue()
// 订阅事件
eventBus.$on('event-b', (param) => {
this.counta = this.counta + '----' + param
})
// 发布事件
eventBus.$emit('event-b', 123)
- 完善黑马头条头像更新的功能
- 准备中介 eventBus
// 统一配置发布订阅模式
// 所有的组件都是Vue的实例对象
Vue.prototype.eventBus = new Vue()
- 订阅事件 Home.vue
created () {
// 自动触发接口调用
this.loadUserInfo()
// 订阅头像更新事件
this.eventBus.$on('update-avatar', () => {
// 重新调用后端接口获取最新的用户信息
this.loadUserInfo()
})
}
- 发布事件 setting/index.vue
// 发布更新右上角头像的事件
this.eventBus.$emit('update-avatar')
# 状态管理必要性分析
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

# Vuex介绍
目标:熟悉Vuex是如何实现上述集中管理组件数据这种思想(模式)的

- state 管理组件数据,管理的数据是响应式的,当数据改变时驱动视图更新。
- mutations 更新数据,state中的数据只能使用mutations去改变数据(只能处理同步的场景)
- actions 获取异步数据,响应成功后把数据提交给mutations(可以处理异步的场景)
- Devtools指的是浏览器的Vue插件调试工具

- getters相当于在State和组件之间添加一个环节(对state中的数据进行加工处理后再提供给组件)
- getters不要修改state中的数据
# 初始化项目
目标:基于脚手架初始化项目
- 第一步:
npm i vuex - 第二步: 创建store.js
import vuex from 'vuex'import vue from 'vue' - 第三步:
Vue.use(vuex) - 第四步:
const store = new Vuex.Store({...配置项}) - 第五步:导出
export default store - 第六步:导入main.js 在根实例配置 store 选项指向 store 实例对象
// 初始化一个vuex的实例(数据仓库) 导出即可
import Vuex from 'vuex'
import Vue from 'vue'
// 使用安装
Vue.use(Vuex)
// 初始化
const store = new Vuex.Store({
// 配置(state|mutations|actions)
state: {
count: 0
}
})
export default store
import store from '@/store'
new Vue({
// 把store对象挂载到vue实例对象中,这样就可以在所有的组件中获取store中的数据了
store,
render: h => h(App),
}).$mount('#app')
# 状态state
# 初始化状态
从组件中获取Store中的状态数据
- 管理数据
// 初始化vuex对象
const store = new vuex.Store({
state: {
// 管理数据
count: 0
}
})
- 在组件获取state的数据:原始用法插值表达式
<div>A组件 state的数据:{{$store.state.count}}</div>
- 使用计算属性:
// 把state中数据,定义在组件内的计算属性中
computed: {
// 1. 最完整的写法
// count: function () {
// return this.$store.state.count
// },
// 2. 缩写
count () {
return this.$store.state.count
}
}
// 不能使用剪头函数 this指向的不是vue实例
# mapState
目标:简化获取store数据的代码
- 把vuex中的state数据映射到组件的计算属性中。
import { mapState } from 'vuex'
- 使用:mapState(对象)
// 使用mapState来生成计算属性 mapState函数返回值是对象
// 使用mapState使用对象传参
// computed: mapState({
// // 1. 基础写法 (state) 代表就是vuex申明的state
// // count: function(state) {
// // return state.count
// // }
// // 2. 使用箭头函数
// // count: state => state.count
// // 3. vuex提供写法 (count是state中的字段名称)
// count: 'count',
// // 4. 当你的计算属性 需要依赖vuex中的数据 同时 依赖组件中data的数据
// count (state) {
// return state.count + this.num
// }
// })
- 使用:mapState(数组)
// 2、mapState参数是一个数组
// computed: mapState(['count', 'total'])
- 如果组件自己有计算属性,state的字段映射成计算属性
// 3、即在内部保留原有的计算属性,又要把store中的数据映射为计算属性
computed: {
// 组件自己的计算属性
calcNum () {
return this.num + 1
},
// 把mapState返回值那个对象进行展开操作(把对象的属性添加到该位置)
...mapState(['count'])
}
# 状态修改mutations
# 状态修改基本操作
目标:Vuex规定必须通过mutation修改数据,不可以直接通过store修改状态数据。
为什么要用mutation方式修改数据?Vuex的规定
为什么要有这样的规定?统一管理数据,便于监控数据变化

- 定义状态修改函数
// mutations是固定的,用于定义修改数据的动作(函数)
mutations: {
// 定义一个mutation,用于累加count值
// increment这个名字是自定义的
increment (state, payload) {
// state表示Store中所有数据
// payload表示组件中传递过来的数据
state.count = state.count + payload
}
}
- 组件中调用
handleClick () {
// 从js语法角度,是否可以这样修改对象的属性值?可以
// 但是我们不应该这样修改数据(Vuex不建议这样修改数据)
// this.$store.state.count++
// 那么Vuex建议如何修改数据?通过mutation修改
// 触发mutation
this.$store.commit('increment', 5)
}
# mapMutations
- 把vuex中的mutations的函数映射到组件的methods中
- 通俗:通过mapMutations函数可以生成methods中函数
// mapMutations用于把mutation映射为方法
import { mapMutations } from 'vuex'
methods: {
// 1、对象参数的写法
// ...mapMutations({
// // 冒号右侧的increment是mutation的名称
// // 冒号左侧的increment是事件函数的名称,可以自定义
// increment: 'increment'
// })
// 2、数组参数的写法(事件函数名称和mutation名称一致)
...mapMutations(['increment'])
// 3、这种写法和第2种等效
// increment (param) {
// // 点击触发该函数后要再次触发mutation的
// this.$store.commit('increment', param)
// }
}
# 异步操作action
# 异步获取数据
目标:主要用于处理异步的任务
- 定义获取数据方法
// 定义异步数据处理
actions: {
queryData (context, payload) {
// context 类似于 this.$store
// 这里可以处理异步任务
// console.log(payload)
setTimeout(() => {
const data = payload
// 触发mutation
// this.$store.commit('updateInfo', data)
context.commit('updateInfo', data)
}, 2000)
}
}
mutations: {
updateInfo (state, payload) {
state.info = payload
}
}
- 组件使用:
methods: {
handleInfo () {
// 触发action(必须调用dispatch方法)
this.$store.dispatch('queryData', 'nihao')
}
}
# mapActions
- mapActions辅助函数,把actions中的函数映射组件methods中
- 通俗:通过mapActions函数可以生成methods中函数
...mapActions(['queryData'])
// ...mapActions({
// handleInfo: 'queryData'
// })
// handleInfo () {
// // 这里要触发Action
// this.$store.dispatch('queryData', 'coniqiwa')
// }
# 回顾
- 为什么需要Vuex?主要解决组件之间复杂的数据传递问题
- Vuex是如何解决这个问题的?
- 组件数据统一存储
- 所有的组件数据都从统一的位置获取
- 更新数据时,就更新统一的数据
- Vuex的核心概念
- state Store中的数据(所有组件的共享数据)
- mapState
- mutation 必须通过mutation变更state中的数据(仅仅可以进行同步修改)
- mapMutations
- action 如果要异步处理数据,必须通过action处理
- mapActions
- getters
- module
- state Store中的数据(所有组件的共享数据)
# 案例实战
# 豆瓣接口介绍
豆瓣接口地址支持jsonp但是不支持cors。
- http://api.douban.com/v2/movie/subject/:id 详情
- http://api.douban.com/v2/movie/in_theaters 正在热映
- http://api.douban.com/v2/movie/coming_soon 即将上映
- http://api.douban.com/v2/movie/top250 top250
注意:
- 豆瓣的接口请求限制,每个外网IP有请求次数限制。
- 豆瓣的图片访问显示,非豆瓣域名下发起的图片请求不给予响应。
- 近期:官方停用搜索相关接口,必须要注册豆瓣api平台获取认证apikey才行。
- 网友提供
apikey=0df993c66c0c636e29ecbb5344252a4a
- 替代接口
- 热映列表 http://test.zjie.wang/api/hot
- 电影详情 http://test.zjie.wang/api/hot/123
# 初始化项目
目标:基于脚手架初始化项目
- main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'
import router from './router'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
store,
router
}).$mount('#app')
- store.js
// 管理数据
import Vuex from 'vuex'
import Vue from 'vue'
Vue.use(Vuex)
const store = new Vuex.Store({
title: '标题',
list: [],
detail: {}
})
export default store
- router.js
// 路由相关功能
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: '/movie',
component: Movie,
redirect: '/movie/list',
children: [
{ path: 'list', component: MovieList },
{ path: 'detail', component: MovieDetail }
]
}
]
export default router
# 配置路由及组件
封装组件
- 头部组件
- 底部组件
- 路由组件
- 正在热映
- 即将上映
- top250
- 电影详情
路由规则
// 路由规则 { path: '/movie', component: Movie, redirect: '/movie/list', children: [ { path: 'list', component: MovieList }, { path: 'detail', component: MovieDetail }, { path: 'will', component: WillMovie }, { path: 'top', component: TopMovie }, ] }<!-- 头部 --> <div class="my-header"> <span>Header</span> </div> <!-- 内容 --> <div class="wrapper"> <router-view></router-view> </div> <!-- 底部 --> <div class="my-footer"> <ul> <router-link to='/movie/list' tag='li'> <a> <span class="iconfont icon-remen"></span> <p>正在热映</p> </a> </router-link> <router-link to='/movie/will' tag='li'> <a> <span class="iconfont icon-dianying"></span> <p>即将上映</p> </a> </router-link> <router-link to='/movie/top' tag='li'> <a> <span class="iconfont icon-top"></span> <p>top250</p> </a> </router-link> </ul> </div>
# 电影列表功能
第一步:申明数据,根据页面需要的数据进行申明。
state: {
title: '',
list: [],
detail: {}
},
第二步:定义修改数据的方法
mutations: {
// payload = {title,list} 约定数据格式
updateList (state, payload) {
state.title = payload.title
state.list = payload.list
}
},
第三步:获取数据的方法
movieList (context) {
// 获取电影列表数据
fetch('http://test.zjie.wang/api/hot')
.then(ret => {
return ret.json()
})
.then(ret => {
context.commit('updateList', {
list: ret.hot,
title: '电影列表'
})
})
},
第四步:调用获取数据的方法
methods: {
...mapActions(['movieList'])
}
created () {
this.movieList()
// this.$store.dispatch('movieList')
},
第五步:获取vuex的数据
computed: {
...mapState(['list'])
},
第六步:渲染页面
<ul class="list">
<li v-for="item in list" :key="item.id">
<router-link :to="'/detail/'+item.id">
<img :src="item.img">
<div class="info">
<h3>{{item.title}}</h3>
<p>豆瓣评分:{{item.score}}</p>
<p><span class="tag" v-for="tag in item.tags.split(',')" :key="tag">{{tag}}</span></p>
</div>
</router-link>
</li>
</ul>
# 电影详情功能
- 电影列表 电影的详情地址 都不一样 都会来到电影详情组件
- 使用动态路由功能 /detail/:id
- 电影详情组件获取id获取详情数据
第一步:路由规则
{ path: 'detail', component: MovieDetail },
<router-link :to='"/movie/detail?id=" + item.id'>
第二步:准备数据
state: {
// 标题
title: '',
// 详情
detail: null
},
第三步:修改数据函数
mutations: {
// payload = {title,item} 约定数据格式
updateDetail (state, payload) {
state.title = payload.title
state.item = payload.item
}
},
第四步:获取数据去修改数据的函数
async movieDetail (context, id) {
// 获取电影列表数据
const ret = await fetch('http://test.zjie.wang/api/hot/' + id)
const movie = await ret.json()
context.commit('updateDetail', {
detail: movie,
title: '电影详情'
})
},
第五步:在组件使用数据
computed: {
...mapState(['detail'])
},
第六步:在组件初始化获取数据
created () {
this.movieDetail(this.$route.params.id)
},
methods: {
...mapActions(['movieDetail'])
}
第七步:渲染页面
<div class="item" v-if="detail">
<img :src="item.img" alt="">
<div>
<p>豆瓣评分:{{item.score}}</p>
<p>产地:{{item.country}}</p>
<p><span class="tag" v-for="tag in detail.tags.split(',')" :key="tag">{{tag}}</span></p>
<p>{{item.summary}}</p>
</div>
</div>
# 关于Getters的用法
getters的作用类似于计算属性
- 定义getter
getters: {
// 相当于计算属性的作用
topMovie (state) {
return state.list.filter(item => {
return item.id < 10
})
}
}
- 使用getter
import { mapGetters } from 'vuex'
computed: {
// ...mapGetters(['topMovie'])
...mapGetters({
list: 'topMovie'
}),
// list () {
// return this.$store.getters.topMovie
// }
}
- 模板中使用
<div>Top电影数量:{{list.length}}</div>
# Store模块化拆分
Store中代码越来越多,不方便后续的维护和扩展
# 拆分Store模块
- 入口文件 store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import listModule from '@/store/module-list.js'
import detailModule from '@/store/module-detail.js'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
list: listModule,
detail: detailModule
}
})
export default store
- 模块内部代码 store/module-list.js
export default {
state: () => ({}),
mutations: { },
actions: { },
getters: { }
}
# 模块成员的访问
- 第一种方式
methods: {
...mapActions(['list/movieList'])
},
computed: {
...mapState('list', ['list'])
//list (state) {
// return state.list.list
//}
}
created () {
this['list/movieList']()
}
- 第二种方式
methods: {
...mapActions('list', ['movieList'])
},
created () {
this.movieList()
}
- 第三种方式
import { createNamespacedHelpers } from 'vuex'
// createNamespacedHelpers的参数是模块的名称
const { mapState, mapActions } = createNamespacedHelpers('list')
methods: {
...mapActions(['movieList'])
},
created () {
this.movieList()
}
# 关于路由的参数传递
- props的值是 true (默认值就是true)
// route.params 将会被设置为组件属性props
- props的值是对象,那么它会被按原样设置为组件属性
{ path: 'detail/:id', component: MovieDetail, props: { id: 1, abc: 'hello' } }
- props的值是函数,函数的形参route等效于 $route
{ path: 'detail/:id', component: MovieDetail, props: (route) => ({ id: route.params.id, abc: 'nihao' }) }