本文所用项目 GitHub
地址:https://github.com/trp1119/vue-router
本文与 Vue Router 官方文档 区别:非官方文档功能罗列,而是从实际应用角度一步步进行 Vue Router
设置及功能介绍。水平有限,难免存有纰漏,欢迎在评论区指正交流。
前言
现在做前端 Web App
,路由是必不可缺的功能。以前网站开发,由链接跳转到后端,进行模板渲染,产生一个新的 HTML
返回给浏览器端,等浏览器显示出页面,完成一次路由跳转。而对于现在的单页面应用开发,页面跳转不经过后端服务器,页面渲染内容全部来自于JavaScript
。既然路由跳转由前端来做,则需要称职合理的工具处理前端路由。
现在基本上每个前端框架都会配备路由管理工具,像 Vue
使用的 Vue Router
,React
使用的 React Router
,还有不和框架耦合的 History
等工具。这些工具,在前端 SAP
开发中起着越来越重要的作用。
什么是 Vue Router
? Vue Router
是 Vue.js
官方的路由管理器。通过将组件映射到路由 routes
,然后使用 Vue Router
进行渲染,实现路由功能。
本文 Vue Router
基于模块化工程使用。在使用之前,需要先安装 vue-router
插件。
npm i vue-router -S
1 Vue Router 之集成
vue-router
插件安装完毕后,需要进行路由的部署。在 src
文件夹下 创建 router
文件夹并创建 router.js
和 routes.js
文件。
为什么分开配置?因为在项目变庞大之后,路由配置会非常多,所以可以单独配置一个和路由映射关系有关的文件 routes.js
,而 router.js
主要进行路由器 router
的内容配置。
1.1 在 routes.js 中配置路由映射关系
export default
一个数组,数组中的每个对象都是一个路由项。
import Login from '../view/login' // 1.引入组件
import Home from '../view/home'
export default [ // 4.导出路由映射关系
{
path: '/login', // 2.设置跳转路由
component: Login // 3.映射组件
},
{
path: '/home',
component: Home
}
]
不推荐这么使用,因为这样每次在全局 import
这个 router
的时候,引入的都是同一个 router
,如果需要每次引入的时候新创建一个 router
,这种方式是做不到的。
另外,由于项目需要服务端渲染。每次 export default
同一个 router
,会导致在服务端渲染时出现内存溢出的问题。每次服务端渲染都会重新生成一个新的 app
,由于 router
只有一个对象(共用同一个),在服务端渲染流程结束后,app
对象没有释放,每次都会缓存新的 app
,导致内存没有下降,一直处于高点,随着内存逐渐累加,出现内存溢出。
1.2 默认路由跳转
在路由配置文件 routes.js
中配置默认路由跳转。
export default [
{
path: '/', // 根路径
redirect: '/login' // 默认跳转路由
},
{
path: '/login',
component: Login
},
{
path: '/home',
component: Home
}
]
1.3 在 route.js 中配置路由器
1.3.1 配置方法1
import Router from 'vue-router' // 1.引入 Vue Router 插件中 Router 路由器
import routes from './routes' // 2.引入路由映射关系
export default new Router({ // 4.创建并导出 Router 对象实例
routes // 3.传入映射关系配置
})
1.3.2 配置方法2
import Router from 'vue-router' // 1.引入 Vue Router 插件中 Router 路由器
import routes from './routes' // 2.引入路由映射关系
export default () => { // 4.创建并导出 Router 方法(注意这里导出的是方法,并没有在此创建对象实例)
return new Router({
routes // 3.传入映射关系配置
})
}
每次生成新的 Router
对象实例,渲染结束同 app
一起释放,不会出现内存溢出问题。
1.4 在入口文件 main.js 中注入路由功能
引入 VueRouter 插件并使用(Vue.use()
)以使用路由功能,引入前面配置好的 Router
方法并使用以使用路由设置及映射关系。
import Vue from 'vue'
import VueRouter from 'vue-router' // 引入 VueRouter 路由插件
import App from './App.vue'
import createRouter from './router/router' // 引入带映射关系的 Router 方法
Vue.config.productionTip = false
Vue.use(VueRouter) // 使用 VueRouter 插件
const router = createRouter() // 在此创建 router 对象
new Vue({
router, // 注入路由。通过在根节点 Vue 实例上挂载 router 对象,使每个组件都能拿到这个 router 对象,从而让整个应用都有路由功能。(VueRouter内部实现此功能)
render: h => h(App),
}).$mount('#app')
1.5 使用 router-view 渲染匹配到的组件
在 App.js
中渲染页面。
1.6 使用 router-link 进行页面跳转
router-link 的实现是 a 标签。点击对应链接后会在
页面中可点击的路由一般都会通过 router-link,因为 router-link 的实现是 a 标签,a 标签中的 href 有利于网站的 SEO,但 a 标签默认的行为是页面跳转,而不是前端路由跳转。即 router-link 不单纯是 a 标签的实现,而是通过内部事件,实现前端路由跳转。
去Login页
去Home页
1.7 Vue Router 基础集成设置完毕
设置完毕,在页面打开路由。可以发现 Vue Router
自动在网站域名后添加了 #/
,这是因为 Vue Router
默认的路由形式是使用哈希路由。作为 SAP
且有服务端渲染的情况下,路由改为 histiory
形式(无 #/
)更为合理且更利于 SEO
优化。
2 Vue Router 之配置
创建 router
实例时常用参数配置及作用。
2.1 mode配置
Vue Router
默认使用哈希路由(路由中带 #
),但哈希路由一般用来定位,而不是做路由状态的记录。同时,哈希路由不会被搜索引擎解析,影响网站 SEO
。所以在服务端渲染时,一般不使用哈西路由。
使用 history
路由,可在路由器配置文件 route.js
中配置 mode
。mode
有两种参数,一个是默认的 hash
,一个是 history
。
export default () => {
return new Router({
routes, // 路由配置项
// mode: 'hash', // 默认哈西路由
mode:'history', // 配置 hisyory 路由
})
}
2.2 base 配置
在 routes.js
配置的所有路由前天添加 /自定义base/
,这个路径作为所有应用的基路径。在应用中不论是用 route-link
还是 route
对象跳转,只要是通过 vue router
的 api
跳转,都会加上此基路径。但是把路径中 /base/
手动去掉,仍然会显示,所有 base
不是强制性的。在区分页面路径和其他类型路径的时候会用到。
在 mode
配置为 hash
时,base
配置不生效。
export default () => {
return new Router({
routes,
mode:'history',
base: '/mybase/', // 注意加前后/,base 在 mode 配置为 hash 时不生效
})
}
2.3 linkActiveClass 与 linkExactActiveClass 配置
查看 1.5 中配置的 route-link
浏览器解析源码,会发现激活的路由默认添加 router-link-exact-active
和 router-link-active
样式,可以在 linkActiveClass
和 linkExactActiveClass
中自定义样式名称,然后在 css
中自定义全局样式。
export default () => {
return new Router({
routes,
mode:'history',
base: '/mybase/',
linkActiveClass: 'active-link', // 配置 route-link 链接 class,路径不完全匹配时添加 class 为 linkActiveClass
linkExactActiveClass: 'exact-active-link', // 路径完全匹配时添加 class 为 linkExactActiveClass linkActiveClass
})
}
配置完毕,可以看到 a
标签 class
样式已经变为配置样式。
那么 linkActiveClass
与 linkExactActiveClass
有什么区别?
展示区别前,先在 routes.js
和 app.vue
中补充路由。
// routes.js
export default [
{
path: '/',
redirect: '/login'
},
{
path: '/login',
component: Login
},
{
path: '/home',
component: Home
},
{
path: '/home/exact', // 补充路由
component: Home
}
]
去Login页
去Home页
去Home/exact页
补充完毕,点击“去Home/exact页”后,查看浏览器源码,会发现 “去Home页”路由少了 my-exact-active-link
样式,但仍保留 my-active-link
样式。即路径不完全匹配时添加 class 为 my-active-link
( linkActiveClass
中配置的样式),路径完全匹配时添加 class 为 my-exact-active-link
my-active-link
(linkExactActiveClass
和 linkActiveClass
中配置的样式)。
2.4 scrollBehavior 配置
scrollBehavior
用于配置页面跳转时是否滚动,scrollBehavior
接收一个方法,有三个参数 to
、from
、savePosition
,其中,参数 to
为去的路由,from
跳转到 to
路由之前的路由。如果之前进入过这个路由,savePosition
用于保存之前滚动条滚动的位置。to
、from
不是字符串,而是完整的 router
的对象,包含路由的 params
、query
等信息。
export default () => {
return new Router({
routes,
mode:'history',
base: '/mybase/',
linkActiveClass: 'active-link',
linkExactActiveClass: 'exact-active-link',
scrollBehavior (to, from, savePosition) { // 页面跳转时是否需要滚动
// to 去往路由 from 来时路由 savePosition 记录滚动条位置
if (savePosition) {
return savePosition // 如果进入过,回到之前的位置
} else {
return { x: 0, y: 0 } // 如果未因如果,滚动到原点
}
},
})
}
2.5 parseQuery 与 stringifyQuery
例如 http://localhost/login?name=zhangsan&password=12345
,url
后带的参数 query
即 name=zhangsan&password=12345
是 string
格式,需转为 JSON Object
,Vue 是默认做转换的,但如果有默认需求,可通过配置 parseQuery
进行转换。将 Object
转为 stiring
可在 stringifyQuery
中配置。
export default () => {
return new Router({
routes,
mode:'history',
base: '/mybase/',
linkActiveClass: 'active-link',
linkExactActiveClass: 'exact-active-link',
scrollBehavior (to, from, savePosition) {
if (savePosition) {
return savePosition
} else {
return { x: 0, y: 0 }
}
},
parseQuery (query) {},
stringifyQuery (obj) {},
})
}
2.6 fallback配置
并不是所有浏览器都支持 history
前端路由(页面不跳转但内容切换),在不支持 history
前端路由的浏览器中,Vue 自动 fallback
hash
路由模式,如果不希望 Vue 进行此操作,可将 fallback
设置为 false
。当 fallback
为 false
时,Vue 不会去自动处理,在不支持 histiory
路由的浏览器中,单页面应用就变为多页面应用,每次页面跳转都会去后端请求数据返回内容,耗时。
export default () => {
return new Router({
routes,
mode:'history',
base: '/mybase/',
linkActiveClass: 'active-link',
linkExactActiveClass: 'exact-active-link',
scrollBehavior (to, from, savePosition) {
if (savePosition) {
return savePosition
} else {
return { x: 0, y: 0 }
}
},
parseQuery (query) {},
stringifyQuery (obj) {},
fallback: true,
})
}
3 route 配置及参数传递
3.1 name 配置
使用 name
对路由进行命名,和 path
、component
的命名无关联,可以使用这个 name
进行路由跳转。
// routes.js
export default [
//...
{
path: '/login',
component: Login,
name: Login, // name 命名
},
//...
]
在路由跳转配置中,name
传入的是 JSON Object
,希望 Vue
去解析它而不是当做 stirng
处理,所以需要使用 v-bind
进行数据绑定
去Login页
去Login页(name 形式)
去Home页
去Home/exact页
3.2 meta 配置
meta
用于保存路由信息,
在 html
head
标签中会使用 meta
保存页面元信息,这些信息有利于 SEO
优化。在写 Vue
组件的时候,很难写入这些信息,则可在路由配置中加入 meta
,这些信息,可以在拿到路由 route
对象时通过 .mate
拿到这些信息并进行设置。
// routes.js
export default [
//...
{
path: '/login',
component: Login,
name: Login,
meta: { // meta 配置
title: 'This is Home Page',
description: 'This is Home Page description'
},
},
//...
]
3.3 children 嵌套路由配置
children
也是个数组,是子路由,配置和父级路由配置相同。
// routes.js
export default [
//...
{
path: '/login',
component: Login,
name: Login,
meta: { // meta 配置
title: 'This is Login Page',
description: 'This is Login Page description'
},
children: [ // 子路由(嵌套路由),路由匹配的内容都是通过 展示的,/login 下的 children子路由,则其 在父级路由组件 Login 中显示。
{
path: 'loginChild',
component: () => import('../view/loginChild')
}
],
},
//...
]
在 login.vue
中加入
Login页内容
3.4 transition 过渡动画
使用 transition
包裹
,并进行动画样式设置。如果包裹根页面(App.vue
)中的
,会为每个路由切换都添加过渡动画。想要某个页面使用切换效果,使用 transition
包裹单个组件的
即可。
去Login页
去Login页(name 形式)
去Home页
去Home/exact页
3.5 :id 动态传参
path: '/login/:id'
,动态绑定 id
,当页面路径为 /login/123
时,则 id
为 123
,在页面中通过 this.$route.params
可以拿到键值对。
// routes.js
export default [
//...
{
path: '/login/:id',
component: Login,
name: Login,
meta: {
title: 'This is Login Page',
description: 'This is Login Page description'
},
children: [
{
path: 'loginChild',
component: () => import('../view/loginChild')
}
],
},
//...
]
Login123
去Login页
去Login页(name 形式)
去Home页
去Home/exact页
页面路径 http://localhost:8080/mybase/login/123?name=zhangsan&password=123456
this.$route
对象打印结果:
可以看出,路由 path
中不会出现 router.js
中设置的 base
,参数以 json
对象的形式存放在 query
中。
3.6 props
定义 props
为 true
后,可以将 :id
作为 props
传入组件 Login
中,即在组件中,用 props
直接接收即可, props: ['id']
,代替父组件传值。这种方式可以实现在组件内不需要写 this.$route
而用 props
直接接收。
如果组件中写了 this.$route
这种写法,路由与组件耦合在一起,组件就不能单独拿去复用(因为组件绑定了路由),可能会存在路由不匹配。如果用 props
声明,在其他地方也可以使用这个组件,因为 依赖的 id
值可以改为通过父组件传入,不再依赖路由读取,实现解耦。
// routes.js
export default [
//...
{
path: '/login/:id',
props: true,
component: Login,
name: Login,
meta: {
title: 'This is Login Page',
description: 'This is Login Page description'
},
children: [
{
path: 'loginChild',
component: () => import('../view/loginChild')
}
],
},
//...
]
Login页内容
路由中 props
配置除了从 path
中取参数,也可以自己定义内容,这时,页面 props
中接收的内容即为 路由 props
中设置的值。
// routes.js
export default [
//...
{
path: '/login/:id',
props: {
id: '456',
sex: 'man'
},
component: Login,
name: Login,
meta: {
title: 'This is Login Page',
description: 'This is Login Page description'
},
children: [
{
path: 'loginChild',
component: () => import('../view/loginChild')
}
],
},
//...
]
Login页内容
路由 props
也可以从路由中取值,例如取路由 query
中的值,这样,页面就无需采用 this.$route.query.xxx
方式去取值。
// routes.js
export default [
//...
{
path: '/login/:id',
props: (route) => ({name:route,query.name}), // 这里的 route 相当于组件中的 this.$route
component: Login,
name: Login,
meta: {
title: 'This is Login Page',
description: 'This is Login Page description'
},
children: [
{
path: 'loginChild',
component: () => import('../view/loginChild')
}
],
},
//...
]
推荐不要拘泥于 this.$route
写法,尽量实现组件与路由解耦,以提高组件复用性。
4 高级功能
4.1 命名视图
前面写的一个页面中只有一个路由出口
,但如果同一个页面中有两部分,在不同的路由下显示不同的内容,这时可在同一个组件内部使用两个
,并对其进行命名,对于不同的
,在不同的名字下赋予不同的组件。
在路由项配置文件 routes.js
中,使用 components
分配不同的组件。
// routes.js
export default [
//...
{
path: '/login/:id',
props: {
id: '456',
sex: 'man'
},
components: {
default: Login, // 未命名,默认使用 default
myRouteView: Home // 不可采用 my-route-view 形式
},
name: Login,
meta: {
title: 'This is Login Page',
description: 'This is Login Page description'
},
children: [
{
path: 'loginChild',
component: () => import('../view/loginChild')
}
],
},
//...
]
Login123
去Login页
去Login页(name 形式)
去Home页
去Home/exact页
4.2 导航守卫(导航钩子)
4.2.1 全局导航守卫
在 main.js
中进行全局导航守卫注册。可以用 beforeEach
验证用户是否登录,如果未登录,自动跳转至登录页面。next()
中参数配置同 route-link
中参数配置。
必须执行 next()
,不然不会进入钩子。
// main.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import App from './App.vue'
import createRouter from './router/router'
Vue.config.productionTip = false
Vue.use(VueRouter)
const router = createRouter()
// 全局导航守卫
router.beforeEach((to, from, next) => {
console.log('main beforeEach invoked')
if (to.fullPath !== '/login') { // 路由匹配到才回去执行 next,如果不匹配,只打印 beforeEach invoked,而不会继续打印beforeResolve invoked 和 afterEach invoked
next('/login')
}else {
next () // 执行 next 后路由才会被跳转
}
})
router.beforeResolve((to, from, next) => {
console.log('main beforeResolve invoked')
next()
})
router.afterEach((to, from) => { // 每次导航跳转结束后被触发,因为已经跳转,所以不再需要 next
console.log('main afterEach invoked')
})
new Vue({
router,
render: h => h(App),
}).$mount('#app')
页面触发顺序(每次路由变化都会被触发):
4.2.2 路由导航守卫
// routes.js
export default [
//...
{
path: '/login/:id',
props: {
id: '456',
sex: 'man'
},
components: {
default: Login,
myRouteView: Home
},
name: Login,
meta: {
title: 'This is Login Page',
description: 'This is Login Page description'
},
children: [
{
path: 'loginChild',
component: () => import('../view/loginChild')
}
],
// 在路由配置时增加钩子
// 进入这个路由前调用这个钩子,调用顺序在 beforeEach 和 beforeResolve 之间
beforeEnter (to, from, next) {
console.log('route beforeEnter')
next()
}
},
//...
]
页面触发顺序:
4.2.3 组件导航守卫
beforeRouteEnter
时,组件尚未被创建(未实例化),故还无法取到 this
,无法调用 this
上任何内容,也无法给 this
赋值。这时可使用组件对象 vm
(即组件创建后 this
的对象)。
beforeRouteLeave
可用于让用户确认是否离开次路由。
Login页内容
进入页面时触发顺序(beforeRouteEnter
):
更新页面时触发顺序(beforeRouteUpdate
,例如 login/123
切换至 login/456
,Login
组件不会被销毁后重建,而是内容更新):
切换页面时触发顺序(beforeRouteLeave
):
提示:例如 login/123
切换至 login/456
是,由于 Login
组件复用,不会再次去调用 mounted
,此时可使用 beforeRouteUpdate
。
4.3 异步组件
路由在非常多的情况下,如果通过 webpack
一次性打包所有代码,会导致 js
文件庞大,初次加载耗时,且访问某一页面时也加载了其他页面的 js
代码,造成资源浪费。如果对于某一路由,只加载对应页面代码及核心代码,其他页面代码待访问时再加载,可使用异步组件加载,提高首屏加载速度。
// routes.js
export default [
//...
{
path: '/login/:id',
props: {
id: '456',
sex: 'man'
},
components: {
default: Login,
myRouteView: Home
},
name: Login,
meta: {
title: 'This is Login Page',
description: 'This is Login Page description'
},
children: [
{
path: 'loginChild',
component: () => import('../view/loginChild') // 异步组件加载,routers.js 顶部不必再引入 loginChild 组件
}
],
beforeEnter (to, from, next) {
console.log('route beforeEnter')
next()
}
},
//...
]