目录
(4)、history 对象的事件——popstate 事件
3、使用路由配置表创建 vue-router 的实例,并将其挂载到 Vue 实例上
(2)、用路由重定向解决:多个路由公用同一个组件时不重新渲染该组件的问题
(2)、全局解析守卫——beforeResolve() ( 2.5.0+)
(1)、渲染组件对应路由前的守卫——beforeRouteEnter()
(2)、更新组件对应路由前的守卫——beforeRouteUpdate() (2.2 新增)
(3)、离开组件对应路由前的守卫——beforeRouteLeave()
十八、vu3 的 组合 API 中 使用 vue-router
3、在 setup 里使用 vue-router 的 API
前言
Vue Router 官网https://router.vuejs.org/zh/
一、vue-router 是什么?
vue-router 是一个基于 Vue 的 SPA 路由管理器。用来建立 url 和页面之间的映射关系。
vue-router 的功能:
- 管理嵌套的路由和视图表
- 实现模块化的、基于组件的路由配置
- 支持路由参数、查询、通配符
- 实现基于 Vue.js 过渡系统的视图过渡效果
- 支持细粒度的导航控制
- 带有自动激活的 CSS class 的链接
- HTML5 history模式或 hash 模式,在 IE9 中自动降级
- 支持自定义的滚动条行为
二、vue-router 的基本概念
1、router、route 和 routes 的区别
- router:是一个机制,用来管理路由。
- route:是一条路由。例如:Home按钮 => home内容, 这是一条路由; about按钮 => about 内容, 这是另一条路由。
- routes:是一组路由,把上面的每一条路由组合起来,形成一个数组。例如:[{home 按钮 =>home内容 }, { about按钮 => about 内容}]。
2、$route 和 $router 的区别
- $route:当前路由信息对象,包括:path,params,hash,query,fullPath,matched,name 等路由信息参数。
- $router:挂载到 Vue 实例上的 VueRouter 实例。包括了路由的跳转方法,钩子函数等。
(1)、$route
① 、$route.path
字符串,对应当前路由的路径,总是解析为绝对路径,如 "/order"。
②、 $route.params
一个 key/value 对象,包含了 动态片段 和 全匹配片段。
如果没有路由参数,就是一个空对象。
③、 $route.query
一个 key/value 对象,表示 URL 查询参数。
例如,对于路径 /foo?user=1,则有 $route.query.user为1。
如果没有查询参数,则是个空对象。
④、 $route.hash
当前路由的 hash 值 (不带 #) ,如果没有 hash 值,则为空字符串。
⑤ 、$route.fullPath
完成解析后的 URL,包含查询参数和 hash 的完整路径。
⑥、 $route.matched
数组,包含当前匹配的路径中所包含的所有片段所对应的配置参数对象。
⑦、 $route.name
当前路径名字。
(2)、$router
挂载到 Vue 实例上的 VueRouter 实例的案例(下文“vue-router 的使用”也会完整的讲一下):
import Vue from 'vue'
import VueRouter from 'vue-router'
import routes from './routes/index'
import App from './App.vue'
Vue.config.productionTip = false
Vue.use(VueRouter);
const router = new VueRouter({
// mode: history,
routes,
})
new Vue({
router,// 挂载到 Vue 实例上的 VueRouter 实例
render: h => h(App),
}).$mount('#app')
$router常见的路由的跳转方法(下文“vue-router 的编程式导航”会细讲):
this.$router.go(-1)//跳转到上一次浏览的页面
this.$router.replace('/menu')//指定跳转的地址
this.$router.replace({name:'menuLink'})//指定跳转路由的名字下
this.$router.push('/menu')//通过push进行跳转
this.$router.push({name:'menuLink'})//通过push进行跳转路由的名字下
三、两种路由模式及其实现原理
vue-router在实现单页面前端路由时,提供了两种方式:Hash模式和History模式;根据mode参数来决定采用哪一种方式。
1、hash 路由模式(默认)
(1)、hash 路由模式
- hash(#)是URL 的锚点,代表的是网页中的一个位置,单单改变#后的部分,浏览器只会滚动到相应位置,因此改变 hash 不会重新加载页面。
- 每次改变 # 后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置。
const router = new VueRouter({
routes: [...]
})
(2)、hash 路由模式的实现原理
hash 模式的原理是:通过 onhashchange 事件在 window 对象上监测 hash 值(“#”后面的字符串)的变化。当 hash 值发生改变时,浏览器就会渲染指定 DOM 位置的数据。
- 必须要把 hashchange 事件添加给 window 对象。
- 当 hash 值发生改变时,页面不会重新加载,浏览器只会渲染指定 DOM 位置的数据。
hashchange 事件的 event 对象新增了两个属性:oldURL 和 newURL。
- oldURL 和 newURL 分别保存着 参数列表变化“前、后”的完整 URL。
- 最好使用 location 对象来确定当前参数列表,因为目前支持 oldURL 和 newURL 属性的浏览器只有 Firefox6+、Chrome 和 Opera10.6+。
hashchange 事件的应用:
var isSupported = ("onhashchange" in window) && (document.documentMode === undefined || document.documentMode > 7);// 检测浏览器是否支持 hashchange 事件
if(isSupported){
window.addEventListener("hashchange", function(event){
console.log(`当前 hash 值${location.hash}`);
console.log('Hash从:' + event.oldURL + '变成了:' + event.newURL);
});
}
(3)、hash 路由的弊端
- hash 本来是拿来做页面定位的,如果拿来做路由的话,原来的锚点功能就不能用了。
- hash 的传参是基于 url 的,如果要传递复杂的数据,会有体积的限制,而 history 模式不仅可以在 url 里放参数,还可以将数据存放在一个特定的对象中。
- 微信分享页面就会把"#"后边的内容处理掉。
2、history 路由模式
由于 hash 模式会在 url 中自带 #,如果不想要很丑的 hash,我们可以用路由的 history 模式,只需要在配置路由规则时,加入"mode: 'history'"即可。
const router = new VueRouter({
mode: 'history',
routes: [...]
})
注意:
使用 history 路由模式以后,你的服务器就不再返回 404 错误页面,因为对于所有路径都会返回 index.html文件。为了避免这种情况,你应该在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面,比如:创建 NotFoundComponent.vue 作为 404 页面。
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '*', component: NotFoundComponent }
]
})
或者,如果你使用 Node.js 服务器,你可以用服务端路由匹配到来的 URL,并在没有匹配到路由的时候返回 404,以实现回退。更多详情请查阅 Vue 服务端渲染文档。
- IE9 及以下版本不兼容 history 路由模式,可使用强制刷新处理。
History 模式的原理是:通过 HTML5 中新增的 pushState() 和 replaceState() 方法修改历史记录。
- history.pushState() 方法:改变浏览器地址栏中的 URL,将数据 push 进会话历史栈,但不加载新的页面。
- history.replaceState() 方法:改变浏览器地址栏中的 URL,更新历史栈上最新的入口,加载新的页面。
(1)、history 对象
Window.history;// 返回当前会话的整个 history 对象。
(2)、history 对象的属性
①、length 属性
返回一个整数,该整数表示会话历史中元素的数目,包括当前加载的页。
history.length;// 查看当前历史堆栈中页面的数量
②、state 属性
返回一个表示历史堆栈顶部的状态的值。这是一种可以不必等待 popstate 事件而查看状态的方式。
history.state;// 返回当前会话的 history 状态。
(3)、history 对象的方法
①、pushState() 方法
将数据 push 进会话历史栈。但浏览器并不会立即向服务器发送请求,不会加载新的页面。
pushState() 方法接收 3 个参数:状态对象、新状态的标题 和(可选的)相对 URL。
- 状态对象:应该尽可能提供初始化页面状态所需的各种信息。
- 新状态的标题:目前还没有完全实现,因此完全可以只传入一个空字符串("")或者一个短标题也可以。
- 新的 URL:(可选的)如果传入这个参数,浏览器地址栏的地址会更新。
- 新的 url 与原来的 url 遵循“同源策略”,否则会报错。
- url可以是绝对路径,也可以是相对路径。例如:当前 url 是 https://www.baidu.com/a/ 时,
- 若执行 history.pushState(null, null, './qq/'),则变成 https://www.baidu.com/a/qq/;
- 若执行 history.pushState(null, null, '/qq/'),则变成 https://www.baidu.com/qq/。
history.pushState(state, title, url);
②、replaceState() 方法
更新历史栈上最新的入口。会加载新的页面。
replaceState() 方法接收 2 个参数:状态对象 和 新状态的标题——与 pushState() 方法的前两个参数相同。
history.replaceState(state, title, url)
③、back() 方法
在浏览器历史记录里,前往上一页,,用户可点击浏览器左上角的返回按钮模拟此方法.。等价于 history.go(-1)。
history.back();
④、forward() 方法
在浏览器历史记录里,前往下一页,用户可点击浏览器左上角的前进按钮模拟此方法。 等价于 history.go(1)。
history.forward() // 前进
⑤、go() 方法
通过当前页面的相对位置从浏览器历史记录( 会话记录 )加载页面。比如:参数为-1的时候为上一页,参数为1的时候为下一页。
当整数参数超出界限时,没有效果,也不会报错。
history.go(1) // 前进一步,-2为后退两步
(4)、history 对象的事件——popstate 事件
popstate 事件用来监听浏览器“前进”和“后退”的事件。当活动历史记录更改时,将触发popstate事件。
- 如果被激活的历史记录条目是通过对 history.pushState() 的调用创建的,或者受到对 history.replaceState() 的调用的影响,popstate 事件的 state 属性包含历史条目的状态对象的副本。
- 调用 history.pushState() 或 history.replaceState() 不会触发 popstate 事件。只有在做出浏览器动作时,才会触发该事件,例如:
- 用户点击浏览器的回退按钮;
- 在 Javascript 代码中调用 history.back() 方法,或者 history.forward() 方法,或者 history.go(1) 方法。
window.addEventListener('popstate', (event) => {
console.log("location: " + document.location + ", state: " + JSON.stringify(event.state));
});
history.pushState({page: 1}, "title 1", "?page=1");
history.pushState({page: 2}, "title 2", "?page=2");
history.replaceState({page: 3}, "title 3", "?page=3");
history.back(); // Logs "location: http://example.com/example.html?page=1, state: {"page":1}"
history.back(); // Logs "location: http://example.com/example.html, state: null
history.go(2); // Logs "location: http://example.com/example.html?page=3, state: {"page":3}
3、路由的底层原理
通过上图中 5 种方式,都会触发 vue-router 组件里的 updateRoute 钩子函数,该钩子函数会改变 Vue.util.defineReactive_route 里的响应式 route 的信息,然后自动触发 router-view 的视图的更新,router-view 会根据我们提供的 URL 匹配到需要渲染的组件。
四、vue-router 的使用
- 安装 vue-router 并在 Vue 上使用它。
- 创建一个路由配置表(也叫 VueRouter 的参数列表)。
- 使用路由配置表创建 vue-router 的实例,并将其挂载到 Vue 实例上。
- 使用 <router-view></router-view> 标签提供一个路由占位,动态挂载 URL 到与之匹配的组件。
1、安装 vue-router 并在 Vue 上使用它
安装 vue-router:
npm i vue-router -S
将 vue-router 挂载到 Vue 实例上:
在 main.js 中
import VueRouter from 'vue-router'
Vue.use(VueRouter);
安装完后,想看官方使用教程 请戳这里。
2、创建一个路由配置表
在项目的 src 目录下,新建 routes 文件夹,来统一管理路由配置表。
然后,我这里新建了一个 index.js 作为我的路由配置表,比如:
// 引入组件
import RouterDemo from "../components/RouterDemo";
// 配置路径与组件的映射
const routes = [
{
path: "/",
redirect: "/bar",
},
{
path: "/bar",
component: RouterDemo,
name: "2",
},
{
path: "/user",
component: RouterDemo,
name: "3",
children: [
{
path: "profile",
component: RouterChildrenDemo,
},
],
}
];
export default routes;
3、使用路由配置表创建 vue-router 的实例,并将其挂载到 Vue 实例上
在 main.js 中,使用路由配置表创建 vue-router 的实例:
import routes from './router'
const router = new VueRouter({
// mode: history,
routes,
})
将 vue-router 实例挂载到 Vue 实例上:
new Vue({
router,// 将 vue-router 实例挂载到 Vue 实例上
render: h => h(App),
}).$mount('#app')
4、提供一个路由占位,动态挂载 URL 到与之匹配的组件
(1)、在组件中提供一个路由占位——router-link
router-link 是 vue-router 提供的一个全局组件,它是用来做路由跳转,底层其实就是一个 <a></a> 链接(实际渲染出来的标签也是 <a></a> 标签)。
router-view 是 vue-router 提供的一个全局组件,它是用来提供一个路由占位,用来挂载 URL 匹配到的组件。
具体使用,请看下面的案例:
在 RouterDemo.vue 中控制路由的切换,并通过 <router-view></router-view> 标签提供一个路由占位:
// RouterDemo.vue
<template>
<div>
<router-link to="/user/profile">Go to /user/profile</router-link>
<router-view></router-view>
</div>
</template>
<script>
export default {
};
</script>
【拓展】
使用路由模块来实现页面跳转的方式:
-
方式1:直接修改地址栏
-
方式2:this.$router.push(‘路由地址’)
-
方式3:使用 <router-link></router-link> 标签
(2)、在路由配置表中提供一个“单纯的路由占位”
如果不用 render 函数,你需要写一个单文件组件,而单文件组件的本质就是render函数——单文件组件通过 vue 的 loader 都会转成 render 函数。
在路由配置表中,设置 component 参数时,使用 rander 函数直接渲染一个 router-view 组件:
const routes = [
{
path: "/",
redirect: "/user"
},
{
path: "/user",
/* 使用 rander 函数直接渲染一个 router-view 组件 */
component: {render:h => h('router-view')},
/* 等价于下面这种写法:
component: {render(h) {
return h('router-view')
}}, */
children:[
{
path: "/user",
redirect: "/user/login"
},
{
path: "login",
name: "Login",
component: () =>
import(/* webpackChunkName: "user" */ "../views/user/Login")
},
{
path: "register",
name: "Register",
component: () =>
import(/* webpackChunkName: "user" */ "../views/user/Register.vue")
}
]
},
]
因此,就没必须要像下面这样定义一个单文件组件,来使用 router-view 组件,然后引入到 routers 里了:
<template>
<div>
<router-view></router-view>
</div>
</template>
<script>
export default {
}
</script>
【结论】
如果只是需要一个路由占位(router-view 组件)而没有别的内容需要展示的话——我称其为“单纯的路由占位”,完全可以采用 render 函数来加载它。
五、vue-router 的嵌套路由——子路由
一般 App.vue 组件里的 <router-view> 是最顶层的出口,渲染最高级路由匹配到的组件:
App.vue 组件中:
<template>
<div id="app">
<h2>router demo</h2>
<router-view></router-view><!-- 根路由 -->
</div>
</template>
<script>
export default {
name: 'app',
}
</script>
同样地,一个被渲染组件同样可以包含自己的嵌套 <router-view>:
RouterDemo.vue 组件中:
<template>
<div>
<router-link to="/user/12">Go to /user/12</router-link>
<br />
<router-link to="/user/12/profile">Go to /user/12/profile</router-link>
<br />
<router-link to="/user/12/posts">Go to /user/12/posts</router-link>
<br />
<p>id: {{ id }}</p>
<router-view></router-view><!-- 子路由 -->
</div>
</template>
<script>
export default {
props: ["id"],
};
</script>
RouterChildrenDemo.vue 组件中:
<template>
<div>
{{ routerInfo }}
</div>
</template>
<script>
export default {
computed: {
routerInfo() {
const { fullPath, path, name, params, query, meta } = this.$route;
return {
fullPath,
path,
name,
params,
query,
meta,
};
},
},
};
</script>
要在嵌套的出口中渲染组件,需要在路由配置表使用 children 配置:
src/routes/index.js 中:
{
path: "/user/:id",
component: RouterDemo,
name: "3",
props: true,
children: [
{
// 当 /user/:id/profile 匹配成功,
// RouterChildrenDemo 会被渲染在 RouterDemo 的 <router-view/> 中
path: "profile",
component: RouterChildrenDemo,
name: "3-1",
},
{
// 当 /user/:id/posts 匹配成功
// RouterChildrenDemo 会被渲染在 RouterDemo 的 <router-view/> 中
path: "posts",
component: RouterChildrenDemo,
name: "3-2",
},
],
},
以“ / ”开头的嵌套路径会被当作根路径。 由上述代码可以看出,在路由配置表中,根路由的 path 属性需要加“/”,比如:path: "/user/:id"。而子路由的 path 属性不需要加“/”,比如:path: "profile"。
六、vue-router 的编程式导航
1、router.push() 方法
会在当前页面打开。
效仿 window.history API 的 window.history.pushState() 方法。
除了使用 <router-link> 创建 a 标签来定义导航链接,我们还可以借助 router 的实例方法,通过编写代码来实现。
当你点击 <router-link> 时,这个方法会在内部调用,所以说,点击 <router-link :to="..."> 等同于调用 router.push(...)。
声明式 | 编程式 |
---|---|
<router-link :to="..."> | router.push(...) |
想要导航到不同的 URL,则使用 router.push 方法。这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的 URL。
该方法可以接收 3 个参数:
- 一个路由描述;
- (可选的)在导航成功完成时执行的回调函数;
- (可选的)在导航终止时执行的回调函数。
该方法返回一个 Promise。
该方法的第一个参数可以是一个字符串路径,或者一个描述地址的对象。例如:
// 字符串
router.push('home')
// 对象
router.push({ path: 'home' })
// 命名的路由
router.push({ name: 'user', params: { userId: '123' }})
// 带查询参数,变成 /register?plan=private
router.push({ path: 'register', query: { plan: 'private' }})
注意:与 router-link 组件的 to 属性一样,如果提供了 path,params 会被忽略。
// 这里的 params 不生效
router.push({ path: '/user', params: { userId }}) // -> /user
2、router.replace() 方法
效仿 window.history API 的 window.history.replaceState() 方法。
声明式 | 编程式 |
---|---|
<router-link :to="..." replace> | router.replace(...) |
与 router.push() 方法的不同是:它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录。
该方法也可以接收 3 个参数,与 router.push() 方法一样。该方法也返回一个 Promise。
3、router.go(n) 方法
效仿 window.history API 的 window.history.go() 方法。
该方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步。
// 在浏览器记录中前进一步,等同于 history.forward()
router.go(1)
// 后退一步记录,等同于 history.back()
router.go(-1)
// 前进 3 步记录
router.go(3)
注意:如果 history 记录不够用,没有效果,也不会报错。
4、router.open
会打开一个新的浏览器页面。
其他与 router.push() 方法类似。
七、vue-router 的命名路由与命名视图
1、命名路由
在 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 }})
2、命名视图
有时候想同时 (同级) 展示多个视图,而不是嵌套展示,例如创建一个布局,有 sidebar (侧导航) 和 main (主内容) 两个视图,这个时候命名视图就派上用场了。
你可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。如果 router-view 没有设置名字,那么默认为 default。
<router-view class="view one"></router-view>
<router-view class="view two" name="a"></router-view>
<router-view class="view three" name="b"></router-view>
一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件。确保正确使用 components 配置 (带上 s):
const router = new VueRouter({
routes: [
{
path: '/',
components: {
default: Foo,
a: Bar,
b: Baz
}
}
]
})
八、vue-router 的路由重定向和别名
1、路由重定向——redirect
(1)、基本的路由重定向
“重定向”的意思是,当用户访问 /a时,URL 将会被替换成 /b,然后匹配路由为 /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 重定向的 字符串路径/路径对象
}}
]
})
(2)、用路由重定向解决:多个路由公用同一个组件时不重新渲染该组件的问题
当多个路由公用同一个组件时,切换路由不会重新触发该组件页面的钩子函数。为了重新触发钩子函数。获取最新数据。可以这样做:
在重定向父路由时,在其子路由列表中进行。
const routes = [
{
path: "/user",
component: () =>
import(/* webpackChunkName: "layout" */ "../layouts/UserLayout.vue"),
children: [
{
path: "/user",
/* 重定向父路由时,在其子路由列表中进行 */
redirect: "/user/login",
},
{
path: "login",
name: "Login",
component: () =>
import(/* webpackChunkName: "user" */ "../views/user/Login"),
},
{
path: "register",
name: "Register",
component: () =>
import(/* webpackChunkName: "user" */ "../views/user/Register.vue"),
},
],
},
];
【拓展】对于该问题,还可以采用下面两种方法来解决:
方法1:假如父组件下的两个子组件A ,B需要公用同一个组件,在父组件中,router-view中添加key ,并保证key值具有类似ID值的唯一性。 这样就能实现,进行相应的子组件路由,会根据相应的key值触发相应子组件的钩子函数。
<router-view :key="$route.XXX"></router-view>
方法2:通过 watch 监听路由的变化,来管理切换路由。
// 推荐这样写:
watch: {
'$route.query.id' () {
// ...
},
'$route.query.page' () {
// ...
}
}
// 不推荐这样写:
watch: {
'$router' (to, from) {
var path = to.path;
if (path.indexOf("id") != -1){
// ...
} else if (path.indexOf("page") != -1){
// ...
}
}
}
(3)、路由重定向的更多高级用法
2、别名——alias
/a 的别名是 /b,意味着,当用户访问 /b 时,URL 会保持为 /b,但是路由匹配则为 /a,就像用户访问 /a 一样。
const router = new VueRouter({
routes: [
{ path: '/a', component: A, alias: '/b' }
]
})
九、 带参数的动态路由匹配
1、用 name 传递参数
路由配置表中:
routes: [
{
path: '/',
name: 'Hello',
component: Hello
}
]
模板里 用$route.name 来接收。比如:
<p>{{ $route.name}}</p>
2、通过 router-link 标签中的 to 属性传参
<router-link :to="{name:xxx,params:{key:value}}">valueString</router-link>
注意:to 属性里,如果提供了 path,params 会被忽略。
// 这里的 params 不生效
<router-link :to="{
name:'xxx',
path:'/xxx',
params:{key:value}}">valueString</router-link>
3、在路由配置表中以冒号的形式设置参数(:参数)
{
path: "/user/:id",
component: RouterDemo,
name: "3",
},
然后在 RouterDemo.vue 组件中,通过 $route.params.XXX 来使用。比如:
<p>{{ $route.params.id}}</p>
上述代码,是在组件中使用 $route,这会使 $route 与其对应路由形成高度耦合,从而使组件只能在某些特定的 URL 上使用,限制了其灵活性。
解决办法:路由组件传参——使用 props 将组件和路由解耦:
{
path: "/user/:id",
component: RouterDemo,
name: "3",
props: true,
},
如果 props 属性 被设置为 true,route.params(这里指的是 id)将会被设置为组件属性。
在 RouterDemo.vue 组件中可以直接使用 id 这个属性:
export default {
props: ["id"],
};
4、使用path来匹配路由,然后通过query来传递参数
<router-link :to="{ name:'Query',query: { queryId: status }}" >
router-link跳转Query
</router-link>
路由配置表中:
{
path: '/query',
name: 'Query',
component: Query
}
获取参数:
this.$route.query.queryId
十、动态路由
动态路由主要通过 router.addRoute()
和 router.removeRoute()
两个函数实现。它们只注册一个新的路由,也就是说,如果新增加的路由与当前位置相匹配,就需要你用 router.push()
或 router.replace()
来 手动导航,才能显示该新路由。
【注意】:在v4中,“router.addRoutes()” 函数已弃用。
举个例子:
export const constantRouterMap: Array<RouteRecordRaw> = [
{
path: '/login',
name: 'Login',
component: () => import('../views/login/index.vue')
},
{
path: '/home',
name: 'Home',
component: () => import('../views/Home.vue')
}
]
export const asyncRouterMap: Array<RouteRecordRaw> = [
{
path: '/about',
name: 'About',
component: () => import('../views/About.vue')
}
]
定义一个静态route的数组和动态route的数组,在通过角色判断的时候,我们把所有的通过对比,实现动态添加路由。
从最上面的route中我们能看到,当前只有home和login页面是可以访问的,about是个动态route,在没有添加进去的时候我们访问。因此我们需要加载进去,我是通过一个点击事件把route加载进去的就是之前页面的点击事件。
<template>
<h1>这是home页面</h1>
<button style="color:blue" @click="addRoute">添加route</button>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import router, { asyncRouterMap } from '@/router/index'
export default defineComponent({
setup () {
const addRoute = () => {
asyncRouterMap.forEach(item => {
router.addRoute(item)
})
router.push('/about')
}
return {
addRoute
}
}
})
</script>
点击后,动态route就这么实现了,至于更多的,其实是对路由做比对,在前端做权限管理。
十一、vue-router 的路由匹配优先级
有时候,同一个路径可以匹配多个路由,此时,匹配的优先级就按照路由的定义顺序:谁先定义的,谁的优先级就最高。
1、高级匹配模式
vue-router 使用 path-to-regexp 作为路径匹配引擎,所以支持很多高级的匹配模式,例如:可选的动态路径参数、匹配零个或多个、一个或多个,甚至是自定义正则匹配。查看它的文档学习高阶的路径匹配,还有这个例子 展示 vue-router 怎么使用这类匹配。
十二、vue-router 的导航守卫
导航守卫是路由跳转过程中的一些钩子函数。
导航守卫的作用:通过 跳转 或 取消 的方式 守卫路由(导航)。
vue-router 提供的导航守卫的方式有 3 种:
- 全局的守卫:beforeEach、beforeResolve( 2.5.0+)、afterEach。
- 单个路由独享的守卫:beforeEnter。
- 组件级的守卫:beforeRouteEnter、beforeRouteUpdate (2.2+)、beforeRouteLeave。
除了 全局后置钩子,其他的守卫方法都接收 3 个参数:to、from 和 next。
- to: Route: 即将要进入的目标 路由对象。
- from: Route: 当前导航正要离开的路由。
- next: Function: 一定要确保 next 函数在任何给定的导航守卫中都被严格调用一次,该方法用来 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() 注册过的回调。
1、全局的守卫
参数或查询的改变并不会触发 beforeEach(进入) 和 afterEach(离开) 。
(1)、全局前置守卫——beforeEach()
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
// ...
})
(2)、全局解析守卫——beforeResolve() ( 2.5.0+)
这和 router.beforeEach() 类似,唯一的区别是:在导航被确认之前,在所有 组件内守卫 和 异步路由组件 被解析之后,解析守卫就会被调用。
const router = new VueRouter({ ... })
router.beforeResolve((to, from, next) => {
// ...
})
(3)、全局后置钩子——afterEach()
- 这个钩子不会接受 next 函数也不会改变导航本身。
- 参数或查询的改变并不会触发 afterEach() 。
router.afterEach((to, from) => {
// ...
})
2、单个路由独享的守卫——beforeEnter
在路由配置表上直接定义 beforeEnter 守卫:
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
3、组件内的守卫
你可以在组件内,像使用Vue的钩子函数那样,直接使用组件内的路由导航守卫。
组件内的路由导航守卫如下:
- 渲染组件对应路由前的守卫——beforeRouteEnter()。
- 更新组件对应路由前的守卫——beforeRouteUpdate() (2.2 新增)。
- 离开组件对应路由前的守卫——beforeRouteLeave()。
(1)、渲染组件对应路由前的守卫——beforeRouteEnter()
- 在渲染该组件的对应路由时,被 confirm 前调用。
- 不能获取组件实例 `this`——当守卫执行前,组件实例还没被创建。
- 不过,你可以通过传一个回调给 next 来访问组件实例。
- beforeRouteEnter 是支持给 next 函数传递回调的唯一守卫。
beforeRouteEnter (to, from, next) {
// ...
},
通过传一个回调给 next 来访问组件实例:
beforeRouteEnter (to, from, next) {
next(vm => {
// 通过 `vm` 访问组件实例
})
}
在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。
(2)、更新组件对应路由前的守卫——beforeRouteUpdate() (2.2 新增)
- 路由更新——在当前路由改变 并且该组件被复用时 调用——当前路由 query 参数变更时,该守卫会被调用。比如:
- 对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候。
- 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
- 可以访问组件实例 `this`。
beforeRouteUpdate (to, from, next) {
// ...
},
(3)、离开组件对应路由前的守卫——beforeRouteLeave()
- 导航离开该组件的对应路由时调用。
- 可以访问组件实例 `this`。
beforeRouteLeave (to, from, 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)
}
}
4、完整的导航解析流程
- 导航被触发。
- 在失活的组件里调用离开守卫。
- 调用全局的 beforeEach 守卫。
- 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
- 在路由配置里调用 beforeEnter。
- 解析异步路由组件。
- 在被激活的组件里调用 beforeRouteEnter。
- 调用全局的 beforeResolve 守卫 (2.5+)。
- 导航被确认。
- 调用全局的 afterEach 钩子。
- 触发 DOM 更新。
- 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。
注意:守卫是异步解析执行,导航在所有守卫resolve 完之前一直处于等待中。
5、路由导航守卫的执行顺序
(1)、当点击切换路由时,路由导航守卫的执行顺序
离开的路由 {
beforeRouterLeave(离开组件对应路由前的守卫)
} -->
进入的路由 {
beforeEach(全局前置守卫) -->
beforeEnter(单个路由独享的守卫)-->
beforeRouteEnter(渲染组件对应路由前的守卫)-->
beforeResolve(全局解析守卫)-->
afterEach(全局后置钩子)-->
Vue 的生命周期 -->
beforeRouteEnter 的 next 的回调(渲染组件对应路由前的守卫的 next 的回调)
}
(2)、当路由更新时,路由导航守卫的执行顺序
组件路由更新(当前路由 query 参数变更):beforeRouteUpdate(更新组件对应路由前的守卫)
十三、vue-router 的路由元信息——meta
定义路由的时候可以配置 meta 字段:
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
children: [
{
path: 'bar',
component: Bar,
// a meta field
meta: { requiresAuth: true }
}
]
}
]
})
一个路由匹配到的所有路由记录会暴露为 $route 对象 (还有在导航守卫中的路由对象) 的 $route.matched 数组。因此,我们需要遍历 $route.matched 来检查路由记录中的 meta 字段。
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
//此路由需要身份验证,请检查是否已登录
//否则,重定向到登录页面。
if (!auth.loggedIn()) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
} else {
next()
}
} else {
next() // 确保一定要调用 next()
}
})
十四、vue-router 的路由的过渡动效
路由的过渡:切换路由时,上一个路由离开,下一个路由进入,整个过程就是路由的过渡。
使用 <transition> 组件给 <router-view> 动态组件添加一些过渡效果:
<transition>
<router-view></router-view>
</transition>
1、单个路由的过渡
如果你想让每个路由组件有各自的过渡效果,可以通过给各路由组件内使用的 <transition> 设置不同的 name 来实现单个路由的过渡。
<transition name="transitionOne">
<router-view></router-view>
</transition>
<transition name="transitionTwo">
<router-view></router-view>
</transition>
2、基于路由的动态过渡
在单个路由过度的基础上,用 v-bind 来实现基于路由的动态过渡。
<!-- 使用动态的 transition name -->
<transition :name="transitionName">
<router-view></router-view>
</transition>
// 接着在父组件内
// watch $route 决定使用哪种过渡
watch: {
'$route' (to, from) {
const toDepth = to.path.split('/').length
const fromDepth = from.path.split('/').length
this.transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left'
}
}
十五、vue-router 的数据获取
vue-router 的数据获取的两种思路:
-
导航完成之后获取(推荐):先完成导航,然后在接下来的组件生命周期钩子(created)中获取数据。在数据获取期间显示“加载中”之类的指示。
-
导航完成之前获取:导航完成前,在路由进入(beforeRouteEnter)的守卫中获取数据,在数据获取成功后执行导航。
1、导航完成之后获取(推荐)
先完成导航,然后在接下来的组件生命周期钩子(created)中获取数据。在数据获取期间显示“加载中”之类的指示。
假设我们有一个 Post 组件,需要基于 $route.params.id 获取文章数据:
<template>
<div class="post">
<div v-if="loading" class="loading">
Loading...
</div>
<div v-if="error" class="error">
{{ error }}
</div>
<div v-if="post" class="content">
<h2>{{ post.title }}</h2>
<p>{{ post.body }}</p>
</div>
</div>
</template>
export default {
data () {
return {
loading: false,
post: null,
error: null
}
},
created () {
// 组件创建完后获取数据,
// 此时 data 已经被 observed 了
this.fetchData()
},
watch: {
// 如果路由有变化,会再次执行该方法
'$route': 'fetchData'
},
methods: {
fetchData () {
this.error = this.post = null
this.loading = true
// replace getPost with your data fetching util / API wrapper
getPost(this.$route.params.id, (err, post) => {
this.loading = false
if (err) {
this.error = err.toString()
} else {
this.post = post
}
})
}
}
}
2、导航完成之前获取
导航完成前,在路由进入(beforeRouteEnter)的守卫中获取数据,在数据获取成功后调用 next 方法来执行导航。
export default {
data () {
return {
post: null,
error: null
}
},
beforeRouteEnter (to, from, next) {
getPost(to.params.id, (err, post) => {
next(vm => vm.setData(err, post))
})
},
// 路由改变前,组件就已经渲染完了
// 逻辑稍稍不同
beforeRouteUpdate (to, from, next) {
this.post = null
getPost(to.params.id, (err, post) => {
this.setData(err, post)
next()
})
},
methods: {
setData (err, post) {
if (err) {
this.error = err.toString()
} else {
this.post = post
}
}
}
}
在为后面的视图获取数据时,用户会停留在当前的界面,因此建议在数据获取期间,显示一些进度条或者别的指示。如果数据获取失败,同样有必要展示一些全局的错误提醒。
十六、vue-router 的滚动行为
需求:使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。
方案:vue-router 提供了一个方法 scrollBehavior 方法来满足此需求——该方案只在支持 history.pushState() 的浏览器中可用——路由必须采用 history 模式了。
scrollBehavior 方法接收 3 个参数:to、from 和 一个函数,比如 savedPosition() 函数。
- to 和 from 都是路由对象。
- 第三个参数 savedPosition 当且仅当 popstate 导航 (通过浏览器的 前进/后退 按钮触发) 时才可用。
const router = new VueRouter({
routes: [...],
scrollBehavior (to, from, savedPosition) {
// return 期望滚动到哪个的位置
}
})
对于所有路由导航,简单地让页面滚动到顶部。并返回 savedPosition() 函数:
scrollBehavior (to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { x: 0, y: 0 }
}
}
如果你要模拟“滚动到锚点”的行为:
scrollBehavior (to, from, savedPosition) {
if (to.hash) {
return {
selector: to.hash
}
}
}
你也可以返回一个 Promise 来得出预期的位置描述——异步滚动:
scrollBehavior (to, from, savedPosition) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ x: 0, y: 0 })
}, 500)
})
}
十七、vue-router 的路由懒加载
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
结合 Vue 的异步组件和 Webpack 的代码分割功能,轻松实现路由组件的懒加载。
- 将异步组件定义为返回一个 Promise 的工厂函数
- 在 Webpack 2 中,我们可以使用动态 import语法来定义代码分块点 。
注意:如果您使用的是 Babel,你将需要添加 syntax-dynamic-import 插件,才能使 Babel 可以正确地解析语法。
结合这两者,这就是如何定义一个能够被 Webpack 自动代码分割的异步组件。
const Foo = () => import('./Foo.vue')
在路由配置中什么都不需要改变,只需要像往常一样使用 Foo:
const router = new VueRouter({
routes: [
{ path: '/foo', component: Foo }
]
})
把组件按组分块
有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。只需要使用 命名 chunk,一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4)。
const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')
Webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。
十八、vu3 的 组合 API 中 使用 vue-router
安装方式没变。
1、编写路由表
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/pages/home/index.vue')
},
{
path: '/about',
name: 'About',
component: () => import('@/pages/about/index.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
2、在 main 里注册
import { createApp } from 'vue'
import App from './App.vue'
import router from '@/route/routes'
const app = createApp(App)
app.use(router)
app.mount('#app')
3、在 setup 里使用 vue-router 的 API
API 参考 | Vue Router composition-apiVue.js 的官方路由https://router.vuejs.org/zh/api/#composition-api
(1)、Hooks
自 vue3 推广以来,vue-router v4 增加了一些支持 vue3 composition API 的 hooks:
- useRouter:等价于之前的 this.$router 。
- useRoute:等价于之前的 this.$route 。
import { useRouter, useRoute } from 'vue-router'
export default {
setup() {
const router = useRouter()
const route = useRoute()
function pushWithQuery(query) {
router.push({
name: 'search',
query: {
...route.query,
},
})
}
},
}
- useLink:Vue Router 将 RouterLink 的内部行为作为一个组合式 API 函数公开。它提供了与 v-slot API 相同的访问属性。
import { RouterLink, useLink } from 'vue-router'
import { computed } from 'vue'
export default {
name: 'AppLink',
props: {
// 如果使用 TypeScript,请添加 @ts-ignore
...RouterLink.props,
inactiveClass: String,
},
setup(props) {
const { route, href, isActive, isExactActive, navigate } = useLink(props)
const isExternalLink = computed(
() => typeof props.to === 'string' && props.to.startsWith('http')
)
return { isExternalLink, href, navigate, isActive }
},
}
(2)、路由守卫函数
组合式 API 里的路由守卫函数:
- onBeforeRouteLeave:等价于原来的 beforeRouteLeave 路由守卫钩子函数。
- onBeforeRouteUpdate:等价于原来的 beforeRouteUpdate 路由守卫钩子函数。
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
import { ref } from 'vue'
export default {
setup() {
// 与 beforeRouteLeave 相同,无法访问 `this`
onBeforeRouteLeave((to, from) => {
const answer = window.confirm(
'Do you really want to leave? you have unsaved changes!'
)
// 取消导航并停留在同一页面上
if (!answer) return false
})
const userData = ref()
// 与 beforeRouteUpdate 相同,无法访问 `this`
onBeforeRouteUpdate(async (to, from) => {
//仅当 id 更改时才获取用户,例如仅 query 或 hash 值已更改
if (to.params.id !== from.params.id) {
userData.value = await fetchUser(to.params.id)
}
})
},
}
【参考资料】:
vue-router 官网:Vue Router
从头开始学习vue-router:从头开始学习vue-router - 简书
遇到面试vue-router原理:遇到面试vue-router原理_凌晨四点半er的博客-CSDN博客_vue-router原理
浅谈vue-router原理:浅谈vue-router原理 - 简书