1.单页应用 #

单页应用(SPA,Single Page Application)是一种现代网络开发模式。与传统的多页应用相比,SPA在浏览器中仅加载一个HTML页面,并在用户与应用交互时动态更新该页面。

2. 路由原理 #

2.1 HashRouter #

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>

2.2 BrowserRouter #

2.2.1 history #

2.2.1.1 pushState #
2.2.1.2 replaceState #
2.2.1.3 onpopstate #
2.2.1.4 案例 #
<!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>

3. 基本路由 #

3.1 使用 #

<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部分:

在JavaScript部分:

运行这段代码,你会看到页面上有三个链接:Home、User 和 Profile。点击这些链接,下面的 <router-view> 中会显示对应的组件。例如,点击 "User" 链接,路由将切换到 '/user',并且 User 组件将被渲染在 <router-view> 中。

3.2 实现 #

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;
                }
            }
        });
    }
});
  1. VueRouter 是一个构造函数,它接收一个 options 对象作为参数。这个对象包含一组路由配置。

    • this.routes = options.routes; 将传入的路由配置保存到 this.routes 中。

    • data 对象是定义的一个局部变量,用于存储当前的路由路径 currentPathcurrentPath 初始值为当前 URL 的哈希部分,如果没有哈希则默认为 '/'

    • new Vue({data}); 创建了一个新的 Vue 实例,使得 currentPath 变成响应式的,这意味着如果 currentPath 改变,那么 Vue 将自动更新与之相关的视图。

    • Object.definePropertyVueRouter 的实例定义了 currentPath 的 getter 和 setter。这样,当我们访问 this.currentPath 或者修改 this.currentPath 时,实际上是访问或者修改 data.currentPath

    • 最后,给 window 添加了一个 hashchange 事件监听器。当 URL 的哈希部分改变时,这个监听器将更新 currentPath 的值。

  2. Vue.component('router-view', ...) 创建了一个全局的 router-view 组件。这个组件是一个函数式组件,它的渲染函数查找与当前路径匹配的路由配置,并渲染对应的组件。如果没有找到匹配的路由配置,渲染函数返回 null

  3. Vue.component('router-link', ...) 创建了一个全局的 router-link 组件。这个组件接收一个 to 属性,表示链接的目标路径。它的渲染函数返回一个 a 元素,这个元素的 href 属性是 '#' + this.to,即目标路径的哈希链接。

  4. Vue.use(...) 安装了一个插件。这个插件的 install 方法在每个 Vue 实例创建之前调用。如果一个 Vue 实例的选项对象 (this.$options) 包含了 router 属性,那么这个 router 属性的值就会被设置为全局的 $router,这样我们就可以在任何 Vue 组件中通过 this.$router 来访问到这个 VueRouter 实例。

整个代码的主要目可以监控 URL 的哈希部分的变化,然后根据新的哈希值来切换并渲染不同的组件。

4. 动态路由匹配 #

我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个 User 组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。那么,我们可以在 vue-router 的路由路径中使用“动态路径参数”(dynamic segment) 来达到这个效果:

4.1 动态路由匹配 #

<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' }

4.2 响应路由参数的变化 #

提醒一下,当使用路由参数时,例如从 /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>

4.3 捕获所有路由或 404 Not found 路由 #

常规参数只会匹配被 / 分隔的 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>

4.4 匹配优先级 #

有时候,同一个路径可以匹配多个路由,此时,匹配的优先级就按照路由的定义顺序:路由定义得越早,优先级就越高。

5. 嵌套路由 #

实际生活中的应用界面,通常由多层嵌套的组件组合而成。同样地,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>

6. 编程式的导航 #

除了使用 <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>

7. 命名路由 #

有时候,通过一个名称来标识一个路由显得更方便一些,特别是在链接一个路由,或者是执行一些跳转的时候。你可以在创建 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 路径。

8. 命名视图 #

8.1 命名视图 #

有时候想同时 (同级) 展示多个视图,而不是嵌套展示,例如创建一个布局,有 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>

8.2 嵌套命名视图 #

我们也有可能使用命名视图创建嵌套视图的复杂布局。这时你也需要命名用到的嵌套 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>

9. 重定向和别名 #

9.1 重定向 #

重定向也是通过 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>

9.2 别名 #

“重定向”的意思是,当用户访问 /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'},
 ]

10. 路由组件传参 #

在组件中使用 $route 会使之与其对应路由形成高度耦合,从而使组件只能在某些特定的 URL 上使用,限制了其灵活性。

使用 props 将组件和路由解耦:

取代与 $route 的耦合

const User = {
  template: '<div>User {{ $route.params.id }}</div>'
}
const router = new VueRouter({
  routes: [{ path: '/user/:id', component: User }]
})

10.1 通过 props 解耦 #

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>

11. 导航守卫 #

“导航”表示路由正在发生改变。 正如其名,vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。 记住参数或查询的改变并不会触发进入/离开的导航守卫。你可以通过观察 $route 对象来应对这些变化,或使用 beforeRouteUpdate 的组件内守卫。

配置位置 钩子名称 钩子触发时机 讲解
全局 beforeEach 在路由跳转之前触发 全局前置守卫
全局 beforeResolve 在路由跳转之前触发 全局解析守卫
全局 afterEach 在路由跳转之后触发 全局后置钩子
路由配置 beforeEnter 在路由跳转之前触发 路由独享守卫
组件内 beforeRouteEnter 在路由跳转之前触发 组件内守卫
组件内 beforeRouteUpdate 在路由跳转之前触发 组件内守卫
组件内 beforeRouteLeave 在路由跳转之前触发 组件内守卫

11.1 全局前置守卫 #

你可以使用 router.beforeEach 注册一个全局前置守卫:

const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {

})

当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中。

每个守卫方法接收三个参数:

确保 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>

11.2 全局解析守卫 #

在 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 方法的调用参数。
})

其中 tofrom 都是路由对象,代表即将进入的新路由和即将离开的旧路由。而 next 是一个函数,必须在守卫中调用,否则钩子就不会被 resolved。调用 next() 会移动到管道中的下一个钩子;也可以调用 next(false) 来中断当前的导航,或者调用 next(route) 来重定向到一个不同的路由。

请注意,beforeResolve 是全局守卫,对所有路由切换都起作用。如果你只希望对某个特定的路由做处理,可以考虑使用 per-route 守卫(在路由配置上直接定义 beforeEnter)或者 component 内的守卫(例如 beforeRouteEnter, beforeRouteUpdate, beforeRouteLeave)。

记住,在使用 beforeResolve 守卫时,你需要确保正确调用 next() 函数,否则可能导致导航无法继续,用户不能顺利导航到他们期望的页面。

beforeEachbeforeResolve 是 Vue Router 中的两个不同类型的全局守卫,它们在不同的时机被调用,因此可以用于处理不同的场景。

beforeEach 守卫

beforeEach 是最常用的全局前置守卫,它在路由导航被触发后立即调用,但在进入路由配置的 beforeEnter 守卫、组件内的守卫和异步路由组件被解析之前调用。你可以利用这个守卫做一些预处理工作,例如检查用户的认证状态,如果用户未登录,就重定向到登录页面。

beforeResolve 守卫

beforeResolve 守卫则稍晚一些,它在路由导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后调用。这个守卫的一个常见用途是在导航确认前做一些最后的处理,例如获取需要在页面渲染前获取的异步数据。

选择哪一个

选择使用 beforeEach 还是 beforeResolve 主要取决于你的具体需求,如果你需要在路由导航最开始的时候就做一些预处理工作,那么 beforeEach 可能是更好的选择。而如果你需要在导航确认前,所有组件内的守卫和异步组件都解析完毕后,再做一些最后的处理,那么 beforeResolve 会是更好的选择。

但需要注意的是,beforeEachbeforeResolve 守卫都是全局守卫,会对所有路由切换产生影响。如果你只希望对某些特定路由做处理,可能需要考虑使用路由独享的守卫(在路由配置上直接定义的 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>

11.3 全局后置钩子 #

你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:

  vm.$router.afterEach((to, from) => {
      console.log('afterEach');
  })

11.4 路由独享的守卫 #

你可以在路由配置上直接定义 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 }
]

11.5 组件内的守卫 #

最后,你可以在路由组件内直接定义以下路由导航守卫:

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>

11.6 完整的导航解析流程 #

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

12. 路由元信息 #

定义路由的时候可以配置 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>

13.数据获取 #

有时候,进入某个路由后,需要从服务器获取数据。例如,在渲染用户信息时,你需要从服务器获取用户的数据。我们可以通过两种方式来实现:

导航完成之后获取:先完成导航,然后在接下来的组件生命周期钩子中获取数据。在数据获取期间显示“加载中”之类的指示。

导航完成之前获取:导航完成前,在路由进入的守卫中获取数据,在数据获取成功后执行导航。

从技术角度讲,两种方式都不错 —— 就看你想要的用户体验是哪种。

13.1 导航完成后获取数据 #

当你使用这种方式时,我们会马上导航和渲染组件,然后在组件的 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>

13.1 在导航完成前获取数据 #

通过这种方式,我们在导航转入新的路由前获取数据。我们可以在接下来的组件的 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>