美文网首页前端Vue专辑移动前端VueJS
前端入门之(vue-router全解析三)

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

作者: vv_小虫虫 | 来源:发表于2018-09-27 09:51 被阅读3次

    上一节前端入门之(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组件,先简单看一下我们要实现的需求:


    20180921180518150.png

    可以看到,页面就两个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>
    
    

    代码比较简单,我就直接上代码了,然后我们运行代码:


    20180921184724383.gif

    好啦,简单的几行代码就可以玩起来了,我们来分析一下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的时候:

    20180926205725143.png

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

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

    的时候,我们会发现页面并没有改变


    20180926205946563.png

    在官网有段话:
    当使用路由参数时,例如从 /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
    
    20180926212813104.png

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

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

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

    最后当我们执行

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

    可以看到,我们页面出现空白,因为我们没有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
        }
    
    20180926220651316.png

    可以看到,我们的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页面:


    20180927084531620.gif

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

    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

    2018092708563067.png

    可以看到,当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
        }
      ]
    })
    
    
    20180927094321974.gif

    可以看到,当我们访问/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群链接:


    20170830160105584.png

    参考链接:

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

    相关文章

      网友评论

        本文标题:前端入门之(vue-router全解析三)

        本文链接:https://www.haomeiwen.com/subject/gxijoftx.html