前端入门之(vue-router全解析二)

Posted vv_小虫

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端入门之(vue-router全解析二)相关的知识,希望对你有一定的参考价值。

前面一篇文章 前端入门之(vue-router全解析一)我们简单的了解了一下vue-router的原理,今天我们继续vue-router,我们从头到尾的全部撸一遍哈.小伙伴开车啦,跟紧了哦~~

我这里以vue-cli+webpack创建的但页面工程为例子了哈,我们直接选带有vue-router的模版代码为demo.

先上一下vue-router的官网:
https://router.vuejs.org/zh/guide/#javascript

在创建好的vue工程中,我们可以看到我们的main.js文件:


import router from './router'
....
new Vue(
  el: '#app',
  router,
  store,
  render(h)
    return h(App)
  
)


然后是我们的router.js文件:

import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)

export default new Router(
  mode:'hash',
  routes: [
    
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld
    
  ]
)

然后在App.vue中配置router-view:

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

就这么简单的几个操作,我们的vue-router就可以跑起来了,好啦! 我们转过头来一步一步的研究vue-router是怎么走起来的.

首先我们看我们的router.js文件:

Vue.use(Router)

export default new Router(
...
 )

我们看一下Vue.use(Router)干了什么~~

我们翻开vue-router的源码,找到install方法:

function install (Vue) 
  if (install.installed && _Vue === Vue)  return 
  install.installed = true;

  _Vue = Vue;

  var isDef = function (v)  return v !== undefined; ;
  
  var registerInstance = function (vm, callVal) 
    var i = vm.$options._parentVnode;
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) 
      i(vm, callVal);
    
  ;

  Vue.mixin(
    beforeCreate: function beforeCreate () 
      if (isDef(this.$options.router)) 
        this._routerRoot = this;
        this._router = this.$options.router;
        this._router.init(this);
        Vue.util.defineReactive(this, '_route', this._router.history.current);
       else 
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
      
      registerInstance(this, this);
    ,
    destroyed: function destroyed () 
      registerInstance(this);
    
  );

  Object.defineProperty(Vue.prototype, '$router', 
    get: function get ()  return this._routerRoot._router 
  );

  Object.defineProperty(Vue.prototype, '$route', 
    get: function get ()  return this._routerRoot._route 
  );

  Vue.component('router-view', View);
  Vue.component('router-link', Link);

  var strats = Vue.config.optionMergeStrategies;
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created;

我们看到这么一段代码:

  Vue.mixin(
    beforeCreate: function beforeCreate () 
      if (isDef(this.$options.router)) 
        this._routerRoot = this;
        this._router = this.$options.router;
        this._router.init(this);
        Vue.util.defineReactive(this, '_route', this._router.history.current);
       else 
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
      
      registerInstance(this, this);
    ,
    destroyed: function destroyed () 
      registerInstance(this);
    
  );

在混入中添加了两个生命周期方法,一个beforeCreate、一个destroyed,在beforeCreate我们可以看到一行代码:

 Vue.util.defineReactive(this, '_route', this._router.history.current);

可以说这一行代码发挥的作用还是挺大的,先剧透一下, 这个方法就是给_routerRoot也就是我们demo中的App.vue中的vue实例注册了一个动态变换属性“_route”, 在上一节中我们知道,vue-router原理就是使用pushState跟hash来做路由切换,vue-router会根据location的变换来动态的改变"_route"变量,而我们的"_route"变量中包含了当前路由需要渲染的组件等信息,而在router-view中,"_route"变量会当成属性传递给router-view,当_route变换的时候,我们的router-view就会重新走render方法,然后render方法中再根据_route中的当前组件,最后把当前页面渲染在页面.

好啦~!! 不知道小伙伴懂了没呢? 不懂也没关系,我们到时讲到vouter-view的时候还会走一遍代码,install方法最重要的也就是这一段代码了,我们继续往下走哈.

接着就是在Vue的原型中定义了两个属性 r o u t e r 跟 router跟 routerroute

  Object.defineProperty(Vue.prototype, '$router', 
    get: function get ()  return this._routerRoot._router 
  );

  Object.defineProperty(Vue.prototype, '$route', 
    get: function get ()  return this._routerRoot._route 
  );

所以我们可以在Vue的所有实例中使用this. r o u t e r 跟 t h i s . router跟this. routerthis.route了,this.$router就是我们传递的router对象:

export default new Router(
  mode:'hash',
  routes: [
    
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld
    ,

this.$route是当前的route对象,其中包含了当前页面中的所有信息.

接着就是注册了两个全局组件了:

 Vue.component('router-view', View);
  Vue.component('router-link', Link);

想必稍微了解过vue-router的小伙伴都知道这两个组件,router-view是router的根组件,所有的页面跳转都是基于router-view做跳转的.
router-link则是vue-router提供的一个方便开发者使用api而封装的一个组件,童鞋们也可以不用router-link,自己走api的方式来实现路由跳转.

好啦~ 分析了好久才把install方法简单地分析完,嘿嘿,真是佩服写框架的大佬们.

分析完install方法,我们紧接着来说一下vue-router组件,我们可以在vue-router的源码中看到vue-router组件的代码:

var View = 
  name: 'router-view',
  functional: true,
  props: 
    name: 
      type: String,
      default: 'default'
    
  ,
  render: function render (_, ref) 
    var props = ref.props;
    var children = ref.children;
    var parent = ref.parent;
    var data = ref.data;
    data.routerView = true;

    // directly use parent context's createElement() function
    // so that components rendered by router-view can resolve named slots
    var h = parent.$createElement;
    var name = props.name;
    var route = parent.$route;
    var cache = parent._routerViewCache || (parent._routerViewCache = );

    // determine current view depth, also check to see if the tree
    // has been toggled inactive but kept-alive.
    var depth = 0;
    var inactive = false;
    while (parent && parent._routerRoot !== parent) 
      if (parent.$vnode && parent.$vnode.data.routerView) 
        depth++;
      
      if (parent._inactive) 
        inactive = true;
      
      parent = parent.$parent;
    
    data.routerViewDepth = depth;

    // render previous view if the tree is inactive and kept-alive
    if (inactive) 
      return h(cache[name], data, children)
    

    var matched = route.matched[depth];
    // render empty node if no matched route
    if (!matched) 
      cache[name] = null;
      return h()
    

    var component = cache[name] = matched.components[name];

    // attach instance registration hook
    // this will be called in the instance's injected lifecycle hooks
    data.registerRouteInstance = function (vm, val) 
      // val could be undefined for unregistration
      var current = matched.instances[name];
      if (
        (val && current !== vm) ||
        (!val && current === vm)
      ) 
        matched.instances[name] = val;
      
    

    // also register instance in prepatch hook
    // in case the same component instance is reused across different routes
    ;(data.hook || (data.hook = )).prepatch = function (_, vnode) 
      matched.instances[name] = vnode.componentInstance;
    ;

    // resolve props
    var propsToPass = data.props = resolveProps(route, matched.props && matched.props[name]);
    if (propsToPass) 
      // clone to prevent mutation
      propsToPass = data.props = extend(, propsToPass);
      // pass non-declared props as attrs
      var attrs = data.attrs = data.attrs || ;
      for (var key in propsToPass) 
        if (!component.props || !(key in component.props)) 
          attrs[key] = propsToPass[key];
          delete propsToPass[key];
        
      
    

    return h(component, data, children)
  
;

代码有点多,我们捡重点的来分析,通过源码我们可以看到,router-view是一个无状态的组件,也就是设置了functional属性,在vue的官网中我们可以找到,当一个组件设置了functional属性后,这个组件也就是一个无状态的组件(也就是没有data),无实例(没有 this 上下文),要深入研究的童鞋可以去看一下vue的官网
https://cn.vuejs.org/v2/guide/render-function.html

render: function render (_, ref) 
    ...
    var route = parent.$route;

	...
    var matched = route.matched[depth];
...

    var component = cache[name] = matched.components[name];

	...
    return h(component, data, children)
  
;

在render方法中,我们可以看到,首先去获取route对象,然后获取route对象中的当前页面component,最后通过render的h方法把component(当前页面)渲染出来.

所以当我们执行vue-router的push、replace、back、go等方法的时候,其实就是改变location然后再改变当前route对象,当route对象改变的时候,就会触发router-view的render方法,然后render方法根据route做页面切换. 好吧,说了半天,想必小伙伴也都累了,我们就以vue-router的push方法为例子走一遍整个流程.

push这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的 URL。

VueRouter.prototype.push = function push (location, onComplete, onAbort) 
  this.history.push(location, onComplete, onAbort);
;

因为vue-router中有hash跟history模式,我们就以html5History为例子了,我们找到HTML5History的push方法:

 HTML5History.prototype.push = function push (location, onComplete, onAbort) 
    var this$1 = this;

    var ref = this;
    var fromRoute = ref.current;
    this.transitionTo(location, function (route) 
      pushState(cleanPath(this$1.base + route.fullPath));
      handleScroll(this$1.router, route, fromRoute, false);
      onComplete && onComplete(route);
    , onAbort);
  ;

幸好代码不是很多,我们可以看到最主要的就是执行了一个叫transitionTo的方法,我们点进去:

History.prototype.transitionTo = function transitionTo (location, onComplete, onAbort) 
    var this$1 = this;

  var route = this.router.match(location, this.current);
  this.confirmTransition(route, function () 
    this$1.updateRoute(route);
    onComplete && onComplete(route);
    this$1.ensureURL();

    // fire ready cbs once
    if (!this$1.ready) 
      this$1.ready = true;
      this$1.readyCbs.forEach(function (cb)  cb(route); );
    
  , function (err) 
    if (onAbort) 
      onAbort(err);
    
    if (err && !this$1.ready) 
      this$1.ready = true;
      this$1.readyErrorCbs.forEach(function (cb)  cb(err); );
    
  );
;

然后可以看到,又执行了一个叫confirmTransition的方法,简单说一下confirmTransition方法,confirmTransition方法主要是用于处理一些导航守卫函数, 一个一个的遍历这些守卫函数,最后执行,直到执行完所有的过渡函数,最后回调在transitionTo方法中传递的函数

History.prototype.transitionTo = function transitionTo (location, onComplete, onAbort) 
    ...
  this.confirmTransition(route, function () 
    this$1.updateRoute(route);
   ...
  , function (err) 
   ...
  );
;

可以看到,当执行完confirmTransition方法后,执行了一行代码:

 this$1.updateRoute(route);

这行代码是干什么的呢? 我们继续往下走:

History.prototype.updateRoute = function updateRoute (route) 
  var prev = this.current;
  this.current = route;
  this.cb && this.cb(route);
  this.router.afterHooks.forEach(function (hook) 
    hook && hook(route, prev);
  );
;

可以看到,最主要的就是改变了current(也就是当前路由),然后执行了

this.cb && this.cb(route);

把当前路由传递给了cb方法,那么cb方法又是啥呢?找呀找呀,我们找到了HtmlHistory的父类:

History.prototype.listen = function listen (cb) 
  this.cb = cb;
;

所以我们需要看一下listen方法在哪调用的,我们继续找呀找呀,最后在vue-router的init方法中找到了它:

VueRouter.prototype.init = function init (app /* Vue component instance */) 
   ....

  history.listen(function (route) 
    this$1.apps.forEach(function (app) 
      app._route = route;
    );
  );
;

那么init方法又是哪调用的呢? 我们继续找呀找呀,最后在install方法中找到了init方法:

function install (Vue) 
  ...
  Vue.mixin(
    beforeCreate: function beforeCreate () 
      if (isDef(this.$options.router)) 
        this._routerRoot = this;
        this._router = this.$options.router;
        this._router.init(this);
        Vue.util.defineReactive(this, '_route', this._router.history.current);
       else 
        ...

我重点看init方法中的这段代码:

VueRouter.prototype.init = function init (app /* Vue component instance */) 
   ....
  history.listen(function (route) 
    this$1.apps.forEach(function (app) 
      app._route = route;
    );
  );
;

遍历apps,然后给apps中的每个_route对象重新赋值最新的route路由信息,apps是什么呢? 从install方法中我们可以看到,apps其实就是像App.vue中的vue实例的一个集合, 因为我们demo中的rootRouter就一个,所以apps集合中也就一个vue实例,也就是App.vue.

文章一开始我们就解析了vue-router的install方法,在install中给rootRouter定义了一个可以变化的属性"_route":

 Vue.util.defineReactive(this, '_route', this._router.history.current);

所以当_route改变的时候,我们的的router-view重新走render方法,页面改变.

我们回到HTML5History的push方法:

 HTML5History.prototype.push = function push (location, onComplete, onAbort) 
    var this$1 = this;

    var ref = this;
    var fromRoute = ref.current;
    this.transitionTo(location, function (route) 
      pushState(cleanPath(this$1.base + route.fullPath));
      handleScroll(this$1.router, route, fromRoute, false);
      onComplete && onComplete(route);
    , onAbort);
  ;

前面分析我们直到,当我们执行完transitionTo方法的时候,其实我们的页面已经改变了,但是我们的链接地址还没变,所以接下来就调用了 pushState方法,在上一节中,我们已经认识了pushState方法了,所以我们点进去看看是不是跟我们上一节说的一样:

function pushState (url, replace) 
  saveScrollPosition();
  // try...catch the pushState call to get around Safari
  // DOM Exception 18 where it limits to 100 pushState calls
  var history = window.history;
  try 
    if (replace) 
      history.replaceState( key: _key , '', url);
     else 
      _key = genKey();
      history.pushState( key: _key , '', url);
    
   catch (e) 
    window.location[replace ? 'replace' : 'assign'](url);
  

哈哈!! 我就不解释了哈,小伙伴感兴趣的可以去看我上一篇文章
https://blog.csdn.net/vv_bug/article/details/82795248

好啦,到这里vue-router的全部流程我们就简单的走完了,小伙伴要深入研究的自己可以去试着看看源码,也可以一起交流学习一下哈,嘿嘿!!! 最后还落了一个router-link组件, 好晚啦,实在写不动了,放到下一节了~

欢迎入群,欢迎交流,qq群链接:

以上是关于前端入门之(vue-router全解析二)的主要内容,如果未能解决你的问题,请参考以下文章

非对称加密法入门:私钥公钥RSA全解析

前端路由简介以及vue-router实现原理

前端路由简介以及vue-router实现原理

前端进阶全栈入门级教程nodeJs博客开发(二)安装mysql完善api接口对接mysql

从头开始学习 vue-router

前端进阶全栈入门级教程nodeJs博客开发(二)安装mysql完善api接口对接mysql