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

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/vv_bug/article/details/82805290

上一节前端入门之(vue-router全解析二)我们带着vue-router的push方法走了一遍源码,然后还分析了router-view的源码,最后还差router-link组件没有分析了,我们今天继续vue-router解析.

我们在源码中找到router-link组件的代码:

var Link = {
  name: 'router-link',
  props: {
    to: {
      type: toTypes,
      required: true
    },
    tag: {
      type: String,
      default: 'a'
    },
    exact: Boolean,
    append: Boolean,
    replace: Boolean,
    activeClass: String,
    exactActiveClass: String,
    event: {
      type: eventTypes,
      default: 'click'
    }
  },
  render: function render (h) {
    var this$1 = this;

    var router = this.$router;
    var current = this.$route;
    var ref = router.resolve(this.to, current, this.append);
    var location = ref.location;
    var route = ref.route;
    var href = ref.href;

    var classes = {};
    var globalActiveClass = router.options.linkActiveClass;
    var globalExactActiveClass = router.options.linkExactActiveClass;
    // Support global empty active class
    var activeClassFallback = globalActiveClass == null
            ? 'router-link-active'
            : globalActiveClass;
    var exactActiveClassFallback = globalExactActiveClass == null
            ? 'router-link-exact-active'
            : globalExactActiveClass;
    var activeClass = this.activeClass == null
            ? activeClassFallback
            : this.activeClass;
    var exactActiveClass = this.exactActiveClass == null
            ? exactActiveClassFallback
            : this.exactActiveClass;
    var compareTarget = location.path
      ? createRoute(null, location, null, router)
      : route;

    classes[exactActiveClass] = isSameRoute(current, compareTarget);
    classes[activeClass] = this.exact
      ? classes[exactActiveClass]
      : isIncludedRoute(current, compareTarget);

    var handler = function (e) {
      if (guardEvent(e)) {
        if (this$1.replace) {
          router.replace(location);
        } else {
          router.push(location);
        }
      }
    };

    var on = { click: guardEvent };
    if (Array.isArray(this.event)) {
      this.event.forEach(function (e) { on[e] = handler; });
    } else {
      on[this.event] = handler;
    }

    var data = {
      class: classes
    };

    if (this.tag === 'a') {
      data.on = on;
      data.attrs = { href: href };
    } else {
      // find the first <a> child and apply listener and href
      var a = findAnchor(this.$slots.default);
      if (a) {
        // in case the <a> is a static node
        a.isStatic = false;
        var extend = _Vue.util.extend;
        var aData = a.data = extend({}, a.data);
        aData.on = on;
        var aAttrs = a.data.attrs = extend({}, a.data.attrs);
        aAttrs.href = href;
      } else {
        // doesn't have <a> child, apply listener to self
        data.on = on;
      }
    }

    return h(this.tag, data, this.$slots.default)
  }
};

function guardEvent (e) {
  // don't redirect with control keys
  if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) { return }
  // don't redirect when preventDefault called
  if (e.defaultPrevented) { return }
  // don't redirect on right click
  if (e.button !== undefined && e.button !== 0) { return }
  // don't redirect if `target="_blank"`
  if (e.currentTarget && e.currentTarget.getAttribute) {
    var target = e.currentTarget.getAttribute('target');
    if (/\b_blank\b/i.test(target)) { return }
  }
  // this may be a Weex event which doesn't have this method
  if (e.preventDefault) {
    e.preventDefault();
  }
  return true
}

代码不是很多,我们直接结合demo展示下router-view跟router-link组件,先简单看一下我们要实现的需求:
在这里插入图片描述

可以看到,页面就两个tab按钮,然后点击每个tab按钮的时候切换不同的页面内容.

我们首先创建两个页面a页面跟b页面:

page-a.vue:

<template>
  <div id="page-a-container">
    我是a页面
  </div>
</template>
<script>

</script>
<style scoped>
  #page-a-container{
    background-color: red;
    color: white;
    font-size: 24px;
    height: 100%;
  }
</style>

page-b.vue:

<template>
  <div id="page-b-container">
    我是b页面
  </div>
</template>
<script>

</script>
<style scoped>
  #page-b-container{
    background-color: yellow;
    color: black;
    font-size: 24px;
    height: 100%;
  }
</style>

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

export default new Router({
  mode:'hash',
  routes: [
    {
      path: '/a',
      name: 'pageA',
      component: pageA
    },
    {
      path: '/b',
      name: 'pageB',
      component: pageB
    },
  ]
})

最后是我们的App.vue文件:

<template>
  <div id="app">
    <div class="tab-container">
      <router-link class="tab" :to="{name:'pageA'}">tab1</router-link>
      <router-link class="tab" :to="{name:'pageB'}">tab2</router-link>
    </div>
    <router-view class="router-view"/>
  </div>
</template>

<script>
  export default {
    name: 'App',
    created() {
      console.log('app', this)
    }
  }
</script>

<style>
  body, html {
    width: 100%;
    overflow: auto;
    height: 100%;
  }

  * {
    margin: 0px;
    padding: 0px;
  }

  #app {
    font-family: 'Avenir', Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-align: center;
    color: #2c3e50;
    position: relative;
    height: 100%;
  }

  .tab-container {
    background-color: #efefef;
    height: 50px;
    display: flex;
    box-sizing: border-box;
    padding: 10px;
  }
  .tab{
    color: black;
    font-size: 24px;
    flex: 1;
    text-decoration: none;
  }
  .router-link-exact-active{
    color: red;
    font-size: 24px;
  }
  .router-view{
    position: absolute;
    top: 50px;
    bottom: 0px;
    left: 0px;
    height: auto;
    width: 100%;
  }
</style>

代码比较简单,我就直接上代码了,然后我们运行代码:
在这里插入图片描述

好啦,简单的几行代码就可以玩起来了,我们来分析一下router-link组件:

var Link = {
  name: 'router-link',
  props: {
    to: {
      type: toTypes, //可以传递string 类型,比如我们demo的a页面"/a"  或者是object :to="{name:'pageA'}"
      required: true //必须传递的属性
    },
    tag: {
      type: String, //渲染的标签类型,默认是a标签
      default: 'a'
    },
    exact: Boolean,
    append: Boolean,
    replace: Boolean,
    activeClass: String, //激活状态的class
    exactActiveClass: String, //精确对比情况下的激活状态的class
    event: {
      type: eventTypes, //要选择触发路由操作的事件
      default: 'click'
    }
  },
  render: function render (h) {
    var this$1 = this;

    var router = this.$router;
    var current = this.$route; //当前route
    var ref = router.resolve(this.to, current, this.append);//解析当前route路由
    var location = ref.location;//获取当前路由的location
    var route = ref.route;
    var href = ref.href;

    var classes = {};
    var globalActiveClass = router.options.linkActiveClass; //全局的激活状态class
    var globalExactActiveClass = router.options.linkExactActiveClass; //精确对比情况下全局的激活状态class
    // Support global empty active class
    var activeClassFallback = globalActiveClass == null
            ? 'router-link-active'
            : globalActiveClass;
    var exactActiveClassFallback = globalExactActiveClass == null
            ? 'router-link-exact-active'
            : globalExactActiveClass;
    var activeClass = this.activeClass == null
            ? activeClassFallback
            : this.activeClass;
    var exactActiveClass = this.exactActiveClass == null
            ? exactActiveClassFallback
            : this.exactActiveClass;
    var compareTarget = location.path
      ? createRoute(null, location, null, router)
      : route;

    classes[exactActiveClass] = isSameRoute(current, compareTarget);//当前路由跟组件对应的路由一样的时候,激活状态
    classes[activeClass] = this.exact
      ? classes[exactActiveClass]
      : isIncludedRoute(current, compareTarget);
    //当点击标签的时候,进行路由操作
    var handler = function (e) {
      if (guardEvent(e)) {
        if (this$1.replace) {
          router.replace(location);
        } else {
          router.push(location);
        }
      }
    };

    var on = { click: guardEvent };
    if (Array.isArray(this.event)) {
      this.event.forEach(function (e) { on[e] = handler; });
    } else {
      on[this.event] = handler;
    }

    var data = {
      class: classes
    };

    if (this.tag === 'a') {
      data.on = on;
      data.attrs = { href: href };
    } else {
      // find the first <a> child and apply listener and href
      var a = findAnchor(this.$slots.default);
      if (a) {
        // in case the <a> is a static node
        a.isStatic = false;
        var extend = _Vue.util.extend;
        var aData = a.data = extend({}, a.data);
        aData.on = on;
        var aAttrs = a.data.attrs = extend({}, a.data.attrs);
        aAttrs.href = href;
      } else {
        // doesn't have <a> child, apply listener to self
        data.on = on;
      }
    }

    return h(this.tag, data, this.$slots.default)
  }
};

在render函数中,通过引用当前route对象,根据当前route信息改变改变当前组件的class(激活class、默认class),然后监听组件的事件进行路由跳转.

我们下面跟着官网的节奏结合demo往下走哈,当然! 童鞋们也可以直接去看官网…

动态路由匹配
我们需求是:当访问的是“/page/pageA”或者是“/page/pageB”我用一个公用的组件当page页面,然后匹配page后面的“pageA”和“pageB”做网络请求,请求对应的页面数据,最后渲染在page组件中.
首先我们创建一个页面叫page.vue,然后把page.vue放到router.js中去:

page.vue文件:

<template>
  <div id="page-container">
    {{pageDesc}}
  </div>
</template>
<script>
  export default {
    name: 'page',
    data(){
      return{
        pageDesc:''
      }
    },
    mounted(){
      this.fetchData();
    },
    methods:{
      fetchData(){
        this.pageDesc=`我是${this.$route.params.pageId}页面`
      }
    }
  }
</script>
<style scoped>
  #page-container{
    background-color: red;
    color: white;
    font-size: 24px;
    height: 100%;
  }
</style>

router.js中把我们的page页面放进去,并且添加pageId字段用来匹配作参数.

import Vue from 'vue'
import Router from 'vue-router'

import page from '@/components/page'
Vue.use(Router)

export default new Router({
  mode:'hash',
  routes: [

    {
      path:'/page/:pageId',
      name:'page',
      component:page
    }
  ]
})

然后当我们访问http://localhost:8080/#/page/pageHome的时候:
在这里插入图片描述

当我们把pageHome改成pageB的时候,我们在浏览器按下回车,或者执行

this.$router.push({path:'/page/pageB'})

的时候,我们会发现页面并没有改变
在这里插入图片描述

在官网有段话:
当使用路由参数时,例如从 /user/foo 导航到 /user/bar,原来的组件实例会被复用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会再被调用。

所以我们这里也是一样的~~~

我们第一次进入页面的时候,我们在页面的mounted生命周期方法中去请求数据:

 mounted(){
      this.fetchData();
    },
    methods:{
      fetchData(){
        this.pageDesc=`我是${this.$route.params.pageId}页面`
      }
    }

当我们切换链接的时候,或者执行下面代码的时候

this.$router.push({path:'/page/pageB'})

我们page.vue中会接受到监听,我们可以监听$route变量的变化,然后重新请求数据:

watch:{
      '$route'(to,from){
        this.fetchData();
      }
    },
    mounted(){
      this.fetchData();
    },
    methods:{
      fetchData(){
        this.pageDesc=`我是${this.$route.params.pageId}页面`
      }
    }

或者用beforeRouteUpdate回调:

 beforeRouteUpdate (to, from, next) {
      next();
      this.fetchData();
    },

这两个方法还是有点区别的,首先beforeRouteUpdate是2.2版本中引入的,然后beforeRouteUpdate从字面意思就可以知道,它是在路由变化之前调用的,而监听$route变化是在route已经改变后回调的.

监听$route变化的原理我就不解释了,我们去源码中看一下beforeRouteUpdate的调用时间:

History.prototype.confirmTransition = function confirmTransition (route, onComplete, onAbort) {
  ...
  var queue = [].concat(
   ...
    extractUpdateHooks(updated),
  ...
  );
....
};

confirmTransition方法我们前面一节分析过,它是在route变化之前对route做的一些列操作的一个方法,感兴趣的小伙伴可以去看我们之前的两篇文章.

好啦!! 有了这两个方法,我们就可以在这两个方法中监听route的变化,然后作网路请求,最后显示数据了,效果我就不演示了哈,小伙伴自己去跑跑代码就知道了~

当我们使用router的push方法去打开这个页面的时候:

const pageId = 123
router.push({ name: 'page', params: { pageId }}) // -> /page/123

在这里插入图片描述

然后我们还可以使用path匹配:

router.push({ path: `/page/${userId}` }) // -> /page/123

跟上面的效果一样,我就不截图啦~~

最后当我们执行

const pageId = 123
      this.$router.push({ path: '/page', params: { pageId }}) // -> 无效

在这里插入图片描述

可以看到,我们页面出现空白,因为我们没有router-view没有匹配到路由.所以无效.

路由组件传参
我们当前页面拿到参数的方式为:

 methods:{
      fetchData(){
        console.log('fetchData',this.$route);
        this.pageDesc=`我是${this.$route.params.pageId}页面`
      }
    }

我们把用this.$route.params.pageId方式改为this.pageId:

 methods:{
      fetchData(){
        this.pageDesc=`我是${this.pageId}页面`
      }
    }
<template>
  <div id="page-container">
    {{pageDesc}}
  </div>
</template>
<script>
  export default {
    name: 'page',
    props:['pageId'],
    data(){
      return{
        pageDesc:''
      }
    },
    watch:{
      pageId(val,oldVal){
        if(val!==oldVal){
          this.fetchData();
        }
      }
    },
    mounted(){
      this.fetchData();
    },
    methods:{
      fetchData(){
        this.pageDesc=`我是${this.pageId}页面`
      }
    }
  }
</script>
<style scoped>
  #page-container{
    background-color: red;
    color: white;
    font-size: 24px;
    height: 100%;
  }
</style>

我们直接把params的pageId直接映射到page.vue的props中了,所以我们可以在页面使用this.pageId,然后通过监听pageId的变化最后做网络请求,渲染页面数据.

当然,在router.js中我们还需要设置page页面的props为true:

export default new Router({
  mode:'hash',
  routes: [
    {
      path:'/page/:pageId',
      name:'page',
      component:page,
      props:true
    }
  ]
})

props除了boolean类型外,还可以设置为“对象模式”跟“函数模式”,

{
      path:'/page/:pageId',
      name:'page',
      component:page,
      props:{pageId:123123}
    }
   {
      path:'/page/:pageId',
      name:'page',
      component:page,
      props:(route)=>{
        return route.params;
      }
    }

我们可以对应找到router-view的源码:

var View = {
  name: 'router-view',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render: function render (_, ref) {
    ....
    // resolve props
    var propsToPass = data.props = resolveProps(route, matched.props && matched.props[name]);
   ...
    return h(component, data, children)
  }
};
function resolveProps (route, config) {
  switch (typeof config) {
    case 'undefined':
      return
    case 'object':
      return config
    case 'function':
      return config(route)
    case 'boolean':
      return config ? route.params : undefined
    default:
      if (process.env.NODE_ENV !== 'production') {
        warn(
          false,
          "props in \"" + (route.path) + "\" is a " + (typeof config) + ", " +
          "expecting an object, function or boolean."
        );
      }
  }
}

如果我们指定了props:true,然后page.vue文件中又没指定props的时候,router-view会把params当属性绑定在vm的el上:

{
      path:'/page/:pageId',
      name:'page',
      component:page,
      props:true
    }

在这里插入图片描述

可以看到,我们的id为“page-container”的标签上有一个pageid属性.

导航守卫

在我们前面文章中有介绍,在vue-router的源码中,我们看到了很多router操作route时的一些回调:

VueRouter.prototype.beforeEach = function beforeEach (fn) {
  return registerHook(this.beforeHooks, fn)
};

VueRouter.prototype.beforeResolve = function beforeResolve (fn) {
  return registerHook(this.resolveHooks, fn)
};

VueRouter.prototype.afterEach = function afterEach (fn) {
  return registerHook(this.afterHooks, fn)
};

VueRouter.prototype.onReady = function onReady (cb, errorCb) {
  this.history.onReady(cb, errorCb);
};

VueRouter.prototype.onError = function onError (errorCb) {
  this.history.onError(errorCb);
};

我们一个一个来认识一下,首先是全局前置守卫,既然是router的方法,所以我们需要拿到router实例,然后调用router的beforeEach方法注册一个回调,我们就直接在App.vue中操作了:

<script>
  export default {
    name: 'App',
    created() {
      this.$router.beforeEach((to, from, next) => {
        if(to.name==='page'&&to.params.pageId==='123'){
          next({path:'/a'});
          return;
        }
        next();
      })
    }
  }
</script>

router.js:

import Vue from 'vue'
import Router from 'vue-router'
import pageA from '@/components/page-a'
import pageB from '@/components/page-b'
import page from '@/components/page'
Vue.use(Router)

export default new Router({
  mode:'hash',
  routes: [
    {
      path: '/a',
      name: 'pageA',
      component: pageA,
      props: true
    },
    {
      path: '/b',
      name: 'pageB',
      component: pageB
    },
    {
      path:'/page/:pageId',
      name:'page',
      component:page,
      props:true
    }
  ]
})

现在的逻辑是,当判断访问的是"/page/123"的时候,直接链接到“/a”路径也就是我们的pageA页面:
在这里插入图片描述

好啦,看完我们的全局前置守卫,我们来看一下其它的几个:

export default {
    name: 'App',
    created() {
      this.$router.beforeEach((to, from, next) => {
        console.log('beforeEach');
        if(to.name==='page'&&to.params.pageId==='123'){
          next({path:'/a'});
          return;
        }
        next();
      });
      this.$router.beforeResolve((to,from,next)=>{
        console.log('beforeResolve');
        next();
      });
      this.$router.afterEach((to,from)=>{
        console.log('afterEach');
      });
      this.$router.onReady(()=>{
        console.log('onReady');
      });
      this.$router.onError((erro)=>{
        console.log('onError',erro);
      });
    }

我们访问一下“http://localhost:8080/#/a”

在这里插入图片描述

可以看到,当vue-router就位的时候,会调用onReady方法,整个onReady方法只会被调用一次:

History.prototype.transitionTo = function transitionTo (location, onComplete, onAbort) {
   ...
  this.confirmTransition(route, function () {
    // fire ready cbs once
    if (!this$1.ready) {
      this$1.ready = true;
      this$1.readyCbs.forEach(function (cb) { cb(route); });
    }
  }, function (err) {
   ...
  });
};

具体的用法啥的我就不带着看了哈,小伙伴自己跑跑看看log就可以了.

好啦,我们现在有一个这样的需求,当我们访问a页面的时候,我们需要强制用户登录,然后才能进入a页面

我们首先创建一个叫login.vue的文件:

<template>
  <div id="page-container">
    <button @click="login()" style="color: white;font-size: 24px">登录成功</button>
  </div>
</template>
<script>
  export default {
    name: 'login',
    methods: {
      login() {
        alert('登录成功')
        this.$router.loggedIn = true;
        this.$router.push({path: this.$route.params.redirect});
      }
    }
  }
</script>
<style scoped>
  #page-container {
    background-color: red;
    color: white;
    font-size: 24px;
    height: 100%;
  }
</style>

代码很简单,就一个登录按钮,然后点击登录按钮模拟一下请求网络接口登录成功,然后跳转到从上个页面传递过来的路径.

然后在router.js中注册一下login页面,并且给a和b页面设置一个路由元信息:

import Vue from 'vue'
import Router from 'vue-router'
import pageA from '@/components/page-a'
import pageB from '@/components/page-b'
import page from '@/components/page'
import login from '@/components/login'
Vue.use(Router)

export default new Router({
  mode:'hash',
  routes: [
    {
      path: '/a',
      name: 'pageA',
      component: pageA,
      props: true,
      meta:{requiresAuth: true}
    },
    {
      path: '/b',
      name: 'pageB',
      component: pageB,
      meta:{requiresAuth: true}
    },
    {
      path:'/page/:pageId',
      name:'page',
      component:page,
      props:true
    },
    {
      path:'/login',
      name:'login',
      component:login
    }
  ]
})

在这里插入图片描述

可以看到,当我们访问/a路径的时候,我们会走:

this.$router.beforeEach((to, from, next) => {
        console.log('beforeEach');
        if (to.meta && !!to.meta.requiresAuth&&!this.$router.loggedIn) {
          next({name: 'login',params:{redirect: to.fullPath}});
          return;
        }
        next();
      });

代码,然后由next({name: ‘login’,params:{redirect: to.fullPath}});链接到了login页面,最后login页面通过传递的redirect地址又重新打开了a页面:

 login() {
        alert('登录成功')
        this.$router.loggedIn = true;
        this.$router.push({path: this.$route.params.redirect});
      }

好啦,我们已经跟这vue-router的官网把大部分的api走了一遍,还有一些api就不一一解析了,小伙伴自己去试试啊,最主要的是要结合demo自己跑一遍,光看是没有用的,要多练~~~

本篇有点长哈,感谢小伙伴的陪伴,不早啦,睡觉啦~~

欢迎入群,欢迎交流~~

qq群链接:
在这里插入图片描述

参考链接:

vue-router官网: https://router.vuejs.org/zh/

猜你喜欢

转载自blog.csdn.net/vv_bug/article/details/82805290