# Vuex

# 组件之间传值

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

  • 父组件向子组件传值 props
  • 子组件向父组件传值 $emit
  • 非父子组件之间传值 : 爷孙;兄弟,其他任何关系
    • 发布订阅模式

image-20210121084735174

  • 基于Vue实现发布订阅模式
// 相当于中介
const eventBus = new Vue()
// 订阅事件
eventBus.$on('event-b', (param) => {
  this.counta = this.counta + '----' + param
})
// 发布事件
eventBus.$emit('event-b', 123)
  • 完善黑马头条头像更新的功能
  1. 准备中介 eventBus
// 统一配置发布订阅模式
// 所有的组件都是Vue的实例对象
Vue.prototype.eventBus = new Vue()
  1. 订阅事件 Home.vue
created () {
  // 自动触发接口调用
  this.loadUserInfo()
  // 订阅头像更新事件
  this.eventBus.$on('update-avatar', () => {
    // 重新调用后端接口获取最新的用户信息
    this.loadUserInfo()
  })
}
  1. 发布事件 setting/index.vue
// 发布更新右上角头像的事件
this.eventBus.$emit('update-avatar')

# 状态管理必要性分析

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

image-20210120232626457

# Vuex介绍

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

image-20210119224725242

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

image-20210123111136131

  • 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'
  1. 使用: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
//   }
// })
  1. 使用:mapState(数组)
// 2、mapState参数是一个数组
// computed: mapState(['count', 'total'])
  1. 如果组件自己有计算属性,state的字段映射成计算属性
// 3、即在内部保留原有的计算属性,又要把store中的数据映射为计算属性
computed: {
  // 组件自己的计算属性
  calcNum () {
    return this.num + 1
  },
  // 把mapState返回值那个对象进行展开操作(把对象的属性添加到该位置)
  ...mapState(['count'])
}

# 状态修改mutations

# 状态修改基本操作

目标:Vuex规定必须通过mutation修改数据,不可以直接通过store修改状态数据。

为什么要用mutation方式修改数据?Vuex的规定

为什么要有这样的规定?统一管理数据,便于监控数据变化

image-20210316112730425

  • 定义状态修改函数
// 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

# 案例实战

# 豆瓣接口介绍

豆瓣接口地址支持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()
}

# 关于路由的参数传递

  1. props的值是 true (默认值就是true)
// route.params 将会被设置为组件属性props
  1. props的值是对象,那么它会被按原样设置为组件属性
{ path: 'detail/:id', component: MovieDetail, props: { id: 1, abc: 'hello' } }
  1. props的值是函数,函数的形参route等效于 $route
{ path: 'detail/:id', component: MovieDetail, props: (route) => ({ id: route.params.id, abc: 'nihao' }) }