前言
在使用vue全家桶进行开发时候,其中一个重要的插件就是vue-router,vue-router功能强大,并且使用方便,是我们构建项目时候不可或缺的一部分,那么vue-router到底是怎么实现的呢?那我们从源码出发,手撸一个基础的vue-router
基础用法
首先我们先看一下在项目中我们是如何使用router的
-
router文件
在项目中我们通常都会有一个router文件夹在存放我们的router文件// src/router/index.js import Vue from'vue'; import VueRouter from 'vue-router'; import Page1 from'@/pages/detail1'; import Page2 from'@/pages/detail2'; // 创建VurRouter的插件 Vue.use(VueRouter); // 配置router let router = [ { path: '/', components: Page1, }, { path: '/page2', components: Page2, } ] // 实例化VurRouter,并将配置注入,导出实例 const Router = new VueRouter(router); export default Router;
-
vue根实例文件
在实例化和挂载vue根实例的时候,我们会将vue-router引入到配置项// src/main.js import Vue from 'vue'; import App from './App.vue'; import router from './router'; new Vue({ router, render: h => h(App), }).$mount('#app')
-
使用路由
利用路由提供的组件,我们可以按照我们的需求展示和跳转路由<template> <div id="app"> <div> <router-link to="/">page1</ router-link> <span>|</ span> <router-link to="/page2">page2</ router-link> </div> <router-view></ router-view> </ div> </ template>
手写vue-router
闲话说了不少,下面我们就开始手撸vue-router,let‘go!
-
实现router的思路
首先要实现router,我们必须清楚我们这次到底要做什么东西,先把他们列出来:- 实现并导出一个Router实例,供外部调用
- 实现一个公共组件RouterView,用来加载路由视图
- 实现一个RouterLink,用来跳转路由
-
整理一下每个组件的需求
- Router实例,需要接收router传入的options,将所有路由以及嵌套路由处理和收集起来,并监听url的变化。并且他作为插件存在,应该有一个install方法
- RouterView组件,根据自己的路由嵌套层级,找出对应的路由里面的组件,并展示出来
- RouterLink组件,创建一个a标签,插入写入的文本,并根据props中to的值进行跳转
-
开始写代码
-
分析完需求,我们首先来写一下vue-router实例
class VueRouter { constructor(options) { // 将传入路由配置保存在属性$options中,方便之后调用 this.$options = options; } }
创建了实例方法,我们开始写install方法
// 将install中的参数Vue缓存起来,这么做原因有两个: // 1.这里的Vue实例在上面的VueRouter实例中也会用到,缓存起来可以直接使用 // 2.因为插件是独立的,直接引入Vue会导致打包时候把Vue打包到插件中进去,增加代码打包后体积 var Vue; VueRouter.install = function(_Vue) { Vue = _Vue; //使用mixin和生命周期beforeCreate做全局混入, 拿到实例后的相关属性并挂载到prototype上 Vue.mixin({ beforeCreate() { if (this.$options.router) { // 这里可以看出我们为何可以使用this.$router拿到router实例 // 也可以看出为何要在main.js中的根实例中将router实例作为options配置进去 Vue.prototype.$router = this.$options.router; } } }) }
接下来我们接着完善router实例
class VueRouter { constructor(options) { // 将传入路由配置保存在属性$options中,方便之后调用 this.$options = options; // 这里我们利用hash的方法来构造路由地址,即我们常看到的http://localhost/#/xxx // 截取url,去掉#号 this.current = window.location.hash.slice(1) ||'/'; // 利用Vue中的defineReactive方法响应式的创建一个数组matched,存放当前路由以及其嵌套路由 Vue.util.defineReactive(this, 'matched', []); // 处理路由以及嵌套路由 this.matchs(this.$options); // 监听拿到地址并修改地址 window.addEventListener('hashchange', this.currentHash.bind(this)); // 页面首次加载的时候也需要监听并修改地址 window.addEventListener('load', this.currentHash.bind(this)); } currentHash() { this.current = window.location.hash.slice(1); this.matched = []; this.matchs(this.$options); } matchs(routers) { // 递归处理路由 // 输出结果一般是['parent', 'child1', 'child1-child1'] routers.forEach(router => { // 一般来说/路径下不会存放嵌套路由 if (this.current ==='/' && router.path === '/') { this.matched.push(router); } else if (this.current.includes(router.path) && router.path !== '/') { this.matched.push(router); if (router.children) { this.matchs(router.children); } } } } }
到这里,对照需求,router的实例基本完成。
-
我们接下来来写RouterView组件
RouterView组件在实现的过程中,我们需要思考一个问题,就是怎么能找到对应的RouterView视图并匹配到对应的路由组件。这里我们根据matched存入的方式,引入一个概念,路由深度,表示路由的层级。// 由于是一个全局组件,我们选择写在install中,在配置插件时候一起注册 VueRouter.install = function(_Vue) { Vue = _Vue; // 挂载router-view组件 Vue.component('RouterView', { render(h) { // 上面提到的深度标记,父组件深度为0 let deep = 0; // 将组件标记 this.routerView = true; // 循环找出所有RouterView的父元素 并标记深度 // 利用父子通信的中的$parent let parent = this.$parent; // 循环查找父组件,以此标记当前组件的深度,当parent不存在时候,说明已经到了顶层组件,退出循环 while(parent) { // 父组件存在且是routerView if (parent && parent.routerView) { deep+=1; } parent = parent.$parent; } // 找到匹配深度层级,找出component let matched = this.$router.matched; let _component = null; // 如果能在matched中找出,为_component赋值 if (matched[deep]) { _component = matched[deep].components; } // 渲染_component return h(_component); } }) }
到这一步RouterView也已经完成了,我们只剩下一个组件了
-
最后我们来实现一下RouterLink组件
从需求中看,这个组件比起RouterView很简单了,他只需要渲染一个a标签,并展示对应文本并跳转,和RouterView一样,我们在install函数中写VueRouter.install = function(_Vue) { Vue = _Vue; // 挂载router-link组件 Vue.component('RouterLink', { // 声明props,to并设置为必填 props: { to: { type: String, required: true } }, render(h) { 'a', { attrs: { href: `#${this.to}` } }, // 利用匿名插槽将组件中文本写入 [ this.$slots.default ] } }) }
RouterLink组件功能也完成了,是不是很简单呢?
-
完整代码
var Vue;
class VueRouter {
constructor(options) {
// 将传入路由配置保存在属性$options中,方便之后调用
this.$options = options;
// 这里我们利用hash的方法来构造路由地址,即我们常看到的http://localhost/#/xxx
// 截取url,去掉#号
this.current = window.location.hash.slice(1) ||'/';
// 利用Vue中的defineReactive方法响应式的创建一个数组matched,存放当前路由以及其嵌套路由
Vue.util.defineReactive(this, 'matched', []);
// 处理路由以及嵌套路由
this.matchs(this.$options);
// 监听拿到地址并修改地址
window.addEventListener('hashchange', this.currentHash.bind(this));
// 页面首次加载的时候也需要监听并修改地址
window.addEventListener('load', this.currentHash.bind(this));
}
currentHash() {
this.current = window.location.hash.slice(1);
this.matched = [];
this.matchs(this.$options);
}
matchs(routers) {
// 递归处理路由
// 输出结果一般是['parent', 'child1', 'child1-child1']
routers.forEach(router => {
// 一般来说/路径下不会存放嵌套路由
if (this.current ==='/' && router.path === '/') {
this.matched.push(router);
} else if (this.current.includes(router.path) && router.path !== '/') {
this.matched.push(router);
if (router.children) {
this.matchs(router.children);
}
}
}
}
}
VueRouter.install = function(_Vue) {
Vue = _Vue;
//使用mixin和生命周期beforeCreate做全局混入, 拿到实例后的相关属性并挂载到prototype上
Vue.mixin({
beforeCreate() {
if (this.$options.router) {
// 这里可以看出我们为何可以使用this.$router拿到router实例
// 也可以看出为何要在main.js中的根实例中将router实例作为options配置进去
Vue.prototype.$router = this.$options.router;
}
}
})
// 挂载router-view组件
Vue.component('RouterView', {
render(h) {
// 上面提到的深度标记,父组件深度为0
let deep = 0;
// 将组件标记
this.routerView = true;
// 循环找出所有RouterView的父元素 并标记深度
// 利用父子通信的中的$parent
let parent = this.$parent;
// 循环查找父组件,以此标记当前组件的深度,当parent不存在时候,说明已经到了顶层组件,退出循环
while(parent) {
// 父组件存在且是routerView
if (parent && parent.routerView) {
deep+=1;
}
parent = parent.$parent;
}
// 找到匹配深度层级,找出component
let matched = this.$router.matched;
let _component = null;
// 如果能在matched中找出,为_component赋值
if (matched[deep]) {
_component = matched[deep].components;
}
// 渲染_component
return h(_component);
}
})
// 挂载router-link组件
Vue.component('RouterLink', {
// 声明props,to并设置为必填
props: {
to: {
type: String,
required: true
}
},
render(h) {
'a',
{
attrs: {
href: `#${this.to}`
}
},
// 利用匿名插槽将组件中文本写入
[
this.$slots.default
]
}
})
}
export default VueRouter;
写在最后
- 到这里,这次的vue-router介绍就基本结束了。其实这里实现的只是一个最简单,最基础的vue-router,也是我在学习过程中的一个总结,旨在为大家做一个拓展和介绍。其中里面的重定向,路由懒加载以及路由守卫等都没有体现,大家可以按照这里分析的思路去看和学习源码。本人不才,文笔也一般,不敢说授人以渔,哪怕是给大家一个提示和启发也是极好的。如果不明白的地方,可以大家一起讨论,有错误的话欢迎大家指正,一起学习,一起进步。
- 下一篇可能会写一篇关于vuex的实现,喜欢的同学们给个赞也是棒棒的,hhhhh。
- 最后的最后,源码在我的github上,觉得有帮助的同学们可以给个小小的star。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。