vue-router:VueRouter模拟实现与源码解读

vue-router:VueRouter模拟实现与源码解读

1、Vue.use( )源码

源码位置:vue/src/core/global-api/use.js

export function initUse (Vue: GlobalAPI) {
    //use方法的参数接收的是一个插件,该插件的类型可以是一个函数,也可以是一个对象
  Vue.use = function (plugin: Function | Object) {
      //_installedPlugins数组中存放已经安装的插件。
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    //判断一下传递过来的插件是否在installedPlugins中存在,如果存在,则直接返回
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // additional parameters
      //将arguments转换成数组,并且将数组中的第一项去除。
    const args = toArray(arguments, 1)
    //把this(也就是Vue,这里是通过Vue.use来调用的)插入到数组中的第一个元素的位置。
    args.unshift(this)
      //这时plugin是一个对象,看一下是否有install这个函数。
    if (typeof plugin.install === 'function') {
        //如果有install这个函数,直接调用
        //这里通过apply将args数组中的每一项展开传递给install这个函数。
        // plugin.install(args[0],args[1])
        //而args[0],就是上面我们所插入的Vue.这一点与我们前面在模拟install方法的时候是一样的。
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
        //如果plugin是一个函数,则直接通过apply去调用
      plugin.apply(null, args)
    }
      //将插件存储到installedPlugins数组中。
    installedPlugins.push(plugin)
    return this
  }
}

2、install方法分析

我们先来看一下vue-router的目录结构
在这里插入图片描述
我们先来核心的文件。

components目录下面,有两个文件。分别为link.jsview.js文件。

link.js文件创建RouterLink组件

view.js文件创建RouterView组件。

history目录下的文件是记录路由的历史记录(hash.js文件是关于hash模式,html5.js关于html5的方式,base.js公共的内容,abstract.js是在服务端渲染中实现的路由历史记录)。

index.js文件是用来创建VueRouter

install.js文件是关于install方法

我们自己模拟的VueRouter也实现上面的目录结构。

下面先来在index.js文件中实现基本代码。

export default class VueRouter {
  //在创建VueRouter对象的时候,会传递选项
  constructor(options) {
    //获取routes选项,该选项中定义路由规则
    this._options = options.routes || [];
  }
  // 注册路由变化的事件。该方法的参数是一个Vue实例,后期完善
  init(Vue) {}
}

下面实现install.js基本代码(通过查看源代码来实现)

export let _Vue = null; //将其导出,在其它文件中也可以使用Vue实例,而不需要单独的引入Vue的js文件
export default function install(Vue) {
  //获取Vue构造函数
  _Vue = Vue;
  _Vue.mixin({
    //通过混入以后,所有的Vue实例中都会有beforeCreate方法
    beforeCreate() {
      //判断是否为Vue的实例,如果条件成立为Vue的实例,否则为其它对应的组件(因为在创建Vue实例的时候会传递选项)
      if (this.$options.router) {
        //通过查看源码发现,Vue的实例会挂在到当前的私有属性_routerRoot属性上
        this._routerRoot = this;

    
        this._router = this.$options.router;
        //调用index.js文件中定义的init方法
        this._router.init(this);
      } else {
      
        this._routerRoot = this.$parent && this.$parent._routerRoot;
      }
    },
  });
}

3、组件创建测试
下面需要将install方法挂载到VueRouter上。

import install from "./install";
export default class VueRouter {
  //在创建VueRouter对象的时候,会传递选项
  constructor(options) {
    //获取routes选项,该选项中定义路由规则
    this._routes = options.routes || [];
  }
  // 注册路由变化的事件。
  init(Vue) {}
}
//将install方法挂载到VueRouter上
VueRouter.install = install;

下面,我们可以简单实现一下Router-link组件与Router-view组件,来做一个简单的测试。(接下来讲解如下内容)

components目录下的view.js文件。

export default {
  render(h) {
    return h("div", "router-view");
  },
};

以上是Router-View组件的基本功能,后面在继续完善。

link.js文件的实现如下:

export default {
  props: {
    to: {
      type: String,
      required: true,
    },
  },
  render(h) {
      //通过插槽获取`a`标签内的文本。
    return h("a", { domProps: { href: "#" + this.to } }, [this.$slots.default]);
  },
};

install.js文件中,导入上面的组件进行测试。

import View from "./components/view";
import Link from "./components/link";
export let _Vue = null; //将其导出,在其它文件中也可以使用Vue实例,而不需要单独的引入Vue的js文件
export default function install(Vue) {
  //获取Vue构造函数
  _Vue = Vue;
  _Vue.mixin({
    //通过混入以后,所有的Vue实例中都会有beforeCreate方法
    beforeCreate() {
      //判断是否为Vue的实例,如果条件成立为Vue的实例,否则为其它对应的组件(因为在创建Vue实例的时候会传递选项)
      if (this.$options.router) {
        //通过查看源码发现,Vue的实例会挂在到当前的私有属性_routerRoot属性上
        this._routerRoot = this;

     
        this._router = this.$options.router;
        //调用index.js文件中定义的init方法
        this._router.init(this);
      } else {
    
        this._routerRoot = this.$parent && this.$parent._routerRoot;
      }
    },
  });
    //完成组件的注册
  Vue.component("RouterView", View);
  Vue.component("RouterLink", Link);
}

在上面的代码中,导入组件,并且完成组件的注册。

下面,我们测试一下。

src目录下,在router.js文件中导入自己定义的VueRouter.

import Router from "./my-vue-router";

4、解析路由规则

下面,我们要做的就是对所有的路由规则进行解析,将其解析到一个数组中。方便根据地址找到对应的组件。

在源码的index.js文件中,创建了VueRouter类,对应的构造方法中,有如下代码:

 this.matcher = createMatcher(options.routes || [], this)

createMatcher方法是在create-matcher.js文件中创建的。

该方法返回的matcher 就是一个匹配器,其中有两个成员,match,另外一个是addRoutes

match:根据路由地址匹配相应的路由规则对象。

addRoutes动态添加路由

首先在我们自己的index.js文件中添加如下的代码:

import install from "./install";
import createMatcher from "./create-matcher";
export default class VueRouter {
  //在创建VueRouter对象的时候,会传递选项
  constructor(options) {
    //获取routes选项,该选项中定义路由规则
    this._routes = options.routes || [];
    this.matcher = createMatcher(this._routes);
  }
  // 注册路由变化的事件。
  init() {}
  //init(Vue){}
}
//将install方法挂载到VueRouter上
VueRouter.install = install;

在上面的代码中,导入了createMatcher方法。

并且在调用该方法的时候传递了路由规则。

create-matcher.js 文件的代码如下:

import createRouteMap from "./create-route-map";
export default function createMatcher(routes) {
  
  const { pathList, pathMap } = createRouteMap(routes);
  function match() {}
  function addRoutes(routes) {
 
    createRouteMap(routes, pathList, pathMap);
  }
  return {
    match,
    addRoutes,
  };
}

下面,我们需要在create-route-map.js 文件中实现createRouteMap这个方法。

export default function createRouteMap(routes, oldPathList, oldPathMap) {
  const pathList = oldPathList || [];
  const pathMap = oldPathMap || {};
  //遍历所有的路由规则,进行解析。同时还要考虑children的形式,
  //所以这里需要使用递归的方式。
  routes.forEach((route) => {
    addRouteRecord(route, pathList, pathMap);
  });

  return {
    pathList,
    pathMap,
  };
}

function addRouteRecord(route, pathList, pathMap, parentRecord) {
  //从路由规则中获取path。
  const path = parentRecord ? `${parentRecord.path}/${route.path}` : route.path;
  //构建记录
  const record = {
    path,
    component: route.component,
    parent: parentRecord, //如果是子路由的话,记录子路由对应的父record对象(该对象中有path,component),相当于记录了父子关系
  };
  //如果已经有了path,相同的path直接跳过
  if (!pathMap[path]) {
    pathList.push(path);
    pathMap[path] = record;
  }
  //判断route中是否有子路由
  if (route.children) {
    //遍历子路由,把子路由添加到pathList与pathMap中。
    route.children.forEach((childRoute) => {
      addRouteRecord(childRoute, pathList, pathMap, record);
    });
  }
}

下面测试一下上面的代码

import createRouteMap from "./create-route-map";
export default function createMatcher(routes) {

  const { pathList, pathMap } = createRouteMap(routes);
  console.log("pathList==", pathList);
  console.log("pathMap==", pathMap);
  function match() {}
  function addRoutes(routes) {
   
    createRouteMap(routes, pathList, pathMap);
  }
  return {
    match,
    addRoutes,
  };
}

在上面的代码中,我们打印了pathListpathMap.

当然,现在在我们所定义的路由规则中,还没有添加children,构建相应的子路由。下面重新修改一下。

在项目根目录下的router.js文件中,添加对应的子路由规则。

import Vue from "vue";
// import Router from "vue-router";
// import Router from "./vuerouter";
import Router from "./my-vue-router";
import Login from "./components/Login.vue";
import Home from "./components/Home.vue";
import About from "./components/About.vue";
import Users from "./components/Users";
Vue.use(Router);
export default new Router({
  // model: "history",
  routes: [
    { path: "/", component: Home },
    { path: "/login", component: Login },
    {
      path: "/about",
      component: About,
      children: [{ path: "users", component: Users }],
    },
  ],
});

这时候可以查看对应的输出结果。

5、match函数实现

create-matcher.js文件中,我们实现了createRouteMap方法,同时还需要实现match方法。

match方法的作用就是根据路由地址,匹配一个路由对象。其实就是从pathMap中根据路由地址,找出对应的路由记录。路由记录中记录了组件信息,找到以后就可以完成组件的创建,渲染了。

 function match(path) {
    const record = pathMap[path];
    if (record) {
      //根据路由地址,创建route路由规则对象
      return createRoute(record, path);
    }
    return createRoute(null, path);
  }

在上面的代码中,我们调用match方法的时候,会传递过来一个路径,我们根据这个路径可以从pathMap中找到对应的路由记录信息(这块在上一小节已经创建完毕),如果找到了,我们还需要做进一步的处理,为什么呢?因为,我们传递过来的路径有可能是子路径,这时不光要获取到对应的子路由信息,我们还需要去查找对应的父路由的信息。所以这里需要进一步的处理,关于这块的处理封装到了createRoute这个方法中,而该方法在其它位置还需要,所以我们定义到util这个目录下import createRoute from "./util/route";

create-matcher.js文件完整代码如下:

import createRouteMap from "./create-route-map";
import createRoute from "./util/route";
export default function createMatcher(routes) {
 
  const { pathList, pathMap } = createRouteMap(routes);
  console.log("pathList==", pathList);
  console.log("pathMap==", pathMap);
    //实现match方法
  function match(path) {
    const record = pathMap[path];
    if (record) {
      //根据路由地址,创建route路由规则对象
      return createRoute(record, path);
    }
    return createRoute(null, path);
  }
  function addRoutes(routes) {
   
    createRouteMap(routes, pathList, pathMap);
  }
  return {
    match,
    addRoutes,
  };
}

下面我们需要在my-vue-router目录下面在创建一个util目录,在该目录下面创建route.js文件,该文件实现的代码如下:

export default function createRoute(record, path) {

  const matched = [];
 
  while (record) {
    matched.unshift(record);
    record = record.parent;
  }

  return {
    path,
    matched,
  };
}

总结:match这个方法的作用就是根据路径,创建出路由规则对象,而所谓的路由规则对象其实就是包含了路径以及对应的路由记录的信息(这里有可能包含了父路由以及子路由记录,这块内容存储到一个数组中)。

以后,我们就可以根据路径直接获取到包含了整个路由记录的这个数组,从而可以将对应的组件全部创建出来。

6、历史记录处理

关于路由有三种模式:hash模式,html5模式,abstract模式(该模式与服务端渲染有关)

在这里我们实现hash模式的历史记录管理,不管是哪种模式,都有相同的内容,这里我们相同的内容定义到

父类中。

在该父类中主要有如下内容:

router属性:路由对象(ViewRouter

current属性,记录当前路径对应的路由规则对象{path:'/',matched:[]},关于该对象,我们在前面已经处理完了。也就是在createRoute方法中返回的内容。

transitionTo()

跳转到指定的路径,根据当前路径获取匹配的路由规则对象route,然后更新视图。

my-vue-router目录下的,history目录下的base.js文件,编写如下的代码:

import createRoute from "../util/route";
export default class History {
  // router路由对象ViewRouter
  constructor(router) {
    this.router = router;
  
    this.current = createRoute(null, "/");
  }
  
  transitionTo(path, onComplete) {
 
    this.current = this.router.matcher.match(path);
    //该回调函数在调用transitionTo方法的时候,会传递过来。
    onComplete && onComplete();
  }
}

父类已经实现了,下面实现对应的子类。也就是HashHistory

HashHistory继承History, 同时要确保首次访问的地址为#/.

History中还需要定义两个方法,第一个方法为:getCurrentLocation( ) 获取当前的路由地址(# 后面的部分)

setUpListener( )方法监听路由地址改变的事件(hashchange)。

history目录下的hash.js文件中的代码实现如下:

import History from "./base";
export default class HashHistory extends History {
  constructor(router) {
    //将路由对象传递给父类的构造函数
    super(router);
    //确保 首次 访问地址加上 #/  (//由于没有添加this,为普通方法)
    ensureSlash();
  }
  // 获取当前的路由地址 (# 后面的部分)所以这里需要去除#
  getCurrentLocation() {
    return window.location.hash.slice(1);
  }
  // 监听hashchange事件
  //也就是监听路由地址的变化
  setUpListener() {
    window.addEventListener("hashchange", () => {
      //当路由地址发生变化后,跳转到新的路由地址。
      this.transitionTo(this.getCurrentLocation());
    });
  }
}

function ensureSlash() {
  //判断当前是否有hash
  // 如果单击的是链接,肯定会有hash
  if (window.location.hash) {
    return;
  }
  
  window.location.hash = "/";
}

7、Init方法实现

我们知道当创建VueRouter 的时候,需要可以传递mode,来指定路由的形式,例如是hash模式还是html5模式等。

所以这里需要根据指定的mode的模式,来选择history目录中中不同js来处理。

所以在my-vue-router目录中的index.js文件中,做如下的修改:

import install from "./install";
import createMatcher from "./create-matcher";
import HashHistory from "./history/hash";
import HTML5History from "./history/html5";
export default class VueRouter {
  //在创建VueRouter对象的时候,会传递选项
  constructor(options) {
    //获取routes选项,该选项中定义路由规则
    this._routes = options.routes || [];
    this.matcher = createMatcher(this._routes);
    //获取传递过来的选项中的mode,mode中决定了用户设置的路由的形式。
    //这里给VueRouter添加了mode属性
    const mode = (this.mode = options.mode || "hash");
    switch (mode) {
      case "hash":
        this.history = new HashHistory(this);
        break;
      case "history":
        this.history = new HTML5History(this);
        break;
      default:
        throw new Error("mode error");
    }
  }
  // 注册路由变化的事件。
  init() {}
  //init(Vue){}
}
//将install方法挂载到VueRouter上
VueRouter.install = install;

首先导入HashHistoryHTML5History.

import HashHistory from "./history/hash";
import HTML5History from "./history/html5";

下面获取选项中的mode,如果在创建VueRouter对象的时候,没有指定mode,那么默认的值为hash.

下面就对获取到的mode进行判断,根据mode的不同的值,创建不同的history的实例。

 //获取传递过来的选项中的mode,mode中决定了用户设置的路由的形式。
    //这里给VueRouter添加了mode属性
    const mode = (this.mode = options.mode || "hash");
    switch (mode) {
      case "hash":
        this.history = new HashHistory(this);
        break;
      case "history":
        this.history = new HTML5History(this);
        break;
      default:
        throw new Error("mode error");
    }

同时html5.js文件,添加了基本的代码

import History from "./base";
export default class HTML5History extends History {}

关于Html5的形式这里不在实现了。

下面完善一下init方法

 // 注册路由变化的事件。
  init() {}

具体的实现代码如下:

  // 注册路由变化的事件(初始化事件监听器,监听路由地址的变化)。
  init() {
    const history = this.history;
    const setUpListener = () => {
      history.setUpListener();
    };
    history.transitionTo(
      history.getCurrentLocation(),
      //如果直接history.setUpListener
      // 这样的话setUpListener里面的this会有问题。
      setUpListener
    );
  }

在这里,调用了transitionTo方法的原因是,在hash.js文件中的ensureSlash方法中,完成了一次地址的修改,所以这里需要跳转一次。

同时完成了hashchange事件的绑定(路由变化的事件)。

下面可以进行测试一下,在base.js文件中的transitionTo方法中,打印出current属性的值。

  transitionTo(path, onComplete) {
    this.current = this.router.matcher.match(path);
    console.log("current===", this.current);
 //该回调函数在调用transitionTo方法的时候,会传递过来。
    onComplete && onComplete();
  }

下面,在浏览器的地址栏中输入了不同的URL地址后,在控制台上呈现出了不同的路由规则对象,也就是路由记录信息。

http://localhost:8080/#/about/users

输入以上地址,该地址为子路由的地址,最终也输出了对应的父路由的记录信息。

后期就可以获取具体的组件来进行渲染。

8、设置响应式的_route

下面我们要做的就是渲染组件。

这里我们先创建一个与路由有关的响应式属性,当路由地址发生变化了,对应的该属性也要发生变化,从而完成页面的重新渲染。

install.js文件中添加如下的代码:


        Vue.util.defineReactive(this, "_route", this._router.history.current);

以上完成了响应式属性的创建,但是要注意的是defineReactive方法为Vue的内部方法,不建议平时通过该方法来创建响应式对象。

  beforeCreate() {
      //判断是否为Vue的实例,如果条件成立为Vue的实例,否则为其它对应的组件(因为在创建Vue实例的时候会传递选项)
      if (this.$options.router) {
        //通过查看源码发现,Vue的实例会挂在到当前的私有属性_routerRoot属性上
        this._routerRoot = this;
        this._router = this.$options.router;
        //调用index.js文件中定义的init方法
        this._router.init(this);
          
          
        //在Vue的实例上创建一个响应式的属性`_route`.
        Vue.util.defineReactive(this, "_route", this._router.history.current);
      } 

下面要考虑的就是当路由地址发生了变化后,需要修改_route属性的值。

在哪完成_route属性值的修改呢?

base.js文件中,因为在该文件中定义了transitionTo方法,而该方法就是用来完成地址的跳转,同时完成组件的渲染。

base.js文件修改后的代码如下:

import createRoute from "../util/route";
export default class History {
  // router路由对象ViewRouter
  constructor(router) {
    this.router = router;
    this.current = createRoute(null, "/");
    //这个回调函数是在hashhistory中赋值,作用是更改vue实例上的_route,_route的值发生变化,视图会进行刷新操作
    this.cb = null;
  }
  //给cb赋值
  listen(cb) {
    this.cb = cb;
  }

  transitionTo(path, onComplete) {
   
    this.current = this.router.matcher.match(path);
    // 调用cb
    this.cb && this.cb(this.current);
    // console.log("current===", this.current);

    //该回调函数在调用transitionTo方法的时候,会传递过来。
    onComplete && onComplete();
  }
}

History中的构造方法中初始化cb函数

  this.cb = null;

定义listen方法给cb函数赋值。

//给cb赋值
  listen(cb) {
    this.cb = cb;
  }

transitionTo 方法中调用cb函数,同时传递获取到的当前的路由规则对象也就是路由记录信息。

  this.cb && this.cb(this.current);

在什么地方调用listen方法呢?

index.js文件中的init方法中完成listen方法的调用。

// 注册路由变化的事件(初始化事件监听器,监听路由地址的变化)。
  init(app) {
    const history = this.history;
    const setUpListener = () => {
      history.setUpListener();
    };
    history.transitionTo(
      history.getCurrentLocation(),
      //如果直接history.setUpListener
      // 这样的话setUpListener里面的this会有问题。
      setUpListener
    );
    //调用父类的中的listen方法
    history.listen((route) => {
      app._route = route;
    });
  }

在上面的代码中调用了父类中的listen方法,然后将箭头函数传递到了listen中。

这时候,在transitionTo方法中调用cb,也就是调用箭头函数,这时传递过来的参数route,为当前更改后的路由规则信息,交给了app中的_route属性。

app这个参数其实就是Vue的实例,因为在install.js文件中调用了init方法,并且传递的就是Vue的实例。

这样就完成了对Vue实例上的响应式属性_route值的修改,从而会更新组件。

9、$route/$router创建

创建$route$router的目的是能够在所有的Vue实例(组件)中,可以获取到。

$route是路由规则对象,包含了path,component等内容

$router为路由对象(ViewRouter对象)。

通过查看源码(install.js)可以发现,其实就是将$router$route挂载到了Vue的原型上。

所以可以直接将源码内容复制过来就可以了。

 Object.defineProperty(Vue.prototype, "$router", {
    get() {
      return this._routerRoot._router;
    },
  });

  Object.defineProperty(Vue.prototype, "$route", {
    get() {
      return this._routerRoot._route;
    },
  });

通过上面的代码,可以看到$route$router 都是只读的,因为对应的值,在前面已经设置完毕,这里只是获取。

$router 是通过_routerRoot来获取。

$route是通过_routerRoot._route来获取。

Vue.util.defineReactive(this, "_route", this._router.history.current);

Vue对象上创建了_route属性,该属性的值为路由规则内容

10、Router-View创建

router-view就是一个占位符,会用具体的组件来替换该占位符。

router-view的创建过程如下:

  • 获取当前组件的$route路由规则对象
  • 找到路由规则对象里面的matched 匹配的record(里面有component)
  • 如果是/about ,matched匹配到一个record,直接渲染对应的组件
  • 如果是/about/users,matched 匹配到两个record(第一个是父组件,第二个是子组件)

my-vue-router/components目录下的view.js代码如下:

export default {
  render(h) {
    //获取当前匹配的路由规则对象
    const route = this.$route;
    //获取路由记录对象.只有一个内容,所以获取的是`matched`中的第一项。
    const record = route.matched[0];
    if (!record) {
      return h();
    }
    //获取记录中对应的组件
    const component = record.component;
    return h(component);
  },
};

以上的代码处理的是没有子路由的情况。

下面,看一下子路由情况的处理。

当然在编写子路由的处理代码之前,我们先把案例中的路由完善一下。

src目录下的App.vue这个组件中,添加一个“关于”的链接。

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link>
      <router-link to="/login">Login</router-link>
      <router-link to="/about">About</router-link>
    </div>
    <router-view></router-view>
  </div>
</template>

<script>
export default {};
</script>

对应在About 这个组件中,完成子路由应用

<template>
  <div>
    关于组件
    <router-link to="/about/users">用户</router-link>
    <router-view></router-view>
  </div>
</template>

<script>
export default {};
</script>

<style>
</style>

下面完善一下对子路由的处理。

export default {
  render(h) {
    //获取当前匹配的路由规则对象
    const route = this.$route;
    let depth = 0;
    //记录当前组件为RouterView
    this.routerView = true;
    let parent = this.$parent;
    while (parent) {
      if (parent.routerView) {
        depth++;
      }
      parent = parent.$parent;
    }
    //获取路由记录对象.
    // 如果是子路由,例如:子路由/about/users
    //子路由是有两部分内容,matched[0]:是父组件内容,matched[1]是子组件内容
    const record = route.matched[depth];
    if (!record) {
      return h();
    }
    //获取记录中对应的组件
    const component = record.component;
    return h(component);
  },
};

假如,现在我们在浏览器的地址栏中输入了:http://localhost:8080/#/about地址,

是没有父组件,那么depth属性的值为0,这时候获取的第一个组件然后进行渲染。

如果地址栏的内容为:http://localhost:8080/#/about/users 这时候有子组件。对应的获取对应的父组件内容,开始进行循环。

在循环的时候,做了一个判断,判断的条件就是当前的父组件必须为:RouterView组件(子组件中router-view与父组件中的router-view构成了父子关系),才让depth加1,然后取出子组件进行渲染。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值