1. 什么是路由
访问一个web应用(或者网站),需要有一个URL,这个URL可以通过HTTP协议 POST或者GET或 PUT或DELETE到服务端,当服务端接收到这个请求地址之后,通过规则匹配URL,然后转发到后端应用程序中的某个方法(函数)调用上,方法(函数)调用完毕之后会生成HTML文本,随后HTML文本会响应到浏览器中,浏览器完成HTML渲染,最终就看到了一个完整的页面。
我们称这种将 URL转发到后端方法(函数)调用的过程叫做路由,匹配的规则集合就叫做路由表。
1.1 后端路由
上面描述的场景是在后端完成的,所以也叫后端路由。这种后端路由是多页面的,页面在服务端生成好直接返回给浏览器,对SEO友好。缺点是页面由后端模板通过程序来维护和生成,维护起来既臃肿又麻烦。
1.2 前端路由
有了前后端分离的开发模式后,后端只需要提供API来返回数据,前端通过Ajax获取数据后,再用一定的方式渲染到页面里,这样做的有点就是前后端分工明确,后端专注数据逻辑,前端专注在交互和可视化上。
前端路由主要使用在单页面应用上(SPA single page web application),前端维护一个路由规则。当地址发生变化的时候,不再向服务端发送HTTP请求,而是通过前端的路由规则(路由表)来进行匹配,匹配后进行页面视图的更新,因为前端需要展现的视图本身就在前端,无需向后端索要,后端只需为前端提供数据即可。
前面学习过 通过 Vue 中 component 的 is 来实现动态加载组件。前端路由与动态组件很相似,只不过是通过 vue-router插件来实现的,路由不同的页面事实上就是动态加载不同的组件。
2. vue-router
vue-router 是一个npm包,要使用它必须先安装它。官方文档
2.1 安装
npm i vue-router -S 或 cnpm i vue-router -S
vue-router是一个vue插件,所以在实例化Vue对象之前,需要使用use方法进行加载。
import Vue from 'vue'
import App from '@/App'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap/dist/js/bootstrap'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const vm = new Vue({
el: '#app',
render: h => h(App)
})
2.2 简单使用
在 App.vue页面上有一个导航,放置两个链接 “首页"和"关于”, 默认显示首页内容,当点击关于的时候,显示关于页面。
About.vue
<template>
<div class="bg">
这是 about 页面
</div>
</template>
<script>
export default {}
</script>
<style scoped>
.bg {
background-color: darkseagreen;
}
</style>
Home.vue
<template>
<div class="bg">
这是 Home页面
</div>
</template>
<script>
export default {}
</script>
<style scoped>
.bg {
background-color: cornflowerblue;
}
</style>
App.vue
<template>
<div>
<div>
<router-link to="/">首页</router-link> |
<router-link to="/about">关于</router-link>
</div>
<!-- 被路由规则匹配到的 组件将会被显示到这里 -->
<router-view />
</div>
</template>
main.js
import Vue from 'vue'
import App from '@/App'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap/dist/js/bootstrap'
import VueRouter from 'vue-router'
import Home from '@/views/Home'
import About from '@/views/About'
Vue.use(VueRouter)
// 路由表
const routes = [{
path: '/',
name: 'Home',
component: Home
}, {
path: '/about',
name: 'About',
component: About
}]
// 路由对象
const router = new VueRouter({
routes // ES6语法 相当于 routes: routes
})
const vm = new Vue({
router, // 配置路由对象
el: '#app',
render: h => h(App)
})
使用总结:
-
npm 安装vue-router
-
导入 VueRouter插件,并使用 Vue.use(VueRouter) 加载插件
-
定义路由规则(路由表)
路由表是一个数组,数组的每个元素就是一个 规则,其中包含了 匹配路径(path),路由名称 (name), 路由对应的组件(component).
当 匹配到路由之后(可以按照path匹配,也可以按照name匹配), 就会自动渲染对应的组件。
-
实例化路由对象,在路由对象中"装入" 路由表对象
-
实例化Vue对象的时候,配置路由选项
<router-link to="/">首页</router-link>
最终渲染出HTML代码 <a href="#/" class="router-link-active">首页</a> ,to 对应路由表中的path,
<router-view />
它相当于是一个占位符,一旦路由匹配到了,路由所对应的组件就会填充这个占位符。
3. 分离路由表
上面的路由表定义在了main.js中,应用一旦变得复杂,将会有很多路由,所以这里将路由表分离到单独的文件中进行维护。
src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/views/Home'
import About from '@/views/About'
Vue.use(VueRouter)
// 路由表
const routes = [{
path: '/',
name: 'Home',
component: Home
}, {
path: '/about',
name: 'About',
component: About
}]
// 路由对象
const router = new VueRouter({
routes // ES6语法 相当于 routes: routes
})
export default router
src/main.js
import Vue from 'vue'
import App from '@/App'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap/dist/js/bootstrap'
// 会默认加载 index.js
import router from '@/router'
const vm = new Vue({
router, // 配置路由对象
el: '#app',
render: h => h(App)
})
4. 路由模式
vue-router在实现单页面前端路由时,提供了两种方式:Hash模式和History模式。
4.1 Hash模式
当浏览器的URL发生变化的时候,总是会向服务器端发起请求。但是在SPA 应用中,路由的变化是不会向服务端发送请求的,这是可以通过 URL Hash 来实现的。
一个URL Hash是这样的:
http://localhost:3000/#/about
这种#号后面 hash 值的变化,并不会导致浏览器向服务器发出请求,浏览器不发出请求,也就不会刷新页面。另外每次 hash 值的变化,还会触发hashchange 这个事件,通过这个事件我们就可以知道 hash 值发生了哪些变化。vue-router的Hash模式就是通过这个监听这个hashchange事件来实现更新页面部分内容的操作的。
上面创建的路由对象并没有指定路由模式,默认使用的就是这种模式。
const router = new VueRouter({
mode: 'hash',
routes
})
创建VueRouter的时候还有很多其它选项,详情可以查看参考文档
4.2 History模式
HTML5标准发布以后,多了两个 API,pushState 和 replaceState,通过这两个 API 可以改变 url 地址且不会发送请求。同时还有popstate事件。通过这些就能用另一种方式来实现前端路由了,但原理都是跟 hash 实现相同的。用了HTML5的实现,单页路由的url就不会多出一个#,变得更加美观。但因为没有 # 号,所以当用户刷新页面之类的操作时,浏览器还是会给服务器发送请求。为了避免出现这种情况,所以这个实现需要服务器的支持,需要把所有路由都重定向到根页面。如:
http://localhost:3000/about
const router = new VueRouter({
mode: 'history',
routes
})
5. 动态路由
动态路由是指:需要某种模式的路由,都映射到同一个组件上。其实就是path上带有动态变化的路径参数,比如:
/users/:id
这里的 :id 就是路径参数,是一个变化的值 如:
/users/123
/users/456
这两个路径都能与 /users/:id 匹配上,那么这两个路径都会被映射到同一个组件上。
实例:
点击用户管理,显示用户列表,点击详情显示详情页面
路由定义:
src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Users from '@/views/Users'
import UserDetail from '@/views/UserDetail'
Vue.use(VueRouter)
// 路由表
const routes = [{
path: '/users',
component: Users
}, {
path: '/users/:id',
component: UserDetail
}]
// 路由对象
const router = new VueRouter({
routes
})
export default router
src/views/Users.vue
<template>
<div>
<table class="table table-bordered">
<thead>
<tr>
<th>#</th>
<th>姓名</th>
<th>年龄</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(user,index) in users" :key="user.id">
<th scope="row">{{ index +1 }}</th>
<td>{{ user.name}}</td>
<td>{{ user.age }}</td>
<td><router-link :to="{path:`/users/${user.id}`,query: user}">详情</router-link> </td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
let users=[
{id:1001,name:'张三',age:20},
{id:1002,name:'李四',age:21},
{id:1003,name:'王五',age:22}
]
export default {
data(){
return {
users
}
}
}
</script>
定义了一个用户数组,然后用表格显示这个列表。
<router-link :to="{path:`/users/${user.id}`,query: user}">详情</router-link>
:to 指定路由, path 中带有用户ID参数, query 查询参数。最终被渲染为:
<a href="#/users/1002?id=1002&name=李四&age=21">详情</a>
vue-router 路径中的参数叫做 params(参数), 而 ?后面的叫做 query, 通常称为查询参数。
src/views/UserDetail.vue 显示用户详情
<template>
<div class="panel panel-default">
<div class="panel-heading">用户详情</div>
<div class="panel-body">
<p> id : {{ user.id}} </p>
<p> 姓名 : {{ user.name}} </p>
<p> 年龄 : {{ user.age}} </p>
</div>
</div>
</template>
<script>
export default {
data(){
return {
user:null
}
},
created(){
console.log(this.$route)
// this.$route.params 路径参数
this.user= this.$route.query // 查询参数
}
}
</script>
当路由加载完 UserDetail组件后(created),会为组件注入 r o u t e 对 象 , 这 个 对 象 中 包 含 了 路 由 的 信 息 , route对象,这个对象中包含了路由的信息, route对象,这个对象中包含了路由的信息,route对象 ,对象中包含以下信息: 更多信息可以查看 API文档
- $route.path 当前路由的路径
- $route.params 路由路径中的参数
- $route.query 查询参数即,?后面的数据
在组件被创建后,这个$route对象就被注入到 组件中了。所以在 created 钩子函数中可以访问这个 $route对象了。从这个对象中取出 查询参数,放到 data上。(根据实际需要,也可以直接在模板 中访问: {{ $route.query }})
src/App.vue
<template>
<div style="margin: 20px">
<div >
<router-link to="/users">用户管理</router-link>
</div>
<router-view />
</div>
</template>
6. 嵌套路由
应用程序中有两个模块组件,用户(Users)和 订单(Orders), 这两个组件是 App的子组件,通过路由控制,在指定的位置 (数字1标注) 显示着两个组件。
用户模块中,还有连个子组件 UserList(用户列表) 和 UserAdd(新增用户)需要通过路由来显示,他们将在 (数字2) 的位置切换显示。这两层路由具备了嵌套关系,这就是嵌套路由
src/router/index.js 路由表
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/views/Home.vue'
import User from '@/views/user/User.vue'
import UserAdd from '@/views/user/UserAdd.vue'
import UserList from '@/views/user/UserList.vue'
import Order from '@/views/order/Order.vue'
import OrderAdd from '@/views/order/OrderAdd.vue'
import OrderList from '@/views/order/OrderList.vue'
Vue.use(VueRouter)
// 路由表
const routes = [{
path: '/',
component: Home
}, {
path: '/users',
component: User,
children: [{
// 注意以不能以/开头, 如果以“/” 开头,会被认为是从根路径开始,子路由不用添加父路径,会自动添加父路径的前缀
path: 'list',
component: UserList
}, {
path: 'add',
component: UserAdd
}]
}, {
path: '/orders',
component: Order,
children: [{
path: 'list',
component: OrderList
}, {
path: 'add',
component: OrderAdd
}]
}]
// 路由对象
const router = new VueRouter({
routes
})
export default router
src/views/order/Order.vue
<template>
<div class="panel panel-primary">
<div class="panel-heading">订单管理</div>
<div class="panel-body">
<router-link to="/orders/list" tag="Button" class="btn btn-primary" >订单列表 </router-link>
<router-link to="/orders/add" tag="Button" class="btn btn-success">添加订单 </router-link>
<router-view />
</div>
</div>
</template>
src/views/order/OrderAdd.vue
<template>
<div>
<h2>新建订单</h2>
{{$route.path}}
</div>
</template>
src/views/order/OrderList.vue
<template>
<div>
<h2>订单列表</h2>
{{$route.path}}
</div>
</template>
src/views/user/User.vue
<template>
<div class="panel panel-success">
<div class="panel-heading">用户管理</div>
<div class="panel-body">
<!-- 这里使用的是从根路径开始 -->
<router-link to="/users/list" tag="Button" class="btn btn-primary" >用户列表 </router-link>
<router-link to="/users/add" tag="Button" class="btn btn-success">添加用户 </router-link>
<router-view />
</div>
</div>
</template>
src/views/user/UserAdd.vue
<template>
<div >
<h2>添加用户</h2>
{{$route.path}}
</div>
</template>
src/views/user/UserList.vue
<template>
<div>
<h2>用户列表</h2>
{{$route.path}}
</div>
</template>
src/views/Home.vue
<template>
<div class="panel panel-primary">
<div class="panel-heading">首页</div>
<div class="panel-body">
<h2>这是首页</h2>
</div>
</div>
</template>
src/App.vue
<template>
<div style="margin: 20px">
<ul class="nav nav-tabs">
<li class="{active:$route.path=='/users'}">
<router-link to="/users">用户管理</router-link>
</li>
<li class="{active:$route.path=='/orders'}">
<router-link to="/orders">订单管理</router-link>
</li>
</ul>
<router-view />
</div>
</template>
运行:
7. 路由导航
页面的导航跳转可以使用两种方式
- 声明式
就是前面使用的 生成 a链接标签,点击链接后发生路由导航跳转
- 编程式
vue实例内部,通过 获取 路由实例对象(this.$router) ,然后调用 它的push方法来实现路路由导航跳转。
7.1 <router-link>
其实这种方式内部依然是调用 路由对象的push方法来完成的。
7.2 编程式导航
// 字符串
router.push('home')
// 对象
router.push({ path: 'home' })
// 命名的路由
router.push({ name: 'user', params: { userId: '123' }})
// 带查询参数,变成 /register?plan=private
router.push({ path: 'register', query: { plan: 'private' }})
const userId = '123'
router.push({ name: 'user', params: { userId }}) // -> /user/123
router.push({ path: `/user/${userId}` }) // -> /user/123
// 这里的 params 不生效,因为params用在动态路由的path上面,这个path不是动态路由
router.push({ path: '/user', params: { userId }}) // -> /user
replace 方法:push
很像,唯一的不同就是,它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录。
声明式 | 编程式 |
---|---|
<router-link :to="…" replace> | router.replace(…) |
8 命名路由
在路由表中,通过name 为路由进行命名,如:
routes: [
{
path: '/user/:userId',
name: 'user',
component: User
}
]
要链接到一个命名路由,可以给 router-link
的 to
属性传一个对象:
<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>
9. 命名视图
一个路由可以对应多个视图 router-view, 可以为每个视图设置一个名称,没有设置则为 default 视图。可以为每个 视图指定一个 组件
- 1 视图名称为header
- 2 视图名称没有指定,默认为 default
- 3 视图名称为 footer
路由表 src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/views/Home.vue'
import Login from '@/views/Login.vue'
import Footer from '@/layout/Footer.vue'
import Header from '@/layout/Header.vue'
Vue.use(VueRouter)
// 路由表
const routes = [{
path: '/',
components: { //注意这里是 components
default: Home,
footer: Footer,
header: Header
}
},
{
path: '/login',
component: Login //渲染到默认的 route-view上
}
]
// 路由对象
const router = new VueRouter({
routes
})
export default router
src/views/Home.vue
<template>
<div class="panel panel-primary" style="width:800px">
<div class="panel-heading">首页</div>
<div class="panel-body">
<h2>这是首页</h2>
</div>
</div>
</template>
src/layout/Header.vue
<template>
<div style="width:800px;height:100px;color:#FFFFFF;background:#CCCCCC">
这是Header <button class="btn btn-info" @click="toUser"> 显示用户页面 </button>
</div>
</template>
<script>
export default {
methods:{
toUser(){
this.$router.push('/login') //调用push方法实现路由切换
}
}
}
</script>
src/layout/Footer.vue
<template>
<div style="width:800px;height:100px;color:#FFFFFF;background:#CCCCCC">
这是Footer
</div>
</template>
src/views/Login.vue
<template>
<div class="panel panel-success" style="width:800px">
<div class="panel-heading">用戶登录</div>
<div class="panel-body">
<h2>用户登录</h2>
</div>
</div>
</template>
可以看到 当切换到 /login之后,路由表中 /login 对应的组件为 Login, 那么这个组件将会被渲染到默认的视图中,而名称为 footer和 header的 视图什么也不会显示。如果将 /login路由配置成:
{
path: '/login',
components: {
default: Login, //渲染到默认的 route-view上
footer: Footer // 显示footer组件
}
}
此时显示的结果为:
10 重定向和别名
来自于 参考文档
10.1 重定向
当访问 /a的时候,重定向到 /b ,地址栏路径由 /a 变成 /b
{ path: '/a', redirect: '/b' } // 重定向到 '/b'
{ path: '/a', redirect: { name: 'foo' }} // 重定向到命名路由
{ path: '/a', redirect: to => {
// 方法接收 目标路由 作为参数
// return 重定向的 字符串路径/路径对象
}
重定向会导致 地址栏中的地址发生变化
10.2 别名
可以为一个路由配置多个别名,别名与 path等效
{ path: '/a', component: A, alias: '/b' }
访问 /a 和 访问 /b 都会渲染组件A ,地址栏不会发生变化。
11 .路由组件传参
当一个路由被匹配后,加载组件并渲染到视图上。此时要想获取路由的参数,则需要通过自动注入的 “$router” 对象来获取,这样 $router就与组件耦合了。
可以将 路由参数 “映射” 到组件的 props 属性中。
例如:
User组件:
User.vue
export default {
data(){
return {}
},
props: ['id','name','query']
}
路由表:
//布尔模式 如果 props 被设置为 true,route.params 将会被设置为组件属性
{ path: '/user/:id', component: User, props: true }
//对象模式 如果 props 是一个对象,它会被按原样设置为组件属性。当 props 是静态的时候有用。
{ path: '/promotion', component: Promotion, props: { id:123,name: 'zhangsan' } }
// 函数模式 你可以创建一个函数返回 props。这样你便可以将参数转换成另一种类型,将静态值与基于路由的值结合等等。URL /search?q=vue 会将 {query: 'vue'} 作为属性传递给 SearchUser 组件。
{ path: '/search', component: SearchUser, props: (route) => ({ query: route.query.q }) }