源码篇(六):手写vue_route版mini源码分析route。附送简版vue-route源码__Vue.js
发布于 4 年前 作者 banyungong 1358 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

前言

路由以及是前端必须掌握的技能之一。“用上"路由很简单,"用好"路由却需要了解以route是什么,才能用得更好。本文适合1.5~3.5经验的博友们,提高自己。

再同步一下笔者的博客进度:

序号 博客主题 相关链接
1 手写vue_mini源码解析 https://juejin.im/post/6847902225138876424
2 手写react_mini源码解析 https://juejin.im/post/6854573212018147342
3 手写webpack_mini源码解析 https://juejin.im/post/6854573219245441038
4 手写jquery_mini源码解析 https://juejin.im/post/6854573220356423694
5 手写vuex_mini源码解析(即本文) https://juejin.im/post/6855295553794736142
6 手写vue_route源码解析 https://juejin.im/post/6859565866552393741
7 手写diff算法源码解析 预计8月
8 手写promise源码解析 预计8月
9 手写原生js源码解析(手动实现常见api) 预计8月
10 手写react_redux,fiberd源码解析等 预计9月
11 手写koa2_mini 预计9月,前端优先

route的概念

什么是路由。可以简单的理解成一个url的地址,来展示不同的内容或者页面。

很久以前,在单页面时代还没开启之前,我们习惯用"页面链接"来描述他,他后来有个叫法叫"后端路由"。后续单页面架构的出现,也就是前端路由。

后端路由,很好理解,就是找到对应的文件地址,讲文件经过一层物理转换,输出到前端。每一次获取地址,都需要连接服务器重新加载(暂不考虑缓存问题)

前端路由,相对后端路由复杂一点。分为hash模式与history模式

hash模式背后的原理是onhashchange事件,可以在window对象上监听这个事件,再匹配到对应的路由规则,再渲染到页面。

而history模式是history.pushState API 来完成 URL 跳转而无须重新加载页面。

看了不少route的文章。其中觉得yck这里的描述最通俗易懂,这里借鉴一下他的图片(结尾会给链接),来帮助大家理解route的原理。

hash模式:

history模式:

我们理解完这两个图,对路由的一个基本的了就有了。

手写html-hash-route

写vue-route的mini版之前,我们来写一个简单html帮忙理解原理。我们新建一个zRouter.js,新建一个类。我们分析一下我们要做什么:

  • 1.从上图分析,我们是要监听原生api的load,跟hashchange事件的,触发对应的地址。我们需要在初始化时,就对他们完成监听
  • 2.监听后,我们需要匹配到对应的规则。我们可以拿到原来的路由地址,再去匹配对应的路由地址,触发他的回调函数。

我们可以推出一个简版本的源码:

zRouter.js:

class zRouter{
    constructor(){
        this.routes = {};
        this.init();
    }
    refresh(){
        let path = location.hash;
        this.routes[ path]();
    }
    init(){
        window.addEventListener( 'load', this.refresh.bind( this ), false );
        window.addEventListener( 'hashchange', this.refresh.bind( this ), false  );
    }
    route( path,  cb ){
        this.routes["#" + path] = cb || function(){};
    }
}

然后我们的页面,再对应的路由,跳转对应的路由地址。一个简单hash处理就出来了。 zRouter.html:

<div id="app" class="body">
    <div class="b_header">
        <span>zRouter</span>
    </div>
    <div class="b_content">
        <div>
            <a href="#/" class="c_tag" >首页</a>
            <a href="#/product" class="c_tag" >商品页面</a>
            <a href="#/order" class="c_tag" >订单页面</a>
        </div>
        <div class="c_inner" id="content" >
            这是首页
        </div>
    </div>
</div>

<script>
    window.onload = function () {
        var cont = document.querySelector('#content');
        var router = new zRouter();
        router.route( "/", function(){
            cont.innerHTML = "这是首页";
        });
        router.route( "/product", function(){
            cont.innerHTML = "这是商品页面";
        });
        router.route( "/order", function(){
            cont.innerHTML = "这是订单页面";
        });
    }
</script>

手写vue-route

写完最简的html-route。我们继续vue-route,其实原理是一样的,只是vue框架中,考虑的事情相对多一点。比如,如何混入vue实例,路由如何完成实施监听等。我们逐步完成他。

1)基本架子搭建

npm install --global vue-cli vue init webpack my-vue-router

然后自己写两个组件,以及配置好路由跑起来。

2)新建vueRouter实例

修改import Router from 'vue-router’为import Router from ‘…/vue-router/index.js’

我们新建抒写vue-router/index.js来替换vue-router。那么源码vue-router中到底有什么?

  • 首先Vue.use(Router)。说明源码中暴露了自己的install方法。
  • 根据new Router({routes: []});,说明源码中,暴露了一个对象,且接受了routes参数
  • 接收到routes要做什么?保存对应的页面内容,方便下次触发路由时,再跳转到对应的内容。

我们可以简单的推出…/vue-router/index.js:

  class VueRouter {
    constructor(options) {
      this.routes = options.routes || [];
      this.routesMap = this.createMap(this.routes);
    }

    createMap(routes) {
      return routes.reduce((pre, current) => {
        pre[current.path] = current.component
        return pre;
      }, {})
     }
  }
  VueRouter.install = function () {}
  export default VueRouter;

3) 存储当前路由值

上述的代码,已经存好hash值跟路由组件的关系(假设是hash模式),那么我们肯定需要,先拿到路由hash值, 我们新建一个currentObj对象来存储他。而且需要监听他的变化。我们写多一个initListener方法:

 class VueRouter {
    constructor(options) {
      ...//省略
      this.currentObj = {
        path: null
      };
      this.initListener();
    }

    initListener() {
      location.hash ? '' : location.hash = "/";
      window.addEventListener("load", () => {
        this.currentObj.path = location.hash.slice(1)
      })
      window.addEventListener("hashchange", () => {
        this.currentObj.path = location.hash.slice(1)
      })
    }
  }

4) 获取VueRouter实例对象

我们的目标是啥,router-view根据hash的值,展示出正确的内容。

首先router-view是个啥?其实就是个组件。官方已经帮我写好了component。那我们只需要找到对应的内容,已经对应的路径即可。我们上边已经将路由关系存在的routesMap。即是此时,拿到当前路由的地址,即可触发

来看代码:

const routerView = {
  name: 'RouterView',
  render(h) {
    const currentPath = null; // "当前地址路径",即上边的 currentObj.path
    const routeMap = null;  // VueRouter实例对象的.routesMap;
    return h(routeMap[currentPath])
  }
}

这样,我们只要拿到currentPath跟routeMap,我们的组件即可渲染。现在的关键点变成,如何拿到VueRouter对象,以及currentPath存储的对象。

这的确是一个开始让我疑惑的地方,我们看一下官方源码如何获取的(https://github.com/vuejs/vue-router/blob/dev/src/install.js#L20):

分析一下他的获取思路,利用Vue.mixin获取对象模式,每个组件都经过Vue.mixin,如果是根组件,存储在_routerRoot, 这就是我们的VueRouter对象。(因为你的Vue.use(vueRouter)是在根组件混入的没错吧? 那vueRouter存储的信息的对象,即是根组件对象)

那么非根组件怎么获取vueRouter对象?根组件居然拿到了,非根组件,那就一层一层往上拿,拿到根为止。

此时,我们来修改install的方法:

VueRouter.install = function (v) {
	 const Vue = v;
     
     Vue.mixin({
        beforeCreate() {
           if (this.$options && this.$options.router) { // 如果是根组件
             this._routerRoot = this; //把当前实例挂载到_routerRoot上
             this._router = this.$options.router;//顺便存储一下router的值
           } else { //如果是子组件
             this._routerRoot = this.$parent && this.$parent._routerRoot;
           }
        }
      });
}

此时_routerRoot就是我们的VueRouter实例对象。

5) 渲染router-view

上述说到,拿到vueRouter对象,即可渲染router-view。我们再来修改一下渲染方法。

const routerView = {
  name: 'RouterView',
  render(h) {
    const currentPath = this._self._routerRoot._router.currentObj.path;
    const routeMap = this._self._routerRoot._router.routesMap;
    return h(routeMap[currentPath])
  }
}

此时,应该拿到路径,渲染出组件。调用组件,初步路由完成!

v.component('RouterView', routerView);

但是,页面却还是没有效果。思路是对的,来调试原因。最后发现currentPath还是underfine。此时如果你写死currentPath的话,组件就正确渲染了。

currentObj.path的获取方法肯定是没问题的,但是读取出来的结果切是underfine。

那只有一种可能,currentObj.path还没有拿到的时候,我们的组件就已经开始渲染了。这首先想到的方法的解决,当然是双向绑定,实时触发数据。

双向绑定?方案1可以将 把我们的: currentObj对象 = 一个new Vue(); 这就使我们的currentObj实时的。

后来看到博友cobish就是这么干的。

但是我们看源码:

源码是通过 Vue.util.defineReactive(this, ‘_route’, this._router.history.current)来实现双线绑定。

defineReactive不知道是什么的朋友看看代码:

我们跟着源码依葫芦画瓢,添加代码:

Vue.util.defineReactive(this, '_route', this._router.currentObj); // 来实现双线绑定

即将我们的this._router.currentObj对象赋值到_route里边。此时,刷新组件,已经成功渲染。代码如下:

VueRouter.install = function (v) {
  const Vue = v;
  Vue.mixin({
    beforeCreate() {
      if (this.$options && this.$options.router) { // 如果是根组件
        this._routerRoot = this; //把当前实例挂载到_root上
        this._router = this.$options.router;
        Vue.util.defineReactive(this, '_route', this._router.currentObj); // 来实现双线绑定
      } else { //如果是子组件
        this._routerRoot = this.$parent && this.$parent._routerRoot
      }
    }
  });
  v.component('RouterView', routerView);
}

6) 添加router-link

写完router-view,我们继续router-link的抒写,其实就是一个a标签,再把里边的元素放到a标签中即可,直接撸代码:

 const routerLink = {
  name: 'RouterLink',
  props: {
    to: String
  },
  render(h) {
    const mode = this._self._routerRoot._router.mode;
    return h('a', {
      attrs: {
        href: (mode === "hash" ? "#" : "") + this.to
      }
    }, this.$slots.default)
  }
}

然后全局混入即可:

v.component('RouterLink', routerLink);

7)暴露对象

此时,整个路由已经完成了。可能会问,vue实例中,拿不到 $ router对象。那么我们在初始化router,顺便暴露$router即可。

我们先看看官方怎么写:

相信看完你就明白,官方用了数据劫持,当vue实例获取$router或者$route,直接通过Object.defineProperty获取vue-router中设置对象的值:

 Vue.mixin({
    beforeCreate() {
        ...//省略
        
        Object.defineProperty(this, '$router', {
         get() {
           return this._routerRoot._router;
         }
       });
       Object.defineProperty(this, '$route', {
         get() {
           return this._routerRoot._router.currentObj.path;
         }
       })
    }
 })

8)支持history模式

以上代码,全部是跟着hash模式走通。需要支持history,其实很简单,当history模式时,监听hashChange改成监听popstate即可。

此外,拿路径的时候注意#跟/怎么处理即可。直接看下方完整mini源码。

完整源码

//渲染routerView组件
const routerView = {
  name: 'RouterView',
  render(h) {
    const currentPath = this._self._routerRoot._router.currentObj.path;
    const routeMap = this._self._routerRoot._router.routesMap;
    return h(routeMap[currentPath])
  }
}

//渲染routerLink组件
const routerLink = {
  name: 'RouterLink',
  props: {
    to: String
  },
  render(h) {
    const isHashMode = this._self._routerRoot._router.isHashMode;
    return h('a', {
      attrs: {
        href: ( isHashMode? "#" : "") + this.to
      }
    }, this.$slots.default)
  }
}

class VueRouter {

  constructor(options) {
    this.isHashMode = !( options.mode == "history" );//只要不是history模式,其他均为hash模式
    this.routes = options.routes || [];//存储路由地址
    this.routesMap = this.getRoutesMap(this.routes);
    this.currentObj = { path: '' };
    this.initListener()
  }

  initListener() {
    //hash模式以hash为准,history以pathname为准,且为空时默认为/
    const initPath = ( this.isHashMode ? location.hash.slice(1) : location.pathname ) || "/";
    const listName =  this.isHashMode ? "hashchange": "popstate";//监听的对象名称,hash模式监听hashchange,history监听popstate
    window.addEventListener("load", () => {
      this.currentObj.path = initPath;
    })
    window.addEventListener( listName, () => {
      this.currentObj.path =  ( this.isHashMode ? location.hash.slice(1) : location.pathname ) || "/";;
    })
}

  getRoutesMap(routes) {
    return routes.reduce((pre, current) => {
      pre[current.path] = current.component
      return pre;
    }, {})
  }

}

VueRouter.install = function (v) {
  v.mixin({
    beforeCreate() {
      if (this.$options && this.$options.router) { // 如果是根组件
        this._routerRoot = this; //把当前实例挂载到_root上
        this._router = this.$options.router;
        v.util.defineReactive(this, '_route', this._router.currentObj); // 来实现双线绑定
      } else { //如果是子组件
        this._routerRoot = this.$parent && this.$parent._routerRoot
      }

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

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

  v.component('RouterLink', routerLink);
  v.component('RouterView', routerView);

}

export default VueRouter;

可下载demo运行。

文件结尾

源码地址

https://github.com/zhuangweizhan/codeShare

参考及感谢

本文为原创。思维上有几个文章借鉴的地方,感谢他们:

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 逐步前行 原文链接:https://juejin.im/post/6860107861134540814

回到顶部