Vuex

组件之间传值

目标:熟悉组件之间传值的各种情况(关注非父子之间传值)

  • 父组件向子组件传值 props
  • 子组件向父组件传值 $emit
  • 非父子组件之间传值 : 爷孙;兄弟
    • 发布订阅模式(微博:大V——粉丝)

image.png

  • 基于Vue实现发布订阅模式
  1. // 相当于中介
  2. const eventBus = new Vue()
  3. // 订阅事件
  4. eventBus.$on('event-b', (param) => {
  5. console.log(param) // 123
  6. })
  7. // 发布事件
  8. eventBus.$emit('event-b', 123)
  • 通过兄弟组件之间的传值进行验证

总结:

  1. 少量的组件之间数据传递可以用这种模式
  2. 但是不建议大量的组件通信采用这种机制(代码比较乱,后期维护比较麻烦)

状态管理必要性分析

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。 如果使用了Vuex,就可以非常方便的进行复杂的组件之间数据传递(非父子关系)

image.png

总结:

  1. 所有组件的数据进行统一管理(存储和变更),每个组件内部就不再需要维护这些数据了
  2. 数据变更时,统一修改Store中数据即可,组件中用到这个数据的组件会自动更新(数据是响应式的)

Vuex介绍

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

image.png

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

image.png

  • getters相当于在State和组件之间添加一个环节(对state中的数据进行加工处理后再提供给组件)
  • getters不要修改state中的数据

初始化项目

目标:基于脚手架初始化项目

  • 第一步:npm i vuex --save/-S
  • 第二步: 创建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 实例对象
  1. // 初始化一个vuex的实例(数据仓库) 导出即可
  2. import Vuex from 'vuex'
  3. import Vue from 'vue'
  4. // 使用安装
  5. Vue.use(Vuex)
  6. // 初始化
  7. const store = new Vuex.Store({
  8. // 配置(state|mutations|actions)
  9. })
  10. export default store
  1. import store from '@/store'
  2. new Vue({
  3. // 把store对象挂载到vue实例对象中,这样就可以在所有的组件中获取store中的数据了
  4. store,
  5. render: h => h(App),
  6. }).$mount('#app')

总结:

  1. 初始化store对象
  2. 把store对象挂载到Vue实例中

状态state

初始化状态

状态state用于存储所有组件的数据。

  • 管理数据
  1. // 初始化vuex对象
  2. const store = new vuex.Store({
  3. state: {
  4. // 管理数据
  5. count: 0
  6. }
  7. })
  • 在组件获取state的数据:原始用法插值表达式
  1. <div>A组件 state的数据:{{$store.state.count}}</div>
  2. <div>A组件 state的数据:{{count}}</div>
  • 使用计算属性:
  1. // 把state中数据,定义在组件内的计算属性中
  2. computed: {
  3. // 1. 最完整的写法
  4. // count: function () {
  5. // return this.$store.state.count
  6. // },
  7. // 2. 缩写
  8. count () {
  9. return this.$store.state.count
  10. }
  11. }
  12. // 不能使用剪头函数 this指向的不是vue实例

总结:

  1. state中的数据是自定义的,但是state属性名是固定的
  2. 获取数据可以通过 $store.state
  3. 可以使用计算属性优化模板中获取数据的方式
  4. 计算属性不可以使用箭头函数(箭头函数本身是没有this的,实际上用的是父级函数中的this)

mapState

目标:简化获取store数据的代码

  • 把vuex中的state数据映射到组件的计算属性中。
  1. import { mapState } from 'vuex'
  1. 使用:mapState(对象)
  1. // 使用mapState来生成计算属性 mapState函数返回值是对象
  2. // 使用mapState使用对象传参
  3. // computed: mapState({
  4. // // 1. 基础写法 (state) 代表就是vuex申明的state
  5. // // count: function(state) {
  6. // // return state.count
  7. // // }
  8. // // 2. 使用箭头函数
  9. // // count: state => state.count
  10. // // 3. vuex提供写法 (count是state中的字段名称)
  11. // count: 'count',
  12. // // 4. 当你的计算属性 需要依赖vuex中的数据 同时 依赖组件中data的数据
  13. // count (state) {
  14. // return state.count + this.num
  15. // }
  16. // })
  1. 使用:mapState(数组)
  1. // 2、mapState参数是一个数组
  2. // computed: mapState(['count', 'total'])
  1. 如果组件自己有计算属性,state的字段映射成计算属性
  1. // 3、即在内部保留原有的计算属性,又要把store中的数据映射为计算属性
  2. computed: {
  3. // 组件自己的计算属性
  4. calcNum () {
  5. return this.num + 1
  6. },
  7. // 把mapState返回值那个对象进行展开操作(把对象的属性添加到该位置)
  8. ...mapState(['count'])
  9. }

总结: 1、是否组件的所有数据都应该放到Store中?不一定(数据仅仅需要在本组件使用,那么没有必要放到Store),放到Store中的数据一般需要多个组件共享。 2、mapState

  • 基本使用
  • 简化用法
  • 自定义和映射计算属性结合。

状态修改mutations

状态修改基本操作

目标:Vuex规定必须通过mutation修改数据,不可以直接通过store修改状态数据。 为什么要用mutation方式修改数据?Vuex的规定 为什么要有这样的规定?统一管理数据,便于监控数据变化

  • 定义状态修改函数
  1. // mutations是固定的,用于定义修改数据的动作(函数)
  2. mutations: {
  3. // 定义一个mutation,用于累加count值
  4. // increment这个名字是自定义的
  5. increment (state, payload) {
  6. // state表示Store中所有数据
  7. // payload表示组件中传递过来的数据
  8. state.count = state.count + payload
  9. },
  10. decrement (state, payload) {
  11. state.count = state.count - payload
  12. }
  13. }
  • 组件中调用
  1. methods: {
  2. handleClick1 () {
  3. // 通过触发mutation修改state中的count的值
  4. this.$store.commit('increment', 2)
  5. },
  6. handleClick2 () {
  7. this.$store.commit('decrement', 1)
  8. }
  9. },

总结:

  1. 先定义(mutations),再出发 this.$store.commit(‘mutation的名称,参数’)
  2. mutation的本质就是方法,方法名称自定义,mutation函数内部负责处理的变更操作。
  3. 一种操作就是一个mutation,不同的mutation处理不同的场景。

mapMutations

  • 把vuex中的mutations的函数映射到组件的methods中
  • 通俗:通过mapMutations函数可以生成methods中函数
  1. methods: {
  2. // 1、对象参数的写法
  3. // ...mapMutations({
  4. // // 冒号右侧的increment是mutation的名称
  5. // // 冒号左侧的increment是事件函数的名称,可以自定义
  6. // increment: 'increment'
  7. // })
  8. // 2、数组参数的写法(事件函数名称和mutation名称一致)
  9. ...mapMutations(['increment'])
  10. // 3、这种写法和第2种等效
  11. // increment (param) {
  12. // // 点击触发该函数后要再次触发mutation的
  13. // this.$store.commit('increment', param)
  14. // }
  15. }

总结:

  1. mapMutations函数的作用:简化methods的定义
  2. 原始方式:通过$store.commit方法触发mutation
  3. 简写方式一:对象写法
  4. 简写方式二:数组写法

异步操作action

异步获取数据

目标:主要用于处理异步的任务

  • 定义获取数据方法
  1. // actions是固定的,用于定义异步操作的动作(函数)
  2. actions: {
  3. // 定义了一个action,用于查询接口数据
  4. async queryData (context, payload) {
  5. console.log(payload)
  6. // 调用接口获取数据
  7. const ret = await axios.get('http://test.zjie.wang/tab')
  8. // 必须触发mutation修改list的值
  9. // context类似于this.$store
  10. context.commit('updateList', ret.data.list)
  11. }
  12. },
  13. mutations: {
  14. updateList (state, payload) {
  15. state.list = payload
  16. }
  17. }
  • 组件使用:
  1. methods: {
  2. handleQuery () {
  3. // 触发action(必须调用dispatch方法)
  4. this.$store.dispatch('queryData', 111)
  5. }
  6. }

总结:

  1. action的作用:处理异步任务,获取异步结果后,把数据交给mutation更新数据
  2. 触发action需要使用 this.$store.dispatch

mapActions

  • mapActions辅助函数,把actions中的函数映射组件methods中
  • 通俗:通过mapActions函数可以生成methods中函数
  1. // 相当于 methods申明了一个函数fn(num){ this.$store.dispatch('queryData', num)}
  2. // ...mapActions({
  3. // fn: 'queryData'
  4. // })
  5. // 相当于 methods申明了一个函数getData(num){ this.$store.dispatch('getData', num)}
  6. ...mapActions(['queryData'])

总结:

  1. 原始方式:this.$store.dispatch(‘queryData’, num)
  2. 简化方式一:对象
  3. 简化方式二:数组

getters用法

目标:熟悉getters的应用场景和具体使用步骤

  • 先定义getters
  1. // 相当于state的计算属性(基于State处理成另外一份数据)
  2. // getters的主要应用场景:模板中需要的数据和State中的数据不完全一样
  3. // 需要基于state中的数据进行加工处理,形成一份新的的数据,给模板使用
  4. getters: {
  5. getPartList (state) {
  6. return state.list.filter(item => {
  7. return item.id > 1
  8. })
  9. }
  10. }
  • 再使用getters
  1. 基本使用
  1. caleList () {
  2. // 注意:获取getters的值,不需要加括号(当属性使用)
  3. return this.$store.getters.getPartList
  4. },
  1. 简化用法
  1. import { mapGetters } from 'vuex'
  2. // mapGetters的作用:把getters映射为计算属性
  3. computed: {
  4. ...mapGetters(['getPartList']),
  5. // ...mapGetters({
  6. // calcList: 'getPartList'
  7. // }),
  8. // calcList () {
  9. // // 注意:获取getters的值,不需要加括号(当属性使用)
  10. // return this.$store.getters.getPartList
  11. // },
  12. }

image.png

总结:

  1. getters相当于在State和组件之间添加一个环节(对state中的数据进行加工处理后再提供给组件)
  2. getters不要修改state中的数据
  3. getters类似之前的计算属性(基于state中的数据进行计算)

总结

  • 基于发布订阅模式处理非父子组件之间的数据传递
    • const eventBus = new Vue()
    • 订阅 eventBus.$on(‘事件名称’,事件处理函数)
    • 发布 eventBus.$emit(‘事件名称’, 传递的数据)
  • vuex
    • vuex的核心思想:组件数据统一管理(统一存储,统一修改)
    • vuex具体实现的核心概念
      • state 用来保存组件的数据
        • 基本使用
        • 简化用法 mapState
      • mutations 用来修改state中的数据(数据是响应式的)
        • 基本使用
        • 简化用法 mapMutations
      • actions 用来处理异步任务,获取异步的结果,但是不可以修改数据
        • 基本使用
        • 简化用法 mapActions
      • getters 用来处理state中的数据,方便给组件提供符合需求的数据格式
        • 基本使用
        • 简化用法 mapGetters

Vuex案例实战

豆瓣接口介绍

豆瓣接口地址支持jsonp但是不支持cors。

注意:

  • 豆瓣的接口请求限制,每个外网IP有请求次数限制。
  • 豆瓣的图片访问显示,非豆瓣域名下发起的图片请求不给予响应。
  • 近期:官方停用搜索相关接口,必须要注册豆瓣api平台获取认证apikey才行。
  • 网友提供 apikey=0df993c66c0c636e29ecbb5344252a4a

总结:

  1. 实现案例的基本路由配置
  2. 基于vuex管理相关的数据
  3. 电影列表功能
  4. 电影详情功能

初始化项目

目标:基于脚手架初始化项目

  • main.js
  1. import Vue from 'vue'
  2. import App from './App.vue'
  3. import store from './store'
  4. import router from './router'
  5. Vue.config.productionTip = false
  6. new Vue({
  7. render: h => h(App),
  8. store,
  9. router
  10. }).$mount('#app')
  • store.js
  1. // 管理数据
  2. import Vuex from 'vuex'
  3. import Vue from 'vue'
  4. Vue.use(Vuex)
  5. const store = new Vuex.Store({
  6. state: {
  7. list: []
  8. }
  9. })
  10. export default store
  • router.js
  1. // 路由相关功能
  2. import Vue from 'vue'
  3. import VueRouter from 'vue-router'
  4. import Home from '../views/movie/index.vue'
  5. Vue.use(VueRouter)
  6. const router = new VueRouter({
  7. // TODO 路由规则
  8. routes: [
  9. {
  10. path: '/',
  11. name: 'Home',
  12. component: Home
  13. }
  14. ]
  15. })
  16. export default router

总结:

  1. 实现项目的基本结构:路由和vuex的配置
  2. 实现主页的基本布局(导入默认提供的样式和字体图标)

配置路由及组件

  • 封装组件
    • 头部组件 NavBar.vue
    • 底部组件 BottomMenu.vue
    • 路由组件
      • 页面根组件 index.vue
      • 正在热映 HotMovie.vue
      • 即将上映 WillMovie.vue
      • top250 TopMovie.vue
      • 电影详情 DetailMovie.vue
  • 路由规则
    • /
    • /hot
    • /movie
    • /top
    • /detail
      1. // 配置路由映射
      2. const router = new VueRouter({
      3. routes: [
      4. {
      5. path: '/',
      6. name: 'Home',
      7. component: Home,
      8. redirect: '/hot',
      9. children: [
      10. { path: 'hot', component: HotMovie },
      11. { path: 'will', component: WillMovie },
      12. { path: 'top', component: TopMovie },
      13. { path: 'detail', component: DetailMovie }
      14. ]
      15. }
      16. ]
      17. })
      1. <template>
      2. <div id="app">
      3. <!-- 顶部导航栏 -->
      4. <nav-bar></nav-bar>
      5. <!-- 中间路由填充位 -->
      6. <div class="wrapper">
      7. <router-view></router-view>
      8. </div>
      9. <!-- 底部菜单 -->
      10. <bottom-menu></bottom-menu>
      11. </div>
      12. </template>

总结:根据功能模块,把页面进行组件化拆分并且配置整体的路由效果

电影列表功能

第一步:申明数据,根据页面需要的数据进行申明。

  1. state: {
  2. mlist: []
  3. },

第二步:定义修改数据的方法

  1. // mutations
  2. mutations: {
  3. // 初始化热映列表
  4. initMlist (state, payload) {
  5. state.mlist = payload
  6. }
  7. },

第三步:获取数据的方法

  1. // actions
  2. actions: {
  3. // 获取电影热映列表
  4. async movieList (context) {
  5. // 调用接口获取热映列表数据
  6. const ret = await axios.get('http://test.zjie.wang/api/hot')
  7. // 把异步获取的数据交给mutation,从而修改state中的数据
  8. context.commit('initMlist', ret.data.hot)
  9. }
  10. }

第四步:调用获取数据的方法

  1. methods: {
  2. ...mapActions(['movieList'])
  3. },
  4. created () {
  5. // 触发加载热映数据的action
  6. // this.$store.dispatch('movieList')
  7. this.movieList()
  8. }

第五步:获取vuex的数据

  1. computed: {
  2. // ...mapState(['mlist']),
  3. ...mapState({
  4. list: 'mlist'
  5. })
  6. // list () {
  7. // // 获取state中的热映数据
  8. // return this.$store.state.mlist
  9. // }
  10. },

第六步:渲染页面

  1. <ul class="list">
  2. <li v-for='item in list' :key='item.id'>
  3. <a href="./item.html">
  4. <img :src="item.img">
  5. <div class="info">
  6. <h3>{{item.title}}</h3>
  7. <p>豆瓣评分:8</p>
  8. <p><span class="tag" :key='index' v-for='(tag, index) in item.tags.split(",")'>{{tag}}</span></p>
  9. </div>
  10. </a>
  11. </li>
  12. </ul>

总结:

  1. 封装action,调用接口获取数据
  2. 封装mutation,用于修改列表数据
  3. 导入状态数据(原始、映射写法)
  4. 导入action方法(原始、映射写法)
  5. 触发action (原始的方式触发dispatch方法)
  6. 模板填充

电影详情功能

  • 电影列表 电影的详情地址 都不一样 都会来到电影详情组件
    • 使用动态路由功能 /detail/:id ; /detail?id=123
    • 电影详情组件获取id获取详情数据 $route.params.id ; $route.query.id

第一步:路由规则

  1. { path: '/detail', component: DetailMovie }
  1. <router-link :to='{path: "/detail", query: {id: item.id}}'>

第二步:准备数据

  1. state: {
  2. mlist: [],
  3. movieInfo: null
  4. },

第三步:修改数据函数

  1. mutations: {
  2. // 更新电影详情数据
  3. updateMovie (state, payload) {
  4. state.movieInfo = payload
  5. }
  6. },

第四步:获取数据去修改数据的函数

  1. actions: {
  2. // 获取电影详情数据
  3. async movieDetail (context, id) {
  4. // 调用接口获取详情数据
  5. const ret = await axios.get('http://test.zjie.wang/api/hot/' + id)
  6. // 触发mutation更新详情数据
  7. context.commit('updateMovie', ret.data)
  8. }
  9. }

第五步:在组件使用数据

第六步:在组件初始化获取数据

  1. import { mapState, mapActions } from 'vuex'
  2. export default {
  3. name: 'DetailMovie',
  4. computed: {
  5. // 这里获取的默认值是null
  6. // 接口调用成功后会给他进行初始化
  7. ...mapState(['movieInfo'])
  8. },
  9. methods: {
  10. ...mapActions(['movieDetail'])
  11. },
  12. created () {
  13. // 调用接口(根据电影的id获取详情数据)
  14. this.movieDetail(this.$route.query.id)
  15. }
  16. }

第七步:渲染页面

  1. <div class="item" v-if='movieInfo'>
  2. <img :src="movieInfo.img" alt="">
  3. <div>
  4. <p>豆瓣评分:{{movieInfo.score}}</p>
  5. <p>产地:{{movieInfo.country}}</p>
  6. <p><span class="tag" :key='index' v-for='(item,index) in movieInfo.tags.split(",")'>{{item}}</span></p>
  7. <p>{{movieInfo.summary}}</p>
  8. </div>
  9. </div>

总结:

  1. 熟悉路由的参数传递用法:动态路由;查询字符串传参
  2. 封装action
  3. 封装mutation
  4. 导入状态
  5. 映射action方法
  6. 触发action方法的调用
  7. 渲染模板(需要判断存在性)

关于路由的参数传递

目标:熟悉采用props方式向路由组件传递参数的方式

  • 路由的链接写法
  1. <router-link :to='"detail/" + item.id + "?title=hot"' >
  • 路由的映射写法
  1. {
  2. path: 'detail/:id',
  3. component: Detail,
  4. // 箭头后面的小括号不可以省略(默认会return这个对象)
  5. // <router-link :to='"/detail/" + item.id + "?title=hot"' >
  6. // to = /detail/99?title=hot
  7. // 此时路由向组件注入了几个属性?2个(title和id)
  8. props: (route) => ({ title: route.query.title, id: route.params.id })
  9. }
  • 组件中获取路由参数
  1. export default {
  2. name: 'Detail',
  3. // 通过props获取路由参数
  4. props: ['id', 'title'],

总结:路由映射中,props取值集中情况? 1、布尔值,route.params的参数会变成组件的props 2、对象,对象中的属性直接变成组件的props 3、函数,函数返回值是一个对象,对象中的属性会变成组件的props

Store模块化拆分

Store中代码越来越多,不方便后续的维护和扩展

拆分Store模块

  • 入口文件 store/index.js
  1. import Vue from 'vue'
  2. import Vuex from 'vuex'
  3. import listModule from '@/store/module-list.js'
  4. import detailModule from '@/store/module-detail.js'
  5. Vue.use(Vuex)
  6. const store = new Vuex.Store({
  7. modules: {
  8. list: listModule,
  9. detail: detailModule
  10. }
  11. })
  12. export default store
  • 模块内部代码 store/module-list.js
  1. export default {
  2. state: () => ({}),
  3. mutations: { },
  4. actions: { },
  5. getters: { }
  6. }

总结

  1. 不同的功能可以拆分为不同的js模块
  2. 在入口文件中可以导入所有的模块并进行统一配置
  3. 拆分模块后,后续的维护会变得方便。

模块成员的访问规则

  • namespaced属性的作用:把模块变成隔离的区域,那么访问模块成员时,需要添加前缀(模块名称)
  • 如果不加这个属性,那么模块中定义的成员都是全局的,如果重名就会冲突
  • 全局和局部模块的访问规则的区别
  1. 全局模式下(不加namespaced)
  1. // 在不添加命名空间的前提下(模块中不添加namespaced),安装如下的方式获取模块的状态数据
  2. ...mapState({
  3. msg: state => state.detail.msg
  4. })
  1. 局部模块下(添加namespaced)
  1. // 如果模块添加的namespaced,那么可以按照如下方式简化
  2. // 如下的参数一表示模块名称,参数二表示state中属性名称
  3. ...mapState('detail', ['msg'])

访问模块成员的方法

  1. 原始写法(state之后多了一个模块名称)
  1. msg () {
  2. return this.$store.state.detail.msg
  3. }
  1. 简化为对象写法(通过形参进行优化,不再需要this.$store)
  1. ...mapState({
  2. msg: state => state.detail.msg
  3. })
  1. 简化为数组写法
  1. ...mapState('detail', ['msg', 'info'])
  1. 通过辅助函数进行简化
  1. import { createNamespacedHelpers } from 'vuex'
  2. // 参数是模块的名称
  3. const { mapState, mapActions } = createNamespacedHelpers('detail')
  4. computed: {
  5. ...mapState(['msg', 'info'])
  6. }
  7. methods: {
  8. // ...mapActions('detail', ['getInfo'])
  9. ...mapActions(['getInfo'])
  10. },

组件同时访问多个模块的成员

  1. 映射多次实现多模块导入
  1. methods: {
  2. ...mapActions('list', {
  3. getInfo1: 'getInfo'
  4. }),
  5. ...mapActions('detail', {
  6. getInfo2: 'getInfo'
  7. })
  8. }
  9. created () {
  10. this.getInfo1()
  11. this.getInfo2()
  12. }
  1. 通过数组方式映射一次
  1. methods: {
  2. ...mapActions([
  3. 'list/getInfo',
  4. 'detail/getInfo'
  5. ])
  6. }
  1. created () {
  2. this['list/getInfo']()
  3. this['detail/getInfo']()
  4. }

总结

  • 熟悉整体案例需求:电影列表;电影详情
  • 核心知识模块:vue-router;vuex
  • 项目路由配置:一级路由和二级路由
  • 基于vuex实现功能:电影列表和电影详情
    • 创建action
    • 创建mutation
    • 组件中导入action
    • 组件中触发action
    • 组件中使用state中的数据
  • 状态数据的模块化管理
    • 好处:方便后期的维护和扩展
    • 基本的拆分流程
      • 通过modules属性配置模块
      • 每一个模块内部包含独立的状态信息:state/mutations/actions/getters
    • 组件中如何使用模块的成员
      • 全局模块 (不包含namespaced属性):不同模块的相同成员名称会冲突
      • 局部模块(包含namespaced属性):不同模块的相同成员名称不会冲突,但是访问时需要添加前缀(模块名称)
    • 访问模块成员方法的简化写法
      • 原始写法
      • 简化为对象方式
      • 简化为数组方式
      • 利用辅助函数
    • 同一组件访问多个模块的用法
      • 多次导入(推荐)
      • 单词导入
    • 基于模块化方式重构案例功能:电影列表和电影详情