手写vue-router源码

一、项目准备

创建vue项目

vue create projectName

进入项目

cd projectName

安装vue-router

vue add router

手写vue-router:在router文件夹中创建jvue-router.js文件。

将index.js文件中引入VueRouter地址改为jvue-router.js的地址

// import VueRouter from "vue-router";
import VueRouter from "./jvue-router";

 

二、手写vue-router源码

1在router/index.js中使用VueRouter时是new VueRouter(),所以要创建一个VueRouter类并暴露出来,同时vue插件都要有一个install进行初始化,install中的参数为vue,保存起来以便后面使用。

let Vue;
class VueRouter {
    constructor() {}
}

VueRouter.install = function(_vue) {
    Vue = _vue;
}

export default VueRouter;

2.在组件中,我们是直接使用<router-link>和<router-view>两个组件,所以install方法中注册这两个组件

let Vue;
class VueRouter {
    constructor() {}
}

VueRouter.install = function(_vue) {
    Vue = _vue;

    // 注册router-view和router-link
    Vue.component('router-view', {
        render(h) {
            return h('div', 'router-view')
        }
    })
    Vue.component('router-link', {
        render(h) {
            return h('a', 'router-link')
        }
    })
}

export default VueRouter;

3.先分析router-link组件:

使用时我们会传入一个to属性表示链接路径:

<router-link to="/about">About组件</router-link>

 编译后是一个a标签和href属性,href属性的值为上面to属性的值:

<a href="/about">About组件</a>

所以我们注册router-link组件时应先用props获取to属性值,用$slots获取插值文本

let Vue;
class VueRouter {
    constructor() {}
}

VueRouter.install = function(_vue) {
    Vue = _vue;

    // 注册router-view
    Vue.component('router-view', {
        render(h) {
            return h('div', 'router-view')
        }
    })
    // 注册router-link
    Vue.component('router-link', {
        props: {
            to: {
                type: String,
                required: true
            }
        },
        render(h) {
            return h("a", { attrs: { href: "#" + this.to } }, this.$slots.default);
        }
    })
}

export default VueRouter;

4.分析router-view组件:

获取当前hash并将其设为响应式数据,监听url变化。由于是响应式数据,所以当hash变化时,相应的组件会重新渲染。

获取路由映射表,根据当前hash在映射表中找到对应的组件,并渲染。

由于组件中使用router时是this.$router形式,所以要将router挂载到Vue原型链上。此时可以在Vue组件中获取到router和routes。

因为VueRouter执行install时Vue实例还没有创建(详见main.js),所以要使用混入的方式,在beforeCreate时再挂载。

路由映射表是在创建路由实例时传入(./router/index.js):

const routes = [
  {
    path: "/",
    name: "Home",
    component: Home
  },
  {
    path: "/about",
    name: "About",
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =>
      import(/* webpackChunkName: "about" */ "../views/About.vue")
  }
];

const router = new VueRouter({
  routes
});

对应代码:

// 设置vue以便后面使用
let Vue;
class VueRouter {
  constructor(options) {
    this.options = options;

    // 初始化current,设置current为响应式数据,当current改变时重新渲染页面
    Vue.util.defineReactive(
      this,
      "current",
      window.location.hash.slice(1) || "/"
    );
    console.log(Vue.util);

    // 监听url变化,重置current
    window.addEventListener("hashchange", () => {
      this.current = window.location.hash.slice(1);
      console.log(this.current);
    });
  }
}

// vue插件都要有install方法:注册router-link和router-view两个组件 使得我们在组件中使用时可以<router-view>,将router挂载到Vue原型链上,使得我们在组件中使用时可以this.$router
VueRouter.install = function(_vue) {
  Vue = _vue;
  console.log(_vue);

  //   将router挂载到Vue原型链上
  // 因为VueRouter执行install时Vue实例还没有创建(详见main.js),所以要使用混入的方式,在beforeCreate时再挂载
  Vue.mixin({
    beforeCreate() {
      // 判断当前组件是否为根组件,只有根组件有$router
      //   因为根组件的vue实例创建时传入了vueRouter实例,vue组件中获取传入的参数可使用this.$options,所以获取传入的vueRouter实例可以用this.$options.router
      if (this.$options.router) {
        Vue.prototype.$router = this.$options.router;
      }
    }
  });
  //   注册router-view
  //   根据当前hash找到对应component
  // hash:window.location.hash.slice(1)
  // 获取路由映射表:VueRouter在创建实例时传入了路由映射表,所以从VueRouter初始化中获取
  Vue.component("router-view", {
    render(h) {
      const { current, options } = this.$router;
      let component = null;
      const route = options.routes.find(route => route.path === current);
      if (route) {
        component = route.component;
      }
      return h(component);
    }
  });
  //   注册router-link
  Vue.component("router-link", {
    props: {
      to: {
        type: String,
        required: true
      }
    },
    render(h) {
      return h("a", { attrs: { href: "#" + this.to } }, this.$slots.default);
    }
  });
};

export default VueRouter;

5.嵌套路由

在router/index.js中about路由中添加子路由intro:

import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../views/Home.vue";

Vue.use(VueRouter);

const routes = [
  {
    path: "/",
    name: "Home",
    component: Home
  },
  {
    path: "/about",
    name: "About",
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =>
      import(/* webpackChunkName: "about" */ "../views/About.vue"),
    children: [
      {
        path: "/about/intro",
        name: "Intro",
        component: {
          render(h) {
            return h('div', "intro page")
          }
        }
      }
    ]
  }
];

const router = new VueRouter({
  routes
});

export default router;

在views/About.vue中添加router-view组件:

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <router-view></router-view>
  </div>
</template>

测试发现上面第四点中路由的写法完全没有考虑到嵌套路由,当在About组件中检测到有router-view组件时,router-view组件内部又去查找现有的url中/about对应的组件,明显查找到的属性又是About组件,就此进入死循环直至栈溢出。

解决:获取路由的层级深度,路由匹配时获取代表深度层级的数组matched,调用router-view组件时找到matched[depth]对应的组件。

// 设置vue以便后面使用
let Vue;
class VueRouter {
  constructor(options) {
    this.options = options;

    // 初始化current,设置current为响应式数据,当current改变时重新渲染页面
    // Vue.util.defineReactive(
    //   this,
    //   "current",
    //   window.location.hash.slice(1) || "/"
    // );

    // 嵌套路由中,router-view组件中不再需要根据current查找渲染对应组件,所以不需要current是响应式的,而需要代表深度层级的响应式matched数组
    this.current = window.location.hash.slice(1) || "/";
    Vue.util.defineReactive(this, "matched", []);
    // 匹配路由
    this.match();

    // 监听url变化,重置current
    window.addEventListener("hashchange", () => {
      this.current = window.location.hash.slice(1);

      // 路径发生变化要把matched数组清空,重新匹配
      this.matched = [];
      this.match();
    });
  }

  // 设置match方法,遍历路由表,获取匹配关系数组
  match(routes){
    routes = routes || this.options.routes;
    // 递归遍历
    for(const route of routes) {
      // 一般不会在Home配置子路由,所以暂时不考虑该路由有子路由的情况
      if(route.path === "/" && this.current === "/") {
        this.matched.push(route)
        console.log(this.matched);
        return;
      }
      // 判断当前hash是否与路由匹配,若匹配则将路由加入matched数组中,并递归其子路由
      if(route.path !== " /"  && this.current.indexOf(route.path) != -1) {
        this.matched.push(route);
        if(route.children) {
          this.match(route.children)
        }
        console.log(this.matched);
        return;
      }
    }
  }

}

// vue插件都要有install方法:注册router-link和router-view两个组件 使得我们在组件中使用时可以<router-view>,将router挂载到Vue原型链上,使得我们在组件中使用时可以this.$router
VueRouter.install = function(_vue) {
  Vue = _vue;
  console.log(_vue);

  //   将router挂载到Vue原型链上
  // 因为VueRouter执行install时Vue实例还没有创建(详见main.js),所以要使用混入的方式,在beforeCreate时再挂载
  Vue.mixin({
    beforeCreate() {
      // 判断当前组件是否为根组件,只有根组件有$router
      //   因为根组件的vue实例创建时传入了vueRouter实例,vue组件中获取传入的参数可使用this.$options,所以获取传入的vueRouter实例可以用this.$options.router
      if (this.$options.router) {
        Vue.prototype.$router = this.$options.router;
      }
    }
  });
  //   注册router-view
  //   根据当前hash找到对应component
  // hash:window.location.hash.slice(1)
  // 获取路由映射表:VueRouter在创建实例时传入了路由映射表,所以从VueRouter初始化中获取
  Vue.component("router-view", {
    render(h) {
      // 嵌套路由
      // 思路:获取路由的层级深度,路由匹配时获取代表深度层级的数组matched,调用router-view组件时找到matched[depth]对应的组件
      // 在虚拟dom中设置routerView表明此组件是否为routerView组件
      this.$vnode.data.routerView = true;
      let depth = 0;
      let parent = this.$parent;
      // 循环获取所有parent
      while(parent) {
        // 获取$vnode.data
        const vnodeData =  parent.$vnode &&  parent.$vnode.data;
        // 判断当前parent是否为router-view组件,若是,则深度加1
        if(vnodeData && vnodeData.routerView) {
          depth ++ 
        }
        parent = parent.$parent
      }

      // 获取path对应的component
      let component = null;
      const route = this.$router.matched[depth];
      if(route) {
        component = route.component
      }
      return h(component)

      // const { current, options } = this.$router;
      // let component = null;
      // const route = options.routes.find(route => route.path === current);
      // if (route) {
      //   component = route.component;
      // }
      // return h(component);
    }
  });
  //   注册router-link
  Vue.component("router-link", {
    props: {
      to: {
        type: String,
        required: true
      }
    },
    render(h) {
      return h("a", { attrs: { href: "#" + this.to } }, this.$slots.default);
    }
  });
};

export default VueRouter;

 

 

 

 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值