Vue Router 是官方的路由管理器。它和 Vue.js 的核心深度集成,路径和组件的映射关系, 让构建单页面应用变得易如反掌。
Vue-Router路由模式
hash模式(默认)
hash是指 url 尾巴后的 # 号以及后面的字符。hash 虽然出现在url中,但不会被包括在http请求中,对后端完全没有影响,因此改变hash不会重新加载页面。
原理
基于浏览器的hashchange事件,地址变化时,通过window.location.hash 获取地址上的hash值;并通过构造Router类,配置routes对象设置hash值与对应的组件内容。
- 优点
- hash值会出现在URL中, 但是不会被包含在Http请求中, 因此hash值改变不会重新加载页面
- hash改变会触发hashchange事件, 能控制浏览器的前进后退
- 兼容性好
- 缺点
- 只可修改 # 后面的部分,因此只能设置与当前 URL 同文档的 URL
- hash有体积限制,故只可添加短字符串
- 设置的新值必须与原来不一样才会触发hashchange事件,并将记录添加到栈中
- 每次URL的改变不属于一次http请求,所以不利于SEO优化
如何获取页面的hash变化
监听$route的变化
watch:{
$route:{
handler:function(val,oldVal){
console.log(val);
}
}
}
window.location.hash读取 # 值
window.location.hash 的值可读可写,读取来判断状态是否改变,写入时可以在不重载网页的前提下,添加一条历史访问记录。
history模式
当使用这种历史模式时,URL 会看起来很 “正常”,例如 https://example.com/user/id。
由于我们的应用是一个单页的客户端应用,如果没有适当的服务器配置,用户在浏览器中直接访问 https://example.com/user/id,就会得到一个 404 错误。
要解决这个问题,你需要做的就是在你的服务器上添加一个简单的回退路由。如果 URL 不匹配任何静态资源,它应提供与你的应用程序中的 index.html 相同的页面。
原理
基于HTML5新增的 pushState() 和 replaceState() 两个api,以及浏览器的 popstate 事件,地址变化时,通过window.location.pathname找到对应的组件。并配置routes对象,设置pathname值与对应的组件内容。
- 优点
- pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL
- pushState() 设置的新URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中
- pushState() 通过 stateObject 参数可以添加任意类型的数据到记录中
- pushState() 可额外设置 title 属性供后续使用
- 浏览器的进后退能触发浏览器的popstate事件,获取window.location.pathname来控制页面的变化
- 缺点
- URL的改变属于http请求,借助 history.pushState 实现页面的无刷新跳转,因此会重新请求服务器。所以前端的 URL必须和实际向后端发起请求的 URL 一致。
- 如果用户输入的URL回车或者浏览器刷新或者分享出去某个页面路径,用户点击后,URL与后端配置的页面请求URL不一致,则匹配不到任何静态资源,就会返回404页面。所以需要后台配置支持,覆盖所有情况的候选资源,如果URL 匹配不到任何静态资源,则应该返回app 依赖的页面或者应用首页
- 兼容性差,特定浏览器支持
abstract模式
支持所有javascript运行模式。vue-router 自身会对环境做校验,如果发现没有浏览器的 API,路由会自动强制进入 abstract 模式。在移动端原生环境中也是使用 abstract 模式。
$route 和 $router
$route
“路由信息对象”
包括 path,params,hash,query,fullPath,matched,name 等路由信息参数。
$router
“路由实例对象”
包括了路由的跳转方法,钩子函数等。
router-link 和 router-view
router-link
使得 Vue Router 可以在不重新加载页面的情况下更改 URL,处理 URL 的生成以及编码,实质上最终会渲染成 a 链接。
点击 < router-link :to=“…”> 相当于调用 router.push(…) 。
在 router-link 上的 to 属性传值:
- /path?参数名=值
接收传递过来的值: $route.query.参数名
- /path/值/值 –> 需要路由对象提前配置 path: “/path/参数名”
接收传递过来的值: $route.params.参数名
router-view
子级路由显示,将显示与 url 对应的组件。你可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。如果 router-view 没有设置名字,那么默认为 default。
<router-view class="view left-sidebar" name="LeftSidebar"></router-view>
<router-view class="view main-content"></router-view>
<router-view class="view right-sidebar" name="RightSidebar"></router-view>
一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件。确保正确使用 components 配置:
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
components: {
default: Home,
// LeftSidebar: LeftSidebar 的缩写
LeftSidebar,
// 它们与 `<router-view>` 上的 `name` 属性匹配
RightSidebar,
},
},
],
})
路由跳转方式
注意:在 Vue 实例中,你可以通过 $router
访问路由实例。因此你可以调用 this.$router.push
- this.$router.push()
router.push 方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,会回到之前的 URL
声明式 | 编程式 |
---|---|
< router-link :to=“…”> | router.push(…) |
this.$router.push( ) 方法的参数可以是一个字符串路径,或者一个描述地址的对象。例如:
// 字符串路径
router.push('/users/eduardo')
// 带有路径的对象
router.push({ path: '/users/eduardo' })
// 命名的路由,并加上参数,让路由建立 url
router.push({ name: 'user', params: { username: 'eduardo' } })
// 带查询参数,结果是 /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } })
// 带 hash,结果是 /about#team
router.push({ path: '/about', hash: '#team' })
如果提供了 path,params 会被忽略,需要提供路由的 name 或手写完整的带有参数的 path :
const username = 'eduardo'
// 我们可以手动建立 url,但我们必须自己处理编码
router.push(`/user/${username}`) // -> /user/eduardo
// 同样
router.push({ path: `/user/${username}` }) // -> /user/eduardo
// 如果可能的话,使用 `name` 和 `params` 从自动 URL 编码中获益
router.push({ name: 'user', params: { username } }) // -> /user/eduardo
// `params` 不能与 `path` 一起使用
router.push({ path: '/user', params: { username } }) // -> /user
- this.$router.replace()
作用类似于 router.push,唯一不同的是,它在导航时不会向 history 添加新记录
声明式 | 编程式 |
---|---|
< router-link :to=“…” replace> | router.replace(…) |
也可以直接在传递给 router.push 的 routeLocation 中增加一个属性 replace: true :
router.push({ path: '/home', replace: true })
// 相当于
router.replace({ path: '/home' })
- this.$router.go(n)
向前或者后跳转n个页面,n可以是正数也可以是负数
// 向前移动一条记录,与 router.forward() 相同
router.go(1)
// 返回一条记录,与 router.back() 相同
router.go(-1)
// 前进 3 条记录
router.go(3)
// 如果没有那么多记录,静默失败
router.go(-100)
router.go(100)
router.push、router.replace 和 router.go 是 window.history.pushState
、window.history.replaceState
和 window.history.go
的翻版
params和query的区别
- 用法
query要用path来引入,params要用name来引入,接收参数分别是 this. r o u t e . q u e r y . n a m e 和 t h i s . route.query.name 和 this. route.query.name和this.route.params.name - url地址显示
query更加类似于ajax中get传参,params则类似于post,前者在浏览器地址栏中显示参数,后者则不显示 - 注意
query刷新不会丢失query里面的数据, params刷新会丢失 params里面的数据
路由跳转和location.href的区别
- 使用 location.href= /url来跳转,简单方便,但是刷新了页面;
- 使用 history.pushState( /url ) ,不刷新页面,静态跳转;
- 使用 router.push( /url ) 来跳转,使用了diff 算法,实现了按需加载,减少了 dom 的消耗;
使用 router 跳转和使用 history.pushState()没什么差别,因为vue-router就是用了 history.pushState() ,尤其是在history模式下。
路由配置参数
path跳转路径、component路径相对的组件
定义路由,每个路由都需要映射到一个组件:
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
]
name命名路由
- 没有硬编码的 URL
- params 的自动编码/解码
- 防止 url 书写错误
- 绕过路径排序(如显示一个)
const routes = [
{
path: '/user/:username',
name: 'user',
component: User,
},
]
要链接到一个命名的路由,可以向 router-link 组件的 to 属性传递一个对象:
<router-link :to="{ name: 'user', params: { username: 'erina' }}">
User
</router-link>
代码调用 router.push() 是一样的:
router.push({ name: 'user', params: { username: 'erina' } })
children嵌套路由
一些应用程序的 UI 由多层嵌套的组件组成。在这种情况下,URL 的片段通常对应于特定的嵌套组件结构,例如:
/user/johnny/profile /user/johnny/posts
+------------------+ +-----------------+
| User | | User |
| +--------------+ | | +-------------+ |
| | Profile | | +------------> | | Posts | |
| | | | | | | |
| +--------------+ | | +-------------+ |
+------------------+ +-----------------+
通过 Vue Router,你可以使用嵌套路由配置来表达这种关系。
顶层的 router-view 渲染顶层路由匹配的组件:
<div id="app">
<router-view></router-view>
</div>
const User = {
template: '<div>User {{ $route.params.id }}</div>',
}
// 这些都会传递给 `createRouter`
const routes = [{ path: '/user/:id', component: User }]
一个被渲染的组件也可以包含自己嵌套的 <router-view>
。例如,如果我们在 User 组件的模板内添加一个 <router-view>
:
const User = {
template: `
<div class="user">
<h2>User {{ $route.params.id }}</h2>
<router-view></router-view>
</div>
`,
}
要将组件渲染到这个嵌套的 router-view 中,我们需要在路由中配置 children:
const routes = [
{
path: '/user/:id',
component: User,
children: [
{
// 当 /user/:id/profile 匹配成功
// UserProfile 将被渲染到 User 的 <router-view> 内部
path: 'profile',
component: UserProfile,
},
{
// 当 /user/:id/posts 匹配成功
// UserPosts 将被渲染到 User 的 <router-view> 内部
path: 'posts',
component: UserPosts,
},
],
},
]
props路由解耦
组件中使用 $route 会与路由紧密耦合,因为它只能用于特定的 URL,这限制了组件的灵活性。
将下面的代码
const User = {
template: '<div>User {{ $route.params.id }}</div>'
}
const routes = [{ path: '/user/:id', component: User }]
替换成
const User = {
// 请确保添加一个与路由参数完全相同的 prop 名
props: ['id'],
template: '<div>User {{ id }}</div>'
}
const routes = [{ path: '/user/:id', component: User, props: true }]
这允许你在任何地方使用该组件,使得该组件更容易重用和测试。
redirect重定向路由
重定向通过 routes 配置来完成,下面例子是从 /home 重定向到 /:
const routes = [{ path: '/home', redirect: '/' }]
重定向的目标也可以是一个命名的路由:
const routes = [{ path: '/home', redirect: { name: 'homepage' } }]
也可以是一个方法,动态返回重定向目标:
const routes = [
{
// /search/screens -> /search?q=screens
path: '/search/:searchText',
redirect: to => {
// 方法接收目标路由作为参数
// return 重定向的字符串路径/路径对象
return { path: '/search', query: { q: to.params.searchText } }
},
},
{
path: '/search',
// ...
},
]
也可以重定向到相对位置:
const routes = [
{
// 将总是把/users/123/posts重定向到/users/123/profile。
path: '/users/:id/posts',
redirect: to => {
// 该函数接收目标路由作为参数
// 相对位置不以`/`开头
// 或 { path: 'profile'}
return 'profile'
},
},
]
meta路由元信息
有时需要将任意信息附加到路由上,如过渡名称、谁可以访问路由等。这些事情可以通过接收属性对象的meta属性来实现,并且它可以在路由地址和导航守卫上都被访问到。
定义路由的时候你可以这样配置 meta 字段:
const routes = [
{
path: '/posts',
component: PostsLayout,
children: [
{
path: 'new',
component: PostsNew,
// 只有经过身份验证的用户才能创建帖子
meta: { requiresAuth: true }
},
{
path: ':id',
component: PostsDetail
// 任何人都可以阅读文章
meta: { requiresAuth: false }
}
]
}
]
路由导航守卫
vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。这里有很多方式植入路由导航中:全局的,单个路由独享的,或者组件级的。
全局前置守卫 router.beforeEach
全局前置守卫,进入路由之前调用。
每个守卫方法接收两个参数:
- to: 即将要进入的目标
- from: 当前导航正要离开的路由
next: 可选的第三个参数:
- next() 没有参数表示放行
- next(‘/login’),next(‘to’) 有参数表示中断当前导航,执行新的导航
确保 next 在任何给定的导航守卫中都被严格调用一次,它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错。
错误用例:
// 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()
})
返回值:
- false: 取消当前的导航。如果浏览器的 URL 改变了(可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
- 一个路由地址: 通过一个路由地址跳转到一个不同的地址,就像你调用 router.push() 一样,你可以设置诸如 replace: true 或 name: ‘home’ 之类的配置。当前的导航被中断,然后进行一个新的导航,就和 from 一样。
判断是否登录,未登录则跳转到登陆页面:
router.beforeEach((to,from,next)=>{
let ifInfo = Vue.prototype.$common.getSession('userData');//判断是否登录
if(!ifInfo){
//sessionStorage没有user信息
if(to.path == '/'){
//如果是登录页面直接next()
next();
}else{//否则跳转到登录页面
Window.location.href = Vue.prototype.$loginUrl;
}
}else{
return next();
}
})
全局解析守卫 router.beforeResolve
全局解析守卫,在 beforeRouteEnter 调用之后调用。
让用户可以访问自定义 meta 属性 requiresCamera 的路由:
router.beforeResolve(async to => {
if (to.meta.requiresCamera) {
try {
await askForCameraPermission()
} catch (error) {
if (error instanceof NotAllowedError) {
// ... 处理错误,然后取消导航
return false
} else {
// 意料之外的错误,取消导航并把错误传给全局处理器
throw error
}
}
}
})
router.beforeResolve 是获取数据或执行任何其他操作(如果用户无法进入页面时你希望避免执行的操作)的理想位置。
全局后置钩子 router.afterEach
全局后置钩子 进入路由之后调用,对于分析、更改页面标题、声明页面等辅助功能以及许多其他事情都很有用。和守卫不同的是,这些钩子不会接受 next 函数,也不会改变导航本身:
跳转之后滚动条回到顶部:
router.afterEach((to,from)=>{
window.scrollTo(0,0);
})
路由独享的守卫 beforeEnter
你可以直接在路由配置上定义 beforeEnter 守卫:
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: (to, from) => {
// reject the navigation
return false
},
},
]
beforeEnter 守卫 只在进入路由时触发,不会在 params、query 或 hash 改变时触发。例如,从 /users/2 进入到 /users/3 或者从 /users/2#info 进入到 /users/2#projects。它们只有在从一个不同的路由导航时才会被触发。
你也可以将一个函数数组传递给 beforeEnter,这在为不同的路由重用守卫时很有用:
function removeQueryParams(to) {
if (Object.keys(to.query).length)
return { path: to.path, query: {}, hash: to.hash }
}
function removeHash(to) {
if (to.hash) return { path: to.path, query: to.query, hash: '' }
}
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: [removeQueryParams, removeHash],
},
{
path: '/about',
component: UserDetails,
beforeEnter: [removeQueryParams],
},
]
组件内的守卫
你可以在路由组件内直接定义路由导航守卫(传递给路由配置的)
- beforeRouteEnter
- beforeRouteUpdate
- beforeRouteLeave
const UserDetails = {
template: `...`,
beforeRouteEnter(to, from) {
// 在渲染该组件的对应路由被验证前调用
// 不能获取组件实例 `this` !
// 因为当守卫执行时,组件实例还没被创建!
},
beforeRouteUpdate(to, from) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
// 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
},
beforeRouteLeave(to, from) {
// 在导航离开渲染该组件的对应路由时调用
// 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
},
}
beforeRouteEnter 守卫 不能 访问 this,可以通过传一个回调给 next 来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数:
beforeRouteEnter (to, from, next) {
next(vm => {
// 通过 `vm` 访问组件实例
})
}
对于 beforeRouteUpdate 和 beforeRouteLeave 来说,this 已经可用了,所以不支持 传递回调
beforeRouteUpdate (to, from) {
// just use `this`
this.name = to.params.name
}
beforeRouteLeave 通常用来预防用户在还未保存修改前突然离开:
beforeRouteLeave (to, from) {
const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
if (!answer) return false
}
完整的导航解析流程
- 导航被触发。
- 在失活的组件里调用 beforeRouteLeave 守卫。
- 调用全局的 beforeEach 守卫。
- 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
- 在路由配置里调用 beforeEnter。
- 解析异步路由组件。
- 在被激活的组件里调用 beforeRouteEnter。
- 调用全局的 beforeResolve 守卫(2.5+)。
- 导航被确认。
- 调用全局的 afterEach 钩子。
- 触发 DOM 更新。
- 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
动态路由
对路由的添加通常是通过 routes 选项来完成的,但是在某些情况下,你可能想在应用程序已经运行的时候添加或删除路由。
添加路由
动态路由主要通过两个函数实现。router.addRoute() 和 router.removeRoute()。
它们只注册一个新的路由,也就是说,如果新增加的路由与当前位置相匹配,就需要你用 router.push() 或 router.replace() 来手动导航,才能显示该新路由。
添加嵌套路由
要将嵌套路由添加到现有的路由中,可以将路由的 name 作为第一个参数传递给 router.addRoute(),这将有效地添加路由,就像通过 children 添加的一样:
router.addRoute({ name: 'admin', path: '/admin', component: Admin })
router.addRoute('admin', { path: 'settings', component: AdminSettings })
等效于:
router.addRoute({
name: 'admin',
path: '/admin',
component: Admin,
children: [{ path: 'settings', component: AdminSettings }],
})
在导航中添加路由
如果你决定在导航守卫内部添加或删除路由,你不应该调用 router.replace(),而是通过返回新的位置来触发重定向:
router.beforeEach(to => {
if (!hasNecessaryRoute(to)) {
router.addRoute(generateRoute(to))
// 触发重定向
return to.fullPath
}
})
删除路由
- 通过添加一个名称冲突的路由。如果添加与现有途径名称相同的途径,会先删除路由,再添加路由:
router.addRoute({ path: '/about', name: 'about', component: About })
// 这将会删除之前已经添加的路由,因为他们具有相同的名字且名字必须是唯一的
router.addRoute({ path: '/other', name: 'about', component: Other })
- 通过调用 router.addRoute() 返回的回调:
const removeRoute = router.addRoute(routeRecord)
removeRoute() // 删除路由如果存在的话
- 通过使用 router.removeRoute() 按名称删除路由:
router.addRoute({ path: '/about', name: 'about', component: About })
// 删除路由
router.removeRoute('about')
当路由被删除时,所有的别名和子路由也会被同时删除
查看现有路由
Vue Router 提供了两个功能来查看现有的路由:
- router.hasRoute():检查路由是否存在。
- router.getRoutes():获取一个包含所有路由记录的数组。
路由懒加载
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。
用动态导入代替静态导入:
// 将
// import UserDetails from './views/UserDetails.vue'
// 替换成
const UserDetails = () => import('./views/UserDetails.vue')
const router = createRouter({
// ...
routes: [{ path: '/users/:id', component: UserDetails }],
})