14-Vue-Router

510 阅读13分钟

介绍

Vue Router是Vue.js官方的路由管理器。它和Vue.js的核心功能深度集成,让构建单页面应用变得易如反掌。包含的功能有:

  • 嵌套的路由 / 视图表
  • 模块化的、基于组件的路由配置
  • 路由参数、查询、通配符
  • 基于Vue.js过渡系统的视图过渡效果
  • 细粒度的导航控制
  • 带有自动激活的 CSS class 的链接
  • HTML5历史模式或hash模式,在IE9中自动降级
  • 自定义的滚动条行为

安装

通过执行npm i vue-router -s安装,推荐使用vue add router安装插件(记得提前提交)。

使用Vue-CLI3创建Vue Router项目

在终端输入vue create 项目名

> Manually select features   

选择手动选择功能

 (*) Babel       
>(*) Router

去掉( ) Linter / Formatter,勾选Babel和Router

Use history mode for router? (Requires proper server setup for index fallback in production) (Y/n) 

输入y,回车,同意使用历史模式

> In dedicated config files

保存在专用配置文件中

认识Vue Router项目目录

router文件夹下的index.js,在这个文件中需要有5步:

  1. 导入vue-router import VueRouter from 'vue-router'
  2. 使用vuerouter Vue.use(VueRouter)
  3. 创建路由信息配置对象
  4. 创建路由对象
  5. 将路由对象抛出
import Vue from 'vue'
//1.导入
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

//2.模块化机制 使用Router
Vue.use(VueRouter)

//3.创建路由信息配置对象
const routes = [{
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    
    //异步组件加载
    component: () => import( /* webpackChunkName: "about" */ '../views/About.vue')
  }
]

//4.创建路由对象
const router = new VueRouter({
  mode: 'history', //history模式可以给我们呈现一个干净的网页地址,前面不会出现`/#`,默认h5模式
  base: process.env.BASE_URL,
  routes
})

//5.将路由对象抛出
export default router
//6.在main.js中导入

同时我们也可以看到,上面用了两种不一样的组件加载方式,component:()=>import('')属于组件加载的异步加载方式。然后下面还有一个mode: 'history'历史模式,它可以给我们呈现一个干净的网页地址,前面不会出现/#,如果不写的话默认时h5模式。

main.js,需要有2步:

  1. 导入router import router from './router'
  2. 将router挂载到vue实例中
import Vue from 'vue'
import App from './App.vue'
//导入
import router from './router'

Vue.config.productionTip = false

new Vue({
  //将路由挂载到Vue的实例中
  router,
  render: h => h(App)
}).$mount('#app')

views文件夹:路由组件全部存放在当前目录下,这些组件会被渲染到App.vue的<router-view />中。

App.vue

    <div id="nav">
      <!-- router-link默认被渲染成a标签 to属性会被默认渲染成href属性 -->
      <router-link to="/">Home</router-link>|
      <router-link to="/about">About</router-link>
    </div>
    <!-- router-view相当于路由组件的出口 -->
    <router-view />

其中的router-link标签会被浏览器默认渲染成a标签,to属性就相当于a标签的href属性。router-view就相当于路由组件的出口,所有的路由组件都通过它来渲染。

命名路由

在创建路由信息配置对象的时候,在对象中添加一个name属性,当有了name属性后就可以动态的对路由进行切换,之前在<router-link to="/">Home</router-link>这种通过路径来切换是静态的。当有name属性后我们可以这样<router-link :to="{name:'Home'}">Home</router-link>,给to属性绑定name。

const routes = [{
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: () => import( /* webpackChunkName: "about" */ '../views/About.vue')
  }
]

动态路由匹配

我们在网页上经常会在地址栏上输入地址,比如http://localhost:8080/user/1,不同的用户对应不同的id,不管是用户1页面还是用户2页面,使用的都是同一个页面,只不过根据他们的id获取各种不同的信息。

首先在views目录下创建一个User组件,然后在index.js中导入import User from '@/views/User.vue',配置路由信息:

  {
    // /:id就是动态路由匹配
    path: '/user/:id',
    name: 'User',
    component: User
  }

其中的/:id就是动态路由匹配,用来接收不同的用户的id,然后在App.vue中:

<router-link :to="{name:'User',params:{id:1}}">User1</router-link>|
<router-link :to="{name:'User',params:{id:2}}">User2</router-link>|

通过在to属性值中配置params:{id:1}传入参数,参数可以有很多,所以这边的params是一个对象。 同时我们可以在User.vue中通过$route.params.id获取id来进行区分。

<template>
  <div>用户页面{{$route.params.id}}</div>
</template>

此时我们确实是可以在用户1页面和用户2页面之间进行切换,并且也可以看到各自对应不同的id。 但是我们在User.vue中添加created生命周期函数,并对id进行打印,此时我们可以发现id只会打印一次,也就是说我们只能够获取到第一次加载时的id,第二次进行切换的时候获取不到第二次的id,当前组件被复用了,因为两个路由渲染的是同一个组件,所以created函数不会再被调用,也就拿不到数据了。

  //当路由参数变化时 /user/1 切换到 /user/2 ,当前组件实例会被复用,因为两个路由渲染的时同一个组件
  created() {
    console.log(this.$route.params.id);
  },

监听路由对象的两种方式

为了解决上述问题,在路由组件复用的情况下,我们需要对路由对象进行一个监听,来响应路由参数的变化。

  1. 通过watch监听
  watch: {
    //to是当前路由信息对象,from是切换之前的
    $route: (to, from) => {
      console.log(to.params.id);
      //发起 ajax,请求后端接口数据(数据驱动视图)
    },
  },
  1. 通过导航守卫beforeRouteUpdate监听 使用时需要注意,在最后一定要调用next(),不然会阻塞路由,会卡在当前函数,页面切换不过来,next()就相当于放行。
  beforeRouteUpdate(to, form, next) {
    console.log(to.params.id);
    //一定要调用next(),不然会阻塞路由,就会卡在当前函数,
    next();
  },

404路由和路由匹配的优先级

当用户在地址栏上输入的地址没有匹配到路由时,我们应该给用户显示一个404页面。我们需要在index.js中通过使用通配符*来配置一个404路由。

  {
    path: '*',
    //异步加载组件
    // @ 表示的就是当前项目目录
    component: () => import('@/views/404')
  }

因为404页面只有在路由不匹配或找不到的情况下才会加载,所以我们可以通过组件的异步加载方式来进行一个导入,异步加载对于提升性能有很大的帮助。

当我们使用通配符的时候配置了一个user-*之类的路由,此时可以通过$route.params.pathMatch在当前页面来获取通配符代表的内容。比如我们在地址栏输入user-admin,则能够通过上述代码获取到admin

路由匹配的优先级

同一个路径可以匹配多个路由,匹配的优先级遵循路由定义的顺序,即谁先定义谁的优先级高。

如果我们配置两个路由的pathname都相同,但加载的组件不同,当我们在浏览器上访问时,只会加载最前面的那个配置的路由,因为路由会从上往下依次匹配。

注意:404路由一定要放在整个路由配置的最后面,因为路由会从上往下依次匹配。

路由查询参数

有时候用户可能会在网页输入类似于这样的一串地址http://localhost:8080/page?id=1&title=book,尤其是在查询的时候,那么我们该怎样来定义这个路由信息呢。面对这种情况,vue-router中给我们提供了一个查询参数query

首先在views中创建一个Page.vue,然后在index.js中进行配置:

  {
    //后面的参数不需要在当前的路由进行配置,只需要在匹配的时候定义query就行
    path: '/page',
    name: 'page',
    component: () => import('@/views/Page')
  },

然后在App.vue中使用查询参数query来进行后面参数的一个配置,和之前的params的一个配置差不多,这边暂时写死,在项目中肯定是需要传参动态变化的。

<router-link :to="{name:'page',query:{id:1,title:'book'}}">Page</router-link>|

配置我们已经完成了,但我们可能还会需要获取到id和title。我们可以在Page页面中通过this.$route.query来获取这个query对象,然后来获取我们想要的值。

  created() {
    console.log(this.$route);
    const { id, title } = this.$route.query;
    console.log(id, title);
    //与后端发生交互
  },

路由重定向和别名

重定向

首先在index.js中配置:

{
    path: '/',
    redirect: '/home'
}

当我们访问http://localhost:8080/的时候会给我们重定向到home页面,同时还可以通过下面这种方式来配置:

{
    path: '/',
    redirect: {
      name: 'home'
    }
  }

别名

我们可以在配置路由信息的时候通过alias给路由起一个别名,然后在地址栏通过http://localhost:8080/aaa也能够访问到page页面。

  {
    path: '/page',
    name: 'page',
    component: () => import('@/views/Page'),
    alias: '/aaa' //给路由起别名
  }

别名的功能:可以自由的将ui的结构映射到任意的url上。

路由组件传值

在组件中我们通过$route.params.id的方式来获取地址栏上的参数,如果我们的项目越来越大,地址栏上的参数可能越来越多,那么在当前组件中通过这种方式来获取值会显得很复杂,并且使得和当前的路由形成高度的耦合,使得当前组件只能在特定的url上使用,限制了灵活性。为了解决这个问题,我们可以在定义路由信息的时候给它加一个props属性。

  {
    path: '/user/:id',
    name: 'User',
    component: User,
    props: true
  },

然后在User.vue中通过props: ["id"],那么我们就可以在当前页面通过{{id}}对id进行直接获取。

这个props也可以是一个函数:

  {
    path: '/user/:id',
    name: 'User',
    component: User,
    //props: true
    props: (router) => ({
      id: router.params.id,
      title: router.query.title
    })
  }

然后我们通过http://localhost:8080/user/1?title=book进行一个访问,能够获取到1和book两个参数。props传值解决了$route耦合的问题。

编程式导航

我们之前使用的<router-link :to="{name:'home'}">Home</router-link>|这种属于声明式导航,我们也可以通过this.$router获取路由实例对象,然后再通过使用其中的方法来实现编程式导航

这边注意区分一下this.$routerthis.$routethis.$router获取的是VueRouter对象,而this.$route获取的是路由信息对象。

通过VueRouter对象中的push()可以实现编程式导航。

<button @click="goHome">跳转到首页</button>

goHome() {
   this.$router.push("/");
},

通过这样的一种方法也可以跳转到首页。同时push中也可以是下面这几种写法 this.$router.push("name");this.$router.push({path: "/",});this.$router.push({name: "User",params: { id: 2 },});this.$router.push({path: "/page",query: { id: 2, title: "tom" },});

同时还有我们比较常用的前进、后退,可以通过VueRouter对象中的go()实现,其中0是刷新、正数是前进、负数是后退,数字的大小是步数,当历史记录小于填入的步数时会失败。

<button @click="goBack">后退</button>

goBack() {
    this.$router.go(-1);
},

嵌套路由

当子组件结构相同,可以复用时,采用的是动态路由匹配,但是当子组件结构不同,不能复用时,我们就需要用到嵌套路由

比如说user组件下有一个user/1/profile还有一个user/1/posts,并且它们的结构是不相同的,难道我们要给user写两套样式吗?

解决:我们可以写两个组件profileposts,将它们渲染到user组件中,那么需要在user组件中提供<router-view />来作为它的两个子组件的出口,即router-view中嵌套router-view,这就是嵌套路由。

嵌套路由3步:

  1. 定制孩子路由信息
  2. 定义跳转(router-link)
  3. 定义路由出口(router-view)

首先我们在index.js中的user路由信息对象中配置一个children来定制它的孩子路由:

注意:在children配置孩子路由信息时,path中不要加/,因为它默认会有一个/

  {
    //:id就是动态路由匹配
    path: '/user/:id',
    name: 'User',
    component: User,
    //props: true
    props: (router) => ({
      id: router.params.id,
      title: router.query.title
    }),
    //配置嵌套路由
    children: [{
        //这边不要加'/',它默认会有一个'/'
        path: 'profile',
        component: () => import('@/views/Profile')
      },
      {
        path: 'posts',
        component: () => import('@/views/Posts')
      }
    ]
  },

然后在App.vue中给这个两个添加跳转

<router-link to="/user/1/profile">user/1/profile</router-link>|
<router-link to="/user/1/posts">user/1/posts</router-link>|

最后在User.vue中给它的两个孩子路由添加一个<router-view/>进行渲染即可。

<!-- 渲染子路由的组件出口 -->
<router-view></router-view>

命名视图

在网页上我们可能需要展示多个视图,比如在首页页面,我们既要展示主要内容,同时还要展示侧边栏内容这两个视图,这种情况不属于嵌套路由,因为此时这两个组件是属于同级的,需要在同一个页面上同时展示,这个时候可以用到命名视图

首先对首页的路由信息重新进行一个配置,之前是通过 component 进行配置加载视图,现在我们需要通过components进行一个重新的配置,使用这种方式配置能够定义更多的视图,default就是当前路由默认的名字,然后在下面可以继续挂载想在当前页面显示的视图,key是给视图命名,值对应想导入的组件。

  {
    path: '/home',
    name: 'home',
    //component: Home
    //通过这种方式能够定义更多的视图
    components: {
      default: Home, //默认的名字
      main: () => import('@/views/Main'), //主要内容
      sideBar: () => import('@/views/sideBar') //侧边栏内容
    }
  },

然后在views中创建导入的Main组件和sideBar组件,最后在App.vue中再添加两个router-view,并添加name属性,然后将components中需要展示的两个视图的命名填入即可,同时我们还可以给router-view添加类名,添加样式布局等。

	<!-- 每个router-view都会被渲染成一个div -->
	<!-- 默认的Home -->
    <router-view />
    <!-- Main -->
    <router-view name="main" />
    <!-- sideBar -->
    <router-view name="sideBar" />

其实我们也可以就在默认的router-view中对主要内容和侧边栏内容进行布局,但是在后期的维护和优化上,命名视图的结构更好。

导航守卫

导航表示路由正在发生改变。vue-router提供的导航守卫主要用来通过跳转或取消的方式守卫导航。

完整的导航解析流程

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫 (2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

全局守卫

你可以在index.js中通过router.beforeEach注册一个全局前置守卫:

const router = new VueRouter({ ... })

router.beforeEach((to, from, next) => {
  // ...
})

那么全局守卫到底能够帮我们干什么?

需求:用户在浏览网站时,会访问很多组件,当用户跳转到/notes,发现用户还没有登录,此时应该让用户登录之后才能查看,应该让用户跳转到登录页面,登录完成后就可以查看/notes页面的内容,这个时候全局守卫起到了关键的作用,因为全局守卫就是不管哪一个路由被访问时都会被调用。

首先在index.js中将两个路由信息进行配置

  {
    path: '/notes',
    name: 'notes',
    component: () => import('../views/Notes.vue')
  },
  {
    path: '/login',
    name: 'login',
    component: () => import('../views/Login.vue')
  },

然后在views中创建这个两个组件Notes.vueLogin.vue,再在App.vue中提供一个<router-link :to="{name:'notes'}">我的笔记</router-link>|

接下来在index.js中注册一个全局守卫,并对代码逻辑进行编码,首先判断用户当前访问的是不是/notes,然后获取一下用户信息,如果获取到用户信息则直接next()放行,允许查看,否则跳转到登录页面,通过在next()中传参跳转路由的path实现,最外层的那个next()放行一定要写,因为不管哪个一个路由被访问全局守卫都会调用。

//to就是当前访问路由
router.beforeEach((to, from, next) => {
  //console.log(to);
  //用户访问了/notes
  if (to.path === '/notes') {
    //获取用户登录信息
    const user = JSON.parse(localStorage.getItem('user'));
    if (user) {
      //用户已经登录,对/notes放行,允许查看我的笔记
      next();
    } else {
      //用户没有登录 跳转到登录页面
      next('/login');
    }
  }
  next();//一定要写
})

接着对Login.vue进行一个编写,handleLogin()首先模拟与后端发生交互,然后将登录信息存储到本地(退出的时候需要进行一个清除),登录成功后通过一个编程式导航this.$router.push("notes");跳转回/notes

<template>
  <div>
    <h2>登录页面</h2>
    <p>
      <label for>用户名:</label>
      <input type="text" v-model="user" />
    </p>
    <p>
      <label for>密码:</label>
      <input type="password" v-model="pwd" />
    </p>
    <button @click="handleLogin">登录</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      user: "",
      pwd: "",
    };
  },
  methods: {
    handleLogin() {
      //1.获取用户名和密码
      //2.与后端发生交互
      setTimeout(() => {
        let data = {
          user: this.user,
        };
        //保存用户名到本地
        if (data.user) {
          localStorage.setItem("user", JSON.stringify(data));
          this.$router.push("notes");
        }
      }, 1000);
    },
  },
};
</script>

<style scoped>
</style>

组件内容的守卫

  • beforeRouteEnter
  • beforeRouteUpdate (2.2 新增)
  • beforeRouteLeave
const Foo = {
  template: `...`,
  beforeRouteEnter (to, from, next) {
    // 在渲染该组件的对应路由被 confirm 前调用
    // 不!能!获取组件实例 `this`
    // 因为当守卫执行前,组件实例还没被创建
  },
  beforeRouteUpdate (to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 可以访问组件实例 `this`
  },
  beforeRouteLeave (to, from, next) {
    // 导航离开该组件的对应路由时调用
    // 可以访问组件实例 `this`
  }
}

beforeRouteEnter守卫不能够访问this,但是可以通过传一个回调给next来访问组件实例,用的较少。

beforeRouteEnter (to, from, next) {
  next(vm => {
    // 通过 `vm` 访问组件实例
  })
}

beforeRouteUpdate前面提过,一般用于组件复用的情况。

beforeRouteLeave这个离开守卫通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消。保证用户的一个体验。

<template>
  <div>
    <h2>编辑页面</h2>
    <textarea v-model="content" cols="30" rows="10"></textarea>
    <button @click="saveContent">保存</button>
    <ul>
      <li v-for="(item,index) in list" :key="index">{{item.title}}</li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      content: "",
      list: [],
    };
  },
  methods: {
    saveContent() {
      //保存到列表
      this.list.push({ title: this.content });
      //清空
      this.content = "";
    },
  },
  //监测用户是否离开当前页面
  beforeRouteLeave(to, from, next) {
    if (this.content) {
      //如果输入框中有内容
      alert("请保存数据之后再离开"); //保证用户体验
      //表示默认失败,恢复当前页面
      next(false);
    } else {
      //如果输入框中没有内容,则直接放行
      next();
    }
  },
};
</script>

<style scoped>
</style>

meta路由元信息实现权限控制

前面的全局守卫中我们是通过给router注册一个beforeEach来实现对访问/notes的一个权限控制,在实际的一个项目中有很多的链接访问我们需要进行一个权限认证,难道我们要在beforeEach中做多个判断吗?或者说是用一个数组将所有不需要认证的添加到白名单,在白名单内的就直接放行,不在白名单内的则需通过认证。

为了解决上述问题,vue给我们提供了一个meta字段,一旦配置meta: {requiresAuth: true}就相当于给当前路由加到黑名单,需要经过认证才能够访问。

  {
    path: '/blog',
    name: 'blog',
    component: () => import('../views/Blog.vue'),
    meta: {
      requiresAuth: true
    }
  },

此时我们需要对之前的beforeEach进行一个修改

router.beforeEach((to, from, next) => {
  //console.log(to);
  //matched 中有我们想要的属性
  //console.log(to.matched);
  //some() 方法测试数组中是不是至少有1个元素通过了被提供的函数测试。它返回的是一个Boolean类型的值。
  if (to.matched.some(res => res.meta.requiresAuth)) {
    //需要权限 黑名单
    if (!localStorage.getItem('user')) {
      //如果用户没有登录,跳转登录页面
      next({
        path: '/login',
        //将跳转前的路径作为查询参数添加
        query: {
          //fullPath 获取完整的路径
          redirect: to.fullPath
        }
      });
    } else {
      //如果用户登录了,直接放行
      next();
    }
  }
  //白名单,直接放行
  next();
})

同时我们还需要不同的页面跳转登录成功后还能够跳回,那么Login.vue中也不能写死,需要进行修改,将next({ path: '/login', //将跳转前的路径作为查询参数添加 query: { //fullPath 获取完整的路径 redirect: to.fullPath } });中的query取出就能获取到跳转之前的路径,用于登录成功后跳回。

handleLogin() {
      //1.获取用户名和密码
      //2.与后端发生交互
      setTimeout(() => {
        let data = {
          user: this.user,
        };
        //保存用户名到本地
        if (data.user) {
          localStorage.setItem("user", JSON.stringify(data));
          //this.$router.push("notes");

		  //修改,不写死
          this.$router.push({
            //为了使不同的页面能够在登录成功后重新跳回,且不写死
            //将beforeEach跳转时传递的查询参数取出
            path: this.$route.query.redirect,
          });
        }
      }, 1000);
    },

数据获取

有时候,进入某个路由后,需要从服务器获取数据。例如,在渲染用户信息时,你需要从服务器获取用户数据。我们可以通过两种方式来实现:

  • 导航完成之后获取:先完成导航,然后在接下来的组件生命周期钩子中获取数据。在数据获取期间显示“加载中”之类的提示。
  • 导航完成之前获取:导航完成前,在路由进入的守卫中获取数据,在数据获取成功后执行导航。

模拟导航完成之后获取数据

  1. 在当前项目目录下创建vue.config.js文件,创建后台数据接口
module.exports = {
    devServer: {
        before: (app, serve) => {
            app.get('/api/post', (req, res) => {
                res.json({
                    title: 'vue-router的数据获取',
                    body: '1.在导航完成之后获取'
                })
            })
        }
    }
}
  1. 通过npm i axios -s安装axios,在main.js中导入并绑定到vue的原型上
import axios from 'axios'

Vue.prototype.$http = axios;
  1. 创建组件配置路由
  {
    path: '/post',
    name: 'post',
    component: () => import('@/views/Post')
  }
  1. 在App.vue提供router-link并绑定to <router-link :to="{name:'post'}">Post</router-link>|
  2. 开发Post,模拟导航完成后获取数据

效果图: 看不到的话可在Network中将网速调成3G,就可以很明显的看到:

<template>
  <div class="post">
    <div v-if="loading" class="loading">Loading......</div>
    <div v-if="error" class="error">{{error}}</div>
    <div v-if="post">
      <h3>{{post.title}}</h3>
      <h3>{{post.body}}</h3>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      post: null,
      error: null,
      loading: false,
    };
  },
  //导航完成之后获取数据
  created() {
    this.getPostData();
  },
  //对路由进行一个监听,每次路由发生变化都需要重新获取数据
  watch: {
    $route: "getPostData",
  },
  methods: {
    async getPostData() {
      try {
        //正在获取数据的时候展示Loading......
        this.loading = true;

        //获取数据
        const res = await this.$http.get("/api/post");
        //console.log(res.data);
        this.post = res.data;

        //获取完数据后
        this.loading = false;
      } catch (error) {
        this.error = error.toString();
      }
    },
  },
};
</script>

<style scoped>
</style>