单页应用(SPA,Single Page Application)是一种现代网络开发模式。与传统的多页应用相比,SPA在浏览器中仅加载一个HTML页面,并在用户与应用交互时动态更新该页面。
public\index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#root{
border:1px solid red;
}
</style>
</head>
<body>
<div id="root"></div>
<ul>
<li><a href="#/a">/a</a></li>
<li><a href="#/b">/b</a></li>
</ul>
<script>
window.addEventListener('hashchange',()=>{
console.log(window.location.hash);
let pathname = window.location.hash.slice(1);//把最前面的那个#删除
root.innerHTML = pathname;
});
</script>
</body>
</html>
history.pushState()
和history.replaceState()
,和1个事件window.onpopstate
replaceState
是替换浏览器历史堆栈的当前历史记录为设定的urlreplaceState
不会改动浏览器历史堆栈的当前指针history.forward
、history.back
、和history.go
触发,因为这些操作有一个共性,即修改了历史堆栈的当前指针onpopstate
事件History
栈,执行pushState
函数可压入设定的url
至栈顶,同时修改当前指针back
和forward
操作时,history栈大小并不会改变(history.length不变),仅仅移动当前指针的位置<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#root{
border:1px solid red;
height:20px;
}
</style>
</head>
<body>
<div id="root"></div>
<script>
var historyObj = window.history;
//监听路径改变事件 表示将当前的状态变更了,弹出了
window.onpushstate = (event) => {
console.log(event.type,event.detail.state);
root.innerHTML = window.location.pathname;
}
window.addEventListener('popstate', (event) => {
console.log(event.type,event.state);
root.innerHTML = window.location.pathname;
});
(function (historyObj) {
let oldPushState = history.pushState;//缓存原生的pushState
historyObj.pushState = function (state, title, pathname) {
let result = oldPushState.apply(history, arguments);
if (typeof window.onpushstate === 'function') {
window.onpushstate(new CustomEvent('pushstate',{detail:{pathname,state}}));
}
return result;
}
})(historyObj);
let oldHistoryLength = historyObj.length;
setTimeout(() => {
historyObj.pushState({ page: 1 }, { title: 'page1' }, '/page1');//page1
console.log(historyObj.length-oldHistoryLength);
}, 1000);
setTimeout(() => {
historyObj.pushState({ page: 2 }, { title: 'page2' }, '/page2');//page2
console.log(historyObj.length-oldHistoryLength);
}, 2000);
setTimeout(() => {
historyObj.pushState({ page: 3 }, { title: 'page3' }, '/page3');//page3
console.log(historyObj.length-oldHistoryLength);
}, 3000);
setTimeout(() => {
historyObj.back();//historyObj.go(-1);//page2
setTimeout(()=>console.log(historyObj.length-oldHistoryLength),100);
}, 4000);
setTimeout(() => {
historyObj.pushState({ page:4 }, { title: 'page4' }, '/page4');//page4
console.log(historyObj.length-oldHistoryLength);
}, 5000);
setTimeout(() => {
historyObj.go(1);
console.log(historyObj.length-oldHistoryLength);//page4
}, 6000);
</script>
</body>
</html>
<body>
<div id="app">
<p>
<!-- 使用 router-link 组件来导航. -->
<!-- 通过传入 `to` 属性指定链接. -->
<!-- <router-link> 默认会被渲染成一个 `<a>` 标签 -->
<router-link to="/">Home</router-link>
<router-link to="/user">User</router-link>
<router-link to="/profile">Profile</router-link>
</p>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</div>
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<!-- <script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script> -->
<script src=""></script>
<script>
// 1. 定义 (路由) 组件。
const Home = { template: '<div>Home</div>' }
const User = { template: '<div>User</div>' }
const Profile = { template: '<div>Profile</div>' }
// 2. 定义路由 每个路由应该映射一个组件
const routes = [
{ path: '/', component: Home },
{ path: '/user', component: User },
{ path: '/profile', component: Profile }
]
// 3. 创建 router 实例,然后传 `routes` 配置
const router = new VueRouter({
routes
})
// 4. 创建和挂载根实例。
const app = new Vue({
router
}).$mount('#app')
</script>
</body>
这段代码使用 Vue.js 和 Vue-Router 创建了一个简单的单页应用 (SPA)。让我们逐行解析这个代码。
在HTML部分:
<router-link to="/">
创建了一个链接,点击这个链接将路由到 '/'
。在页面中,这将显示为一个链接 (a标签),点击后会导航到 '/'
路由。<router-view></router-view>
是一个容器,它会根据当前的路由渲染对应的组件。在JavaScript部分:
Home
、User
和 Profile
是三个简单的 Vue 组件,它们只有一个模板。routes
是一个数组,它定义了路由的配置。每个路由配置都包含一个路径 path
和一个对应的组件 component
。当用户导航到一个路径时,Vue-Router 会渲染对应的组件。new VueRouter({routes})
创建了一个新的 Vue-Router 实例。这个实例的路由配置来自于 routes
。new Vue({el: '#app', router})
创建了一个新的 Vue 实例,它使用 '#app'
作为挂载点,并且使用我们创建的 router
作为它的路由。运行这段代码,你会看到页面上有三个链接:Home、User 和 Profile。点击这些链接,下面的 <router-view>
中会显示对应的组件。例如,点击 "User" 链接,路由将切换到 '/user',并且 User 组件将被渲染在 <router-view>
中。
var VueRouter = function (options) {
this.routes = options.routes;
let data = {currentPath: window.location.hash.slice(1) || '/'}
new Vue({data});
Object.defineProperty(this, 'currentPath', {
get: function () {
return data.currentPath;
},
set: function (newVal) {
data.currentPath = newVal;
}
});
window.addEventListener('hashchange', () => {
this.currentPath = window.location.hash.slice(1) || '/';
}, false);
window.location.hash = this.currentPath;
}
Vue.component('router-view', {
functional: true,
render: (createElement, { parent }) => {
const matched = parent.$router.routes.find(route => route.path === parent.$router.currentPath);
return matched ? createElement(matched.component) : null;
}
})
Vue.component('router-link', {
props: { to: String },
render: function (createElement) {
return createElement('a', { attrs: { href: '#' + this.to } }, this.$slots.default)
}
})
Vue.use({
install: function (Vue) {
Vue.mixin({
beforeCreate: function () {
if (this.$options.router) {
Vue.prototype.$router = this.$options.router;
}
}
});
}
});
VueRouter
是一个构造函数,它接收一个 options
对象作为参数。这个对象包含一组路由配置。
this.routes = options.routes;
将传入的路由配置保存到 this.routes
中。
data
对象是定义的一个局部变量,用于存储当前的路由路径 currentPath
。currentPath
初始值为当前 URL 的哈希部分,如果没有哈希则默认为 '/'
。
new Vue({data});
创建了一个新的 Vue 实例,使得 currentPath
变成响应式的,这意味着如果 currentPath
改变,那么 Vue 将自动更新与之相关的视图。
用 Object.defineProperty
为 VueRouter
的实例定义了 currentPath
的 getter 和 setter。这样,当我们访问 this.currentPath
或者修改 this.currentPath
时,实际上是访问或者修改 data.currentPath
。
最后,给 window
添加了一个 hashchange
事件监听器。当 URL 的哈希部分改变时,这个监听器将更新 currentPath
的值。
Vue.component('router-view', ...)
创建了一个全局的 router-view
组件。这个组件是一个函数式组件,它的渲染函数查找与当前路径匹配的路由配置,并渲染对应的组件。如果没有找到匹配的路由配置,渲染函数返回 null
。
Vue.component('router-link', ...)
创建了一个全局的 router-link
组件。这个组件接收一个 to
属性,表示链接的目标路径。它的渲染函数返回一个 a
元素,这个元素的 href
属性是 '#' + this.to
,即目标路径的哈希链接。
Vue.use(...)
安装了一个插件。这个插件的 install
方法在每个 Vue 实例创建之前调用。如果一个 Vue 实例的选项对象 (this.$options
) 包含了 router
属性,那么这个 router
属性的值就会被设置为全局的 $router
,这样我们就可以在任何 Vue 组件中通过 this.$router
来访问到这个 VueRouter 实例。
整个代码的主要目可以监控 URL 的哈希部分的变化,然后根据新的哈希值来切换并渲染不同的组件。
我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个 User 组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。那么,我们可以在 vue-router 的路由路径中使用“动态路径参数”(dynamic segment) 来达到这个效果:
<body>
<div id="app">
<p>
<router-link to="/">Home</router-link>
<router-link to="/user/1">User1</router-link>
<router-link to="/user/2">User2</router-link>
<router-link to="/profile">Profile</router-link>
</p>
<router-view></router-view>
</div>
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
<script>
const Home = { template: '<div>Home</div>' }
const User = { template: '<div>User</div>' }
const Profile = { template: '<div>Profile</div>' }
const routes = [
{ path: '/', component: Home },
{ path: '/user/:id', component: User },
{ path: '/profile', component: Profile }
]
const router = new VueRouter({
routes
})
new Vue({
el: '#app',
router
})
</script>
</body>
现在呢,像 /user/foo 和 /user/bar 都将映射到相同的路由。
一个“路径参数”使用冒号 : 标记。当匹配到一个路由时,参数值会被设置到 this.$route.params
,可以在每个组件内使用。于是,我们可以更新 User 的模板,输出当前用户的 ID:
const User = { template: '<div>User {{ $route.params.id }}</div>' }
你可以在一个路由中设置多段“路径参数”,对应的值都会设置到 $route.params 中。例如:
模式 | 匹配路径 | $route.params |
---|---|---|
/user/:username | /user/zhangsan | { username: 'zhangsan' } |
/user/:username/post/:post_id | /user/zhangsan/post/100 | { username: 'evan', post_id: '100' } |
提醒一下,当使用路由参数时,例如从 /user/foo 导航到 /user/bar,原来的组件实例会被复用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会再被调用。
复用组件时,想对路由参数的变化作出响应的话,你可以简单地 watch (监测变化) $route 对象:
或者使用 2.2 中引入的 beforeRouteUpdate 导航守卫:
<body>
<div id="app">
<p>
<router-link to="/">Home</router-link>
<router-link to="/user/1">User1</router-link>
<router-link to="/user/2">User2</router-link>
<router-link to="/profile">Profile</router-link>
</p>
<router-view></router-view>
</div>
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
<script>
const Home = { template: '<div>Home</div>' }
const User = {
template: '<div>User {{ $route.params.id }}</div>',
+ watch: {
+ $route(to, from) {
+ console.log(to, from, this.$route.params.id);
+ }
+ },
+ beforeRouteUpdate(to, from, next) {
+ console.log(to, from, this.$route.params.id);
+ next();
+ }
}
const Profile = { template: '<div>Profile</div>' }
const routes = [
{ path: '/', component: Home },
{ path: '/user/:id', component: User },
{ path: '/profile', component: Profile }
]
const router = new VueRouter({
routes
})
new Vue({
el: '#app',
router
})
</script>
</body>
常规参数只会匹配被 / 分隔的 URL 片段中的字符。如果想匹配任意路径,我们可以使用通配符 (*):
{
// 会匹配所有路径
path: '*'
}
{
// 会匹配以 `/user-` 开头的任意路径
path: '/user-*'
}
当使用通配符路由时,请确保路由的顺序是正确的,也就是说含有通配符的路由应该放在最后。路由 { path: '*' } 通常用于客户端 404 错误。如果你使用了History 模式,请确保正确配置你的服务器。
当使用一个通配符时,$route.params 内会自动添加一个名为 pathMatch 参数。它包含了 URL 通过通配符被匹配的部分:
<body>
<div id="app">
<p>
<router-link to="/">Home</router-link>
<router-link to="/user/1">User1</router-link>
<router-link to="/user/2">User2</router-link>
<router-link to="/profile">Profile</router-link>
+ <router-link to="/goods-phone">goods-phone</router-link>
</p>
<router-view></router-view>
</div>
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
<script>
const Home = { template: '<div>Home</div>' }
const User = {
template: '<div>User {{ $route.params.id }}</div>',
watch: {
$route(to, from) {
console.log(to, from, this.$route.params.id);
}
},
beforeRouteUpdate(to, from, next) {
console.log(to, from, this.$route.params.id);
next();
}
}
const Profile = { template: '<div>Profile</div>' }
+ const Goods = { template: '<div>{{$route.params.pathMatch}}</div>' }
+ const NotFound = { template: '<div>{{$route.params.pathMatch}}</div>' }
const routes = [
{ path: '/', component: Home },
{ path: '/user/:id', component: User },
{ path: '/profile', component: Profile },
+ { path: '/goods-*', component: Goods },
+ { path: '*', component: NotFound }
]
const router = new VueRouter({
routes
})
new Vue({
el: '#app',
router
})
</script>
</body>
有时候,同一个路径可以匹配多个路由,此时,匹配的优先级就按照路由的定义顺序:路由定义得越早,优先级就越高。
实际生活中的应用界面,通常由多层嵌套的组件组合而成。同样地,URL 中各段动态路径也按某种结构对应嵌套的各层组件,例如:
/user/foo/profile /user/foo/posts
+------------------+ +-----------------+
| User | | User |
| +--------------+ | | +-------------+ |
| | Profile | | +------------> | | Posts | |
| | | | | | | |
| +--------------+ | | +-------------+ |
+------------------+ +-----------------+
借助 vue-router,使用嵌套路由配置,就可以很简单地表达这种关系。
接着上节创建的 app:
<div id="app">
<router-view></router-view>
</div>
const User = {
template: '<div>User {{ $route.params.id }}</div>'
}
const router = new VueRouter({
routes: [{ path: '/user/:id', component: User }]
})
这里的 <router-view>
是最顶层的出口,渲染最高级路由匹配到的组件。同样地,一个被渲染组件同样可以包含自己的嵌套 <router-view>
。例如,在 User 组件的模板添加一个 <router-view>
:
const User = {
template: `
<div class="user">
<h2>User {{ $route.params.id }}</h2>
<router-view></router-view>
</div>
`
}
要在嵌套的出口中渲染组件,需要在 VueRouter 的参数中使用 children 配置:
const router = new VueRouter({
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
}
]
}
]
})
要注意,以 / 开头的嵌套路径会被当作根路径。 这让你充分的使用嵌套组件而无须设置嵌套的路径。
你会发现,children 配置就是像 routes 配置一样的路由配置数组,所以呢,你可以嵌套多层路由。
此时,基于上面的配置,当你访问 /user/foo 时,User 的出口是不会渲染任何东西,这是因为没有匹配到合适的子路由。如果你想要渲染点什么,可以提供一个 空的 子路由:
const router = new VueRouter({
routes: [
{
path: '/user/:id',
component: User,
children: [
// 当 /user/:id 匹配成功,
// UserHome 会被渲染在 User 的 <router-view> 中
{ path: '', component: UserHome }
// ...其他子路由
]
}
]
})
<body>
<div id="app">
<ul>
<li><router-link to="/">Home</router-link></li>
<li><router-link to="/user/1">User1</router-link></li>
<li><router-link to="/user/2">User2</router-link></li>
<li><router-link to="/profile">Profile</router-link></li>
<li><router-link to="/goods-phone">goods-phone</router-link></li>
+ <li><router-link to="/user/1">/user/1</router-link></li>
+ <li><router-link to="/user/2/profile">/user/1/profile</router-link></li>
+ <li><router-link to="/user/3/posts">/user/2/post</router-link></li>
</ul>
<router-view></router-view>
</div>
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
<script>
const Home = { template: '<div>Home</div>' }
const User = {
template: `
<div>
User {{ $route.params.id }}
<router-view></router-view>
</div>
`,
watch: {
$route(to, from) {
console.log(to, from, this.$route.params.id);
}
},
beforeRouteUpdate(to, from, next) {
console.log(to, from, this.$route.params.id);
next();
}
}
const Profile = { template: '<div>Profile</div>' }
const Goods = { template: '<div>{{$route.params.pathMatch}}</div>' }
const NotFound = { template: '<div>{{$route.params.pathMatch}}</div>' }
+ const UserHome = { template: '<div>UserHome</div>' }
+ const UserProfile = { template: '<div>UserProfile</div>' }
+ const UserPosts = { template: '<div>UserPosts</div>' }
const routes = [
{ path: '/', component: Home },
{
path: '/user/:id',
component: User,
+ children: [
+ {
+ // 当 /user/:id/profile 匹配成功,
+ // UserProfile 会被渲染在 User 的 <router-view> 中
+ path: '',
+ component: UserHome
+ },
+ {
+ // 当 /user/:id/profile 匹配成功,
+ // UserProfile 会被渲染在 User 的 <router-view> 中
+ path: 'profile',
+ component: UserProfile
+ },
+ {
+ // 当 /user/:id/posts 匹配成功
+ // UserPosts 会被渲染在 User 的 <router-view> 中
+ path: 'posts',
+ component: UserPosts
+ }
+ ]
+ },
{ path: '/profile', component: Profile },
{ path: '/goods-*', component: Goods },
{ path: '*', component: NotFound }
]
const router = new VueRouter({
routes
})
new Vue({
el: '#app',
router
})
</script>
</body>
除了使用 <router-link>
创建 a 标签来定义导航链接,我们还可以借助 router 的实例方法,通过编写代码来实现
router.push(location, onComplete?, onAbort?)
注意:在 Vue 实例内部,你可以通过 $router 访问路由实例。因此你可以调用 this.$router.push
想要导航到不同的 URL,则使用 router.push 方法。这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的 URL。
当你点击 <router-link>
时,这个方法会在内部调用,所以说,点击 <router-link :to="...">
等同于调用 router.push(...)。
声明式 | 编程式 |
---|---|
<router-link :to="..."> |
router.push(...) |
该方法的参数可以是一个字符串路径,或者一个描述地址的对象。例如:
// 字符串
router.push('home')
// 对象
router.push({ path: 'home' })
// 命名的路由
router.push({ name: 'user', params: { userId: '123' }})
// 带查询参数,变成 /register?plan=private
router.push({ path: 'register', query: { plan: 'private' }})
注意:如果提供了 path,params 会被忽略,上述例子中的 query 并不属于这种情况。取而代之的是下面例子的做法,你需要提供路由的 name 或手写完整的带有参数的 path:
const userId = '123'
router.push({ name: 'user', params: { userId }}) // -> /user/123
router.push({ path: `/user/${userId}` }) // -> /user/123
// 这里的 params 不生效
router.push({ path: '/user', params: { userId }}) // -> /user
同样的规则也适用于 router-link 组件的 to 属性。
在 2.2.0+,可选的在 router.push 或 router.replace 中提供 onComplete 和 onAbort 回调作为第二个和第三个参数。这些回调将会在导航成功完成 (在所有的异步钩子被解析之后) 或终止 (导航到相同的路由、或在当前导航完成之前导航到另一个不同的路由) 的时候进行相应的调用。在 3.1.0+,可以省略第二个和第三个参数,此时如果支持 Promise,router.push 或 router.replace 将返回一个 Promise。
注意: 如果目的地和当前路由相同,只有参数发生了改变 (比如从一个用户资料到另一个 /users/1 -> /users/2),你需要使用 beforeRouteUpdate 来响应这个变化 (比如抓取用户信息)
router.replace(location, onComplete?, onAbort?)
跟 router.push 很像,唯一的不同就是,它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录。
声明式 | 编程式 |
---|---|
router.replace(...) |
router.go(n)
这个方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步,类似 window.history.go(n)。
例子
// 在浏览器记录中前进一步,等同于 history.forward()
router.go(1)
// 后退一步记录,等同于 history.back()
router.go(-1)
// 前进 3 步记录
router.go(3)
// 如果 history 记录不够用,那就默默地失败呗
router.go(-100)
router.go(100)
操作 History
你也许注意到 router.push、 router.replace 和 router.go 跟 window.history.pushState、 window.history.replaceState 和 window.history.go (opens new window)好像, 实际上它们确实是效仿 window.history API 的。
因此,如果你已经熟悉 Browser History APIs (opens new window),那么在 Vue Router 中操作 history 就是超级简单的。
还有值得提及的,Vue Router 的导航方法 (push、 replace、 go) 在各类路由模式 (history、 hash 和 abstract) 下表现一致。
<body>
<div id="app">
<ul>
<li><router-link to="/">Home</router-link></li>
<li><router-link to="/user/1">User1</router-link></li>
<li><router-link to="/user/2">User2</router-link></li>
<li><router-link to="/profile">Profile</router-link></li>
<li><router-link to="/goods-phone">goods-phone</router-link></li>
<li><router-link to="/user/1">/user/1</router-link></li>
<li><router-link to="/user/2/profile">/user/1/profile</router-link></li>
<li><router-link to="/user/3/posts">/user/2/post</router-link></li>
</ul>
<router-view></router-view>
</div>
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
<script>
const Home = { template: '<div>Home</div>' }
const User = {
template: `
<div>
+ <p>User paramsId {{ $route.params.id }}</p>
+ <p>User queryId {{ $route.query.id }}</p>
<router-view></router-view>
</div>
`,
watch: {
$route(to, from) {
console.log(to, from, this.$route.params.id);
}
},
beforeRouteUpdate(to, from, next) {
console.log(to, from, this.$route.params.id);
next();
}
}
const Profile = { template: '<div>Profile</div>' }
const Goods = { template: '<div>{{$route.params.pathMatch}}</div>' }
const NotFound = { template: '<div>{{$route.params.pathMatch}}</div>' }
const UserHome = { template: '<div>UserHome</div>' }
const UserProfile = { template: '<div>UserProfile</div>' }
const UserPosts = { template: '<div>UserPosts</div>' }
const routes = [
{ path: '/', component: Home },
{
name:'user',
path: '/user/:id',
component: User,
children: [
{
// 当 /user/:id/profile 匹配成功,
// UserProfile 会被渲染在 User 的 <router-view> 中
path: '',
component: UserHome
},
{
// 当 /user/:id/profile 匹配成功,
// UserProfile 会被渲染在 User 的 <router-view> 中
path: 'profile',
component: UserProfile
},
{
// 当 /user/:id/posts 匹配成功
// UserPosts 会被渲染在 User 的 <router-view> 中
path: 'posts',
component: UserPosts
}
]
},
{ path: '/profile', component: Profile },
{ path: '/goods-*', component: Goods },
{ path: '*', component: NotFound }
]
const router = new VueRouter({
routes
})
var vm = new Vue({
el: '#app',
router
})
+ //字符串
+ vm.$router.push('/')
+ // 对象
+ vm.$router.push({ path: '/user/1' })
+ // 命名的路由
+ vm.$router.push({ name: 'user', params: { id: '100' }})
+ // 带查询参数,变成 /user?userId=200
+ vm.$router.push({ name: 'user', query: { id: '200' }})
+ // 替换掉当前的 history 记录
+ vm.$router.replace({ path: '/profile'})
+ // 后退一步记录,等同于 history.back()
+ vm.$router.go(-1)
+ // 在浏览器记录中前进一步,等同于 history.forward()
+ vm.$router.go(1)
</script>
</body>
有时候,通过一个名称来标识一个路由显得更方便一些,特别是在链接一个路由,或者是执行一些跳转的时候。你可以在创建 Router 实例的时候,在 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 } })
这两种方式都会把路由导航到 /user/123 路径。
有时候想同时 (同级) 展示多个视图,而不是嵌套展示,例如创建一个布局,有 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
}
}
]
})
<body>
<div id="app">
<ul>
<li><router-link to="/">Home</router-link></li>
<li><router-link to="/user/1">User1</router-link></li>
<li><router-link to="/user/2">User2</router-link></li>
<li><router-link to="/profile">Profile</router-link></li>
<li><router-link to="/goods-phone">goods-phone</router-link></li>
<li><router-link to="/user/1">/user/1</router-link></li>
<li><router-link to="/user/2/profile">/user/1/profile</router-link></li>
<li><router-link to="/user/3/posts">/user/2/post</router-link></li>
<li><router-link to="/layout">layout</router-link></li>
</ul>
+ <router-view name="header"></router-view>
+ <router-view></router-view>
+ <router-view name="footer"></router-view>
</div>
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
<script>
const Home = { template: '<div>Home</div>' }
const User = {
template: `
<div>
<p>User paramsId {{ $route.params.id }}</p>
<p>User queryId {{ $route.query.id }}</p>
<router-view></router-view>
</div>
`,
watch: {
$route(to, from) {
console.log(to, from, this.$route.params.id);
}
},
beforeRouteUpdate(to, from, next) {
console.log(to, from, this.$route.params.id);
next();
}
}
const Profile = { template: '<div>Profile</div>' }
const Goods = { template: '<div>{{$route.params.pathMatch}}</div>' }
const NotFound = { template: '<div>{{$route.params.pathMatch}}</div>' }
const UserHome = { template: '<div>UserHome</div>' }
const UserProfile = { template: '<div>UserProfile</div>' }
const UserPosts = { template: '<div>UserPosts</div>' }
const routes = [
{ path: '/', component: Home },
{
name: 'user',
path: '/user/:id',
component: User,
children: [
{
// 当 /user/:id/profile 匹配成功,
// UserProfile 会被渲染在 User 的 <router-view> 中
path: '',
component: UserHome
},
{
// 当 /user/:id/profile 匹配成功,
// UserProfile 会被渲染在 User 的 <router-view> 中
path: 'profile',
component: UserProfile
},
{
// 当 /user/:id/posts 匹配成功
// UserPosts 会被渲染在 User 的 <router-view> 中
path: 'posts',
component: UserPosts
}
]
},
{ path: '/profile', component: Profile },
{ path: '/goods-*', component: Goods },
+ {
+ path: '/layout',
+ components: {
+ header: { template: '<div>header</div>' },
+ default: { template: '<div>main</div>' },
+ footer: {template: '<div>footer</div>'}
+ },
+ },
{ path: '*', component: NotFound }
]
const router = new VueRouter({
routes
})
var vm = new Vue({
el: '#app',
router
})
//字符串
vm.$router.push('/')
// 对象
vm.$router.push({ path: '/user/1' })
// 命名的路由
vm.$router.push({ name: 'user', params: { id: '100' } })
// 带查询参数,变成 /user?userId=200
vm.$router.push({ name: 'user', query: { id: '200' } })
// 替换掉当前的 history 记录
vm.$router.replace({ path: '/profile' })
// 后退一步记录,等同于 history.back()
vm.$router.go(-1)
// 在浏览器记录中前进一步,等同于 history.forward()
vm.$router.go(1)
</script>
</body>
我们也有可能使用命名视图创建嵌套视图的复杂布局。这时你也需要命名用到的嵌套 router-view 组件。我们以一个设置面板为例:
/settings/emails /settings/profile
+-----------------------------------+ +------------------------------+
| UserSettings | | UserSettings |
| +-----+-------------------------+ | | +-----+--------------------+ |
| | Nav | UserEmailsSubscriptions | | +------------> | | Nav | UserProfile | |
| | +-------------------------+ | | | +--------------------+ |
| | | | | | | | UserProfilePreview | |
| +-----+-------------------------+ | | +-----+--------------------+ |
+-----------------------------------+ +------------------------------+
注意:我们先忘记 HTML/CSS 具体的布局的样子,只专注在用到的组件上。
UserSettings 组件的 <template>
部分应该是类似下面的这段代码:
<!-- UserSettings.vue -->
<div>
<h1>User Settings</h1>
<NavBar/>
<router-view/>
<router-view name="helper"/>
</div>
然后你可以用这个路由配置完成该布局:
{
path: '/settings',
// 你也可以在顶级路由就配置命名视图
component: UserSettings,
children: [{
path: 'emails',
component: UserEmailsSubscriptions
}, {
path: 'profile',
components: {
default: UserProfile,
helper: UserProfilePreview
}
}]
}
<body>
<div id="app">
<ul>
<li><router-link to="/">Home</router-link></li>
<li><router-link to="/user/1">User1</router-link></li>
<li><router-link to="/user/2">User2</router-link></li>
<li><router-link to="/profile">Profile</router-link></li>
<li><router-link to="/goods-phone">goods-phone</router-link></li>
<li><router-link to="/user/1">/user/1</router-link></li>
<li><router-link to="/user/2/profile">/user/1/profile</router-link></li>
<li><router-link to="/user/3/posts">/user/2/post</router-link></li>
<li><router-link to="/layout">layout</router-link></li>
</ul>
+ <router-view></router-view>
</div>
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
<script>
const Home = { template: '<div>Home</div>' }
const User = {
template: `
<div>
<p>User paramsId {{ $route.params.id }}</p>
<p>User queryId {{ $route.query.id }}</p>
<router-view></router-view>
</div>
`,
watch: {
$route(to, from) {
console.log(to, from, this.$route.params.id);
}
},
beforeRouteUpdate(to, from, next) {
console.log(to, from, this.$route.params.id);
next();
}
}
const Profile = { template: '<div>Profile</div>' }
const Goods = { template: '<div>{{$route.params.pathMatch}}</div>' }
const NotFound = { template: '<div>{{$route.params.pathMatch}}</div>' }
const UserHome = { template: '<div>UserHome</div>' }
const UserProfile = { template: '<div>UserProfile</div>' }
const UserPosts = { template: '<div>UserPosts</div>' }
+ const Layout = {
+ template: `
+ <div>
+ <router-view name="header"></router-view>
+ <router-view></router-view>
+ <router-view name="footer"></router-view>
+ </div>
+ `,
+ }
const routes = [
{ path: '/', component: Home },
{
name: 'user',
path: '/user/:id',
component: User,
children: [
{
path: '',
component: UserHome
},
{
path: 'profile',
component: UserProfile
},
{
path: 'posts',
component: UserPosts
}
]
},
{ path: '/profile', component: Profile },
{ path: '/goods-*', component: Goods },
+ {
+ path: '/layout',
+ component: Layout,
+ children: [
+ {
+ path: '',
+ components: {
+ header: { template: '<div>header</div>' },
+ default: { template: '<div>main</div>' },
+ footer: { template: '<div>footer</div>' }
+ }
+ }
+ ]
+ },
{ path: '*', component: NotFound }
]
const router = new VueRouter({
routes
})
var vm = new Vue({
el: '#app',
router
})
</script>
</body>
重定向也是通过 routes 配置来完成,下面例子是从 /a 重定向到 /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 重定向的 字符串路径/路径对象
}}
]
})
注意导航守卫并没有应用在跳转路由上,而仅仅应用在其目标上。在下面这个例子中,为 /a 路由添加一个 beforeEnter 守卫并不会有任何效果。
<body>
<div id="app">
<ul>
<li><router-link to="/">Home</router-link></li>
<li><router-link to="/user/1">User1</router-link></li>
<li><router-link to="/user/2">User2</router-link></li>
<li><router-link to="/profile">Profile</router-link></li>
<li><router-link to="/goods-phone">goods-phone</router-link></li>
<li><router-link to="/user/1">/user/1</router-link></li>
<li><router-link to="/user/2/profile">/user/1/profile</router-link></li>
<li><router-link to="/user/3/posts">/user/2/post</router-link></li>
<li><router-link to="/layout">layout</router-link></li>
</ul>
<router-view></router-view>
</div>
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
<script>
const Home = { template: '<div>Home</div>' }
const User = {
template: `
<div>
<p>User paramsId {{ $route.params.id }}</p>
<p>User queryId {{ $route.query.id }}</p>
<router-view></router-view>
</div>
`,
watch: {
$route(to, from) {
console.log(to, from, this.$route.params.id);
}
},
beforeRouteUpdate(to, from, next) {
console.log(to, from, this.$route.params.id);
next();
}
}
const Profile = { template: '<div>Profile</div>' }
const Goods = { template: '<div>{{$route.params.pathMatch}}</div>' }
const NotFound = { template: '<div>{{$route.params.pathMatch}}</div>' }
const UserHome = { template: '<div>UserHome</div>' }
const UserProfile = { template: '<div>UserProfile</div>' }
const UserPosts = { template: '<div>UserPosts</div>' }
const Layout = {
template: `
<div>
<router-view name="header"></router-view>
<router-view></router-view>
<router-view name="footer"></router-view>
</div>
`,
}
const routes = [
+ { name: 'home',path: '/', component: Home },
{
name: 'user',
path: '/user/:id',
component: User,
children: [
{
path: '',
component: UserHome
},
{
path: 'profile',
component: UserProfile
},
{
path: 'posts',
component: UserPosts
}
]
},
{ path: '/profile', component: Profile },
{ path: '/goods-*', component: Goods },
{
path: '/layout',
component: Layout,
children: [
{
path: '',
components: {
header: { template: '<div>header</div>' },
default: { template: '<div>main</div>' },
footer: { template: '<div>footer</div>' }
}
}
]
},
+ { path: '/a', redirect:'/' },
+ { path: '/b', redirect:{name:'home'} },
+ { path: '/c', redirect:(to)=>{
+ return {name:'home'};
+ } },
{ path: '*', component: NotFound }
]
const router = new VueRouter({
routes
})
var vm = new Vue({
el: '#app',
router
})
</script>
</body>
“重定向”的意思是,当用户访问 /a时,URL 将会被替换成 /b,然后匹配路由为 /b,那么“别名”又是什么呢?
/a 的别名是 /b,意味着,当用户访问 /b 时,URL 会保持为 /b,但是路由匹配则为 /a,就像用户访问 /a 一样。
上面对应的路由配置为:
const router = new VueRouter({
routes: [
{ path: '/a', component: A, alias: '/b' }
]
})
“别名”的功能让你可以自由地将 UI 结构映射到任意的 URL,而不是受限于配置的嵌套路由结构。
const routes = [
+ { name: 'home',path: '/', component: Home ,alias:'/homepage'},
]
在组件中使用 $route 会使之与其对应路由形成高度耦合,从而使组件只能在某些特定的 URL 上使用,限制了其灵活性。
使用 props 将组件和路由解耦:
取代与 $route 的耦合
const User = {
template: '<div>User {{ $route.params.id }}</div>'
}
const router = new VueRouter({
routes: [{ path: '/user/:id', component: User }]
})
const User = {
props: ['id'],
template: '<div>User {{ id }}</div>'
}
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: User, props: true },
// 对于包含命名视图的路由,你必须分别为每个命名视图添加 `props` 选项:
{
path: '/user/:id',
components: { default: User, sidebar: Sidebar },
props: { default: true, sidebar: false }
}
]
})
这样你便可以在任何地方使用该组件,使得该组件更易于重用和测试。
布尔模式 如果 props 被设置为 true,route.params 将会被设置为组件属性。
#对象模式 如果 props 是一个对象,它会被按原样设置为组件属性。当 props 是静态的时候有用。
const router = new VueRouter({
routes: [
{
path: '/promotion/from-newsletter',
component: Promotion,
props: { newsletterPopup: false }
}
]
})
函数模式 你可以创建一个函数返回 props。这样你便可以将参数转换成另一种类型,将静态值与基于路由的值结合等等。
const router = new VueRouter({
routes: [
{
path: '/search',
component: SearchUser,
props: route => ({ query: route.query.q })
}
]
})
URL /search?q=vue 会将 {query: 'vue'} 作为属性传递给 SearchUser 组件。
请尽可能保持 props 函数为无状态的,因为它只会在路由发生变化时起作用。如果你需要状态来定义 props,请使用包装组件,这样 Vue 才可以对状态变化做出反应。
<body>
<div id="app">
<ul>
<li><router-link to="/">Home</router-link></li>
<li><router-link to="/user/1">User1</router-link></li>
<li><router-link to="/user/2">User2</router-link></li>
<li><router-link to="/profile">Profile</router-link></li>
<li><router-link to="/goods-phone">goods-phone</router-link></li>
<li><router-link to="/user/1">/user/1</router-link></li>
<li><router-link to="/user/2/profile">/user/1/profile</router-link></li>
<li><router-link to="/user/3/posts">/user/2/post</router-link></li>
<li><router-link to="/layout">layout</router-link></li>
</ul>
<router-view></router-view>
</div>
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
<script>
const Home = { template: '<div>Home</div>' }
const User = {
+ props:['id','customeId','queryId'],
template: `
<div>
+ <p>User paramsId {{ id }}</p>
+ <p>User queryId {{ $route.query.queryId }}</p>
+ <p>User customeId {{ customeId }}</p>
<router-view></router-view>
</div>
`,
watch: {
$route(to, from) {
console.log(to, from, this.$route.params.id);
}
},
beforeRouteUpdate(to, from, next) {
console.log(to, from, this.$route.params.id);
next();
}
}
const Profile = { template: '<div>Profile</div>' }
const Goods = { template: '<div>{{$route.params.pathMatch}}</div>' }
const NotFound = { template: '<div>{{$route.params.pathMatch}}</div>' }
const UserHome = { template: '<div>UserHome</div>' }
const UserProfile = { template: '<div>UserProfile</div>' }
const UserPosts = { template: '<div>UserPosts</div>' }
const Layout = {
template: `
<div>
<router-view name="header"></router-view>
<router-view></router-view>
<router-view name="footer"></router-view>
</div>
`,
}
const routes = [
{ name: 'home',path: '/', component: Home ,alias:'/homepage'},
{
name: 'user',
path: '/user/:id',
component: User,
+ //props: true,
+ //props: {customeId:'250'},
+ props: route => ({ queryId: route.query.queryId,id: route.params.id,customeId:'250' }),
children: [
{
path: '',
component: UserHome
},
{
path: 'profile',
component: UserProfile
},
{
path: 'posts',
component: UserPosts
}
]
},
{ path: '/profile', component: Profile },
{ path: '/goods-*', component: Goods },
{
path: '/layout',
component: Layout,
children: [
{
path: '',
components: {
header: { template: '<div>header</div>' },
default: { template: '<div>main</div>' },
footer: { template: '<div>footer</div>' }
}
}
]
},
{ path: '/a', redirect:'/' },
{ path: '/b', redirect:{name:'home'} },
{ path: '/c', redirect:(to)=>{
return {name:'home'};
} },
{ path: '*', component: NotFound }
]
const router = new VueRouter({
routes
})
var vm = new Vue({
el: '#app',
router
})
</script>
</body>
“导航”表示路由正在发生改变。 正如其名,vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。 记住参数或查询的改变并不会触发进入/离开的导航守卫。你可以通过观察 $route 对象来应对这些变化,或使用 beforeRouteUpdate 的组件内守卫。
配置位置 | 钩子名称 | 钩子触发时机 | 讲解 |
---|---|---|---|
全局 | beforeEach | 在路由跳转之前触发 | 全局前置守卫 |
全局 | beforeResolve | 在路由跳转之前触发 | 全局解析守卫 |
全局 | afterEach | 在路由跳转之后触发 | 全局后置钩子 |
路由配置 | beforeEnter | 在路由跳转之前触发 | 路由独享守卫 |
组件内 | beforeRouteEnter | 在路由跳转之前触发 | 组件内守卫 |
组件内 | beforeRouteUpdate | 在路由跳转之前触发 | 组件内守卫 |
组件内 | beforeRouteLeave | 在路由跳转之前触发 | 组件内守卫 |
你可以使用 router.beforeEach 注册一个全局前置守卫:
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
})
当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中。
每个守卫方法接收三个参数:
to: Route: 即将要进入的目标 路由对象
from: Route: 当前导航正要离开的路由
next: Function: 一定要调用该方法来 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() 注册过的回调。
确保 next 函数在任何给定的导航守卫中都被严格调用一次。它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错。这里有一个在用户未能验证身份时重定向到 /login 的示例:
<body>
<div id="app">
<ul>
<li><router-link to="/">Home</router-link></li>
<li><router-link to="/user">User</router-link></li>
<li><router-link to="/profile">Profile</router-link></li>
</ul>
<router-view></router-view>
</div>
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
<script>
const Home = { template: '<div>Home</div>' }
const User = { template: '<div>User</div>' }
const Profile = { template: '<div>Profile</div>' }
const Login = { template: '<div>Login</div>' }
const routes = [
{ name: 'home', path: '/', component: Home, alias: '/homepage' },
{ name: 'user', path: '/user', component: User },
{ name: 'profile', path: '/profile', component: Profile },
{ name: 'login', path: '/login', component: Login }
]
const router = new VueRouter({
routes
})
var vm = new Vue({
el: '#app',
router
})
function isLogin(){
return !!localStorage.getItem('isLogin');
}
//注册vm.$router的onError回调
vm.$router.onError((err) => {
console.log(err.message);
})
vm.$router.beforeEach((to, from, next) => {
if(to.name === 'profile' && !isLogin()){
//中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
//next(false);
//next('/') 或者 next({ path: '/' }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航
//next('/login');
//next({ name: 'login' })
//果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给 router.onError() 注册过的回调
next(new Error('用户未登录'));
}else{
//进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)
next();
}
})
</script>
</body>
在 2.5.0+ 你可以用 router.beforeResolve 注册一个全局守卫。这和 router.beforeEach 类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。
在 Vue Router 中,beforeResolve
是一个全局解析守卫。它和 beforeEach
守卫非常相似,但 beforeResolve
守卫会在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。
beforeResolve
守卫的用途可以有很多,其中一种常见的场景是,当你希望在用户导航到一个新页面之前执行一些代码,比如请求一些额外的数据,或者做一些清理工作。
beforeResolve
守卫的签名和 beforeEach
守卫一样:
router.beforeResolve((to, from, next) => {
// to: Route: 即将要进入的目标路由对象
// from: Route: 当前导航正要离开的路由
// next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。
})
其中 to
和 from
都是路由对象,代表即将进入的新路由和即将离开的旧路由。而 next
是一个函数,必须在守卫中调用,否则钩子就不会被 resolved。调用 next()
会移动到管道中的下一个钩子;也可以调用 next(false)
来中断当前的导航,或者调用 next(route)
来重定向到一个不同的路由。
请注意,beforeResolve
是全局守卫,对所有路由切换都起作用。如果你只希望对某个特定的路由做处理,可以考虑使用 per-route 守卫(在路由配置上直接定义 beforeEnter
)或者 component 内的守卫(例如 beforeRouteEnter
, beforeRouteUpdate
, beforeRouteLeave
)。
记住,在使用 beforeResolve
守卫时,你需要确保正确调用 next()
函数,否则可能导致导航无法继续,用户不能顺利导航到他们期望的页面。
beforeEach
和 beforeResolve
是 Vue Router 中的两个不同类型的全局守卫,它们在不同的时机被调用,因此可以用于处理不同的场景。
beforeEach
守卫
beforeEach
是最常用的全局前置守卫,它在路由导航被触发后立即调用,但在进入路由配置的 beforeEnter
守卫、组件内的守卫和异步路由组件被解析之前调用。你可以利用这个守卫做一些预处理工作,例如检查用户的认证状态,如果用户未登录,就重定向到登录页面。
beforeResolve
守卫
beforeResolve
守卫则稍晚一些,它在路由导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后调用。这个守卫的一个常见用途是在导航确认前做一些最后的处理,例如获取需要在页面渲染前获取的异步数据。
选择哪一个
选择使用 beforeEach
还是 beforeResolve
主要取决于你的具体需求,如果你需要在路由导航最开始的时候就做一些预处理工作,那么 beforeEach
可能是更好的选择。而如果你需要在导航确认前,所有组件内的守卫和异步组件都解析完毕后,再做一些最后的处理,那么 beforeResolve
会是更好的选择。
但需要注意的是,beforeEach
和 beforeResolve
守卫都是全局守卫,会对所有路由切换产生影响。如果你只希望对某些特定路由做处理,可能需要考虑使用路由独享的守卫(在路由配置上直接定义的 beforeEnter
)或者组件内的守卫(例如 beforeRouteEnter
, beforeRouteUpdate
, beforeRouteLeave
)。
<body>
<div id="app">
<ul>
<li><router-link to="/">Home</router-link></li>
<li><router-link to="/user">User</router-link></li>
<li><router-link to="/profile">Profile</router-link></li>
</ul>
<router-view></router-view>
</div>
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
<script>
const Home = { template: '<div>Home</div>' }
const User = { template: '<div>User</div>' }
const Profile = { template: '<div>Profile</div>' }
const Login = { template: '<div>Login</div>' }
const routes = [
{ name: 'home', path: '/', component: Home, alias: '/homepage' },
{ name: 'user', path: '/user', component: User },
{ name: 'profile', path: '/profile', component: Profile },
{ name: 'login', path: '/login', component: Login }
]
const router = new VueRouter({
routes
})
var vm = new Vue({
el: '#app',
router
})
function isLogin() {
return !!localStorage.getItem('isLogin');
}
//注册vm.$router的onError回调
vm.$router.onError((err) => {
console.log(err.message);
})
+ vm.$router.beforeEach((to, from, next) => {
+ console.log('beforeEach');
+ next();
+ })
+ router.beforeResolve((to, from, next) => {
+ // to: Route: 即将要进入的目标路由对象
+ // from: Route: 当前导航正要离开的路由
+ // next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。
+ console.log('beforeResolve');
+ next();
+ })
</script>
</body>
你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:
vm.$router.afterEach((to, from) => {
console.log('afterEach');
})
你可以在路由配置上直接定义 beforeEnter 守卫:
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
这些守卫与全局前置守卫的方法参数是一样的。
const routes = [
{ name: 'home', path: '/', component: Home, alias: '/homepage' },
{
name: 'user',
path: '/user',
component: User,
+ beforeEnter: (to, from, next) => {
+ console.log('beforeEnter');
+ next();
}
},
{ name: 'profile', path: '/profile', component: Profile },
{ name: 'login', path: '/login', component: Login }
]
最后,你可以在路由组件内直接定义以下路由导航守卫:
const Foo = {
template: `...`,
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}
beforeRouteEnter 守卫 不能 访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。
不过,你可以通过传一个回调给 next来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。
beforeRouteEnter (to, from, next) {
next(vm => {
// 通过 `vm` 访问组件实例
})
}
注意 beforeRouteEnter 是支持给 next 传递回调的唯一守卫。对于 beforeRouteUpdate 和 beforeRouteLeave 来说,this 已经可用了,所以不支持传递回调,因为没有必要了。
beforeRouteUpdate (to, from, next) {
// just use `this`
this.name = to.params.name
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)
}
}
<body>
<div id="app">
<ul>
<li><router-link to="/">Home</router-link></li>
+ <li><router-link to="/user/1">User1</router-link></li>
+ <li><router-link to="/user/2">User2</router-link></li>
<li><router-link to="/profile">Profile</router-link></li>
</ul>
<router-view></router-view>
</div>
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
<script>
const Home = {
template: '<div>Home</div>',
+ beforeRouteEnter(to, from, next) {
+ console.log(to.name,'beforeRouteEnter');
+ next();
+ },
+ beforeRouteUpdate(to, from, next) {
+ console.log(to.name,'beforeRouteUpdate');
+ next();
+ },
+ beforeRouteLeave(to, from, next) {
+ console.log(from.name,'beforeRouteLeave');
+ next();
+ }
}
const User = {
template: '<div>User</div>',
+ beforeRouteEnter(to, from, next) {
+ console.log(to.name,'beforeRouteEnter');
+ next();
+ },
+ beforeRouteUpdate(to, from, next) {
+ console.log(to.name,'beforeRouteUpdate');
+ next();
+ },
+ beforeRouteLeave(to, from, next) {
+ console.log(from.name,'beforeRouteLeave');
+ next();
+ }
}
const Profile = { template: '<div>Profile</div>' }
const Login = { template: '<div>Login</div>' }
const routes = [
{
name: 'home', path: '/', component: Home, alias: '/homepage',
+ beforeEnter: (to, from, next) => {
+ console.log(to.name, 'beforeEnter');
+ next();
+ }
},
{
name: 'user',
path: '/user/:id',
component: User,
+ beforeEnter: (to, from, next) => {
+ console.log(to.name, 'beforeEnter');
+ next();
+ }
},
{ name: 'profile', path: '/profile', component: Profile },
{ name: 'login', path: '/login', component: Login }
]
const router = new VueRouter({
routes
})
var vm = new Vue({
el: '#app',
router
})
function isLogin() {
return !!localStorage.getItem('isLogin');
}
+ //注册vm.$router的onError回调
+ vm.$router.onError((err) => {
+ console.log(err.message);
+ })
+ vm.$router.beforeEach((to, from, next) => {
+ console.log(from.name, to.name, 'beforeEach');
+ next();
+ })
+ vm.$router.beforeResolve((to, from, next) => {
+ console.log(from.name, to.name, 'beforeResolve');
+ next();
+ })
+ vm.$router.afterEach((to, from) => {
+ console.log(from.name, to.name, 'afterEach');
+ })
</script>
</body>
1.导航被触发。 2.在失活的组件里调用 beforeRouteLeave 守卫。 3.调用全局的 beforeEach 守卫。 4.在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。 5.在路由配置里调用 beforeEnter。 6.解析异步路由组件。 7.在被激活的组件里调用 beforeRouteEnter。 8.调用全局的 beforeResolve 守卫 (2.5+)。 9.导航被确认。 10.调用全局的 afterEach 钩子。 11.触发 DOM 更新。 12.调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
home=>user1
home beforeRouteLeave
home user beforeEach
user beforeEnter
user beforeRouteEnter
home user beforeResolve
home user afterEach
user1=>user1
user user beforeEach
user beforeRouteUpdate
user user beforeResolve
user user afterEach
定义路由的时候可以配置 meta 字段:
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
children: [
{
path: 'bar',
component: Bar,
// a meta field
meta: { requiresAuth: true }
}
]
}
]
})
那么如何访问这个 meta 字段呢?
首先,我们称呼 routes 配置中的每个路由对象为 路由记录。路由记录可以是嵌套的,因此,当一个路由匹配成功后,他可能匹配多个路由记录
例如,根据上面的路由配置,/foo/bar 这个 URL 将会匹配父路由记录以及子路由记录。
一个路由匹配到的所有路由记录会暴露为 $route 对象 (还有在导航守卫中的路由对象) 的 $route.matched 数组。因此,我们需要遍历 $route.matched 来检查路由记录中的 meta 字段。
下面例子展示在全局导航守卫中检查元字段:
<body>
<div id="app">
<ul>
<li><router-link to="/">Home</router-link></li>
<li><router-link to="/user/1">User1</router-link></li>
<li><router-link to="/user/2">User2</router-link></li>
<li><router-link to="/profile">Profile</router-link></li>
</ul>
<router-view></router-view>
</div>
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
<script>
const Home = {
template: '<div>Home</div>',
beforeRouteEnter(to, from, next) {
console.log(to.name, 'beforeRouteEnter');
next();
},
beforeRouteUpdate(to, from, next) {
console.log(to.name, 'beforeRouteUpdate');
next();
},
beforeRouteLeave(to, from, next) {
console.log(from.name, 'beforeRouteLeave');
next();
}
}
const User = {
template: '<div>User</div>',
beforeRouteEnter(to, from, next) {
console.log(to.name, 'beforeRouteEnter');
next();
},
beforeRouteUpdate(to, from, next) {
console.log(to.name, 'beforeRouteUpdate');
next();
},
beforeRouteLeave(to, from, next) {
console.log(from.name, 'beforeRouteLeave');
next();
}
}
const Profile = {
template: '<div>Profile</div>',
}
const Login = { template: '<div>Login</div>' }
const routes = [
{
name: 'home', path: '/', component: Home, alias: '/homepage',
beforeEnter: (to, from, next) => {
console.log(to.name, 'beforeEnter');
next();
}
},
{
name: 'user',
path: '/user/:id',
component: User,
beforeEnter: (to, from, next) => {
console.log(to.name, 'beforeEnter');
next();
}
},
+ { name: 'profile', path: '/profile', component: Profile ,meta: { requiresAuth: true }},
{ name: 'login', path: '/login', component: Login }
]
const router = new VueRouter({
routes
})
var vm = new Vue({
el: '#app',
router
})
function isLogin() {
return !!localStorage.getItem('isLogin');
}
//注册vm.$router的onError回调
vm.$router.onError((err) => {
console.log(err.message);
})
vm.$router.beforeEach((to, from, next) => {
+ if (to.matched.some(record => record.meta.requiresAuth)) {
+ if (!isLogin()) {
+ next('/login')
+ } else {
+ next()
+ }
+ } else {
+ next() // 确保一定要调用 next()
+ }
})
vm.$router.beforeResolve((to, from, next) => {
console.log(from.name, to.name, 'beforeResolve');
next();
})
vm.$router.afterEach((to, from) => {
console.log(from.name, to.name, 'afterEach');
})
</script>
</body>
有时候,进入某个路由后,需要从服务器获取数据。例如,在渲染用户信息时,你需要从服务器获取用户的数据。我们可以通过两种方式来实现:
导航完成之后获取:先完成导航,然后在接下来的组件生命周期钩子中获取数据。在数据获取期间显示“加载中”之类的指示。
导航完成之前获取:导航完成前,在路由进入的守卫中获取数据,在数据获取成功后执行导航。
从技术角度讲,两种方式都不错 —— 就看你想要的用户体验是哪种。
当你使用这种方式时,我们会马上导航和渲染组件,然后在组件的 created 钩子中获取数据。这让我们有机会在数据获取期间展示一个 loading 状态,还可以在不同视图间展示不同的 loading 状态。
假设我们有一个 Post 组件,需要基于 $route.params.id 获取文章数据:
<body>
<div id="app">
<ul>
<li><router-link to="/">Home</router-link></li>
<li><router-link to="/user/1">User1</router-link></li>
<li><router-link to="/user/2">User2</router-link></li>
<li><router-link to="/profile">Profile</router-link></li>
+ <li><router-link to="/post/1">/post/1</router-link></li>
+ <li><router-link to="/post/2">/post/2</router-link></li>
</ul>
<router-view></router-view>
</div>
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
<script>
function getPost(id, callback) {
callback(null, {
id,
title: 'title' + id,
body: 'body' + id
});
}
const Home = {
template: '<div>Home</div>',
beforeRouteEnter(to, from, next) {
console.log(to.name, 'beforeRouteEnter');
next();
},
beforeRouteUpdate(to, from, next) {
console.log(to.name, 'beforeRouteUpdate');
next();
},
beforeRouteLeave(to, from, next) {
console.log(from.name, 'beforeRouteLeave');
next();
}
}
const User = {
template: '<div>User</div>',
beforeRouteEnter(to, from, next) {
console.log(to.name, 'beforeRouteEnter');
next();
},
beforeRouteUpdate(to, from, next) {
console.log(to.name, 'beforeRouteUpdate');
next();
},
beforeRouteLeave(to, from, next) {
console.log(from.name, 'beforeRouteLeave');
next();
}
}
const Profile = {
template: '<div>Profile</div>',
}
const Login = { template: '<div>Login</div>' }
+ const Post = {
+ template: `
+ <div class="post">
+ <div v-if="loading" class="loading">
+ Loading...
+ </div>
+ <div v-if="error" class="error">
+ {{ error }}
+ </div>
+ <div v-if="post" >
+ <h2>{{ post.title }}</h2>
+ <p>{{ post.body }}</p>
+ </div>
+ </div>
+ `,
+ data() {
+ return {
+ loading: false,
+ post: null,
+ error: null
+ }
+ },
+ created() {
+ // 组件创建完后获取数据,
+ // 此时 data 已经被 observed 了
+ this.fetchData()
+ },
+ watch: {
+ // 如果路由有变化,会再次执行该方法
+ '$route': this.fetchData
+ },
+ methods: {
+ fetchData() {
+ this.error = this.post = null
+ this.loading = true
+ getPost(this.$route.params.id, (err, post) => {
+ this.loading = false
+ if (err) {
+ this.error = err.toString()
+ } else {
+ this.post = post
+ }
+ })
+ }
+ }
+ }
const routes = [
{
name: 'home', path: '/', component: Home, alias: '/homepage',
beforeEnter: (to, from, next) => {
console.log(to.name, 'beforeEnter');
next();
}
},
{
name: 'user',
path: '/user/:id',
component: User,
beforeEnter: (to, from, next) => {
console.log(to.name, 'beforeEnter');
next();
}
},
{ name: 'profile', path: '/profile', component: Profile, meta: { requiresAuth: true } },
{ name: 'login', path: '/login', component: Login },
+ { name: 'post', path: '/post/:id', component: Post }
]
const router = new VueRouter({
routes
})
var vm = new Vue({
el: '#app',
router
})
function isLogin() {
return !!localStorage.getItem('isLogin');
}
//注册vm.$router的onError回调
vm.$router.onError((err) => {
console.log(err.message);
})
vm.$router.beforeEach((to, from, next) => {
console.log(to, 'beforeEach');
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!isLogin()) {
next('/login')
} else {
next()
}
} else {
next() // 确保一定要调用 next()
}
})
vm.$router.beforeResolve((to, from, next) => {
console.log(from.name, to.name, 'beforeResolve');
next();
})
vm.$router.afterEach((to, from) => {
console.log(from.name, to.name, 'afterEach');
})
</script>
</body>
通过这种方式,我们在导航转入新的路由前获取数据。我们可以在接下来的组件的 beforeRouteEnter 守卫中获取数据,当数据获取成功后只调用 next 方法。
在为后面的视图获取数据时,用户会停留在当前的界面,因此建议在数据获取期间,显示一些进度条或者别的指示。如果数据获取失败,同样有必要展示一些全局的错误提醒。
<body>
<div id="app">
<ul>
<li><router-link to="/">Home</router-link></li>
<li><router-link to="/user/1">User1</router-link></li>
<li><router-link to="/user/2">User2</router-link></li>
<li><router-link to="/profile">Profile</router-link></li>
<li><router-link to="/post/1">/post/1</router-link></li>
<li><router-link to="/post/2">/post/2</router-link></li>
</ul>
<router-view></router-view>
</div>
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
<script>
function getPost(id, callback) {
callback(null, {
id,
title: 'title' + id,
body: 'body' + id
});
}
const Home = {
template: '<div>Home</div>',
beforeRouteEnter(to, from, next) {
console.log(to.name, 'beforeRouteEnter');
next();
},
beforeRouteUpdate(to, from, next) {
console.log(to.name, 'beforeRouteUpdate');
next();
},
beforeRouteLeave(to, from, next) {
console.log(from.name, 'beforeRouteLeave');
next();
}
}
const User = {
template: '<div>User</div>',
beforeRouteEnter(to, from, next) {
console.log(to.name, 'beforeRouteEnter');
next();
},
beforeRouteUpdate(to, from, next) {
console.log(to.name, 'beforeRouteUpdate');
next();
},
beforeRouteLeave(to, from, next) {
console.log(from.name, 'beforeRouteLeave');
next();
}
}
const Profile = {
template: '<div>Profile</div>',
}
const Login = { template: '<div>Login</div>' }
const Post = {
template: `
<div class="post">
<div v-if="loading" class="loading">
Loading...
</div>
<div v-if="error" class="error">
{{ error }}
</div>
<div v-if="post" >
<h2>{{ post.title }}</h2>
<p>{{ post.body }}</p>
</div>
</div>
`,
data() {
return {
loading: false,
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
+ }
+ },
fetchData() {
this.error = this.post = null
this.loading = true
getPost(this.$route.params.id, (err, post) => {
this.loading = false
if (err) {
this.error = err.toString()
} else {
this.post = post
}
})
}
}
}
const routes = [
{
name: 'home', path: '/', component: Home, alias: '/homepage',
beforeEnter: (to, from, next) => {
console.log(to.name, 'beforeEnter');
next();
}
},
{
name: 'user',
path: '/user/:id',
component: User,
beforeEnter: (to, from, next) => {
console.log(to.name, 'beforeEnter');
next();
}
},
{ name: 'profile', path: '/profile', component: Profile, meta: { requiresAuth: true } },
{ name: 'login', path: '/login', component: Login },
{ name: 'post', path: '/post/:id', component: Post }
]
const router = new VueRouter({
routes
})
var vm = new Vue({
el: '#app',
router
})
function isLogin() {
return !!localStorage.getItem('isLogin');
}
//注册vm.$router的onError回调
vm.$router.onError((err) => {
console.log(err.message);
})
vm.$router.beforeEach((to, from, next) => {
console.log(to, 'beforeEach');
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!isLogin()) {
next('/login')
} else {
next()
}
} else {
next() // 确保一定要调用 next()
}
})
vm.$router.beforeResolve((to, from, next) => {
console.log(from.name, to.name, 'beforeResolve');
next();
})
vm.$router.afterEach((to, from) => {
console.log(from.name, to.name, 'afterEach');
})
</script>
</body>