1. 基本路由 #

1.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> 中。

1.2 实现 #

var VueRouter = function ({routes}) {
  this.routes = routes;
  let data = {
    currentPath: window.location.hash.slice(1) || '/'
  };
  Vue.observable(data);
  Object.defineProperty(this, 'currentPath', {
    get() {
      return data.currentPath;
    },
    set(newCurrentPath) {
      data.currentPath = newCurrentPath;
    }
  });
  window.addEventListener('hashchange', () => this.currentPath = window.location.hash.slice(1));
  window.location.hash = this.currentPath;
};
Vue.component('router-link', {
  functional: true,
  render(createElement, context) {
    let slots = context.slots();
    let {
      to
    } = context.props;
    let currentPath = context.parent.$router.currentPath;
    let classNames = {
      'router-link-active': currentPath.startsWith(to),
      'router-link-exact-active': currentPath === to
    };
    return createElement('a', {
      class: classNames,
      attrs: {
        href: `#${context.props.to}`
      }
    }, slots.default);
  }
});
Vue.component('router-view', {
  functional: true,
  render(createElement, context) {
    let {
      currentPath,
      routes
    } = context.parent.$router;
    let matched = routes.find(route => {
      return route.path === currentPath;
    });
    if (matched) {
      return createElement(matched.component);
    }
    return null;
  }
});
Vue.use({
  install: function (Vue) {
    Vue.mixin({
      beforeCreate() {
        if (this.$options.router) {
          Vue.prototype.$router = this.$options.router;
        }
      }
    });
  }
});

2. 动态路由匹配 #

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

2.1 动态路由匹配 #

现在呢,像 /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' }
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
    .router-link-active{
        color:lightgreen;
    }
    .router-link-exact-active{
        color:green;
    }
    </style>
  </head>
  <body>
    <div id="app">
      <ul>
        <li><router-link to="/">Home</router-link></li>
        <li><router-link to="/user/100/post/1000?a=1&b=2&c=3#top">张三</router-link></li>
        <li><router-link to="/user/200/post/2000#top">李四</router-link></li>
        <li><router-link to="/profile">Profile</router-link></li>
      </ul>
      <router-view></router-view>
    </div>
    <script src="vue.js"></script>
    <script src="my-vue-router.js"></script>
    <script>
      //定义了三个将要用来匹配并渲染的路由组件
      const Home = { template: "<div>Home</div>" };
      const User = { template: `
      <div>
        User:{{$route.params.id}}<br/>
        Post:{{$route.params.post_id}}<br/>
        Query:{{$route.query}}<br/>
        Hash:{{$route.hash}}<br/>
      </div>` };
      const Profile = { template: "<div>Profile</div>" };
      //定义了路由的配置,每个路由配置包含了一个路径path和一个对应的组件
      //当用户导航到一个路径的时候,会渲染对应的组件到router-view
      const routes = [
        { path: "/", component: Home },
        { path: "/user/:id/post/:post_id", component: User },
        { path: "/profile", component: Profile },
      ];
      //创建VueRouter的实例,传入路由配置
      var router = new VueRouter({
        routes
      });
      //创建和挂载Vue实例
      var vm = new Vue({
        el: "#app",
        router
      });
    </script>
  </body>
</html>
var VueRouter = function ({routes}) {
  this.routes = routes;
  let data = {
    currentPath: window.location.hash.slice(1) || '/'
  };
  Vue.observable(data);
+ this.$route = {
+   params: {},
+   query: {},
+   hash: ''
+ };
  Vue.observable(this.$route);
  Object.defineProperty(this, 'currentPath', {
    get() {
      return data.currentPath;
    },
    set(newCurrentPath) {
      data.currentPath = newCurrentPath;
+     this.updateRoute();
    }
  });
  window.addEventListener('hashchange', () => this.currentPath = window.location.hash.slice(1));
  window.location.hash = this.currentPath;
+  this.updateRoute();
};
+VueRouter.prototype.updateRoute = function() {
+  this.routes.find(route => {
+    let regExp = new RegExp('^' + route.path.replace(/:([^/]+)/g, '([^/]+)') + '$');
+    let match = this.currentPath.match(regExp);
+    if (match) {
+      let paramNames = (route.path.match(/:([^/]+)/g) || []).map(name => name.slice(1));
+      this.$route.params = {};
+      paramNames.forEach((name, index) => {
+        this.$route.params[name] = match[index + 1];
+      });
+      let [, queryString] = this.currentPath.split('?');
+      let hashIndex = queryString ? queryString.indexOf('#') : -1;
+      if (hashIndex !== -1) {
+        this.$route.hash = queryString.slice(hashIndex);
+        queryString = queryString.slice(0, hashIndex);
+      }
+      this.$route.query = {};
+      (queryString || '').split('&').forEach(pair => {
+        let [key, value] = pair.split('=');
+        if (key) {
+          this.$route.query[key] = decodeURIComponent(value || '');
+        }
+      });
+      return true;
+    }
+    return false;
+  });
+};
Vue.component('router-link', {
  functional: true,
  render(createElement, context) {
    let slots = context.slots();
    let {
      to
    } = context.props;
    let currentPath = context.parent.$router.currentPath;
    let classNames = {
      'router-link-active': currentPath.startsWith(to),
      'router-link-exact-active': currentPath === to
    };
    return createElement('a', {
      class: classNames,
      attrs: {
        href: `#${context.props.to}`
      }
    }, slots.default);
  }
});
Vue.component('router-view', {
  functional: true,
  render(createElement, context) {
+   let {currentPath,routes} = context.parent.$router;
+   let matched = routes.find(route => {
+     let regExp = new RegExp('^' + route.path.replace(/:([^/]+)/g, '([^/]+)') + '$');
+     return currentPath.match(regExp);
+   });
    if (matched) {
      return createElement(matched.component);
    }
    return null;
  }
});
Vue.use({
  install: function (Vue) {
    Vue.mixin({
      beforeCreate() {
        if (this.$options.router) {
          Vue.prototype.$router = this.$options.router;
+         Vue.prototype.$route = this.$options.router.$route;
        }
      }
    });
  }
});

2.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>

2.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>

2.4 匹配优先级 #

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

3. 嵌套路由 #

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

4. 编程式的导航 #

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

5. 命名路由 #

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

6. 命名视图 #

6.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>

6.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>

7. 重定向和别名 #

7.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>

7.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'},
 ]

8. 路由组件传参 #

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

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

取代与 $route 的耦合

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

8.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>

9. 导航守卫 #

“导航”表示路由正在发生改变。

正如其名,vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。

有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。

记住参数或查询的改变并不会触发进入/离开的导航守卫。你可以通过观察 $route 对象来应对这些变化,或使用 beforeRouteUpdate 的组件内守卫。

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

导航解析流程

导航(即从一个路由到另一个路由的过程)是一个相对复杂的过程,涉及多个步骤和钩子(hook)。以下是一个概括的导航解析流程:

  1. 导航触发:导航首先由某种方式触发,比如用户点击了一个 <router-link>,或者手动调用了 router.push()

  2. 取消之前的导航:如果之前已经有一个导航在进行中,那么它会被取消,除非新的导航和当前导航是相同的。

  3. 全局前置守卫 (beforeEach):全局的 beforeEach 守卫将会被调用。这些守卫可以用来进行认证检查或其它全局的检查逻辑。

  4. 路由独享守卫 (beforeEnter):接下来,如果为目标路由定义了 beforeEnter 守卫,它会被执行。

  5. 组件内的 beforeRouteEnter 守卫:如果目标组件定义了 beforeRouteEnter 守卫,那么这个守卫会被调用。

  6. 解析异步路由组件:如果路由配置涉及到异步组件,这时候会等待异步组件解析完成。

  7. 全局解析守卫 (beforeResolve):所有组件内守卫和异步路由组件被解析之后,全局的 beforeResolve 守卫会被调用。

  8. 导航确认:到这一步,如果所有守卫都调用了 next() 方法,导航就会被确认,URL 会变更,同时新的组件会被渲染。

  9. 组件内的 beforeRouteUpdate 守卫:如果导航复用了一个组件(即新旧路由指向同一组件),那么该组件内的 beforeRouteUpdate 守卫会被调用。

  10. 组件渲染:目标路由的组件将被渲染到 <router-view> 里。

  11. 全局后置钩子 (afterEach):全局的 afterEach 钩子会在导航和组件渲染完成之后被调用。

  12. 组件内的 beforeRouteLeave 守卫:最后,当从一个路由组件导航到另一个路由组件时,之前路由组件的 beforeRouteLeave 守卫会被调用。

9.1 beforeEach全局前置守卫 #

beforeEach 是一个非常重要的导航守卫(navigation guard)。它主要用于在路由改变之前执行一些逻辑,比如权限控制、重定向等。

beforeEach 函数接受三个参数:

  1. to: 即将要进入的目标路由对象。
  2. from: 当前导航正要离开的路由。
  3. next: 一个函数,必须执行该方法以解析这个钩子。

下面是一个非常简单的例子,演示了如何使用 beforeEach

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Vue 2.x beforeEach Example</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
  <script src="https://unpkg.com/vue-router@3.5.3/dist/vue-router.js"></script>
</head>
<body>
  <div id="app">
    <router-link to="/home">Home</router-link>
    <router-link to="/about">About</router-link>
    <router-link to="/secret">Secret</router-link>

    <router-view></router-view>
  </div>

  <script>
    const Home = { template: '<div>Home</div>' };
    const About = { template: '<div>About</div>' };
    const Secret = { template: '<div>Secret</div>' };

    const routes = [
      { path: '/home', component: Home },
      { path: '/about', component: About },
      { path: '/secret', component: Secret }
    ];

    const router = new VueRouter({
      routes
    });

    // 使用 beforeEach 导航守卫
    router.beforeEach((to, from, next) => {
      if (to.path === '/secret') {
        // 假设用户需要登录才能访问 /secret 路由
        const isAuthorized = false; // 模拟未授权

        if (isAuthorized) {
          next(); // 允许进入 /secret
        } else {
          next('/home'); // 重定向到 /home
        }
      } else {
        next(); // 允许进入其他路由
      }
    });

    new Vue({
      el: '#app',
      router
    });
  </script>
</body>
</html>

在这个例子中,我们定义了一个简单的 Vue 应用程序,包含三个路由:/home/about/secret。我们通过 beforeEach 导航守卫来检查用户是否有权限访问 /secret 路由。

由于在这个例子中我们将 isAuthorized 设置为 false,因此当用户尝试访问 /secret 路由时,将会被重定向到 /home

9.2 beforeResolve全局解析守卫 #

beforeResolve 是一个导航守卫(navigation guard),它的行为类似于 beforeEach,但有一个关键的区别:beforeResolve 是在所有组件内守卫和异步路由组件被解析之后调用的。

这意味着,当你的路由中有异步组件或者在组件内使用了 beforeEnter 等守卫时,这些都会在 beforeResolve 执行之前完成。

beforeResolve 的参数与 beforeEach 相同:

  1. to: 即将要进入的目标路由对象。
  2. from: 当前导航正要离开的路由。
  3. next: 一个函数,必须执行该方法以解析这个钩子。

下面是一个可以在浏览器中直接运行的完整 HTML 代码示例:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Vue 2.x beforeResolve Example</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
  <script src="https://unpkg.com/vue-router@3.5.3/dist/vue-router.js"></script>
</head>
<body>
  <div id="app">
    <router-link to="/home">Home</router-link>
    <router-link to="/about">About</router-link>

    <router-view></router-view>
  </div>

  <script>
    // 假设我们有一个异步加载的 About 组件
    const About = resolve => {
      setTimeout(() => {
        resolve({ template: '<div>About</div>' });
      }, 1000);
    };

    const Home = { template: '<div>Home</div>' };

    const routes = [
      { path: '/home', component: Home },
      { path: '/about', component: About }
    ];

    const router = new VueRouter({
      routes
    });

    // 使用 beforeResolve 导航守卫
    router.beforeResolve((to, from, next) => {
      console.log('beforeResolve:', to.path);
      next(); // 可以使用 next() 来解析这个钩子,也可以用 next('/somePath') 来重定向
    });

    new Vue({
      el: '#app',
      router
    });
  </script>
</body>
</html>

在这个例子中,About 组件是一个异步组件,它会在 1 秒后被解析。beforeResolve 在这个异步操作完成后才会被调用。

9.3 afterEach 全局后置钩子 #

afterEach 是一个全局后置钩子(global after hook)。与 beforeEachbeforeResolve 不同,afterEach 在导航确认之后被调用,不会接受 next 函数也不会改变导航本身。

该钩子接受两个参数:

  1. to: 导航完成后,即将要进入的目标路由对象。
  2. from: 导航完成后,正要离开的路由对象。

这个钩子主要用于执行如改变页面标题、发送数据统计等在导航完成后的任务。

以下是一个可以直接在浏览器里运行的完整 HTML 示例代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Vue 2.x afterEach Example</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
  <script src="https://unpkg.com/vue-router@3.5.3/dist/vue-router.js"></script>
</head>
<body>
  <div id="app">
    <router-link to="/home">Home</router-link>
    <router-link to="/about">About</router-link>

    <router-view></router-view>
  </div>

  <script>
    const Home = { template: '<div>Home</div>' };
    const About = { template: '<div>About</div>' };

    const routes = [
      { path: '/home', component: Home },
      { path: '/about', component: About }
    ];

    const router = new VueRouter({
      routes
    });

    // 使用 afterEach 导航守卫
    router.afterEach((to, from) => {
      console.log(`Navigated from ${from.path} to ${to.path}`);
      // 你可以在这里执行一些在路由导航完成后的操作,例如改变页面标题
      document.title = `Current route: ${to.path}`;
    });

    new Vue({
      el: '#app',
      router
    });
  </script>
</body>
</html>

在这个例子中,我们使用了 afterEach 钩子来改变页面的标题并在控制台输出一条消息,表示从哪个路由导航到哪个路由。

9.4 beforeEnter路由独享的守卫 #

beforeEnter 是一个路由独享的守卫(route-specific guard)。与全局守卫 beforeEachbeforeResolve 不同,beforeEnter 定义在单个路由配置对象上,用于处理进入该路由前的逻辑。

beforeEnter 函数接受三个参数:

  1. to: 即将要进入的目标路由对象。
  2. from: 当前导航正要离开的路由。
  3. next: 一个函数,必须执行该方法以解析这个钩子。

以下是一个可以直接在浏览器里运行的完整 HTML 示例代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Vue 2.x beforeEnter Example</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
  <script src="https://unpkg.com/vue-router@3.5.3/dist/vue-router.js"></script>
</head>
<body>
  <div id="app">
    <router-link to="/home">Home</router-link>
    <router-link to="/about">About</router-link>
    <router-link to="/secret">Secret</router-link>

    <router-view></router-view>
  </div>

  <script>
    const Home = { template: '<div>Home</div>' };
    const About = { template: '<div>About</div>' };
    const Secret = { template: '<div>Secret</div>' };

    const routes = [
      { path: '/home', component: Home },
      { path: '/about', component: About },
      { 
        path: '/secret',
        component: Secret,
        beforeEnter: (to, from, next) => {
          // 这里是进入 /secret 路由前的逻辑
          const isAuthorized = false; // 模拟未授权

          if (isAuthorized) {
            next(); // 允许进入 /secret
          } else {
            next('/home'); // 重定向到 /home
          }
        }
      }
    ];

    const router = new VueRouter({
      routes
    });

    new Vue({
      el: '#app',
      router
    });
  </script>
</body>
</html>

在这个例子中,我们有一个名为 Secret 的路由,该路由有一个 beforeEnter 钩子。这个钩子用于检查是否有权限访问 /secret 路由。由于在这个例子中 isAuthorized 被设置为 false,用户将被重定向到 /home 路由。

9.5 beforeRouteEnter #

在 Vue Router 2.x 中,beforeRouteEnter 是一个组件内导航守卫。它会在路由进入该组件的配置解析时调用,这意味着它仅在特定组件上生效。这个钩子通常用于访问权限检查、预加载数据等。

beforeRouteEnter 的参数与全局守卫相同:

  1. to: 即将要进入的目标路由对象。
  2. from: 当前导航正要离开的路由。
  3. next: 一个函数,必须执行该方法以解析这个钩子。

以下是一个可以在浏览器里运行的完整 HTML 示例代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Vue 2.x beforeRouteEnter Example</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
  <script src="https://unpkg.com/vue-router@3.5.3/dist/vue-router.js"></script>
</head>
<body>
  <div id="app">
    <router-link to="/home">Home</router-link>
    <router-link to="/about">About</router-link>
    <router-link to="/profile">Profile</router-link>

    <router-view></router-view>
  </div>

  <script>
    const Home = { template: '<div>Home</div>' };
    const About = { template: '<div>About</div>' };
    const Profile = { 
      template: '<div>Profile</div>',
      beforeRouteEnter (to, from, next) {
        // 在进入该组件前执行逻辑
        const isAuthenticated = false;  // 模拟未登录

        if (isAuthenticated) {
          next(); // 如果验证通过,允许进入
        } else {
          next('/home'); // 否则,重定向到首页
        }
      }
    };

    const routes = [
      { path: '/home', component: Home },
      { path: '/about', component: About },
      { path: '/profile', component: Profile }
    ];

    const router = new VueRouter({
      routes
    });

    new Vue({
      el: '#app',
      router
    });
  </script>
</body>
</html>

在这个示例中,Profile 组件有一个 beforeRouteEnter 守卫。该守卫会检查一个名为 isAuthenticated 的变量,以决定是否允许用户进入该路由。因为我们在示例中将其设置为 false,任何尝试访问 /profile 的导航都会被重定向到 /home

9.6 beforeRouteUpdate #

在 Vue Router 2.x 中,beforeRouteUpdate 是一个组件内导航守卫。这个钩子在当前路由改变,但该组件被复用时调用。换句话说,如果你从一个路由导航到另一个路由,而这两个路由共用同一个组件,那么 beforeRouteUpdate 就会被调用。

这个钩子的参数与其他导航守卫相同:

  1. to: 即将要进入的目标路由对象。
  2. from: 当前导航正要离开的路由。
  3. next: 一个函数,必须执行该方法以解析这个钩子。

下面是一个在浏览器中可直接运行的 HTML 示例代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Vue 2.x beforeRouteUpdate Example</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
  <script src="https://unpkg.com/vue-router@3.5.3/dist/vue-router.js"></script>
</head>
<body>
  <div id="app">
    <router-link to="/user/1">User 1</router-link>
    <router-link to="/user/2">User 2</router-link>

    <router-view></router-view>
  </div>

  <script>
    const User = { 
      template: '<div>User {{ $route.params.id }}</div>',
      beforeRouteUpdate (to, from, next) {
        // 当路由改变,但该组件被复用时,执行这里的逻辑
        console.log(`Updating from ${from.params.id} to ${to.params.id}`);
        next();
      }
    };

    const routes = [
      { path: '/user/:id', component: User }
    ];

    const router = new VueRouter({
      routes
    });

    new Vue({
      el: '#app',
      router
    });
  </script>
</body>
</html>

在这个示例中,我们有一个名为 User 的组件,该组件用于显示用户的 ID。当我们从一个用户页面(比如 /user/1)导航到另一个用户页面(比如 /user/2)时,组件不会被销毁和重新创建,而是被复用,并且 beforeRouteUpdate 钩子会被触发。

9.7 beforeRouteLeave #

在 Vue Router 2.x 中,beforeRouteLeave 是一个组件内导航守卫,用于在当前路由改变且组件即将被销毁前执行某些操作。这通常用于警告用户如果离开当前页面可能会导致未保存的更改被丢弃。

beforeRouteLeave 钩子的参数与其他导航守卫相同:

  1. to: 即将要进入的目标路由对象。
  2. from: 当前导航正要离开的路由。
  3. next: 一个函数,必须执行该方法以解析这个钩子。

以下是一个可以在浏览器里运行的完整 HTML 示例代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Vue 2.x beforeRouteLeave Example</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
  <script src="https://unpkg.com/vue-router@3.5.3/dist/vue-router.js"></script>
</head>
<body>
  <div id="app">
    <router-link to="/editor">Editor</router-link>
    <router-link to="/home">Home</router-link>

    <router-view></router-view>
  </div>

  <script>
    const Home = { template: '<div>Home</div>' };
    const Editor = { 
      template: '<div>Editor - Unsaved changes</div>',
      beforeRouteLeave (to, from, next) {
        // 当路由改变且组件即将被销毁时执行这里的逻辑
        const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
        if (answer) {
          next(); // 如果确认,继续导航
        } else {
          next(false); // 如果取消,停止导航
        }
      }
    };

    const routes = [
      { path: '/home', component: Home },
      { path: '/editor', component: Editor }
    ];

    const router = new VueRouter({
      routes
    });

    new Vue({
      el: '#app',
      router
    });
  </script>
</body>
</html>

在这个例子中,Editor 组件有一个 beforeRouteLeave 守卫。当用户尝试离开这个组件时,将弹出一个确认对话框询问用户是否真的想离开页面。

要测试这个代码,您可以复制上面的 HTML 内容,保存为一个 .html 文件,然后用浏览器打开。然后,您可以尝试点击 "Editor" 和 "Home" 链接。当您从 "Editor" 页面尝试切换到 "Home" 页面时,应该会看到一个确认对话框,询问您是否确定离开。您可以选择取消以停留在当前页面,或选择确定以继续导航。

9.8 完整的导航解析流程 #

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

10. 路由元信息 #

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

11.数据获取 #

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

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

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

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

11.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>

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

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

12 过渡动效 #

router-view 是基本的动态组件,所以我们可以用 transition 组件给它添加一些过渡效果:

上面的用法会给所有路由设置一样的过渡效果,如果你想让每个路由组件有各自的过渡效果,可以在各路由组件内使用 transition 并设置不同的 name。

还可以基于当前路由与目标路由的变化关系,动态设置过渡效果:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      .slide-left-enter-active, .slide-left-leave-active {
        transition: all 3s ease;
      }
      .slide-left-enter, .slide-left-leave-to {
        transform: translateX(100%);
      }
      .slide-right-enter-active, .slide-right-leave-active {
        transition: all 3s ease;
      }
      .slide-right-enter, .slide-right-leave-to {
        transform: translateX(-100%);
      }
      .view{
        background-color: green;
      }
      .child-view{
        background-color:brown;
      }
    </style>

  </head>
  <body>
    <div id="app">
      <ul>
        <li><router-link to="/">/</router-link></li>
        <li><router-link to="/parent">/parent</router-link></li>
        <li><router-link to="/parent/foo">/parent/foo</router-link></li>
        <li><router-link to="/parent/bar">/parent/bar</router-link></li>
      </ul>
      <transition name="fade" mode="out-in">
        <router-view class="view"></router-view>
      </transition>
    </div>
    <script src="vue.js"></script>
    <script src="vue-router.js"></script>
    <script>
      const Home = {
        template: `
          <div class="home">
            <h2>Home</h2>
            <p>Hello</p>
          </div>
        `,
      };
      const Parent = {
        data() {
          return {
            transitionName: 'slide-left',
          };
        },
        beforeRouteUpdate(to, from, next) {
          const toDepth = to.path.split('/').length;
          const fromDepth = from.path.split('/').length;
          this.transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left';
          next();
        },
        template: `
          <div class="parent">
            <h2>Parent</h2>
            <transition :name="transitionName">
              <router-view class="child-view"></router-view>
            </transition>
          </div>
        `,
      };
      const Default = { template: '<div class="default">default</div>' };
      const Foo = { template: '<div class="foo">foo</div>' };
      const Bar = { template: '<div class="bar">bar</div>' };
      const routes = [
        { path: '/', component: Home },
        {
          path: '/parent',
          component: Parent,
          children: [
            { path: '', component: Default },
            { path: 'foo', component: Foo },
            { path: 'bar', component: Bar },
          ],
        },
      ];
      const router = new VueRouter({
        routes,
      });
      new Vue({
        el: '#app',
        router,
      });
    </script>
  </body>
</html>

13. 滚动行为 #

scrollBehavior 是一个在 Vue Router(特别是在 Vue Router 3.x 版本)中用于控制页面滚动行为的方法。当路由切换导致导航到一个新页面时,你可能希望页面滚动到顶部,或者保留之前的滚动位置,或者甚至滚动到某个特定的位置。这一切都可以通过配置 scrollBehavior 方法来实现。

该方法接受三个参数:

  1. to: 即将进入的路由对象。
  2. from: 即将离开的路由对象。
  3. savedPosition: 在按下浏览器的前进/后退按钮时,保存的滚动位置。

scrollBehavior 方法需要返回一个滚动位置对象,格式如下:

例子

下面的代码在你提供的基础代码上进行了添加,以演示如何使用 scrollBehavior

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue Router scrollBehavior Demo</title>
</head>
<body>
    <div id="app">
        <router-link to="/">Home</router-link>
        <router-link to="/about">About</router-link>
        <router-view></router-view>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue-router@3.5.1/dist/vue-router.js"></script>
    <script>
        const Home = { template: '<div><h1>Home</h1><p>Scroll to top when you navigate here.</p></div>' };
        const About = { template: '<div><h1>About</h1><p>Scroll to position { x: 0, y: 100 } when you navigate here.</p></div>' };
        const routes = [
            { path: '/', component: Home },
            { path: '/about', component: About }
        ];
        const router = new VueRouter({
            routes,
            scrollBehavior(to, from, savedPosition) {
                if (to.path === '/about') {
                    return { x: 0, y: 100 }
                } else if (savedPosition) {
                    return savedPosition;
                } else {
                    return { x: 0, y: 0 }
                }
            }
        });
        new Vue({
            el: '#app',
            router
        });
    </script>
</body>
</html>

在这个例子中,当用户导航到 "About" 页面时,页面将滚动到 { x: 0, y: 100 } 的位置。在其他情况下,页面将滚动到顶部 { x: 0, y: 0 }。如果用户通过点击浏览器的前进/后退按钮进行导航,并且有保存的滚动位置,页面将恢复到该滚动位置。

13.1 index.js #

const http = require('http')
const fs = require('fs')
const httpPort = 8080
http.createServer((req, res) => {
  fs.readFile('index.html', 'utf-8', (err, content) => {
    if (err) {
      console.log('We cannot open "index.html" file.')
    }
    res.writeHead(200, {
      'Content-Type': 'text/html; charset=utf-8'
    })
    res.end(content)
  })
}).listen(httpPort, () => {
  console.log('Server listening on: http://localhost:%s', httpPort)
})

13.2 index.html #

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vue Router Demo</title>
    <style>
        /* General Styles */
        body {
            font-family: Arial, sans-serif;
        }
        ul {
            background-color: #f9f9f9;
            border-radius: 8px;
            padding: 10px;
            list-style: none;
        }
        li {
            margin: 5px 0;
        }
        /* View & Transitions */
        .view {
            padding: 20px;
            border: 1px solid #ccc;
            border-radius: 8px;
            margin-top: 20px;
            background-color: #fff;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
        }
        .fade-enter-active,
        .fade-leave-active {
            transition: opacity 1s;
        }
        .fade-enter,
        .fade-leave-to {
            opacity: 0;
        }
    </style>
</head>
<body>
    <div id="app">
        <ul>
            <li><router-link to="/">Home</router-link></li>
            <li><router-link to="/foo">Foo</router-link></li>
            <li><router-link to="/bar">Bar</router-link></li>
            <li><router-link to="/bar#anchor">Bar Anchor</router-link></li>
            <li><router-link to="/bar#anchor2">Bar Anchor2</router-link></li>
            <li><router-link to="/bar#1number">Bar with Number</router-link></li>
        </ul>
        <transition name="fade" mode="out-in" @after-leave="afterLeave">
            <router-view class="view"></router-view>
        </transition>
    </div>
    <script src="https://static.zhufengpeixun.com/vue_1695401193557.js"></script>
    <script src="https://static.zhufengpeixun.com/vuerouter_1695401204264.js"></script>
    <script>
        const Home = { template: '<div class="home">home</div>' };
        const Foo = { template: '<div class="foo">foo</div>' };
        const Bar = {
            template: `
                <div class="bar">
                    bar
                    <div style="height:1500px"></div>
                    <p id="anchor" style="height:500px">Anchor</p>
                    <p id="anchor2" style="height:500px">Anchor2</p>
                    <p id="1number">with number</p>
                </div>
            `
        };
        const scrollBehavior = function (to, from, savedPosition) {
            if (savedPosition) {
                return savedPosition
            } else {
                const position = {}
                if (to.hash) {
                    position.selector = to.hash
                    if (to.hash === '#anchor2') {
                        position.offset = { y: 100 }
                    }
                    if (/^#\d/.test(to.hash) || document.querySelector(to.hash)) {
                        return position
                    }
                    return false
                }
                return new Promise(resolve => {
                    if (to.matched.some(m => m.meta.scrollToTop)) {
                        position.x = 0
                        position.y = 0
                    }
                    this.app.$root.$once('triggerScroll', () => {
                        resolve(position)
                    })
                })
            }
        }
        const routes = [
            { path: '/', component: Home, meta: { scrollToTop: true } },
            { path: '/foo', component: Foo },
            { path: '/bar', component: Bar, meta: { scrollToTop: true } }
        ];
        const router = new VueRouter({
            mode: 'history',
            scrollBehavior,
            routes
        });
        new Vue({
            el: '#app',
            router,
            methods: {
                afterLeave() {
                    this.$root.$emit('triggerScroll');
                }
            }
        });
    </script>
</body>
</html>

14. 路由懒加载 #

在 Vue.js 2.x 中,路由懒加载通常是为了优化页面加载速度而使用的一种技术。当使用懒加载时,特定的组件只会在需要时才会被加载和渲染,而不是在应用启动时就一次性加载所有组件。

14.1 index.html #

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vue Router Demo with Lazy Loading</title>
</head>
<body>
    <div id="app">
        <router-link to="/">Home</router-link>
        <router-link to="/about">About</router-link>
        <!-- 使用 <router-view> 组件来渲染对应的路由组件 -->
        <router-view></router-view>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue-router@3.5.3/dist/vue-router.js"></script>
    <script type="module">
        // 定义异步组件
        const Home = () => import('./Home.js');
        const About = () => import('./About.js');
        // 定义路由配置
        const routes = [
            { path: '/', component: Home, meta: { scrollToTop: true } },
            { path: '/about', component: About }
        ];
        // 创建 VueRouter 实例
        const router = new VueRouter({
            routes
        });
        // 创建 Vue 实例
        new Vue({
            el: '#app',
            router,
            methods: {}
        });
    </script>
</body>
</html>

14.2 Home.js #

export default {
  name:'Home',
  template:'<div>Home</div>'
}

14.3 About.js #

export default {
  name:'About',
  template:'<div>About</div>'
}

15. 导航故障 #

在 Vue 2 中,Vue Router 提供了一种方式来捕获和处理导航故障(navigation failures)。这些导航故障是当导航过程中出现错误或阻止导航完成时发生的。Vue Router 提供了一个函数 isNavigationFailure 和一个枚举 NavigationFailureType,用于更准确地识别和处理这些故障。

以下是一些常见的 NavigationFailureType 类型:

  1. NavigationFailureType.canceled:导航被一个新的导航取消。
  2. NavigationFailureType.duplicated:导航到当前位置的尝试。
  3. NavigationFailureType.redirected:导航由一个导航守卫重定向。

这种方式特别有用,例如,在用户尝试访问需要登录的页面但未登录时,您可能希望显示一个提示。

下面是将导航故障的代码片段添加到您给出的 HTML 文件中的完整示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vue Router Demo</title>
</head>
<body>
    <div id="app">
        <router-link to="/">Home</router-link>
        <router-link to="/admin">Admin</router-link>
        <router-view></router-view>
    </div>
    <script src="vue.js"></script>
    <script src="vue-router.js"></script>
    <script>
        const Home = { template: '<div>Home</div>' };
        const Admin = { template: '<div>Admin</div>', beforeEnter(to, from, next) {
            // 这里仅作演示,您可以将其替换为真实的登录检查
            next('/'); // 将导航重定向到首页
        }};
        const routes = [
            { path: '/', component: Home, meta: { scrollToTop: true } },
            { path: '/admin', component: Admin }
        ];
        const router = new VueRouter({
            routes
        });
        // 导入 isNavigationFailure 和 NavigationFailureType
        const { isNavigationFailure, NavigationFailureType } = VueRouter;
        // 添加导航守卫
        router.beforeEach((to, from, next) => {
            // 此处可以添加其他逻辑
            next();
        });
        // 尝试访问 admin 页面
        router.push('/admin').catch(failure => {
            if (isNavigationFailure(failure, NavigationFailureType.redirected)) {
                // 向用户显示一个小通知
                alert('Login in order to access the admin panel');
            }
        });
        new Vue({
            el: '#app',
            router,
            methods: {}
        });
    </script>
</body>
</html>

注意:在这个示例中,我们添加了一个 beforeEnter 守卫到 Admin 组件。该守卫将导航重定向到首页,这将触发一个 NavigationFailureType.redirected 类型的导航故障。然后,我们捕获这个故障,并显示一个警告。

16. 参考 #

16.1 path-to-regexp #

path-to-regexp 是一个用于匹配和解析 URL 路径的 JavaScript 库。这个库在很多路由库(如 Express)中都有应用,它提供了一种灵活且强大的方式来识别路径模式和从中提取相关信息。

安装

你可以通过 npm 安装这个包:

npm install path-to-regexp

基础用法

这个库主要提供几个核心函数:pathToRegexp, match, parsecompile

pathToRegexp

这个函数接受一个路径字符串(可包含参数),并返回一个用于匹配该路径的正则表达式。

const pathToRegexp = require('path-to-regexp');
const re = pathToRegexp('/user/:id');

// 使用这个正则表达式来匹配路径
const result = '/user/123'.match(re);
console.log(result);  // 输出匹配信息

match

这个函数用于从一个路径字符串中获取参数。

const { match } = require('path-to-regexp');
const matchFunc = match('/user/:id');

const result = matchFunc('/user/123');
console.log(result.params);  // 输出 { id: '123' }

parse

这个函数可以将路径字符串解析为一组标记。

const { parse } = require('path-to-regexp');
const tokens = parse('/user/:id');
console.log(tokens);

compile

这个函数接受一个路径字符串,并返回一个用于生成与该路径匹配的字符串的函数。

const { compile } = require('path-to-regexp');
const compileFunc = compile('/user/:id');

const path = compileFunc({ id: '123' });
console.log(path);  // 输出 '/user/123'

高级特性

这个库还支持更复杂的路径模式,如可选参数、通配符等。

const re = pathToRegexp('/user/:id?');  // id 是可选参数

const re2 = pathToRegexp('/files/*');  // 匹配 /files/ 下的所有路径

16.2 History API #

History API 是 Web 浏览器中用于操纵浏览历史的接口。这个 API 主要用于单页面应用(Single-Page Applications, SPA)中,用于无需重新加载整个页面就能改变浏览器 URL,从而实现平滑的用户体验。

以下是一些主要的 History API 方法:

history.pushState(state, title, url)

该方法将一个新的状态和 URL 添加到历史堆栈中。

例如:

history.pushState({ page: 1 }, "title 1", "?page=1");

history.replaceState(state, title, url)

这个方法用于修改当前的历史记录项而不是添加新的历史记录。

例如:

history.replaceState({ page: 2 }, "title 2", "?page=2");

history.back()

该方法用于导航到历史堆栈中的前一个 URL,等同于用户点击浏览器的后退按钮。

history.back();

history.forward()

该方法用于导航到历史堆栈中的下一个 URL,等同于用户点击浏览器的前进按钮。

history.forward();

history.go(n)

该方法用于导航到距当前 URL n 步远的历史记录项。

例如:

history.go(-1);

history.length

这是一个只读属性,表示历史堆栈中的 URL 数量。

console.log(history.length);

监听 popstate 事件

当激活历史记录条目时(例如,通过后退/前进按钮或 history.gohistory.backhistory.forward 方法),将触发 popstate 事件。

window.addEventListener("popstate", function(event) {
  console.log("state: ", event.state);
});

这样,你就可以根据历史状态更新页面内容。

注意:pushStatereplaceState 本身不会触发 popstate 事件。这个事件只在由用户导致的 URL 变化时触发。