Vuex学习笔记

Vue最核心的两个功能:数据驱动和组件化。
每个组件都有自己的状态、视图和行为等部分组成。

new Vue({
	//state
	data(){
		return {
			count:0
		}
	},
	//view
	template:`
		<div>{{count}}</div>
	`,
	//actions
	methods:{
		increment(){
			this.count++;	
		}
	}
}) 

状态管理包括:
(1)state,驱动应用的数据源
(2)view,以声明方式将state映射到视图
(3)actions,响应在view上的用户输入导致的状态变化。

简易的状态管理方案

如果多个组件之间要共享状态(数据),使用上面的方式虽然可以实现,但是比较麻烦,而且多个组件之间互相传值很难跟踪数据的变化,如果出现问题很难定位问题。
当遇到多个组件需要共享状态的时候,典型的场景:购物车。我们如果使用上述的方案都不合适,我们会遇到以下的问题

  • 多个视图依赖于同一状态。
  • 来自不同视图的行为需要变更同一状态。

对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。
对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。

因此我们把组件的共享状态抽取出来,以一个全局单例模式管理。在这种模式下,我们的组件树构成了一个巨大的视图,不管树在哪里,任何组件都能获取状态或者触发行为。

我们可以把多个组件的状态,或者整个程序的状态放到一个集中的位置存储,并且可以检测到数据的更改。

  • (1)首先创建一个共享的仓库store对象。
export defualt {
	debug:true,
	state: {
		user: {
			name:'xiaoming',
			age:18,
			sex:'男'
		}
	},
	setUserNameAction(name) {
		if(this.debug) {
			console.log('setUserNameAction trigged: ',name)
		}
		this.state.user.name = name
	}
}
  • (2)把共享的仓库store对象,引入到需要共享状态的组件的data中。
import store from './store'
export default {
	methods:{
		//点击按钮的时候通过action修改状态
		change() {
			store.setUserNameAction('componentB')
		}
	},
	data() {
		return {
			privateState:{},
			sharedState:store.state
		}
	}
}

接着我们继续延伸约定,组件不允许直接变更属于 store 对象的 state,而应执行 action 来分发(dispatch) 事件通知 store 去改变,这样最终的样子跟 Vuex 的结构就类似了。这样约定的好处是,我们能够记录所有 store 中发生的 state 变更,同时实现能做到记录变更、保存状态快照、历史回滚/时光旅行的先进的调试工具。

Vuex核心概念

什么是Vuex

  • Vuex是专门为Vue.js设计的状态管理库
  • Vuex采用集中式方式的存储需要共享的状态
  • 从使用角度,它就是一个JavaScript库
  • 它的作用就是进行状态管理,解决复杂组件通信,数据共享。

为什么要使用Vuex

如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex。一个简单的 store 模式就足够您所需了。但是,如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。

当你的应用中具有以下需求场景的时候:

  • 多个视图依赖于同一个状态
  • 来自不同视图的行为需要变更同一个状态
    建议这种场景的业务使用Vuex进行数据管理,例如非常典型的场景:购物车。
    注意:不要滥用Vuex,不符合以上的业务不要使用,反而会让你的应用变得更加复杂。

Vuex代码结构

在使用Vue CLI生成的项目时会让你选择store,选择了后会在页面给你生成一个store.js,这就是最初的store,里面结构如下:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {

  },
  mutations: {

  },
  actions: {

  }
})

State

Vuex 使用单一状态树,用一个对象就包含了全部的应用层级状态。
State负责存储整个应用的状态数据,一般需要在使用的时候在跟节点注入store对象,后期就可以使用this.$store.state直接获取状态。
比如我们在store.js中定义一个状态count:

export default new Vuex.Store({
  state: {
    count: 10
  },
})

这样我们就有一个集中管理的状态count,可以在组件的计算属性中来获取

export default {
  data() {
    
  },
  computed: {
    count() {
      return this.$store.state.count;
    }
  }
}

mapState

使用 mapState 简化 State 在视图中的使用,mapState 返回计算属性。
有时候需要获取多个状态,但是使用计算属性会调用多次,显得麻烦,这里借助mapState方法来获取state。 使用mapState需要引入该方法。

import { mapState } from 'vuex';

注意:这里使用了mapState方法后,computed的写法有点区别,比如默认情况你的computed属性是这样写的:

data(){
  return{
    msg: 'hello '
  }
}
computed: {
  msg() {
    return this.msg + 'world!';
  }
}

那么你使用了mapState后需要这样写computed,把msg()放入mapState,不然会报错。

data(){
  return{
    msg: 'hello ',
    localCount: 20
  }
}
computed: mapState({
  msg() {  // 最初的
    return this.msg + 'world!';
  },
  // 使用mapState从store中引入state
  count(state) {
    return state.count;
  },
  name(state) {
    return state.firstName + ' ' + state.lastName;
  },
  mixCount(state) { // 结合store和组件状态进行计算
    return state.count + this.localCount;
  },
})

如果你使用了展开运算符…,那么computed属性不需要改造,按正常写法写

computed: { // 使用展开的话可以按这种方式写,否则要使用另外一种方式,不然要报错
  msg() {
    return this.$store.state.msg;
  },
  // 这里返回一个状态count,
  // 返回多个你可以这样写...mapState(['count', 'firstName', 'lastName'])
  ...mapState(['count'])
},

Getter

getter就是对状态进行处理的提取出来的公共部分,当状态需要进行筛选这些操作时,我们可以通过getter处理过来在返回给组件使用,比如我们在state部分定义了一个list数组:

export default new Vue.Store({
	state:{
		list:[1,2,3,4,5,6]
	}
})

我们想要筛选出来数组里面的偶数然后再在组件中使用,那么这个筛选可以放在getter里面来完成。

export default new Vuex.Store({
	state:{
		list:[1,2,3,4,5,6]
	},
	getters { //这个主要是对状态的处理,相当于把状态处理的方法抽成公共部分来管理了
		mofifyArr(state) { //一般化getter
			return state.list.filter((item,index.arr)=>{
				return item % 2 === 0
			})
		},
		getLength(state,getter) { //方法里面传getter,调用modifyArr来计算长度
			reutrn getter.modifyArr.length
		}
	}
})

之后再在其他组件的computed里面去调用getter来获取想要的状态

computed: {
    list() {
      return this.$store.getters.modifyArr;
    },
}

mapGetters

mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性,当我们想在组件里面引入多个getter时,可以使用mapGetter:

import {mapGetters} from 'vuex';

比如像刚才在在上面定义的modifyArr,getLength。我们想引入这个两个并获取其值:

computed: {
  ...mapGetter(['modifyArr', 'getLength'])
}

你当然可以为其指定别名,不一定非得用store里面getters定义的名字:

computed: {
  mapGetter({
    arr: 'modifyArr',   // 把 `this.arr` 映射为 `this.$store.getters.modifyArr`,下面同理
    length: 'getLength'
  })
}

如果你的computed属性包含其他计算方法,那你就只能使用展开运算符的写法,这里跟mapState有点区别,其他计算属性如果写在mapGetter里面会报错,说不存在的getter,所以用以下的写法:

computed: {
  msg() {
    return this.num * 10;
  },
  ...mapGetters([
    'modifyArr',
    'getLength'
  ])
}

或者指定别名

computed: { 
  msg() {
    return this.num * 10;
  },
  ...mapGetters({
    getList: 'modifyArr',
    length: 'getLength'
  })
}

Mutation

当我们需要修改store里面的状态时,我们不是在组件里面直接去修改它们,而是通过mutation里面的方法来进行修改,这样有利于追踪状态的改变。 比如state里面有一个count变量,我们点击加减按钮来控制它的值:

mutations: {
  add(state) {
    state.count++;
  },
  reduce(state) {
    state.count--;
  }
},

在其他组件里面,我们通过定义methods并绑定时间来触发改变,这里需要使用commit:

methods: {
  add() {
    this.$store.commit('add');
  },
  reduce() {
    this.$store.commit('reduce');
  }
}

提交载荷

这个就是在commit时提交额外的参数,比如我传了额外的值加到count上面:

mutations: {
  loadAdd(state, payload) {  // 提交载荷,额外参数
    state.count += payload;
  }
},

然后再组件里面使用:

methods: {
  loadAdd() {
    this.$store.commit('loadAdd', 100); // 传递额外参数
  }
}

再这里官方文档建议载荷(也就是那个额外参数)最好使用对象来传,这样可以包含多个字段并且记录的 mutation 会更易读,像这样:

this.$store.commit('loadAdd', {
  extraCount: 100
}); // 传递额外参数

调用commit时我们也可以把所有参数写在一个对象里面:

this.$store.commit( {
  type: 'addLoad'
  extraCount: 100
}); // 传递额外参数

Mutation 需遵守 Vue 的响应规则

这个主要是说你再开发过程中需要向state里面添加额外数据时,需要遵循响应准则。 这里我直接照搬官方文档的说明: Vuex 中的 mutation 也需要与使用 Vue 一样遵守一些注意事项:

  • 最好提前在你的 store 中初始化好所有所需属性。
  • 当需要在对象上添加新属性时,你应该使用 Vue.set(obj, ‘newProp’, 123), 或者以新对象替换老对象。例如,利用 stage-3 的对象展开运算符
state.obj = { ...state.obj, newProp: 123 }

还是举个栗子: 我在mutation里面声明了一个方法

addNewState(state, payload) { // 我打算再这儿添加新的属性到state
  // Vue.set(state, 'newProp', '添加一个新值!'); // 这是一种写法
  // 这种写法用新对象替换老对象
  // state= {...state, newProp: '添加一个新值!'} // 这个玩意儿不管用了,用下面的replaceState()方法
  this.replaceState({...state, newProp: '添加一个新值!'})
}

然后再组件里面去调用,定义一个method:

addNewProp() {
  this.$store.commit('addNewState', {});
}

这样再执行了这个方法后,会及时更新到state,再组件的computed属性里面定义:

newMsg() {
  return this.$store.state.newProp || '还没添加新值';
}

Mutation 必须是同步函数

下面这种写法必须避免(直接官方例子加持):

mutations: {
  someMutation (state) {
    api.callAsyncMethod(() => {
      state.count++
    })
  }
}

mapMutations

import {mapMutations} from 'vuex';

然后在组件的methods里面使用,这里使用官方代码:

export default {
  // ...
  methods: {
    ...mapMutations([
      'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`

      // `mapMutations` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
    ]),
    ...mapMutations({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
    })
  }
}

Action

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作。
    前面说过mutation只能包含同步事务,所以在处理异步事务就需要Action,通过Action控制了异步这一过程,之后再去调用mutation里面的方法来改变状态。
    这里我直接贴代码来一目了然,首先我定义了一个状态product:
state: {
  product: 'car'
}

然后再mutation中定义一个方法:

changeProduct(state, payload) {
  state.product = payload.change;
}

在action中定义:

actions: {
  changeProduct(context, payload) { // 这个context是一个与 store 实例具有相同方法和属性的对象
    // 调用mutation里的changeProduct方法
    // context.commit('changeProduct', {change: 'ship'});
    // 改成异步方式
    // setTimeout(() => {
    //   context.commit('changeProduct', {change: 'ship'});
    // }, 1500)
    // 使用载荷
    let temp = 'ship+' + payload.extraInfo; 
    setTimeout(() => {
      context.commit('changeProduct', {change: temp});
    }, 1500)
  }
}

在组件methods中定义事件触发分发:

methods: {
  selectProduct() {
    // this.$store.dispatch('changeProduct')
    // 载荷方式分发
    // this.$store.dispatch('changeProduct', {
    //   extraInfo: 'sportcar'
    // })
    // 或者这种
    this.$store.dispatch({
      type: 'changeProduct',
      extraInfo: '->sportcar'
    })
  }
},

mapActions

import {mapActions} from 'vuex';
export default {
  // ...
  methods: {
    ...mapActions([
      'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`

      // `mapActions` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
    ]),
    ...mapActions({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
    })
  }
}

有时候我们想知道action里面异步执行后的状态然后再去修改其他信息,这个可以借助Promise来实现。这里在state里面声明一个状态:

state: {
  userInfo: { // 这个变量用来测试组合变量
    name: 'lee',
    age: 23
  }
}

接着声明mutation:

mutations: {
    // 以下用来测试组合action
    changeInfo(state, payload) {
      state.userInfo.name = 'lee haha';
    }
}

声明action:

actions: {
  changeInfo(context, payload) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        context.commit('changeInfo');
        resolve();
      }, 2000)
    })
  }
}

这时我们在组件里面定义方法去派发这个action:

data() {
  return {
    status: '信息还没修改!'
  }
}
methods: {
  modifyInfo() {
    this.$store.dispatch('changeInfo').then(() => {
      this.status = '信息修改成功';
    });
  }
}

当我们点击修改信息后,就会派发action,当修改成功的时候会同步修改上面说的展示信息。 当然其他定义的action方法也可以互相使用,这里直接贴官方代码了:

actions: {
  actionA ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('someMutation')
        resolve()
      }, 1000)
    })
  },
  actionB ({ dispatch, commit }) {
    return dispatch('actionA').then(() => {
      commit('someOtherMutation')
    })
  }
}

Module

当所有状态集中在一个对象中时,会变的相当臃肿,这个时候就需要模块的管理办法。
比如我在store里面定义了两个模块:

// 定义的模块A
const moduleA = {
  state: {
    name: 'lee',
    age: 23,
  },
  mutations: {

  },
  getters: {

  },
  actions: {

  }
};
// 定义模块B
const moduleB = {
  state: {
    name: 'wang',
    age: 22
  },
  mutations: {

  },
  getters: {

  },
  actions: {

  }
}

然后再Vuex里面声明模块:

export default new Vuex.Store({
  modules: {
    ma: moduleA,
    mb: moduleB
  },
  state: {
    ........... // 其他状态
  }
});

这样一来,如果我们想要在组件里面访问其他模块的状态,可以这样,比如这里我想调用B模块里的状态:

computed: {
  msg() {
    return this.$store.mb; // 这里返回的是:{name: 'wang', age: 22}
  }
}

关于模块内部的局部状态,这里跟普通的store用法没有多大的区别,主要区别以下外部传进来的状态,比如对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState,这里截取官方代码:

const moduleA = {
  // ...
  actions: {
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  }
}

对于模块内部的 getter,根节点状态会作为第三个参数暴露出来:

const moduleA = {
  // ...
  getters: {
    sumWithRootCount (state, getters, rootState) {
      return state.count + rootState.count
    }
  }
}

那么对于getters、mutations、actions里面的方法我们像基本的store那样调用就可以了,不存在作用域限制,还是贴代码栗子吧,下面是我在store.js里面定义的模块B:

const moduleB = {
  state: {
    name: 'wang',
    age: 22,
    desc: 'nope'
  },
  mutations: {
    modifyDesc(state, payload) {
      state.desc = payload.newMsg;
    }
  },
  getters: {

  },
  actions: {

  }
}

在组件里面,我定义了以下内容:

<template>
  <div>
    <h2>7、module使用示例</h2>
    <div>
      <p>名字:{{ name }}</p>
      <p>描述:{{ desc }}</p>
      <button @click="handleClick">修改描述</button>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      name: this.$store.state.mb.name,
      // desc: this.$store.state.mb.desc 注意这个如果涉及到要在store里面会被改变的状态,一定要写在
      // computed属性里面,不然不能及时反馈到视图上
    }
  },
  computed: {
    desc() {
      return this.$store.state.mb.desc;
    }
  },
  methods: {
    handleClick() {
      this.$store.commit('modifyDesc', {newMsg: 'lao wang is beautiful!'});
    }
  },
}
</script>

命名空间模块

默认情况下,mutations、actions、getters这些都是注册在全局上面的,你可以直接调用,如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。
首先我新建一个js文件用来声明模块C:

/* 
* 这个文件用来声明模块C
*/

export const moduleC = {
  namespaced: true,
  state: {
    name: 'moduleC',
    desc: '这是模块C,用来测试命名空间的!',
    list: [1, 2, 3, 4]
  },
  getters: {
    filterList(state) {
      return state.list.filter((item, index, arrSelf) => {
        return item % 2 !== 0;
      });
    }
  },
  mutations: {
    modifyName(state, payload) {
      state.name = payload.newName;
    }
  },
  actions: {
    
  }
}

然后在store.js里面引入:

import { moduleC } from './module_c.js';

export default new Vuex.Store({
  modules: {
    mc: moduleC
  },
});

要想这个模块成为带有命名空间的模块,在上面声明属性namespaced: true就可以了,那么里面的mutations、getters和actions里面的方法的调用就要多走一层路径,比如我在组件里面去调用mutations里面的方法(getters和actions同理):

methods: {
  modify() {
    // this.$store.commit('mc/modifyName', {
    //   newName: '命名空间模块C'
    // })
    this.$store.commit({
      type: 'mc/modifyName',
      newName: '命名空间模块C'
    })
  }
}

当然模块里面再嵌套模块也可以,路径要不要多走一层主要看你的namespaced: true有没有声明,这里贴一下官方的代码:

const store = new Vuex.Store({
  modules: {
    account: {
      namespaced: true,

      // 模块内容(module assets)
      state: { ... }, // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
      getters: {
        isAdmin () { ... } // -> getters['account/isAdmin']
      },
      actions: {
        login () { ... } // -> dispatch('account/login')
      },
      mutations: {
        login () { ... } // -> commit('account/login')
      },

      // 嵌套模块
      modules: {
        // 继承父模块的命名空间
        myPage: {
          state: { ... },
          getters: {
            profile () { ... } // -> getters['account/profile']
          }
        },

        // 进一步嵌套命名空间
        posts: {
          namespaced: true,

          state: { ... },
          getters: {
            popular () { ... } // -> getters['account/posts/popular']
          }
        }
      }
    }
  }
})

在带命名空间的模块内访问全局内容

如果想要在模块内部的getters、mutations和actions里面访问到全局的内容,这儿Vuex已经封装好了,你只需要多传几个参数即可。官方演示来一波,简单明了:

modules: {
  foo: {
    namespaced: true,

    getters: {
      // 在这个模块的 getter 中,`getters` 被局部化了
      // 你可以使用 getter 的第四个参数来调用 `rootGetters`
      someGetter (state, getters, rootState, rootGetters) {
        getters.someOtherGetter // -> 'foo/someOtherGetter'
        rootGetters.someOtherGetter // -> 'someOtherGetter'
      },
      someOtherGetter: state => { ... }
    },

    actions: {
      // 在这个模块中, dispatch 和 commit 也被局部化了
      // 他们可以接受 `root` 属性以访问根 dispatch 或 commit
      someAction ({ dispatch, commit, getters, rootGetters }) {
        getters.someGetter // -> 'foo/someGetter'
        rootGetters.someGetter // -> 'someGetter'

        dispatch('someOtherAction') // -> 'foo/someOtherAction'
        dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'

        commit('someMutation') // -> 'foo/someMutation'
        commit('someMutation', null, { root: true }) // -> 'someMutation'
      },
      someOtherAction (ctx, payload) { ... }
    }
  }
}

在模块里面使用辅助函数mapState、mapGetters、mapMutations和mapActions

由于存在命名空间,在组件里面采用上面的写法会出现问题,这里要想使用辅助函数来映射模块里面的东西需要指定空间名称来告诉辅助函数应该去哪儿找这些。
这儿我以上面我的C模块为例,首先对于mapSatate函数可以这样玩,我在全局的modules里面声明了mc,那我的空间名称就是mc:

computed: {
  ...mapState('mc', ['name', 'desc']) // 这里模块里面要使用辅助函数的话要多传一个参数才行
}

然后在模版里面写name,desc即可,或者可以这样:

computed: {
  ...mapState('mc', {
    name(state) {
      return state.name;
    },
    desc(state) {
      return state.desc;
    }
  })
},

对于actions、mutations和getters方式类似,主要是要指定空间名称,比如对于声明的mutations

methods: {
  ...mapMutations('mc', ['modifyName'])
}

如果你确实不想在每个辅助函数里写空间名称,Vuex也提供了其它办法,使用createNamespacedHelpers创建基于某个命名空间辅助函数,它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数:

import { createNamespacedHelpers } from 'vuex';

const { mapState, mapMutations } = createNamespacedHelpers('mc');
复制代码

模拟实现

实现思路

  • 实现 install 方法
    vuex是Vue的一个插件,先实现Vue插件约定的install方法。
  • 实现Store类
    (1)实现构造函数,接受options
    (2)state的响应化处理
    (3)getter的实现
    (4)commit、dispatch的方法

install方法

let _Vue = null 
function install (Vue) { 
	_Vue = Vue
	_Vue.mixin({ 
		beforeCreate () { 
			if (this.$options.store) { 
			Vue.prototype.$store = this.$options.store 
			} 
		} 
	})
}

Store 类

class Store { 
	constructor (options) { 
		const { 
			state = {}, 
			getters = {}, 
			mutations = {}, 
			actions = {} 
		} = options
		this.state = _Vue.observable(state)
		//此处不直接 this.getters = getters,是因为下面的代码中要方法 getters 中的 key
		//如果这么写的话,会导致 this.getters 和 getters 指向同一个对象
		//当访问 getters 的 key 的时候,实际上就是访问 this.getters 的 key 会触发 key 属性 的 getter
		//会产生死递归
	
		this.getters = Object.create(null)
		Object.keys(getters).forEach(key => { 
			Object.defineProperty(this.getters, key, {
			 get: () => getters[key](this.state)
			  }) 
		})
		this.mutations = mutations
		this.actions = actions
	}
	commit (type, payload) { 
		this.mutations[type](this.state, payload) 
	}
	
	dispatch (type, payload) { 
		this.actions[type](this, payload) 
	}
}

// 导出模块 
export default { Store, install }

使用自己实现的Vuex

src/store/index.js 中修改导入 Vuex 的路径,测试

import Vuex from '../myvuex' // 注册插件 
Vue.use(Vuex)

Vuex源码学习

https://lxchuan12.gitee.io/vuex/#_1-%E5%89%8D%E8%A8%80

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值