# Vue-cli 中使用路由

# 1. 改造『老』Demo

在之前的 vue.js 中我们已经演示了路由的基本概念和使用,现在我们在 vue-cli 中将 vue.js 的路由 demo 改造成多模块形式。

  1. 创建独立的 LoginForm.vue 和 RegisterForm.vue 文件

    <template>
      <div>
        <h2>登录页</h2>
        <label>用户名:<input type="text"></label><br/>
        <label>&emsp;码:<input type="password"></label><br/>
      </div>
    </template>
    <script>
    export default {
      name: "LoginForm"
    }
    </script>
    

    <template>
      <div>
        <h2>注册页</h2>
        <label>&ensp;&ensp;名:<input type="text"></label><br/>
        <label>&emsp;&emsp;码:<input type="password"></label><br/>
        <label>确认密码:<input type="password"></label><br/>
      </div>
    </template>
    
    <script>
    export default {
      name: "RegisterForm"
    }
    </script>
    
  2. 在 App.vue 中引用上述组件,并添加 router-linkrouter-view

    <div id="app">
      <router-link to="/login">登录</router-link>
      <router-link to="/register">注册</router-link>
      <hr/>
      <div>
        <login-form></login-form>
        <register-form></register-form>
      </div>
      <router-view/>
    </div>
    
  3. 在 router 中添加路由信息

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    /* 引入 Vue compentents 组件页面。@ 表示相对于 src 的路径 */
    import LoginForm from "@/components/LoginForm";
    // import RegisterForm from "@/components/RegisterForm"; 第二种写法不需要导入
    
    Vue.use(VueRouter)
    
    const routes = [
      /* 请求重定向:/ => 映射到 /#/login */
      { path: "/", redirect: '/login' },
      /* 请求方式为 /#/login  =>  映射到 LoginForm 组件页面 */
      { path: "/login", name: 'LoginForm', component: LoginForm },
      /* 第二种加载页面组件的写法 */
      { path: "/register", name: 'RegisterForm', component: () => import('@/components/RegisterForm') }
    ]
    
  4. 最后:在 main.js 引入我们的 router.js 文件。这部分代码 vue-cli 会自动生成。

# 2. 路由重定向

上例中已经演示了路由重定向的效果。

路由重定向指的是,当用户访问 A-URI 时,强制跳转至 B-URI,此时,用户在 <router-view> 看到的自然就是 B URI 对应的组件。

说明

逻辑上,路由重定向就是让多个 URI 对应同一个组件。

路由重定向是通过 routers 中的 redirect 属性实现的。

routes: [ 
  { path: "/", redirect: '/login' },
  { path: "/login", name: 'LoginForm', component: LoginForm },
  ...
]

上述规则中,当访问 / 路径时,会重定向到 /login,从而在页面的 <router-view> 中展现 LoginForm 组件。

逻辑上,无论时访问 /,还是访问 /login,看到的都是 LoginForm 组件。

# 3. 嵌套路由

在设计项目的 URI 时,我们通常会将相关功能的 URI 归类到同一个 URI 路径下,例如:

  • /about/xxx
  • /about/yyy
  • /about/zzz
  • /about/...

对于这样的情况,我们可以使用『嵌套路由』来实现这样的功能。

一级组件/首页(<router-view>)
│
│── 二级组件-1
│
└── 二级组件-2(<router-view>)
    │
    │── 三级组件-A
    │
    └── 三级组件-B

嵌套路由的页面方面的设计,就是将组件之间的关系组织成如上形式。二级组件和三级组件之间的关系,就是一级组件(或首页)和二级组件之间的关系的“重现”。

在这里,我们利用 vue-cli 自动生成的 HomeAbout 来演示。

我们可以将原用于 App.vue 中的 LoginForm 和 RegisterForm 整体『搬家』到 Home.vue 中:

<div class="home">
  <router-link to="/login">登录</router-link>
  <router-link to="/register">注册</router-link>
  <hr/>
  <div>
    <login-form></login-form>
    <register-form></register-form>
  </div>
  <router-view/>
</div>

然后将 App.vue 中替换为 Home 和 About:

<div id="app">
  <router-link to="/home">Home</router-link>
  <router-link to="/about">About</router-link>
  <hr/>
  <router-view/>
</div>

这样,它们几个就组成了类似于如下的层次结构:

App(<router-view>)
│── About
└── Home(<router-view>)
    │── LoginForm 
    └── RegisterForm

最后,我们去做路由配置。在这里,嵌套路由的设置,需要利用上一级路由设置的 children 属性:

const routes = [
    // {path: "/", redirect: '/home'},
    {
        path: "/home", component: () => import('@/views/Home'), children: [
            // 使用的是绝对路径 
            {path: "/login", name: 'LoginForm', component: LoginForm},
            {path: "/register", name: 'RegisterForm', component: () => import('@/components/RegisterForm')}
        ]
    },
    {path: "/about", component: () => import('@/views/About')},
]

绝对路径和相对路径

  • 如果,你在子路由中使用的是绝对路径(以 / 开头),那么,你是访问 /login/register 来控制两个子路由的显示;
  • 如果,你在子路由中使用的是相对路径,那么,子路由的访问路径是父路由路径 + 子路由路径,即,/home/login/home/register 来控制两个子路由的显示。

无论是绝对路径和相对路径,父路由都不用以 / 结尾。

# 4. 动态路由匹配

有时(特别是在 RESTful 风格的项目中),我们会在代表查询的 GET 请求 URI 中嵌入 ID,以表示查询某个人/物的相关信息。例如:

  • /user/1
  • /user/2
  • /user/3
  • ...

但是问题是,如果系统中有 10086 个用户信息,那岂不是我们在路由配置中,要写 10086 个配置项?很显然,不可能是这样。对于这种情况,vue-router 提出了『动态路由匹配』。

# 基本使用

在路由配置中,我们可以采用下面的写法(来顶替 10086 个配置项)

var router = new VueRouter({
  routes: [
    // 动态路径参数 以冒号开头
    { path: '/user/:id', component: User },
    ...
  ]
})

说明

这里是不是和 Spring MVC 的 @PathVariable 注解的使用场景很像?

这样配置以后,无论 URI 是 /user/1,还是 /user/2,亦或者是 /user/10086,都会是在 <router-view> 处显示 User 组件 。

而在 User 组件中,如果你需要用到『嵌』在 URI 中的那个 ID 值,你可以像下面这样取到它:

<div class="user">
  <!--路由组件中通过 $route.params 获取路由参数 -->
  User: {{ $route.params.id }}
</div>
  • 当 URI 路径是 /user/1 时,User 组件中取到并显示就是 1 ;

  • 当 URI 路径是 /user/2 时,User 组件中取到并显示就是 2 ;

  • 当 URI 路径是 /user/10096 时,User 组件中取到并显示就是 10086 ;

这里的 id 和路由配置中的 :id 是遥相呼应的。如果路由配置中是 xxx,那么这里自然也要使用 xxx

# 动态路由的参数解耦

上一章节中,User 组件使用了 $route.params.id 来获取 URI 路径中『嵌』着的 id 。这种方式虽然可行,但是它让 User 组件和路由配置耦合在了一起。

当然,你也可能不在乎这个耦合的问题,那么这一章就没什么好说的了。

你可以利用 props 将组件和路由解耦。

# props 的简单使用

首先,你需要在路由配置中开启 props 功能:

const router = new VueRouter({
  routes: [
    { path: '/user/:id', component: User, props: true }
  ]
})

props 被设置为 true,之前你所见到并使用的 route.params 将会被设置为组件属性。

然后,你需要在 User 组件中声明对应的 props

export default {
  name: "User",
  props: ['id'] // 使用 props 接收路由参数
}

取值时,你也不需要再啰里吧嗦地写那么长了。

<div class="user">
  User {{ id }}
</div>

# props 的值是对象

  • 路由设置

    const router = new VueRouter({
      routes: [
        // 如果 props 是一个对象,它会被按原样设置为组件属性
        { path: '/user/:id', component: User, props: { uname: 'lisi', age: 12 }}
      ]
    })
    
  • 取值

    <template>
      <div class="user">
        User: {{ uname + ', ' + age }}
      </div>
    </template>
    
    <script>
    export default {
      name: "User",
      props: ['uname', 'age']
    }
    </script>
    

# props 的值是函数

const router = new VueRouter({
  routes: [
    // 如果 props 是一个函数,则这个函数接收 route 对象为自己的形参
    { 
      path: '/user/:id',
      component: User,
      props: route => ({ uname: 'tom', age: 20, id: route.params.id })
    }
  ]
})
<template>
  <div class="user">
    User: {{ uname + ', ' + age + ', ' + id}}
  </div>
</template>

<script>
export default {
  name: "User",
  props: ['uname', 'age', 'id']
}
</script>

# 动态路由和 beforeRouteUpdated 守卫

/user/1234/user/4567 相互切换时,其中相同的组件会被重用,于是 Vue 的生命周期钩子,例如 mounted ,是不会被调用的。那如何让两个页面显示不同的数据呢?

这里可以使用 beforeRouteUpdated 导航守卫,在 URL 动态部分发生变化时,运行一些代码(这些代码发起 ajax 请求去后台读取数据)

具体实现思路在《路由守卫》章节专项讲解。

# 5. 为路由命名

为了更方便地表示路由的路径,可以给路由规则起一个别名,即为 命名路由

routes: [
    { path: '/user/:id', name: 'user', component: User },
    ...
]

这么干的好处在于,你可以在 <router-link> ,以及未来的 router.push() 中,用 name 来代替 path 。

<router-link :to="{ name: 'user', params: { id: 123 }}">User</router-link>
router.push({ name: 'user', params: { id: 123 }})

# 6. 编程式导航

页面的导航方式有 2 种:

  • 声明式导航:通过 点击链接 实现导航。

    vue 中的 <router-link> 就会被渲染成 <a> 元素。

  • 编程式导航:通过执行 JavaScript 代码,调用 BOM 的 API 实现导航。

    本质上就是去改变 location 的 href 属性。

Vue 中编程式导航的常用的核心 API 有 2 个:

this.$router.push('hash地址')
this.$router.go(n)
<template>
  <div class="user">
    <p>
      <button @click="goRegister">跳转到 About 页面</button>
    </p>
  </div>
</template>

<script>
export default {
  name: "User",
  methods: {
    goRegister: function(){
      // 用编程的方式控制路由跳转
      this.$router.push('/about');
    }
  }
}
</script>

router.push() 方法的常见参数形式有:

// 字符串(路径名称)
router.push('/home')

// 对象
router.push({ path: '/home' })

// 命名的路由,变成 /user/123
router.push({ name: '/user', params: { userId: 123 }})

// 带查询参数,变成 /register?uname=lisi
router.push({ path: '/register', query: { uname: 'lisi' }})

# 7. 解决 Vue-router 报 NavigationDuplicated 的三种方法

有时候,你会发现控制台会报 [NavigationDuplicated {_name: "NavigationDuplicated", name: "NavigationDuplicated"}] 错误信息。其原因在于 Vue-router 在 3.1 之后把 $router.push() 方法改为了 Promise 。所以假如没有回调函数,错误信息就会交给全局的路由错误处理,因此就会报上述的错误。

vue-router 是先报了一个 Uncaught (in promise) 的错误(因为 push 没加回调),然后再点击路由的时候才会触发 NavigationDuplicated 的错误(路由出现的错误,全局错误处理打印了出来)。

  • 方案一:降低版本,固定 vue-router 版本到 3.0.7 以下。

    npm install vue-router@3.0 -S
    
    yarn add vue-router@3.0 -S
    
  • 方案二:禁止全局路由错误处理打印。这是最常见的解决方案。

    在引入 vue-router 的文件中增加以下代码,对 Router 原型链上的 push、replace 方法进行重写,这样就不用每次调用方法都要加上 catch:

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    
    Vue.use(VueRouter)
    
    const originalPush = VueRouter.prototype.push
    VueRouter.prototype.push = function push(location, onResolve, onReject) {
      if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject)
        return originalPush.call(this, location).catch(err => err)
    }
    
  • 按照 vue-router 的要求,老老实实为每一个 push 方法的调用增加回调函数。


// 路由导航守卫
router.beforeEach(((to, from, next) => {
  if (to.path === '/login') {
    return next()
  }
  // 如果访问地址不为 /login 检验本地存储 token 值是否过期,不存在跳转到 login 页面,存在则放行
  const tokenStr = window.sessionStorage.getItem('token')
  if (!tokenStr) {
    return next('/login')
  } else {
    next()
  }
}));