Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。包含的功能有:
- 嵌套的路由/视图表
- 模块化的、基于组件的路由配置
- 路由参数、查询、通配符
- 基于 Vue.js 过渡系统的视图过渡效果
- 细粒度的导航控制
- 带有自动激活的 CSS class 的链接
- HTML5 历史模式或 hash 模式,在 IE9 中自动降级
- 自定义的滚动条行为
创建目录
根据vue-cli脚手架自动生成一个简单的带有router文件夹的项目目录
1 | Vue CLI v4.5.9 |
Vue Router使用
- 1.注册路由插件
- 2.创建一个router对象,创建过程中配置一些路由规则
- 3.注册router对象,也就是在创建vue实例的时候,要在选项里面,来配置我们创建好的router对象
- 4.创建路由组件占位和跳转链接
注册路由插件
在 router/index.js 文件中1
2
3
4
5
6
7import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
// 1.注册路由插件
// Vue.use() 是用来注册插件,会调用传入对象的install方法
Vue.use(VueRouter)创建router对象配置路由规则
在 router/index.js 文件中1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 路由规则
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]
// 2.创建router路由对象
const router = new VueRouter({
routes
})
export default router注册router对象
在main.js文件中1
2
3
4
5
6
7
8
9
10
11import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
new Vue({
// 3. 注册router 对象
router,
render: h => h(App)
}).$mount('#app')创建路由组件占位和跳转链接
1
2
3
4
5
6
7
8
9<div id="app">
<div id="nav">
<!-- 创建路由连接 -->
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<!-- 创建路由占位 -->
<router-view/>
</div>
$route和$router区别
在main.js中
1 | import Vue from 'vue' |
先将router
注释,查看打印结果发现没有$route
和$router
放开注释后,会发现多了$route
和$router
$route: 表示路由规则,存储的是当前路由的一些路由数据,存储了一些路径
$router: 表示路由对象,就是vuerouter的一个实例,这个路由对象中会提供一些跟路由相关的一些方法
需要注意的是$router
里面有一个currentRoute
,这个当前路由规则,有的时候不方便获取$route
,比如在一个插件里面没有办法获取到$route
,可以想办法获取到$router
,然后获取cunrrentRoute
,这样也就获取到了当前路由的规则
动态路由传参
动态传参连接跳转
- query会在地址栏出现
id='XX'
eg:http://localhost:8080/#/?id=1
- params会在地址栏
/xx
,eg:http://localhost:8080/#/detail/1
,props方式需要用这个方式1
2
3
4
5
6
7
8
9
10
11
12<div id="app">
<div id="nav">
<!-- 创建路由连接 -->
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link> |
<!-- <router-link to="/detail">Detail</router-link> -->
<!-- <router-link :to="{name:'Detail',query:{id:1}}">Detail</router-link> -->
<router-link :to="{name:'Detail',params:{id:1}}">Detail</router-link>
</div>
<!-- 创建占位 -->
<router-view/>
</div>路由配置
在router/index.js文件中1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// 路由规则
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
},
{
path: '/detail/:id',
name: 'Detail',
// 开启props会把url中的参数传递给组件
// 当为true时,会将url中的参数传递给相应的组件,在组件中通过props来接收就可以了
props: true,
component: () => import(/* webpackChunkName: "about" */ '../views/Detail.vue')
}
]接收方式
两种方式接收(方式1需要强依赖,更推荐方式2):嵌套路由
在router/index.js文件中1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// 路由规则
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
children: [
{
path: 'detail/:id',
// 开启props会把url中的参数传递给组件
// 当为true时,会将url中的参数传递给相应的组件,在组件中通过props来接收就可以了
props: true,
component: () => import(/* webpackChunkName: "about" */ '../views/Detail.vue')
}
]
}
]编程式导航
1
2
3
4
5
6
7
8
9
10
11
12
13this.$router.push('/')
this.$router.push({name:'Home'})
this.$router.push({name:'Home',params:{id:1}})
this.$router.push({path:'/home',query:{id:1}})
// 不会记录本次历史
this.$router.replace('/home')
// 跳转到历史的某一次,如果是负数就是后退
this.$router.go(-2)Hash模式 和 History模式区别
无论那种模式,都是客户端路由的实现方式,也就是当路径发生变化之后不会向服务器发送请求,使用JS监视路径的变化,然后根据不同的地址渲染不同的内容,如果需要服务器端内容的话会发送ajax请求去获取。表现形式的区别
- hash模式:
1
https://music.163.com/#/playlist?id=121212
- history模式:
1
https://music.163.com/playlist/31032232
原理区别
Hash模式
hash模式是基于锚点,以及onhashchange事件,通过锚点的值作为路由地址,地址发生变化出发onhashchange事件,在根据路径决定页面上呈现的内容History模式
history是基于HTML5中的History API - history.pushState()
- history.replaceState()
history.pushState和 history.push区别
- history.pushState() 不会向服务器发送请求,只会改变浏览器地址栏中的地址,并且把地址记录到历史记录中,所以可以实现客户端路由 另外,IE10 以后才支持
- history.push() 路径发生变化,需要向服务器发送请求
History模式使用
- history需要服务器的支持
- 单页面应用中,只有一个index.html这样的页面,服务端不存在http://www.test.com/login 这样的地址,服务端不存在/login这样的页面
- 在服务端应该除了静态资源外都返回单页应用的index.html因为vue-cli自带的服务器已经配置好了上面说的内容,下面将代码打包部署的到node服务器中进行一个测试
1
2
3
4
5// 2.创建router对象
const router = new VueRouter({
mode: 'history', //默认是hash
routes
}) - Node服务器配置 history模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19const path = require('path')
// 导入history模式的模块
const history = require('connect-history-api-fallback')
// 导入express
const express = require('express')
const app = express()
// 注册处理history模式的中间件
// app.use(history())
// 处理静态资源的中间件 网站根目录 ../web
app.use(express.static(path.join(__dirname,'../web')))
// 开启服务
app.listen(3000, ()=>{
console.log('服务器开启,端口:3000')
})
这是先关闭处理history(注释app.use(history())),当前求地址后,刷新页面就会出错了
如下放开注释,打开history支持,再去刷新页面,就可以了
1 | // 注册处理history模式的中间件 |
加载过程是:
服务器会判断当前请求的页面,服务器上没有会把我们单页应用默认的index.html返回给浏览器,浏览器接收到这个index.html,会再去判断路由地址,然后根据路由去加载对应配置的组件,这就是加载的过程。
- Nginx配置 history模式 mac下载nginx教程地址参考:https://www.cnblogs.com/meng1314-shuai/p/8335140.html
将打包好的前端项目,复制到html中
启动nginx点击访问可以,但是一旦刷新,就会出现如下问题
这个时候就需要去nginx.config里面配置一下
1 | try_files $uri $uri/ /index.html; #新加一个 |
try_files 试着去请求当前浏览器请求的路径所对应的文件,没找到就去找这个目录下的默认首页,如果没有找到就去单文件应用下的首页(/index.html) 重启nginx,现在访问的时候再刷新页面就不会出现上面的404了
实现原理
hash模式
- URL中#后面的内容作为路径地址
- 监听hashchange事件
- 根据当前路由地址找到对应的组件重新渲染
history模式
- 通过history.pushState()方法改变地址栏
- 监听popstate事件
- 根据当前路由地址找到对应组件重新渲染
实现一个自己的vue-router
原文件router/index.js
中生成的默认vue-router如下:
1 | // 注册插件 |
创建router-link
1 | let _Vue = null |
引入自己写的vueRouter
这个时候页面直接去引入这个方法,
1 | import Vue from 'vue' |
然后发现页面会有报错
因为Vue的构建版本分为运行时版
和 完整版
- 运行时版:不支持template模版,需要打包的时候提前编译
- 完整版:包含运行时和编译器,体积比运行时版本大10K左右,程序运行的时候把模版转换成render函数
我们的vue-vli创建的项目默认的是运行时版,所有就出现上面的错误了,解决如下:
方法一:通过完整版来解决问题,https://cli.vuejs.org/zh/config/#全局-cli-配置
在根目录下创建一个vue.config.js文件1
2
3module.exports = {
runtimeCompiler: true
}这样配置后,页面中router-link就能显示了,还有一个报错,是因为自己手写时候还没有写router-view 的组件
方法二:通过运行时版来解决问题,运行时版本的vue不带编译器,也就是不支持组件中的template选项,编译器的作用就是将template编译成render函数,所有直接写render函数来解决
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16initComponents (Vue) { // 参数是vue构造函数
// 创建router-link 组件
Vue.component('router-link', {
props: {
to: String
},
// template: `<a :href = "to"><slot></slot></a>`
render (h){
return h('a',{ // h函数 可以接收三个参数,1创建元素的选择器 2.设置属性 3.生成元素的子元素所以数组的形式
attrs: {
href: this.to // 传的props中的 to
}
}, [this.$slots.default]) //this.$slots.default获取默认插槽的内容
}
})
}创建router-view 这个组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25initComponents (Vue) { // 参数是vue构造函数
// 创建router-link 组件
Vue.component('router-link', {
props: {
to: String
},
// template: `<a :href = "to"><slot></slot></a>`
render (h){
return h('a',{ // h函数 可以接收三个参数,1创建元素的选择器 2.设置属性 3.生成元素的子元素所以数组的形式
attrs: {
href: this.to // 传的props中的 to
}
}, [this.$slots.default]) //this.$slots.default获取默认插槽的内容
}
})
const self = this
// 创建router-view 组件
Vue.component('router-view',{
render(h){ // h作用是创建虚拟DOM
// 找到当前路由的地址 根据当前路由地址,去routeMap对象中,找到对应的组件,然后再调用h函数,把找到的组件转换成虚拟DOM直接返回
const component = self.routeMap[self.data.current] //获取组件
return h(component) // 调用h函数,返回虚拟DOM ,h函数还可以直接把组件转换成虚拟DOM
}
})
}这个时候去页面查看,没有报错了,可以显示了,但是当点击切换的时候,页面会刷新,需要给这个超链接注册一个点击时间,取消后续执行,还需要将地址栏中的值,改为超链接href中的值,就需要history.pushState()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29// 创建router-link 组件
Vue.component('router-link', {
props: {
to: String
},
// template: `<a :href = "to"><slot></slot></a>`
render (h){
return h('a',{ // h函数 可以接收三个参数,1创建元素的选择器 2.设置属性 3.生成元素的子元素所以数组的形式
attrs: {
href: this.to // 传的props中的 to
},
// 给a标签添加事件
on: {
click: this.clickHandler
}
}, [this.$slots.default]) //this.$slots.default获取默认插槽的内容
},
methods:{
clickHandler(e){
history.pushState({},'', this.to) //三个参数:1.data 将来触发pushState这个事件对象的参数 2.title网页标题 3.url地址
// 将当前的路径,记录到data.current里面
this.$router.data.current = this.to
// 阻止跳转默认行为
e.preventDefault()
}
}
})现在就可以点击了,不会刷新了,地址栏也会跟着点击标签的href所改变了,虽然现在可以,但是当我们点击浏览器前进,后退的时候,地址栏发生了变化,但是组件没有发生变化,因为前进后退的时候,没有重新加载地址栏对应的组件,下面通过popstate进行改进,当历史发生变化触发,到这里模拟router的history模式就搞定了
1
2
3
4
5
6// 解决前进后退 地址栏变化组件不变化的问题
initEvent(){
window.addEventListener('popstate', ()=> {
this.data.current = window.location.pathname //路径部分
})
}完整代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98let _Vue = null
export default class VueRouter {
// 导出一个静态方法 install
// Vue.use() 中调用install方法时候,会传递两个参数,一个是vue构造函数 另一个是 可选的选项对象
static install (Vue) { //这里传递构造函数
// 1. 判断当前插件是否已经被安装
if(VueRouter.install.installed){
return
}
VueRouter.install.installed = true
// 2. 把vue构造函数记录到全局变量
_Vue = Vue
// 3. 把创建vue实例时候传入的router 对象注入到vue实例上
// 混入
_Vue.mixin({
beforeCreate(){
// 只有vue $options里面才会有router 组件没有
if(this.$options.router){
_Vue.prototype.$router = this.$options.router
// 先找到route对象,然后再调用init方法
this.$options.router.init()
}
}
})
}
// 创建一个构造函数
constructor(options){
this.options = options //作用就是记录构造函数传入的options路由规则
this.routeMap = {} // routeMap里面是一个键值对形式,健存的就是路由地址,值就是路由组件 将来在router-view这个组件里面会根据当前的路由地址到routeMap里面找到对应的组件然后渲染到浏览器中
this.data = _Vue.observable({ // 响应式对象
current: '/' // current用来记录当前路由地址,默认情况下是 / 斜杠
})
}
// 创建一个初始化方法 方便调用
init () {
this.createRouteMap()
this.initComponents(_Vue) // 传入构造函数
this.initEvent()
}
// 这个方法的作用是去遍历所有的路由规则,把这些路由规则解析成键值对的形式存储到routeMap中
createRouteMap(){
// this.options.routes // 所有的路由规则
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
})
}
initComponents (Vue) { // 参数是vue构造函数
// 创建router-link 组件
Vue.component('router-link', {
props: {
to: String
},
// template: `<a :href = "to"><slot></slot></a>`
render (h){
return h('a',{ // h函数 可以接收三个参数,1创建元素的选择器 2.设置属性 3.生成元素的子元素所以数组的形式
attrs: {
href: this.to // 传的props中的 to
},
// 给a标签添加事件
on: {
click: this.clickHandler
}
}, [this.$slots.default]) //this.$slots.default获取默认插槽的内容
},
methods:{
clickHandler(e){
history.pushState({},'', this.to) //三个参数:1.data 将来触发pushState这个事件对象的参数 2.title网页标题 3.url地址
// 将当前的路径,记录到data.current里面
this.$router.data.current = this.to
// 阻止跳转默认行为
e.preventDefault()
}
}
})
const self = this
// 创建router-view 组件
Vue.component('router-view',{
render(h){ // h作用是创建虚拟DOM
// 找到当前路由的地址 根据当前路由地址,去routeMap对象中,找到对应的组件,然后再调用h函数,把找到的组件转换成虚拟DOM直接返回
const component = self.routeMap[self.data.current] //获取组件
return h(component) // 调用h函数,返回虚拟DOM ,h函数还可以直接把组件转换成虚拟DOM
}
})
}
// 解决前进后退 地址栏变化组件不变化的问题
initEvent(){
window.addEventListener('popstate', ()=> {
this.data.current = window.location.pathname // 路径部分就是/斜杠和斜杠后面的部分 eg: /home
})
}
}