03_Vue Router

一、什么是路由

路由概述

路由(route)其实是一种映射关系,类似于key===>value的键值对的关系,其中key表示请求的路径path。

路由是根据不同的 url 地址展示不同的内容或页面;

路由分为前端路由和后端路由

​ 前端路由:前端路由的value表示组件,一个path映射一个组件;

​ 后端路由:后端路由的value表示处理请求的回调函数,针对不同请求的 path,处理不同的业务逻辑

前端路由

前端路由很重要的一点是页面不刷新,前端路由就是把不同路由对应不同的内容或页面的任务交给前端来做,每跳转到不同的URL都是使用前端的锚点路由.

随着(SPA)单页应用的不断普及,前后端开发分离,目前项目基本都使用前端路由,在项目使用期间页面不会重新加载。

优点:

​ 1.用户体验好,和后端网速没有关系,不需要每次都从服务器全部获取,快速展现给用户

​ 2.可以在浏览器中输入指定想要访问的url路径地址。

​ 3.实现了前后端的分离,方便开发。有很多框架都带有路由功能模块。

缺点:

​ 1.使用浏览器的前进,后退键的时候会重新发送请求,没有合理地利用缓存

​ 2.单页面无法记住之前滚动的位置,无法在前进,后退的时候记住滚动的位置

后端路由

​ 浏览器在地址栏中切换不同的url时,每次都向后台服务器发出请求,服务器响应请求,在后台拼接html文件传给前端显示, 返回不同的页面,

​ 意味着浏览器会刷新页面,网速慢的话说不定屏幕全白再有新内容。

优点:

​ 分担了前端的压力,html和数据的拼接都是由服务器完成。

缺点:

​ 当项目十分庞大时,加大了服务器端的压力,同时在浏览器端不能输入制定的url路径进行指定模块的访问。另外一个就是如果当前网速过慢,那将会延迟页面的加载,对用户体验不是很友好。

组件和路由

组件 (Component) 是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码。在较高层面上,组件是自定义元素,Vue.js 的编译器为它添加特殊功能。

组件是对特点功能代码(html,css,js)的封装, 通过组件的名字可以重复利用该组件中的代码。

路由是负责将进入的浏览器请求映射到特定的 组件 代码中。 即决定了由谁(组件)去响应客户端请求。简单说路由就是url地址和对应的资源(组件)的映射,通过一个路径的url地址,可以唯一找到一个资源。路由不包含在vue中,是一个插件,需要单独下载。

组件我们一般定义在src/components下面;路由我们一般定义在src/views或src/pages下面;

区分路由组件和非路由组件的最大区别在于:组件有没有被注册为路由!

二、Vue Router概述

github: https://github.com/vuejs/vue-router

中文文档: http://router.vuejs.org/zh-cn/

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

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

Vue Router的三个基本概念

route:首先它是个单数,译为路由,即我们可以理解为单个路由或者某一个路由;当前激活的路由信息对象,可通过this.$route获取。route对象是只读的,里面的属性是不可变的,不过可以使用watch (侦听器)监测它的变化。

routes:它是个复数,表示多个的集合才能为复数;即我们可以理解为多个路由的集合,JS中表示多种不同状态的集合的形式只有数组和对象两种,事实上官方定义routes是一个数组;所以我们记住了,routes表示多个路由的集合;

router:译为路由器,上面都是路由,这个是路由器,我们可以理解为一个容器包含上述两个或者说它是一个管理者,负责管理上述两个;举个常见的场景的例子:当用户在页面上点击按钮的时候,这个时候router就会去routes中去查找route,就是说路由器会去路由集合中找对应的路由;它是VueRouter的一个对象,通过Vue.use(VueRouter)和VueRouter构造函数得到一个router的实例对象,这个对象中是一个全局的对象,他包含了所有的路由包含了许多关键的对象和属性

Vue Router实现原理

SPA(single page application):单一页面应用程序,只有一个完整的页面。它在加载页面时,不会加载整个页面,而是只更新某个指定的容器中内容。单页面应用(SPA)的核心之一是:更新视图而不重新请求页面。Vue Router在实现单页面前端路由时,提供了两种方式:Hash模式和History模式。Vue Router中默认使用的是hash模式。

hash模式

随着 AJAX 的流行,异步数据请求交互运行在不刷新浏览器的情况下进行。而异步交互体验的更高级版本就是 SPA —— 单页应用。单页应用不仅仅是在页面交互是无刷新的,连页面跳转都是无刷新的,为了实现单页应用,所以就有了前端路由。类似于服务端路由,前端路由实现起来其实也很简单,就是匹配不同的 url 路径,进行解析,然后动态的渲染出区域 html 内容。但是这样存在一个问题,就是 url 每次变化的时候,都会造成页面的刷新。那解决问题的思路便是在改变url的情况下,保证页面的不刷新。在 2014 年之前,大家是通过 hash 来实现路由,url hash 就是类似于:localhost:8080/#/login

这种 #。后面 hash 值的变化,并不会导致浏览器向服务器发出请求,浏览器不发出请求,也就不会刷新页面。另外每次 hash 值的变化,还会触发hashchange 这个事件,通过这个事件我们就可以知道 hash 值发生了哪些变化。然后我们便可以监听hashchange来实现更新页面部分内容的操作:

function matchAndUpdate () {
   // todo 匹配 hash 做 dom 更新操作
}
window.addEventListener('hashchange', matchAndUpdate)

history 模式

因为HTML5标准发布,多了两个 API,pushState 和 replaceState,通过这两个 API 可以改变 url 地址且不会发送请求。同时还有popstate事件。通过这些就能用另一种方式来实现前端路由了,但原理都是跟 hash 实现相同的。用了HTML5的实现,单页路由的url就不会多出一个#,变得更加美观。但因为没有 # 号,所以当用户刷新页面之类的操作时,浏览器还是会给服务器发送请求。为了避免出现这种情况,所以这个实现需要服务器的支持,需要把所有路由都重定向到根页面。

function matchAndUpdate () {
   // todo 匹配路径 做 dom 更新操作
}

window.addEventListener('popstate', matchAndUpdate)

相关API

1、VueRouter(): 用于创建路由器的构建函数

new VueRouter({ 
    // 多个配置项 
})

2、路由配置

routes: [
    { 
        // 一般路由 
        path: '/about', 
        component: About 
    },
    { // 自动跳转路由 
        path: '/', 
        redirect: '/about' 
    }
]

3、注册路由器

import router from './router

new Vue({ 
    router 
})

4、使用路由组件标签

1. <router-link>: 用来生成路由链接
	<router-link to="/xxx">Go to XXX</router-link>

2. <router-view>: 用来显示当前路由组件界面
    <router-view></router-view>

三、安装

直接下载 / CDN

https://unpkg.com/vue-router/dist/vue-router.js

Unpkg.com 提供了基于 NPM 的 CDN 链接。上面的链接会一直指向在 NPM 发布的最新版本。你也可以像 https://unpkg.com/vue-router@2.0.0/dist/vue-router.js 这样指定 版本号 或者 Tag。

在 Vue 后面加载 vue-router,它会自动安装的:

<script src="/path/to/vue.js"></script>
<script src="/path/to/vue-router.js"></script>

NPM

npm install vue-router

如果在一个模块化工程中使用它,必须要通过 Vue.use() 明确地安装路由功能:

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

如果使用全局的 script 标签,则无须如此 (手动安装)。

构建开发版

如果你想使用最新的开发版,就得从 GitHub 上直接 clone,然后自己 build 一个 vue-router

git clone https://github.com/vuejs/vue-router.git node_modules/vue-router

cd node_modules/vue-router

npm install

npm run build

四、起步

用 Vue.js + Vue Router 创建单页应用,是非常简单的。使用 Vue.js 已经可以通过组合组件来组成应用程序,当把 Vue Router 添加进来后,需要做的是将组件 (components) 映射到路由 (routes),然后告诉Vue Router 在哪里渲染它们。下面是个基本例子:

HTML

<div id="app">
  <h1>Hello Vue Router</h1>
  <!--
      <router-link>标签,进行路由导航,它会被Vue解析为一个超链接
      to属性配置路由路径
  -->
  <router-link to="/login">登录</router-link>
  <router-link to="/reg">注册</router-link>
  <!--
      通过router-link标签导航之后,组件如何渲染到页面中?
      <router-view>标签,渲染某个路由路径对应的组件
      称为路由出口
  -->
  <router-view></router-view>
</div>

<script src="./js/vue.js" type="text/javascript" charset="utf-8"></script>
<script
  src="./js/vue-router.js"
  type="text/javascript"
  charset="utf-8">
</script>

请注意,我们没有使用常规的 a 标签,而是使用一个自定义组件 router-link 来创建链接。这使得Vue Router 可以在不重新加载页面的情况下更改 URL,处理 URL 的生成以及编码。我们将在后面看到如何从这些功能中获益。

router-view

router-view 将显示与 url 对应的组件。你可以把它放在任何地方,以适应你的布局。

JavaScript

<!-- index.html -->
<div id="root">
  <router-link to="/login">登录</router-link>
  <router-link to="/register">注册</router-link>
  <router-view></router-view>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router@3.5.3/dist/vue-router.js"></script>
<script>
  // 1. 定义 (路由) 组件。可以从其他文件 import 进来
  var login = {
    template: '<div><h1>登录组件</h1></div>'
  }
  
  var register = {
    template: '<div><h1>注册组件</h1></div>'
  }
  
  // 2. 定义路由列表
  /**
    * 在routes里面设置每个路由组件对应的路由参数:
    * 1) path:表示路由字符串,监听某个路由地址,path中需要有斜杠(/)。
    * 2) component:如果path匹配成功,则去寻找对应都组件,
    * 所以component指向的是一个Vue.extend() 创建的组件构造器,或者只是一个组件配置对象。
   */
  const routes = [
    { path: '/login', component: login },
    { path: '/register', component: register }
  ]
  
  // 3. 创建 router 实例,然后传 routes 配置
  const router = new VueRouter({
    routes // 缩写,相当于 routes: routes
  })
  
  // 4. 将创建的好路由实例,绑定给Vue实例的router属性
  // 从而让整个应用都有路由功能
  var vue = new Vue({
    el: '#root',
    router
  });
</script>

五、动态路由匹配

我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个 User 组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。那么,我们可以在 vue-router 的路由路径中使用“动态路径参数”来达到这个效果:当我们在地址后面直接添加任意字符,我们会发现文档内容随着我们的更改而改变。

动态路径参数

路由中添加新的组件和路由映射:

const User = {template:'<div>用户组件</div>'}
const router = new VueRouter({
    routes: [
        // 动态路径参数 以冒号开头
        { path: '/user/:id', component: User }
    ]
})

现在呢,像 /user/101/user/102 都将映射到相同的路由。

一个“路径参数”使用冒号(:)标记。当匹配到一个路由时,参数值会被设置到 this.$route.params

可以在每个组件内使用。于是,我们可以更新 User 的模板,输出当前用户的 ID:

const User = {template: '<div>用户组件 {{ $route.params.id }}</div>'}

你可以在一个路由中设置多段“路径参数”,对应的值都会设置到 $route.params 中。例如:

模式 匹配路径 $route.params
/user/:username /user/tom
/user/:id/:username /user/103/jerry

除了 \(route.params 外,\)route 对象还提供了其它有用的信息,例如, $route.query (如果 URL 中有查询参数)、 $route.hash 等等。

示例代码:

<div id="app">
  <h1>Hello Vue Router</h1>
  <router-link to="/login">登录</router-link>
  <router-link to="/reg">注册</router-link>
  <router-link to="/user/101">用户101</router-link>
  <router-link to="/user/102">用户102</router-link>
  <router-view></router-view>
</div>
<script src="./js/vue.js" type="text/javascript" charset="utf-8"></script>
<script
  src="./js/vue-router.js"
  type="text/javascript"
  charset="utf-8"
></script>
<script type="text/javascript">
  var login = {
    template: "<div><h1>登录组件</h1></div>",
  };
  var register = {
    template: "<div><h1>注册组件</h1></div>",
  };
  var user = {
    template: "<div><h1>用户组件{{$route.params.id}}</h1></div>",
  };
  var router = new VueRouter({
    routes: [
      {
        path: "/login",
        component: login,
      },
      {
        path: "/reg",
        component: register,
      },
      //动态路由路径
      {
        path: "/user/:id",
        component: user,
      },
    ],
  });
  let vm = new Vue({
    el: "#app",
    data: {
      message: "hello vuejs",
    },
    router,
  });
</script>

响应路由参数的变化

当使用路由参数时,例如从 /user/101 导航到 /user/102相同的组件实例将被重复使用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会再被调用。

复用组件时,想对路由参数的变化作出响应的话,可以简单地 watch (侦听器)监测变化 $route 对象:

const User = {
    template: '...',
    watch: {
    	'$route' (to, from) {
    		// 对路由变化作出响应...
    		console.log('变化之前的路由对象to:', to)
    		console.log('变化之后的路由对象from:', from)
    	 }
    }
}

或者使用 2.2 中引入的 beforeRouteUpdate 导航守卫:

const User = {
    template: '...',
    beforeRouteUpdate (to, from, next) {
        // 对路由变化作出响应...
        // 不要忘记调用 next() 函数
    }
}

示例代码:

<div id="app">
  <h1>Hello Vue Router</h1>
  <router-link to="/login">登录</router-link>
  <router-link to="/reg">注册</router-link>
  <router-link to="/user/101">用户101</router-link>
  <router-link to="/user/102">用户102</router-link>
  <router-view></router-view>
</div>
<script src="./js/vue.js" type="text/javascript" charset="utf-8"></script>
<script
  src="./js/vue-router.js"
  type="text/javascript"
  charset="utf-8"
></script>
<script type="text/javascript">
  var login = { template: "<div><h1>登录组件</h1></div>" };
  var register = { template: "<div><h1>注册组件</h1></div>" };
  var user = {
    template: "<div><h1>用户组件{{$route.params.id}}</h1></div>",
    //侦听器:侦听当前路由对象的变化
    watch: {
      //to: 导航之后(跳转之后)的路由对象
      //from: 导航之前的路由对象
      $route(to, from) {
        console.log("导航之前:", from);
        console.log("导航之后:", to);
      },
    },
  };
  var router = new VueRouter({
    routes: [
      { path: "/login", component: login },
      { path: "/reg", component: register },
      { path: "/user/:id", component: user },
    ],
  });
  let vm = new Vue({
    el: "#app",
    data: {
      message: "hello vuejs",
    },
    router,
  });
</script>

捕获所有路由或404 Not found路由

常规参数只会匹配被 / 分隔的 URL 片段中的字符。如果想匹配任意路径,我们可以使用通配符 ( * ):

{
    // 会匹配所有路径
    path: '*'
}
{
    // 会匹配以 `/user-` 开头的任意路径
    path: '/user-*'
}

当使用通配符路由时,请确保路由的顺序是正确的,也就是说含有通配符的路由应该放在最后。路由{path: '*'} 通常用于客户端 404 错误。

当使用一个通配符时, $route.params 内会自动添加一个名为 pathMatch 参数。它包含了 URL 通过通配符被匹配的部分:

// 给出一个路由 { path: '/user-*' }
this.$router.push('/user-add')
this.$route.params.pathMatch // 'add'
// 给出一个路由 { path: '*' }
this.$router.push('/non-existing')
this.$route.params.pathMatch // '/non-existing'

示例代码:

<div id="app">
  <h1>Hello Vue Router</h1>
  <router-link to="/login">登录</router-link>
  <router-link to="/reg">注册</router-link>
  <router-link to="/user/101">用户101</router-link>
  <router-link to="/user/102">用户102</router-link>
  <router-link to="/emp-query">员工查询</router-link>
  <router-link to="/emp-add">员工新增</router-link>
  <router-link to="/emp-edit">员工编辑</router-link>
  <router-link to="/emp-remove">员工删除</router-link>
  <router-view></router-view>
</div>
<script src="./js/vue.js" type="text/javascript" charset="utf-8"></script>
<script
  src="./js/vue-router.js"
  type="text/javascript"
  charset="utf-8"
></script>
<script type="text/javascript">
  var login = { template: "<div><h1>登录组件</h1></div>" };
  var register = { template: "<div><h1>注册组件</h1></div>" };
  var user = { template: "<div><h1>用户组件{{$route.params.id}}</h1></div>" };
  var emp = { template: "<div><h1>员工CURD组件</h1></div>" };
  var error = { template: "<div><h1>404访问的组件未找到</h1></div>" };
  var router = new VueRouter({
    routes: [
      { path: "/login", component: login },
      { path: "/reg", component: register },
      { path: "/user/:id", component: user },
      { path: "/emp-*", component: emp },
      //路由路径通配符
      //路由列表的匹配模式是自上而下逐个路由匹配,带有通配符的路由一定放在路由列表最后
      { path: "/*", component: error },
    ],
  });
  let vm = new Vue({
    el: "#app",
    data: {
      message: "hello vuejs",
    },
    router,
  });
</script>

六、嵌套路由

实际生活中的应用界面,通常由多层嵌套的组件组合而成。同样地,URL 中各段动态路径也按某种结构对应嵌套的各层组件,例如:

/user/list                            /user/add
+------------------+                  +-----------------+
| User             |                  | User            |
| +--------------+ |                  | +-------------+ |
| | UserList     | |                  | | UserAdd     | |
| |              | |                  | |             | |
| +--------------+ |                  | +-------------+ |
+------------------+                  +-----------------+

借助 vue-router,使用嵌套路由配置,就可以很简单地表达这种关系。官方文档中给我们提供了一个children属性,这个属性是一个数组类型,里面实际放着一组路由;这个时候父子关系结构就出来了,所以children属性里面的是路由相对来说是children属性外部路由的子路由;

七、编程式的导航

Vue路由导航两种方式:

标签导航:标签导航<router-link><router-link>是通过转义为<a></a>标签进行跳转,其中router-link标签中的to属性会被转义为a标签中的href属性;

<router-link to="/home/news">新闻中心</router-link>

编程式导航:我们可以通过this.$router.push()这个方法来实现编程式导航,当然也可以实现参数传递,这种编程式导航一般是用于按钮点击之后跳转

push方法

this.$router.push(path):	相当于点击路由链接(可以返回到当前路由界面)

注意:在 Vue 实例内部,你可以通过 $router 访问路由器实例。因此你可以调用 this.$router.push()

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

当你点击 <router-link> 时,这个方法会在内部调用,所以说,点击 <router-link :to="..."> 等同于调用 router.push(...)

声明式 编程式
<router-link :to="..."> router.push(...)

该方法的参数可以是一个字符串路径,或者一个描述地址的对象。例如:

// 字符串
router.push('home')

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

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

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

注意:如果提供了 path,params 会被忽略,上述例子中的 query 并不属于这种情况。取而代之的是下面例子的做法,你需要提供路由的 name 或手写完整的带有参数的 path:

const userId = '123'
router.push({ name: 'user', params: { userId }}) // -> /user/123
router.push({ path: `/user/${userId}` }) // -> /user/123
// 这里的 params 不生效
router.push({ path: '/user', params: { userId }}) // -> /user

同样的规则也适用于 router-link 组件的 to 属性。

replace方法

this.$router.replace(path): 用新路由替换当前路由(不可以返回到当前路由界面)

router.push 很像,唯一的不同就是,它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录。

声明式 编程式
<router-link :to="..." replace> router.replace(...)

go方法

this.$router.back(): 请求(返回)上一个记录路由

this.$router.go(-1): 请求(返回)上一个记录路由

this.$router.go(1): 请求下一个记录路由

这个方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步,类似 window.history.go(n)

例子

// 在浏览器记录中前进一步,等同于 history.forward()
router.go(1)

// 后退一步记录,等同于 history.back()
router.go(-1)
router.back()

// 前进 3 步记录
router.go(3)

// 如果 history 记录不够用,那就默默地失败呗
router.go(-100)
router.go(100)

操作 History

你也许注意到 router.pushrouter.replacerouter.gowindow.history.pushStatewindow.history.replaceStatewindow.history.go 好像, 实际上它们确实是效仿 window.history API 的。因此,如果你已经熟悉 Browser History APIs,那么在 Vue Router 中操作 history 就是超级简单的。

示例代码

在消息列表页添加按钮,查看消息详情;

<div id="app">
  <h1>Hello Vue Router</h1>
  <p>
    <router-link to="/login">登录</router-link>
    <router-link to="/reg">注册</router-link>
    <router-link to="/user">用户</router-link>
    <router-link to="/emp">员工</router-link>
  </p>
  <p>
    <button type="button" @click="doRedirect('login')">登录按钮</button>
    <button type="button" @click="doRedirect('reg')">注册按钮</button>
    <button type="button" @click="doRedirect('user')">用户按钮</button>
    <button type="button" @click="doRedirect('emp')">员工按钮</button>
    <button type="button" @click="doReplace()">替换</button>
  </p>
  <p>
    <button type="button" @click="doBackward()">后退</button>
    <button type="button" @click="doForward()">前进</button>
  </p>
  <div>
    <router-view></router-view>
  </div>
</div>
<script src="./js/vue.js" type="text/javascript" charset="utf-8"></script>
<script
  src="./js/vue-router.js"
  type="text/javascript"
  charset="utf-8"
></script>
<script type="text/javascript">
  var login = { template: "<div><h1>登录组件</h1></div>" };
  var register = { template: "<div><h1>注册组件</h1></div>" };
  var user = { template: "<div><h1>用户组件</h1></div>" };
  var emp = { template: "<div><h1>员工组件</h1></div>" };
  var error = { template: "<div><h1>404访问的组件未找到</h1></div>" };
  var router = new VueRouter({
    routes: [
      { path: "/login", component: login },
      { path: "/reg", component: register },
      { path: "/user", component: user },
      { path: "/emp", component: emp },
      { path: "/*", component: error },
    ],
  });
  let vm = new Vue({
    el: "#app",
    data: {
      message: "hello vuejs",
    },
    router,
    methods: {
      doRedirect(str) {
        /**
         * 编程式导航,路由跳转
         * this.$router.push(路由路径字符串)
         * this.$router.push({path: 路由路径字符串})
         *
         * 路由在导航后会形成history历史记录
         * push() 就是在历史列表中追加内容
         * replace() 就是在历史列表替换上一个记录
         */
        // this.$router.push('/' + str)
        this.$router.push({ path: `/${str}` });
      },
      doReplace() {
        // 404 -> 登录 -> 注册 -> 用户 -> 登录
        this.$router.replace("/login");
      },
      doForward() {
        this.$router.go(1);
      },
      doBackward() {
        this.$router.go(-1);
      },
    },
  });
</script>

八、命名路由

概述

有时候,通过一个名称来标识一个路由显得更方便一些,特别是在链接一个路由,或者是执行一些跳转的时候。你可以在创建 Router 实例的时候,在 routes 配置中给某个路由设置名称。

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

要链接到一个命名路由,可以给 router-link 的 to 属性传一个对象:

<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>

这跟代码调用 router.push() 是一回事:

router.push({ name: 'user', params: { userId: 123 }})

这两种方式都会把路由导航到 /user/123 路径。

示例代码

<div id="app">
  <h1>Hello Vue Router</h1>
  <p>
    <router-link :to="{name: 'login'}">登录</router-link>
    <router-link :to="{name: 'register'}">注册</router-link>
    <router-link :to="{name: 'user'}">用户</router-link>
    <router-link :to="{name: 'employee'}">员工</router-link>
  </p>
  <p>
    <button type="button" @click="doRedirect('login')">登录按钮</button>
    <button type="button" @click="doRedirect('register')">注册按钮</button>
    <button type="button" @click="doRedirect('user')">用户按钮</button>
    <button type="button" @click="doRedirect('employee')">员工按钮</button>
  </p>
  <div>
    <router-view></router-view>
  </div>
</div>
<script src="./js/vue.js" type="text/javascript" charset="utf-8"></script>
<script
  src="./js/vue-router.js"
  type="text/javascript"
  charset="utf-8"
></script>
<script type="text/javascript">
  var login = { template: "<div><h1>登录组件</h1></div>" };
  var register = { template: "<div><h1>注册组件</h1></div>" };
  var user = { template: "<div><h1>用户组件</h1></div>" };
  var emp = { template: "<div><h1>员工组件</h1></div>" };
  var error = { template: "<div><h1>404访问的组件未找到</h1></div>" };
  var router = new VueRouter({
    routes: [
      //命名路由:为每个路由的配置添加name属性
      { path: "/login", name: "login", component: login },
      { path: "/reg", name: "register", component: register },
      { path: "/user", name: "user", component: user },
      { path: "/emp", name: "employee", component: emp },
      { path: "/*", name: "error", component: error },
    ],
  });
  let vm = new Vue({
    el: "#app",
    data: {
      message: "hello vuejs",
    },
    router,
    methods: {
      doRedirect(str) {
        //通过路由名称导航
        this.$router.push({ name: str });
      },
    },
  });
</script>

九、命名视图

概述

有时候想同时 (同级) 展示多个视图,而不是嵌套展示,例如创建一个布局,有 header (头部),sidebar (侧导航) 和 main (主内容) 三个视图,这个时候命名视图就派上用场了。你可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。如果 router-view 没有设置名字,那么默认为 default。

<router-view class="view one"></router-view>
<router-view class="view two" name="aside"></router-view>
<router-view class="view three" name="main"></router-view>

一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件。确保正确使用 components 配置 (带上 s):

const router = new VueRouter({
  routes: [
    {
      path: '/',
      components: {
        // 视图名:组件
        default: Header,
        aside: Aside,
        main: Main
      }
    }
  ]
})

示例代码

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title></title>
    <style type="text/css">
      body {
        margin: 0;
      }
      #app {
        height: 100vh;
        /* 流式布局 */
        display: flex;
        flex-flow: column nowrap;
      }
      .header {
        height: 80px;
        text-align: center;
        background-color: aqua;
      }
      .container {
        display: flex;
        /* background-color: red; */
        flex: auto;
      }
      .aside {
        width: 350px;
        background-color: aquamarine;
      }
      .main {
        background-color: burlywood;
        flex: auto;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <!--
命名视图:带有名称的路由出口,不同路由出口渲染不同组件
-->
      <router-view class="header" name="default"></router-view>
      <div class="container">
        <router-view class="aside" name="aside"></router-view>
        <router-view class="main" name="main"></router-view>
      </div>
    </div>
    <script src="./js/vue.js" type="text/javascript" charset="utf-8"></script>
    <script
      src="./js/vue-router.js"
      type="text/javascript"
      charset="utf-8"
    ></script>
    <script type="text/javascript">
      var myHeader = { template: "<div><h1>页面头部组件</h1></div>" };
      var myAside = { template: "<div><h1>页面侧边组件</h1></div>" };
      var myMain = { template: "<div><h1>页面主区域组件</h1></div>" };
      var router = new VueRouter({
        routes: [
          {
            path: "/",
            name: "index",
            components: {
              //此路由路径对应的多个组件
              default: myHeader,
              aside: myAside,
              main: myMain,
            },
          },
        ],
      });
      let vm = new Vue({
        el: "#app",
        data: {
          message: "hello vuejs",
        },
        router,
      });
    </script>
  </body>
</html>

十、重定向和别名

重定向

重定向也是通过 routes 配置来完成,下面例子是从 /a 重定向到 /b:

const router = new VueRouter({
  routes: [
    { path: '/a', redirect: '/b' }
  ]
})

重定向的目标也可以是一个命名的路由:

const router = new VueRouter({
  routes: [
    { path: '/a', redirect: { name: 'foo' }}
  ]
})

甚至是一个方法,动态返回重定向目标:

const router = new VueRouter({
  routes: [
    { path: '/a', redirect: to => {
      // 方法接收 目标路由 作为参数
      // return 重定向的 字符串路径/路径对象
    }}
  ]
})

注意导航守卫并没有应用在跳转路由上,而仅仅应用在其目标上。在下面这个例子中,为 /a 路由添加一个 beforeEach 守卫并不会有任何效果。

别名

“重定向”的意思是,当用户访问 /a时,URL 将会被替换成 /b,然后匹配路由为 /b,那么“别名”又是什么呢?

/a 的别名是 /b,意味着,当用户访问 /b 时,URL 会保持为 /b,但是路由匹配则为 /a,就像用户访问 /a 一样。

上面对应的路由配置为:

const router = new VueRouter({
  routes: [
    { path: '/a', component: A, alias: '/b' }
  ]
})

“别名”的功能让你可以自由地将 UI 结构映射到任意的 URL,而不是受限于配置的嵌套路由结构。

示例代码

<div id="app">
  <h1>Hello Vue Router</h1>
  <p>
    <router-link :to="{name: 'main'}">主页</router-link>
    <router-link to="/user">用户</router-link>
    <router-link :to="{name: 'emp'}">员工</router-link>
    <router-link to="/uu">用户别名</router-link>
  </p>
  <div>
    <router-view></router-view>
  </div>
</div>
<script src="./js/vue.js" type="text/javascript" charset="utf-8"></script>
<script
  src="./js/vue-router.js"
  type="text/javascript"
  charset="utf-8"
></script>
<script type="text/javascript">
  var main = { template: "<div><h1>主页组件</h1></div>" };
  var user = { template: "<div><h1>用户组件</h1></div>" };
  var emp = { template: "<div><h1>员工组件</h1></div>" };
  var error = { template: "<div><h1>404访问的组件未找到</h1></div>" };
  var router = new VueRouter({
    routes: [
      //通过重定向来实现组件的复用
      // {path: '/', name: 'index', redirect: '/main'},
      { path: "/", name: "index", redirect: { name: "main" } },
      { path: "/main", name: "main", component: main },
      //路由别名
      { path: "/user", name: "user", component: user, alias: "/uu" },
      { path: "/emp", name: "emp", component: emp },
      { path: "/*", name: "error", component: error },
    ],
  });
  let vm = new Vue({
    el: "#app",
    data: {
      message: "hello vuejs",
    },
    router,
  });
</script>

十一、路由传参

query

  • 参数设置:通过 ? 拼接路由路径中。
  • 参数获取:$route.query 一个 key/value 对象,表示 URL 查询参数,如果没有查询参数,则是个空对象。
  • 对于路径 /register?username=tom&password=123,则有 $route.query.username == tom$route.query.password == 123

params

  • 参数设置:通过路由路径传参。
  • 参数获取:$route.params 一个 key/value 对象,包含了动态片段和全匹配片段,如果没有路由参数,就是一个空对象。
  • 对于路径 /login/:username/:password --> /login/jack/456,则有 $route.params.username == jack$route.params.password == 456

props

在组件中使用 $route 会使之与其对应路由形成高度耦合,从而使组件只能在某些特定的 URL 上使用,限制了其灵活性。

使用 props 将组件和路由解耦:

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

    // 对于包含命名视图的路由,你必须分别为每个命名视图添加 `props` 选项:
    {
      path: '/user/:id',
      components: { default: User, sidebar: Sidebar },
      props: { default: true, sidebar: false }
    }
  ]
})

这样你便可以在任何地方使用该组件,使得该组件更易于重用和测试。

如果 props 被设置为 true,route.params 将会被设置为组件属性。

示例代码

<div id="app">
  <h1>Hello Vue Router</h1>
  <p>
    <router-link to="/login?username=jerry&userpwd=123">登录</router-link>
    <router-link :to="{ name: 'register', params: { abc: 123 } }"
      >注册</router-link
    >
    <router-link to="/user/10/zhangsan">用户zhangsan</router-link>
    <router-link to="/user/20/lisi">用户lisi</router-link>
  </p>
  <p>
    <button type="button" @click="doLogin">登录按钮</button>
    <button type="button" @click="doRegister">注册按钮</button>
  </p>
  <div>
    <router-view></router-view>
  </div>
</div>

<template id="login">
  <div>
    <h1>登录组件</h1>
    <p>{{ $route.query }}</p>
    <p>{{ $route.query.username }}</p>
    <p>{{ $route.query.userpwd }}</p>
  </div>
</template>

<template id="reg">
  <div>
    <h1>注册组件</h1>
    <p>{{ $route.params }}</p>
    <p>{{ $route.params.uname }}</p>
    <p>{{ $route.params.upwd }}</p>
    <p>{{ $route.params.email }}</p>
    <p>{{ $route.params.mobile }}</p>
  </div>
</template>

<template id="user">
  <div>
    <h1>用户组件</h1>
    <p>用户编号: {{ id }}</p>
    <p>用户名称: {{ username }}</p>
  </div>
</template>

<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router@3.5.3/dist/vue-router.js"></script>
<script>
  var login = { template: "#login" };
  var register = { template: "#reg" };
  var user = {
    template: "#user",
    // 1. 定义props配置项
    props: {
      id: Number,
      username: String,
    },
  };

  var router = new VueRouter({
    routes: [
      { path: "/login", name: "login", component: login },
      {
        path: "/register",
        name: "register",
        component: register,
        meta: { title: "注册页面" },
      },
      // 路由路径中的占位符名称必须与组件props变量名称一致
      // 2. 在路径配置中开启props传参
      {
        path: "/user/:id/:username",
        name: "user",
        component: user,
        props: true,
      },
    ],
  });

  let vm = new Vue({
    el: "#app",
    data: {
      message: "hello vuejs",
    },
    router,
    methods: {
      doLogin() {
        var user = {
          username: "tom",
          userpwd: "456",
        };
        // 解析为:/login?username=tom&userpwd=456
        this.$router.push({ path: "/login", query: user });
      },
      doRegister() {
        var data = {
          email: "zhangsan@qq.com",
          mobile: "13112345678",
        };
        /**
         * params传参+编程式导航
         * 1. 参数隐藏,不在路径中出现
         * 2. 路径固定
         * 3. 必须通过name导航
         */
        this.$router.push({ name: "register", params: data });
        // this.$router.push({ path: '/register', params: data }) // 错误的
      },
    },
  });
</script>

十二、导航守卫

“导航”表示路由正在发生改变。vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。

记住参数或查询的改变并不会触发进入/离开的导航守卫。你可以通过观察 $route 对象来应对这些变化,或使用 beforeRouteUpdate 的组件内守卫。

全局前置守卫

你可以使用 router.beforeEach 注册一个全局前置守卫:

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

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

当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于等待中。

每个守卫方法接收三个参数:

  • to: Route对象,即将要进入的目标路由对象
  • from: Route对象,当前导航正要离开的路由
  • next: Function函数,一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。
    • next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
    • next(false): 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
    • next('/') 或者 next({ path: '/' }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向 next 传递任意位置对象,且允许设置诸如 replace: true、name: 'home' 之类的选项以及任何用在 router-link 的 to prop 或 router.push 中的选项。
    • next(error): (2.4.0+) 如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给 router.onError() 注册过的回调。

确保 next 函数在任何给定的导航守卫中都被严格调用一次。它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错。

这里有一个在用户未能验证身份时重定向到 /login 的示例:

// BAD
router.beforeEach((to, from, next) => {
  if (to.name !== 'Login' && !isAuthenticated){
    next({ name: 'Login' })
  }
  // 如果用户未能验证身份,则 `next` 会被调用两次
  next()
})
// GOOD
router.beforeEach((to, from, next) => {
  if (to.name !== 'Login' && !isAuthenticated){
    next({ name: 'Login' })
  } else {
    next()
  }
})

全局解析守卫

在 2.5.0+ 你可以用 router.beforeResolve 注册一个全局守卫。这和 router.beforeEach 类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。

全局后置钩子

你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:

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

路由独享的守卫

你可以在路由配置上直接定义 beforeEnter 守卫:

const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      beforeEnter: (to, from, next) => {
        // ...
      }
    }
  ]
})

这些守卫与全局前置守卫的方法参数是一样的。

组件内的守卫

最后,你可以在路由组件内直接定义以下路由导航守卫:

  • beforeRouteEnter
  • beforeRouteUpdate
  • 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` 访问组件实例
  })
}

注意 beforeRouteEnter 是支持给 next 传递回调的唯一守卫。对于 beforeRouteUpdate 和 beforeRouteLeave 来说,this 已经可用了,所以不支持传递回调,因为没有必要了。

beforeRouteUpdate (to, from, next) {
  // just use `this`
  this.name = to.params.name
  next()
}

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

beforeRouteLeave (to, from, next) {
  const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
  if (answer) {
    next()
  } else {
    next(false)
  }
}

完整的导航解析流程

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

十三、路由元信息

定义路由的时候可以配置 meta 字段:

const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      children: [
        {
          path: 'bar',
          component: Bar,
          // a meta field
          meta: { requiresAuth: true }
        }
      ]
    }
  ]
})

那么如何访问这个 meta 字段呢?

首先,我们称呼 routes 配置中的每个路由对象为 路由记录。路由记录可以是嵌套的,因此,当一个路由匹配成功后,他可能匹配多个路由记录

例如,根据上面的路由配置,/foo/bar 这个 URL 将会匹配父路由记录以及子路由记录。

一个路由匹配到的所有路由记录会暴露为 $route 对象 (还有在导航守卫中的路由对象) 的 $route.matched 数组。因此,我们需要遍历 $route.matched 来检查路由记录中的 meta 字段。

下面例子展示在全局导航守卫中检查元字段:

router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    // this route requires auth, check if logged in
    // if not, redirect to login page.
    if (!auth.loggedIn()) {
      next({
        path: '/login',
        query: { redirect: to.fullPath }
      })
    } else {
      next()
    }
  } else {
    next() // 确保一定要调用 next()
  }
})

十四、路由懒加载

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。

结合 Vue 的异步组件和 Webpack 的代码分割功能,轻松实现路由组件的懒加载。

首先,可以将异步组件定义为返回一个 Promise 的工厂函数 (该函数返回的 Promise 应该 resolve 组件本身):

const Foo = () => Promise.resolve({ /* 组件定义对象 */ })

第二,在 Webpack 2 中,我们可以使用动态 import语法来定义代码分块点 (split point):

import('./Foo.vue') // 返回 Promise

结合这两者,这就是如何定义一个能够被 Webpack 自动代码分割的异步组件。

const Foo = () => import('./Foo.vue')

在路由配置中什么都不需要改变,只需要像往常一样使用 Foo:

const router = new VueRouter({
    routes: [
    	{ path: '/foo', component: Foo }
    ]
})
posted @ 2023-11-04 17:28  城市炊烟  阅读(22)  评论(0编辑  收藏  举报