原文地址 www.zhufengpeixun.com

一.Vue-Router基本应用

通过Vue路由的基本配置来探索Vue-Router

  1. import Vue from 'vue'
  2. import Router from 'vue-router'
  3. import Home from './views/Home.vue'
  4. import About from './views/About.vue'
  5. Vue.use(Router);// 使用Vue-Router插件
  6. export default new Router({ // 创建Vue-router实例,将实例注入到main.js中
  7. routes: [
  8. {
  9. path: '/',
  10. name: 'home',
  11. component: Home
  12. },
  13. {
  14. path: '/about',
  15. name: 'about',
  16. component: About,
  17. children: [
  18. {
  19. path: 'a', component: {
  20. render(h) {return <h1>about A</h1>}
  21. }
  22. },
  23. {
  24. path: 'b', component: {
  25. render(h) {return <h1>about B</h1>}
  26. }
  27. }
  28. ]
  29. }
  30. ]
  31. })
  32. new Vue({
  33. router, // 在根实例中注入router实例
  34. render: h => h(App)
  35. }).$mount('#app')

这里我们不难发现核心方法就是Vue.use(Router),在就是new Router产生router实例

二.编写Vue-Router

从这里开始我们自己来实现一个Vue-router插件,这里先来标注一下整体目录结构

  1. ├─vue-router
  2. ├─components # 存放vue-router的两个核心组件
  3. ├─link.js
  4. └─view.js
  5. ├─history # 存放浏览器跳转相关逻辑
  6. ├─base.js
  7. └─hash.js
  8. ├─create-matcher.js # 创建匹配器
  9. ├─create-route-map.js # 创建路由映射表
  10. ├─index.js # 引用时的入口文件
  11. ├─install.js # install方法

默认我们引用Vue-Router使用的是index.js文件,use方法默认会调用当前返回对象的install方法

  1. import install from './install'
  2. export default class VueRouter{}
  3. VueRouter.install = install; // 提供的install方法

好吧!我们先去看下install中做了什么?


2.1 编写install方法

  1. export let _Vue;
  2. export default function install(Vue) {
  3. _Vue = Vue;
  4. Vue.mixin({ // 给所有组件的生命周期都增加beforeCreate方法
  5. beforeCreate() {
  6. if (this.$options.router) { // 如果有router属性说明是根实例
  7. this._routerRoot = this; // 将根实例挂载在_routerRoot属性上
  8. this._router = this.$options.router; // 将当前router实例挂载在_router上
  9. this._router.init(this); // 初始化路由,这里的this指向的是根实例
  10. } else { // 父组件渲染后会渲染子组件
  11. this._routerRoot = this.$parent && this.$parent._routerRoot;
  12. // 保证所有子组件都拥有_routerRoot 属性,指向根实例
  13. // 保证所有组件都可以通过 this._routerRoot._router 拿到用户传递进来的路由实例对象
  14. }
  15. }
  16. })
  17. }

这里我们应该在Vue-Router上增加一个init方法,主要目的就是初始化功能
这里在强调下,什么是路由? 路由就是匹配到对应路径显示对应的组件

  1. import createMatcher from './create-matcher'
  2. import install from './install'
  3. export default class VueRouter{
  4. constructor(options){
  5. // 根据用户传递的routes创建匹配关系,this.matcher需要提供两个方法
  6. // match:match方法用来匹配规则
  7. // addRoutes:用来动态添加路由
  8. this.matcher = createMatcher(options.routes || []);
  9. }
  10. init(app){}
  11. }
  12. VueRouter.install = install;

2.2 编写createMatcher方法

  1. import createRouteMap from './create-route-map'
  2. export default function createMatcher(routes) {
  3. // 收集所有的路由路径, 收集路径的对应渲染关系
  4. // pathList = ['/','/about','/about/a','/about/b']
  5. // pathMap = {'/':'/的记录','/about':'/about记录'...}
  6. let {pathList,pathMap} = createRouteMap(routes);
  7. // 这个方法就是动态加载路由的方法
  8. function addRoutes(routes){
  9. // 将新增的路由追加到pathList和pathMap中
  10. createRouteMap(routes,pathList,pathMap);
  11. }
  12. function match(){} // 稍后根据路径找到对应的记录
  13. return {
  14. addRoutes,
  15. match
  16. }
  17. }

这里需要创建映射关系,需要createRouteMap方法

2.3 编写createRouteMap方法

  1. export default function createRouteMap(routes,oldPathList,oldPathMap){
  2. // 当第一次加载的时候没有 pathList 和 pathMap
  3. let pathList = oldPathList || [];
  4. let pathMap = oldPathMap || Object.create(null);
  5. routes.forEach(route=>{
  6. // 添加到路由记录,用户配置可能是无限层级,稍后要递归调用此方法
  7. addRouteRecord(route,pathList,pathMap);
  8. });
  9. return { // 导出映射关系
  10. pathList,
  11. pathMap
  12. }
  13. }
  14. // 将当前路由存储到pathList和pathMap中
  15. function addRouteRecord(route,pathList,pathMap,parent){
  16. // 如果是子路由记录 需要增加前缀
  17. let path = parent?`${parent.path}/${route.path}`:route.path;
  18. let record = { // 提取需要的信息
  19. path,
  20. component:route.component,
  21. parent
  22. }
  23. if(!pathMap[path]){
  24. pathList.push(path);
  25. pathMap[path] = record;
  26. }
  27. if(route.children){ // 递归添加子路由
  28. route.children.forEach(r=>{
  29. // 这里需要标记父亲是谁
  30. addRouteRecord(r,pathList,pathMap,route);
  31. })
  32. }
  33. }

该方法主要是处理路径和不同路径对应的记录

matcher我们先写到这,稍后在来补全match方法的实现

2.4 编写浏览器历史相关代码

  1. import HashHistory from './history/hash'
  2. constructor(options){
  3. this.matcher = createMatcher(options.routes || []);
  4. // vue路由有三种模式 hash / h5api /abstract ,为了保证调用时方法一致。我们需要提供一个base类,在分别实现子类,不同模式下通过父类调用对应子类的方法
  5. this.history = new HashHistory(this);
  6. }

这里我们以hash路由为主,创建hash路由实例

  1. import History from './base'
  2. // hash路由
  3. export default class HashHistory extends History{
  4. constructor(router){
  5. super(router);
  6. }
  7. }
  8. // 路由的基类
  9. export default class History {
  10. constructor(router){
  11. this.router = router;
  12. }
  13. }

如果是hash路由,打开网站如果没有hash默认应该添加#/

  1. import History from './base';
  2. function ensureSlash(){
  3. if(window.location.hash){
  4. return
  5. }
  6. window.location.hash = '/'
  7. }
  8. export default class HashHistory extends History{
  9. constructor(router){
  10. super(router);
  11. ensureSlash(); // 确保有hash
  12. }
  13. }

稍后我们在继续扩展路由相关代码,我们先把焦点转向初始化逻辑

  1. init(app){
  2. const history = this.history;
  3. // 初始化时,应该先拿到当前路径,进行匹配逻辑
  4. // 让路由系统过度到某个路径
  5. const setupHashListener = ()=> {
  6. history.setupListener(); // 监听路径变化
  7. }
  8. history.transitionTo( // 父类提供方法负责跳转
  9. history.getCurrentLocation(), // 子类获取对应的路径
  10. // 跳转成功后注册路径监听,为视图更新做准备
  11. setupHashListener
  12. )
  13. }

这里我们要分别实现 transitionTo(基类方法)、 getCurrentLocation 、setupListener
getCurrentLocation实现

  1. function getHash(){
  2. return window.location.hash.slice(1);
  3. }
  4. export default class HashHistory extends History{
  5. // ...
  6. getCurrentLocation(){
  7. return getHash();
  8. }
  9. }

setupListener实现

  1. export default class HashHistory extends History{
  2. // ...
  3. setupListener(){
  4. window.addEventListener('hashchange', ()=> {
  5. // 根据当前hash值 过度到对应路径
  6. this.transitionTo(getHash());
  7. })
  8. }
  9. }

可以看到最核心的还是transitionTo方法
TransitionTo实现

  1. export function createRoute(record, location) { // {path:'/',matched:[record,record]}
  2. let res = [];
  3. if (record) { // 如果有记录
  4. while(record){
  5. res.unshift(record); // 就将当前记录的父亲放到前面
  6. record = record.parent
  7. }
  8. }
  9. return {
  10. ...location,
  11. matched: res
  12. }
  13. }
  14. export default class History {
  15. constructor(router) {
  16. this.router = router;
  17. // 根据记录和路径返回对象,稍后会用于router-view的匹配
  18. this.current = createRoute(null, {
  19. path: '/'
  20. })
  21. }
  22. // 核心逻辑
  23. transitionTo(location, onComplete) {
  24. // 去匹配路径
  25. let route = this.router.match(location);
  26. // 相同路径不必过渡
  27. if(
  28. location === route.path &&
  29. route.matched.length === this.current.matched.length){
  30. return
  31. }
  32. this.updateRoute(route); // 更新路由即可
  33. onComplete && onComplete();
  34. }
  35. updateRoute(route){ // 跟新current属性
  36. this.current =route;
  37. }
  38. }
  1. export default class VueRouter{
  2. // ...
  3. match(location){
  4. return this.matcher.match(location);
  5. }
  6. }

终于这回可以完善一下刚才没有写完的match方法

  1. function match(location){ // 稍后根据路径找到对应的记录
  2. let record = pathMap[location]
  3. if (record) { // 根据记录创建对应的路由
  4. return createRoute(record,{
  5. path:location
  6. })
  7. }
  8. // 找不到则返回空匹配
  9. return createRoute(null, {
  10. path: location
  11. })
  12. }

我们不难发现路径变化时都会更改current属性,我们可以把current属性变成响应式的,每次current变化刷新视图即可

  1. export let _Vue;
  2. export default function install(Vue) {
  3. _Vue = Vue;
  4. Vue.mixin({ // 给所有组件的生命周期都增加beforeCreate方法
  5. beforeCreate() {
  6. if (this.$options.router) { // 如果有router属性说明是根实例
  7. // ...
  8. Vue.util.defineReactive(this,'_route',this._router.history.current);
  9. }
  10. // ...
  11. }
  12. });
  13. // 仅仅是为了更加方便
  14. Object.defineProperty(Vue.prototype,'$route',{ // 每个实例都可以获取到$route属性
  15. get(){
  16. return this._routerRoot._route;
  17. }
  18. });
  19. Object.defineProperty(Vue.prototype,'$router',{ // 每个实例都可以获取router实例
  20. get(){
  21. return this._routerRoot._router;
  22. }
  23. })
  24. }

Vue.util.defineReactive 这个方法是vue中响应式数据变化的核心
当路径变化时需要执行此回调更新_route属性, 在init方法中增加监听函数

  1. history.listen((route) => { // 需要更新_route属性
  2. app._route = route
  3. });
  1. export default class History {
  2. constructor(router) {
  3. // ...
  4. this.cb = null;
  5. }
  6. listen(cb){
  7. this.cb = cb; // 注册函数
  8. }
  9. updateRoute(route){
  10. this.current =route;
  11. this.cb && this.cb(route); // 更新current后 更新_route属性
  12. }
  13. }

三.编写Router-Link及Router-View组件

3.1 router-view组件

  1. export default {
  2. functional:true,
  3. render(h,{parent,data}){
  4. let route = parent.$route;
  5. let depth = 0;
  6. data.routerView = true;
  7. while(parent){ // 根据matched 渲染对应的router-view
  8. if (parent.$vnode && parent.$vnode.data.routerView){
  9. depth++;
  10. }
  11. parent = parent.$parent;
  12. }
  13. let record = route.matched[depth];
  14. if(!record){
  15. return h();
  16. }
  17. return h(record.component, data);
  18. }
  19. }

3.2 router-link组件

  1. export default {
  2. props:{
  3. to:{
  4. type:String,
  5. required:true
  6. },
  7. tag:{
  8. type:String
  9. }
  10. },
  11. render(h){
  12. let tag = this.tag || 'a';
  13. let handler = ()=>{
  14. this.$router.push(this.to);
  15. }
  16. return <tag onClick={handler}>{this.$slots.default}</tag>
  17. }
  18. }

四.beforeEach实现

  1. this.beforeHooks = [];
  2. beforeEach(fn){ // 将fn注册到队列中
  3. this.beforeHooks.push(fn);
  4. }

将用户函数注册到数组中

  1. function runQueue(queue, iterator,cb) { // 迭代queue
  2. function step(index){
  3. if(index >= queue.length){
  4. cb();
  5. }else{
  6. let hook = queue[index];
  7. iterator(hook,()=>{ // 将本次迭代到的hook 传递给iterator函数中,将下次的权限也一并传入
  8. step(index+1)
  9. })
  10. }
  11. }
  12. step(0)
  13. }
  14. export default class History {
  15. transitionTo(location, onComplete) {
  16. // 跳转到这个路径
  17. let route = this.router.match(location);
  18. if (location === this.current.path && route.matched.length === this.current.matched.length) {
  19. return
  20. }
  21. let queue = [].concat(this.router.beforeHooks);
  22. const iterator = (hook, next) => {
  23. hook(route,this.current,()=>{ // 分别对应用户 from,to,next参数
  24. next();
  25. });
  26. }
  27. runQueue(queue, iterator, () => { // 依次执行队列 ,执行完毕后更新路由
  28. this.updateRoute(route);
  29. onComplete && onComplete();
  30. });
  31. }
  32. updateRoute(route) {
  33. this.current = route;
  34. this.cb && this.cb(route);
  35. }
  36. listen(cb) {
  37. this.cb = cb;
  38. }
  39. }