当前位置:   article > 正文

手写简易版vue-router

简易版的 vuerouter

关注 前端开发博客,回复“加群”

加入我们一起学习,天天进步

作者:wangkaiwd

https://github.com/wangkaiwd/simple-vue-router

vue-router是开发vue项目中必不可少的依赖,为了能更好的理解其实现原理,而源码阅读起来又过于复杂和枯燥,笔者这里实现一个简易版本的vue-rouer,帮助自己来更好的理解源码。

其功能如下:

  • 通过Vue插件形式使用

  • 支持hash模式

  • 支持嵌套路由

  • router-view组件

  • router-link组件

  • 路由守卫

基本使用

基础demo单独新建了一个分支 ,方便学习和查看

在实现自己的router之前,我们先使用官方的包来书写一个基础demo,之后我们会以这个demo为需求,一步步实现我们自己的vue-router

demo的代码逻辑如下:

  • App页面中拥有HomeAbout俩个链接

  • 点击Home会跳转到Home页面

  • 点击About会跳转到About页面

  • About又有to ato b俩个链接,分别跳转到ab页面

下面开始使用我们自己写的vue-router来实现上边展示的功能。

intall方法

vue-router使用方式如下:

  1. import Vue from 'vue';
  2. import VueRouter from '../my-router';
  3. import Home from '../views/Home.vue';
  4. import About from '@/views/About';
  5. Vue.use(VueRouter);
  6. const routes = [
  7.   {
  8.     path: '/home',
  9.     name: 'Home',
  10.     component: Home
  11.   },
  12.   {
  13.     path: '/about',
  14.     name: 'About',
  15.     component: About
  16.   }
  17. ];
  18. const router = new VueRouter({
  19.   routes
  20. });
  21. export default router;

之后会在main.js中将router作为配置项传入:

  1. import Vue from 'vue';
  2. import App from './App.vue';
  3. import router from './router';
  4. Vue.config.productionTip = false;
  5. new Vue({
  6.   router,
  7.   render: h => h(App)
  8. }).$mount('#app');

由用法我们可以知道vue-router是一个类,并且它有一个静态方法install:

  1. import install from '@/my-router/install';
  2. class VueRouter {
  3.   constructor (options) {
  4.   
  5.   }
  6.   init(app) {
  7.   
  8.   }
  9. }
  10. VueRouter.install = install;
  11. export default VueRouter;

install方法中会为所有组件添加$router以及$route属性,并且会全局注册router-view以及router-link组件。我们在install.js中来单独书写install的逻辑:

  1. import RouterView from '@/my-router/components/view';
  2. import RouterLink from '@/my-router/components/link';
  3. const install = (Vue) => {
  4.   Vue.mixin({
  5.     beforeCreate () {
  6.       const { router } = this.$options;
  7.       // mount $router property for all components
  8.       if (router) {
  9.         // 用_rootRouter来存储根实例
  10.         this._rootRouter = this;
  11.         // 为根实例添加$router属性
  12.         this.$router = router;
  13.         // 在实例上定义响应式属性,但是这个API可能会发生变化,所以Vue并没有在文档中提供
  14.         Vue.util.defineReactive(this, '$route', this.$router.history.current);
  15.         // 初始化路由
  16.         router.init(this);
  17.       } else {
  18.         this._rootRouter = this.$parent && this.$parent._rootRouter;
  19.         if (this._rootRouter) {
  20.           this.$router = this._rootRouter.$router;
  21.           // 在为$route赋值时,会重新指向新的地址,导致子组件的$route不再更新
  22.           // this.$route = this._rootRouter.$route;
  23.           Object.defineProperty(this, '$route', {
  24.             get () {
  25.               return this._rootRouter.$route;
  26.             }
  27.           });
  28.         }
  29.       }
  30.     }
  31.   });
  32.   Vue.component('RouterView', RouterView);
  33.   Vue.component('RouterLink', RouterLink);
  34. };

install方法中做了如下几件事:

  • 为所有组件的实例添加_rootRouter,值为根实例,方便获取根实例上的属性和方法

  • 在根实例执行beforeCreate钩子时执行VueRouter实例的init方法

  • 为所有组件的实例添加$router属性,值为VueRouter实例

  • 为所有组件添加$route属性,值为当前的路由信息(之后会介绍它的由来)

hashchange事件

vue-routerhash模式下可以利用hash值的切换来渲染对应的组件,原理其实是利用页面地址hash值发生改变不会刷新页面,并且会触发hashchange事件。

history目录下,新建hash.js来存放hash值变化,组件进行切换的逻辑:

  1. import { getHash } from '@/my-router/util';
  2. import { createRoute } from '@/my-router/create-matcher';
  3. const ensureSlash = () => {
  4.   if (!location.hash) {
  5.     location.hash = '/';
  6.   }
  7. };
  8. class HashHistory {
  9.   constructor (router) {
  10.     this.router = router;
  11.     // 绑定this指向
  12.     this.onHashchange = this.onHashchange.bind(this);
  13.     // 默认hash值为'/'
  14.     ensureSlash();
  15.   }
  16.   listenEvent () {
  17.     window.addEventListener('hashchange', this.onHashchange);
  18.   }
  19.   
  20.   onHashchange () {
  21.   }
  22. }
  23. export default HashHistory;

VueRouter实例执行init方法时,监听hashchange事件:

  1. class VueRouter {
  2.   constructor (options) {
  3.     this.history = new HashHistory(this);
  4.   }
  5.   init (app) {
  6.     // 第一次渲染时也需要手动执行一次onHashchange方法
  7.     this.history.onHashchange();
  8.     this.history.listenEvent();
  9.   }
  10. }

onHashchange方法中,需要根据当前页面地址的hash值来找到其对应的路由信息:

  1. class HashHistory {
  2.   // some code ...
  3.   onHashchange () {
  4.     const path = getHash();
  5.     const route = this.router.match(path);
  6.   }
  7. }

匹配路由信息

为了找到当前的路由信息,HashHistory中调用了VueRoutermatch方法。match方法放到了create-matcher.js中来实现:

  1. // create-matcher.js
  2. export const createRoute = (route, path) => {
  3.   const matched = [];
  4.   // 递归route的所有父路由,生成matched数组,并和path一起返回,作为当前的路由信息
  5.   while (route) {
  6.     matched.unshift(route);
  7.     route = route.parent;
  8.   }
  9.   return {
  10.     path,
  11.     matched
  12.   };
  13. };
  14. function createMatcher (routes) {
  15.   const pathMap = createRouteMap(routes);
  16.   // need to get all matched route, then find current routes by matched and router-view
  17.   const match = (path) => {
  18.     const route = pathMap[path];
  19.     return createRoute(route, path);
  20.   };
  21.   return {
  22.     match
  23.   };
  24. }
  1. // create-route-map.js
  2. function addRouteRecord (routes, pathMap, parent) {
  3.   routes.forEach(route => {
  4.     const { path, children, ...rest } = route;
  5.     // 拼接子路由path
  6.     const normalizedPath = parent ? parent.path + '/' + path : path;
  7.     // 将parent也放入到属性中,方便之后生成matched数组
  8.     pathMap[normalizedPath] = { ...rest, path: normalizedPath, parent };
  9.     if (children) {
  10.       // 继续遍历子路由
  11.       addRouteRecord(children, pathMap, route);
  12.     }
  13.   });
  14. }
  15. const createRouteMap = (routes, pathMap = {}) => {
  16.   addRouteRecord(routes, pathMap);
  17.   return pathMap;
  18. };

createMatcher会通过createRouteMap生成hash值和路由的映射关系:

  1. const pathMap = {
  2.   '/about': {
  3.     path: '/about',
  4.     name: 'About',
  5.     children: [
  6.       // ...  
  7.     ],
  8.     parent: undefined
  9.   }
  10.   // ...  

这样我们可以很方便的通过hash值来获取路由信息。

最终我们调用match方法得到的路由信息结构如下:

  1. {
  2.   "path""/about/a",
  3.   "matched": [
  4.     {
  5.       "path""/about",
  6.       "name""About",
  7.       "component": About,
  8.       "children": [
  9.         {
  10.           "path""a",
  11.           "name""AboutA",
  12.           "component": A
  13.         },
  14.         {
  15.           "path""b",
  16.           "name""AboutB",
  17.           "component": B
  18.         }
  19.       ]
  20.     },
  21.     // ...  
  22.   ]
  23. }

需要注意的是对象中的matched属性,它里面存放的是当前hash匹配的所有路由信息组成的数组。在实现嵌套路由时会用到matched数组,因为嵌套路由本质上是router-view组件的嵌套,所以可以根据router-view在组件中的深度在matched中找到对应的匹配项,然后进行展示。

现在我们回到hashHistoryonHashchange方法,它会调用VueRouter实例的match方法,代码如下:

  1. class VueRouter {
  2.   constructor (options) {
  3.     this.matcher = createMatcher(options.routes);
  4.     this.history = new HashHistory(this);
  5.   }
  6.   init (app) {
  7.     this.history.onHashchange();
  8.     this.history.listenEvent();
  9.   }
  10.   match (path) {
  11.     return this.matcher.match(path);
  12.   }
  13. }

hashHistory中将其赋值给实例中的current属性:

  1. class HashHistory {
  2.   constructor (router) {
  3.     // pass instance of VueRoute class, can call methods and properties of instance directly
  4.     this.router = router;
  5.     // 当前的路由信息,在current更新后,由于其不具有响应性,所以尽管值更新了,但是不会触发页面渲染
  6.     // 需要将其定义为响应式的数据
  7.     this.current = createRoute(null, '/');
  8.     this.onHashchange = this.onHashchange.bind(this);
  9.     ensureSlash();
  10.   }
  11.   listenEvent () {
  12.     window.addEventListener('hashchange', this.onHashchange);
  13.   }
  14.   onHashchange () {
  15.     const path = getHash();
  16.     const route = this.router.match(path);
  17.     this.current = route
  18.   }
  19. }

为了方便用户访问当前路由信息,并且让其具有响应性,会通过Vue.util.defineReactive来为vue的根实例提供响应性的$route属性,并在每次页面初始化以及路径更新时更新$route:

  1. class HashHistory {
  2.   constructor (router) {
  3.     // pass instance of VueRoute class, can call methods and properties of instance directly
  4.     this.router = router;
  5.     // 当前的路由信息,在current更新后,由于其不具有响应性,所以尽管值更新了,但是不会触发页面渲染
  6.     // 需要将其定义为响应式的数据
  7.     this.current = createRoute(null, '/');
  8.     this.onHashchange = this.onHashchange.bind(this);
  9.   }
  10.   // some code ...
  11.   onHashchange () {
  12.     const path = getHash();
  13.     const route = this.router.match(path);
  14.     // 将当前路由赋值给根实例,app会在router.init方法中进行初始化
  15.     this.router.app.$route = this.current = route
  16.   }
  17. }

install方法中为根实例定义$route属性,并将所有子组件实例的$route属性赋值为根实例的$route属性:

  1. const install = (Vue) => {
  2.   Vue.mixin({
  3.     beforeCreate () {
  4.       const { router } = this.$options;
  5.       // mount $router property for all components
  6.       if (router) {
  7.         this._rootRouter = this;
  8.         this.$router = router;
  9.         // 定义响应性$route属性
  10.         Vue.util.defineReactive(this, '$route', this.$router.history.current);
  11.         router.init(this);
  12.       } else {
  13.         this._rootRouter = this.$parent && this.$parent._rootRouter;
  14.         if (this._rootRouter) {
  15.           this.$router = this._rootRouter.$router;
  16.           // 这样直接赋值会导致引用刷新而无法改变$route
  17.           // this.$route = this._rootRouter.$route;
  18.           // 获取根组件实例的$route属性,其具有响应性
  19.           Object.defineProperty(this, '$route', {
  20.             get () {
  21.               return this._rootRouter.$route;
  22.             }
  23.           });
  24.         }
  25.       }
  26.     }
  27.   });
  28. };

到这里,我们已经可以在地址切换时获取到对应的路由信息,接下来我们实现router-view来展示对应的组件。

实现router-view组件

router-view组件需要展示当前匹配的hash所对应的component,这里采用函数式组件来实现:

  1. export default {
  2.   name: 'RouterView',
  3.   render (h) {
  4.     // 记录组件的深度,默认为0
  5.     let depth = 0;
  6.     const route = this.$parent.$route;
  7.     let parent = this.$parent;
  8.     // 递归查找父组件,如果父组件是RouterView组件,深度++
  9.     // 最终的深度即为路由的嵌套层数
  10.     while (parent) {
  11.       if (parent.$options.name === 'RouterView') {
  12.         depth++;
  13.       }
  14.       parent = parent.$parent;
  15.     }
  16.     // 根据深度从matched中找到对应的记录
  17.     const record = route.matched[depth];
  18.     if (record) { // /about会匹配About页面,会渲染About中的router-view,此时record为undefined
  19.       return h(record.component);
  20.     } else {
  21.       return h();
  22.     }
  23.   }
  24. };

这里必须要为组件指定name属性,方便进行递归查找,进行深度标识。

到这里,我们为路由信息中添加的matched数组,终于派上了用场,其与router-view组件的深度depth进行巧妙结合,最终展示出了所有匹配到的路由组件。

实现router-link组件

router-link主要支持以下几个功能:

  • 进行路由跳转

  • 通过传入的tag渲染不同标签

  • 为当前激活的router-link添加router-link-active类名

在页面中vue-router也支持通过router-link来进行路由跳转,其实现比较简单:

  1. export default {
  2.   props: {
  3.     to: {
  4.       type: String,
  5.     },
  6.     tag: {
  7.       type: String,
  8.       default: () => 'a'
  9.     }
  10.   },
  11.   computed: {
  12.     active () {
  13.       return this.$route.matched.map(item => item.path).includes(this.to);
  14.     }
  15.   },
  16.   methods: {
  17.     onClick () {
  18.       this.$router.push(this.to);
  19.     }
  20.   },
  21.   render () {
  22.     return (
  23.       <this.tag
  24.         onClick={this.onClick}
  25.         href="javascript:;"
  26.         class={{ 'router-link-active': this.active }}
  27.       >
  28.         {this.$slots.default}
  29.       </this.tag>
  30.     );
  31.   }
  32. };

router-link可以接受tag来渲染不同的标签,默认会渲染a标签。当点击router-link的时候,其内部会调用VueRouter实例的push方法:

  1. class VueRouter {
  2.   // some code ...
  3.   push (path) {
  4.     location.hash = path;
  5.   }
  6.   // some code ...
  7. }
  8. // some code ...

push方法会切换页面的hash,当hash发生变化后,就会触发hashchange事件,执行事件处理函数onHashchange,重新通过path匹配对应的路由信息。

在代码中我们通过计算属性active来计算当前的router-link是否激活,需要注意的是当子路由激活时父路由也会激活。如果matchedpath属性组成的数组中包含this.to,说明该router-link被激活。用户可以通过router-link-active类来设置激活样式。

路由beforeEach钩子

在日常开发中,经常会用到beforeEach全局前置守卫,让我们在进入页面之前执行一些逻辑:

  1. // some code ....
  2. const router = new VueRouter({
  3.   routes
  4. });
  5. // 在每次进入页面之前,都会先执行所有的beforeEach中的回调函数
  6. router.beforeEach((to, from, next) => {
  7.   console.log(1);
  8.   setTimeout(() => {
  9.     // 调用下一个回调函数
  10.     next();
  11.   }, 1000);
  12. });
  13. router.beforeEach((to, from, next) => {
  14.   console.log(2);
  15.   next();
  16. });

在每次进入页面之前,vue-router会先执行beforeEach中的回调函数,并且只有当用户调用回调函数中传入的next函数后,才会执行之后的beforeEach中的回调。

当所有beforeEach中的回调执行完毕后,调用next函数会更新路由信息,然后通过router-view来显示对应的组件。其实现如下:

  1. // my-router/index.js
  2. class VueRouter {
  3.   constructor (options) {
  4.     // some code ...
  5.     this.beforeEachs = [];
  6.   }
  7.   // cache in global, execute before get matched route record
  8.   beforeEach (fn) {
  9.     this.beforeEachs.push(fn);
  10.   }
  11. }
  12. // my-router/history/hash.js
  13. class HashHistory {
  14.   // some code ...
  15.   onHashchange () {
  16.     const path = getHash();
  17.     const route = this.router.match(path);
  18.     // 当用户手动调用next时,会执行下一个beforeEach钩子,在所有的钩子执行完毕后,会更新当前路由信息
  19.     const next = (index) => {
  20.       const { beforeEachs } = this.router;
  21.       if (index === beforeEachs.length) {
  22.         //update route after executed all beforeEach hook
  23.         this.router.app.$route = this.current = route;
  24.         return;
  25.       }
  26.       const hook = beforeEachs[index];
  27.       hook(route, this.current, () => next(index + 1));
  28.     };
  29.     next(0);
  30.   }
  31. }

上述代码的执行流程如下:

  • beforeEach中传入的函数放到全局的数组beforeEachs

  • 在根据路径匹配最新的路由信息时,先执行beforeEachs中存储的函数

  • 根据一个递增的index来读取beforeEachs中的函数,执行时传入新的路由信息route、旧的路由信息this.current,以及需要用户调用的回调函数

  • 当用户调用回调后,index+1继续执行next函数,进而执行beforeEachs中的下一个函数

  • 当执行完beforeEachs中的所有函数后,为$route赋值最新的路由信息

庆祝一下????,这里我们已经完成了文章开头定下的所有目标!

结语

希望在读完文章之后,能让读者对vue-router的底层实现有更深入的了解,明白日常使用的API是怎么来的,从而更加熟练的使用vue-router

最后,如果文章有帮到你的话,希望能点赞鼓励一下作者????。

最后

公众号后台回复:vue router,获取源码地址。

“在看”吗?在看就点一下吧

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/289475
推荐阅读
相关标签
  

闽ICP备14008679号