Vue 系列 | Vue-Router

注意: 本文所有示例代码详见:vue-rouer-demo

What | 什么是Vue Router

Vue Router是Vue.js提供的官方路由管理器,它和Vue.js深度集成,使构建单页面应用非常方便。

Why | 为什么要使用Vue Router

大家打开LorneNote个网站,这是我的一个blog网站,采用传统的开发模式,鼠标右击,在出现的菜单里选择View Page Source 查看资源文件,大家会看到它对应的是一个HTML文件,然后你回到网站里点击归档栏,再次右击查看源文件,你会看到整个页面被重新加载,对应归档的HTML文件,即:

也就是说每一个URL对应一个HTML文件,这样每次切换页面的时候我们都需要重新加载我们的页面,非常影响用户体验。

然后就诞生了单页面形式SPA(single page applications)。在单页面模式下,不管我们访问什么页面,都返回index.html,类似这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!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">
<link rel="icon" href="/favicon.ico">
<title>hello-world</title>
<link href="/app.js" rel="preload" as="script"></head>
<body>
<noscript>
<strong>We're sorry but hello-world doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
<script type="text/javascript" src="/app.js"></script></body>
</html>

在单文件模式里,我们依然可以跳到不同的HTML文件,但我们一般不会这样做,一个index.html就满足了我们的需求,用户切换URL的时候,不再是重新加载我们的页面,而是根据URL的变化,执行相应的逻辑,数据会通过接口的形式返回给我们,而后做页面更新。Vue Router就是为了解决这样的事情,它可以做这些事情:

  • 提供URL和组件之间的映射关系,并在URL变化的时候执行对象逻辑
  • 提供多种方式改变URL的API(URL的变化不会导致浏览器刷新)

How | Vue Router 如何使用

核心使用步骤

  1. 安装并导入VueRouter,调用Vue.use(VueRouter),这样的话可以使用<router-view><router-link>等全局组件
1
2
3
4
5
6
7
// 安装命令
$ npm install vue-router

// main.js
import App from './App.vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
  1. 定义路由组件入口
1
2
3
4
5
6
7
// App.vue
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<router-view></router-view>
</div>
</template>
  1. 使用定义好的路由组件配置路由列表
1
2
3
4
5
6
7
// routes.js
import HelloWorld from './components/HelloWorld'
const routes = [
{path: '/', component: HelloWorld, name:'home'}
]

export default routes
  1. 创建路由实例,并通过路由列表初始化路由实例
1
2
3
4
5
// main.js
import routes from'./routes'
const router = new VueRouter({
routes
})
  1. 将创建好的路由实例挂载到跟实例,这样我们就可以使用this.$route全局钩子来操作路由管理器为我们提供的一些属性。
1
2
3
4
5
// main.js
new Vue({
router,
render: h => h(App),
}).$mount('#app')

注意
this.$router代表我们刚刚实例化的全局路由实例,它和我们导入router相同,只是为了使用方便,不用在组件中导入了。
this.$route,写法上少了一个r,代表任何组件的当前路由。

路由动态匹配

有时候我们需要映射不同的路由给相同的组件

1
2
3
4
5
// routes.js
{ path:'/user/:id', component:User}

// HelloWorld.vue
<router-link to="/user/123">张三-single params</router-link><br>

在User组件中,可以以this.$route.params的形式接收参数id

1
2
3
4
5
<template>
<div>User
{{ this.$route.params.id }} // 123
</div>
</template>

动态匹配是以冒号区隔的,我们可以有多个动态段:

1
2
3
4
5
6
7
8
9
10
11
12
// routes.js
{ path:'/user/:userName/userId/:id', component:User},

// HelloWorld.vue
<router-link to="/user/王五/userId/789">王五-mutiple params</router-link>

// User.vue
<template>
<div>User
{{ this.$route.params }} // { "userName": "王五", "id": "789" }
</div>
</template>

路由routes与path、$route.params三者的关系见下表:

路由模式 匹配path $route.params
/user/:userName/userId/:id /user/王五/userId/789 { “userName”: “王五”, “id”: “789” }

除了$route.parmas,$route还封装了其他的信息如:
$route.query:用于获取URL中的查询,
$route.hash:用于获取url中#之后的信息(自己写的路径带#,值会包含#,如果是history模式下自动追加的#,则值不包含这个#),如果没有,值为空,如果URL中有多个#,从最后一个开始。

我们重写一下上文的例子:

1
2
3
4
5
6
7
8
9
10
11
// HelloWorld.vue
<router-link to="/user/Query/userId/101?code=123">Query</router-link><br>
<router-link to="/user/Hash/userId/102#hash=123">Hash</router-link><br>

// User.vue
<template>
<div>User
{{ this.$route.query }} // { "code": "123" }
{{ this.$route.hash }} // #hash=123 原路径:http://localhost:8081/#/user/Hash/userId/102#hash=123
</div>
</template>

由于vue-router使用 path-to-regexp作为路径path的匹配引用,所以我们可以使用正则表达式作为路径:

1
2
3
4
5
6
7
8
9
10
11
12
// routes.js
{ path:'/icon-:flower(\\d+).png', component:User},

// HelloWorld.vue
<router-link to="/icon-flower123">正则匹配</router-link><br>

// User.vue
<template>
<div>User
{{ this.$route.hash }} // /icon-flower123 原路径:http://localhost:8081/#/icon-flower123
</div>
</template>

我们可以使用星号(*)来代表通配符,只有*则匹配所有,常代表404页面,因为路由的优先级是优先配置优先匹配,相同的组件谁在前面先匹配谁。所以404页面通常写在最后一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// routes.js
const routes = [
// ...
{ path:'/user-*', component:User},
{ path:'*', component:User}
]

// HelloWorld.vue
<router-link to="/user-admin">*Admin</router-link><br>
<router-link to="/none">404</router-link><br>

// User.vue--*Admin
<template>
<div>User
{{ this.$route.params.pathMatch }} // admin
</div>
</template>

// User.vue--404
<template>
<div>User
{{ this.$route.params.pathMatch }} // /none
</div>
</template>

如上:
当使用*号路由时,一个参数pathMatch会自动追加给$route.params,它匹配星号后面的部分。

嵌套路由

真实的应用中常常用到组件之间的嵌套,使用vue-router可以很简单的表达这种嵌套关系,我们可以在子组件中嵌套使用<router-view>,如:

1
2
3
4
5
6
7
8
9
10
// NestedRoutes.vue
<template>
<div>
<a href="/">←返回</a><br>
<router-link to="/nestedRoutes/profile">profile</router-link><br>
<router-link to="/nestedRoutes/archive">archive</router-link><br>
<p>NestedRoutes</p>
<router-view></router-view>
</div>
</template>

为了做嵌套渲染,我们使用children操作符在路由中做结构配置:

1
2
3
4
5
6
7
// routes.js 嵌套路由
{path:'/nestedroutes', component: NestedRoutes, name: 'nestedroutes',
children: [
{path:'profile', component: resolve => require(['./components/nested-routes/Profile'],resolve)}, // 匹配 /nestedroutes/profile
{path:'archive', component: resolve => require(['./components/nested-routes/Archive'],resolve)} // 匹配 /nestedroutes/archive
]
},

嵌套路由以/为根路径,拼接路径的剩余部分,children其实和routes的最外层一样,是数组,所以我们可以根据需要继续嵌套。

当我们访问/nestedroutes的时候,不会在这里渲染任何内容,因为没有子路由,如果我们想要渲染,可以在路由里配置一个空的path的子路由:

1
2
3
4
5
6
7
8
// 嵌套路由
{path:'/nestedroutes', component: NestedRoutes, name: 'nestedroutes',
children: [
{path:'', component: resolve => require(['./components/nested-routes/NestedRoutesHome'],resolve)},
{path:'profile', component: resolve => require(['./components/nested-routes/Profile'],resolve)},
{path:'archive', component: resolve => require(['./components/nested-routes/Archive'],resolve)}
]
},

编程式的导航

除了以<router-link>的标签形式导航之外,我们还可以使用编程的方式导航,vue-router为我们提供了对应的实例方法this.$router.push

这个方法会将URL页面推到历史堆栈,所以当我们点击浏览器的返回按钮的时候指向前一个URL。当我们点击<router-link>的时候,内部其实调用的是router.push(...),所以这两个形式功能是等同的。

1
router.push(location, onComplete?, onAbort?)

location:导航位置,是一个字符串path或位置描述符对象,
第二个和第三个为可选参数,导航执行完成会执行这两个参数。
onComplete导航执行完会执行这个参数(所有的异步钩子都被执行完之后)
使用方式如下:

1
2
3
4
5
6
7
8
 <!--path-->
<button @click="$router.push('programmatic-navigation')">Path</button><br>
<!--object-->
<button @click="$router.push({path:'programmatic-navigation'})">Object</button><br>
<!-- named route-->
<button @click="$router.push({name:'programmatic-navigation', params: { id:123 }})">NamedRoute</button><br>
<!--with query reuslting in /programmatic-navigation?id=123-->
<button @click="$router.push({path:'programmatic-navigation', query:{ id:123 }})">Query</button><br>

注意:
如果使用了pathparams就被忽略了,但上面的query不会被忽略。所以如果我们要传递参数,可以用两种方式,一种是提供一个name路由,一种是在path里面手动拼接参数:

1
2
3
4
5
6
7
// routes.js
{ path:'/programmatic-navigation/params=/:id', component: resolve => require(['./components/programmatic-navigation/ProgrammaticNavigation'],resolve), name: 'programmatic-navigation-params' },

// ProgrammaticNavigation.vue
<!--传递参数 返回 /programmatic-navigation/params=/123-->
<button @click="$router.push({ name:'programmatic-navigation-params', params: { id }})">WidthParmas01</button><br>
<button @click="$router.push({ path:`/programmatic-navigation/params=/${ id }`})">WidthParmas02</button><br>

这个规则也同样适用于router-linkto属性。

如果我们的当前路径和跳转路径相同,那么我们需要在beforeRouteUpdate方法中响应数据更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
ProgrammaticNavigation.vue
<template>
<div>

<a href="/">←返回</a><br>
<p>ProgrammaticNavigation</p>
<pre>{{ JSON.stringify(params) }}</pre>

<button @click="$router.push({ path:`/programmatic-navigation/params=/${ 456 }`})">WidthParmas03</button><br>
</div>
</template>

<script>

export default {
name: "ProgrammaticNavigation",
data() {
return {
params:this.$route.params
}
},
beforeRouteUpdate (to, from, next) {
if (to.path == '/programmatic-navigation/params=/456') {
this.params = to.params
}
}
}
</script>

router.replace

router.replacerouter.push类似,唯一不同的是它不是推送到一个新的历史页,而是替换当前页。

router.go(n)

这个方法和window.history.go(n)类似,n用整数表示,表示在历史页中向前后向后走多少页。

1
2
3
4
5
6
// 后退一页
router.go(-1)
// 前进一页
router.go(1)
// 如果没有,导航失败,页面保持不变
router.go(10)

操作History

路由的router.push,router,replacerouter.go对应window.history.pushState, window.history.replaceStatewindow.history.go, 他们模仿的是window.historyAPIs。
Vue Router的导航方法(push, replace, go)在所有的路由模式下都可以工作(historyhashabstract)。

命名路由

有时候给路由一个名字更方便,用法也很简单:

1
2
3
4
5
6
7
8
9
10
// routes.js
{
path:'/named-routes/:id',
component: resolve => require(['./components/named-routes/NamedRoutes'],resolve),
name:'named-routes'
},

// HelloWorld.vue
<router-link :to="{name: 'named-routes', params: { id:123 }}">named-routes router-link</router-link><br>
<button @click="$router.push({name: 'named-routes', params: { id:123 }})">named-routes router.push</button>

这两种方式都是对象传递,注意to前面要加冒号的,表示内部是对象表达式而不是单纯的字符串。

命名视图

有时候我们可能需要在同一页展示多个视图,而不是做视图嵌套,比如说主页面或者是sidebar侧边栏。这个时候我们就会用到命名视图,也就是说在同一页面下使用多个<router-view>,给每个<router-view>一个不同的名字,如果没给名字,默认名为default

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  // routes.js 命名View
{
path:'/named-views',
component: resolve => require(['./components/named-views/NamedViews'],resolve),
children: [
{
path:'',
components: {
default:resolve => require(['./components/named-views/ViewA'],resolve),
b: resolve => require(['./components/named-views/ViewB'],resolve),
c: resolve => require(['./components/named-views/ViewC'],resolve),
},
}
]
},

// NamedViews.vue
<template>
<div>
<a href="/">←返回</a><br>
<p>NamedViews</p>
<router-view></router-view>
<router-view name="b"></router-view>
<router-view name="c"></router-view>
</div>
</template>

嵌套命名视图

我们可以在嵌套视图里使用命名视图创造更复杂的布局。我们来看一个例子:

1
2
3
4
5
6
7
8
9
// NestedNamedViews.vue
<template>
<div>
<p>NestedNamedViews</p>
<ViewNav/>
<router-view></router-view>
<router-view name="b"></router-view>
</div>
</template>

这里:

  • NestedNamedViews本身是View组件
  • ViewNav是一个常规的组件
  • router-view内部是被嵌套的视图组件

路由配置这样来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// routes.js
{ path: '/nested-named-views',
component: resolve => require(['./components/named-views/NestedNamedViews'], resolve),
children: [
{
path:'profile',
component: resolve => require(['./components/nested-routes/Profile'],resolve)
},
{
path:'archive',
components: {
default:resolve => require(['./components/named-views/ViewA'],resolve),
b: resolve => require(['./components/named-views/ViewB'],resolve),
}
}
]
},

转发和别名

转发
是我们可以在访问a的时候跳转b。
支持路径和命名访问两种形式:

1
2
3
4
5
6
7
// routes.js
{
path:'/orignal01' ,redirect: '/forward'
},
{
path:'/orignal02' ,redirect: {name: 'forward'}
},

别名是这样的,如果/b是组件a的别名,那意味着我们访问/b的时候,匹配的组件依然是a

1
2
3
4
5
6
7
// routes.js
{
path: '/forward',
name: 'forward',
component: resolve => require(['./components/redirect-views/RedirectViews'], resolve),
alias: '/alias'
},

传递Props给路由组件

为了使路由的组件更加灵活,vue支持在路由组件中传递Props,使用props操作符。支持三种模式:
布尔值模式

props设置为true的时候,route.params将被设置作为组件的props。

1
2
3
4
5
6
// routes.js
{
path: '/props-to-route/:id', // http://10.221.40.28:8080/props-to-route/123
component: resolve => require(['./components/props-route/PropsRoute'], resolve),
props: true
},

对象模式

当props是一个对象的时候,这个对象也一样会被设置给组件的props,这在props为固定值的时候很有用:

1
2
3
4
5
6
// routes.js
{
path: '/static', // http://10.221.40.28:8080/static
component: resolve => require(['./components/props-route/PropsRoute'], resolve),
props: {name: 'static'}
},

函数模式

我们可以创建一个函数返回props,这可以让你转化参数为其他类型,经静态值与基于路由的值结合,等等。

1
2
3
4
5
6
// routes.js
{
path: '/function-mode', // http://10.221.40.28:8080/function-mode?keyword=%27beijing%27
component: resolve => require(['./components/props-route/PropsRoute'], resolve),
props: (route) => ({ query: route.query.keyword})
},

如果URL为/function-mode?keyword='beijing'将传递{query: 'beijing'}作为组件的props。

注意:
props函数是无状态的,仅仅计算路由的改变。如果需要状态去定义props,那么Vue官方建议封装一个组件,这样vue就能够对状态做出反应。

路由进阶

导航警卫

这是vue-router提供的一些控制路由进程的函数,有三种表现方式:全局定义,路由内定义和组件中定义。

全局定义

分为前置警卫、解析警卫和后置钩子。我们依次看一下:

1.前置警卫

1
2
3
router.beforeEach((to, from, next) => {
// ...
})

无论哪个导航先触发之前都会先调用它,警卫的解析可能是异步的,所以在所有的钩子解析完成之前,导航处于悬停状态(pending)。实践中经常在这里判断是否携带了进入页面的必要信息,否则做跳转。(比如通过URL地址栏手动输入地址非法进入子页面,需要跳转到登录页让用户登录)

注意:别忘了写next函数,否则钩子函数将不会被解析。

2.全局解析警卫

1
2
3
router.afterEach((to, from) => {
// ...
})

和前置警卫一样,只不过是在导航确认之前,所有组件内警卫和异步路由组件被解析之后会立即调用。

3.全局后置钩子
这些钩子和警卫不同的是没有next函数,并且不影响导航。

1
2
3
router.afterEach((to, from) => {
// ...
})

路由独享的警卫

我们可以在路由的配置对象里配置:beforeEnter

1
2
3
4
5
6
7
8
9
10
11
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})

这个和全局前置警卫作用相同。

组件内警卫

在组件内可以定义一些路由导航警卫(会被传递给路由配置表):

  • beforeRouteEnter
  • beforeRouteUpdate
  • beforeRouteLeave

beforeRouteEnter不能访问this,因为组件还没有被创建。然后我们可以在next回调里访问,在导航被确认时,组件实例会被当做参数传递给这个回调:

1
2
3
4
5
beforeRouteEnter (to, from, next) {
next(vm => {
// access to component instance via `vm`
})
}

而在beforeRouteUpdatebeforeRouteLeavethis已经可用。所以next回调没有必要因此也就不支持了。

1
2
3
4
5
beforeRouteUpdate (to, from, next) {
// just use `this`
this.name = to.params.name
next()
}

离开警卫常常用于阻止用户意外离开未经保存的内容,导航可用通过next(false)取消。

1
2
3
4
5
6
7
8
beforeRouteLeave (to, from, next) {
const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
if (answer) {
next()
} else {
next(false)
}
}

完整的导航解析流程

  1. 导航被触发
  2. beforeRouteLeave警卫在失活组件被调用
  3. 全局的beforeEach警卫被调用
  4. 在可复用的组件内调用beforeRouteUpdate
  5. 在路由配置中调用beforeEnter
  6. 解析异步路由组件
  7. 在激活组件中调用beforeRouterEnter
  8. 调用全局beforeResolve警卫
  9. 导航被确认
  10. 调用全局afterEach钩子
  11. DOM更新被触发
  12. 用创建好的实例调用beforeRouteEnter守卫中传给next的回调函数。

路由Meta字段

定义路由的时候可以包含一个meta字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
children: [
{
path: 'bar',
component: Bar,
// a meta field
meta: { requiresAuth: true }
}
]
}
]
})

怎么样使用这个字段呢?

首先,在routes配置里的每一个路由对象称为路由记录。路由可能会被嵌套,因此当一个路由被匹配的时候,可能会匹配超过一个记录。

例如,上面的路由配置,/foo/bar将匹配父路由记录和子路由记录两个路由配置。

所有的被匹配的路由记录都被导出在$route对象(也在导航警卫的路由对象里)作为$route.matched数组。因此我们需要迭代这个数组来找到路由记录里的meta字段。

在全局导航警卫中检查meta字段的一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
// this route requires auth, check if logged in
// if not, redirect to login page.
if (!auth.loggedIn()) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
} else {
next()
}
} else {
next() // make sure to always call next()!
}
})

过渡

简单说就是给路由加一些过渡效果。有几种方式可以实现:
1.在统一的router-view入口给一个一致的过渡

1
2
3
<transition>
<router-view></router-view>
</transition>

查看所有的过渡APIs

2.在每一个组件内部给一个特别的过渡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Foo = {
template: `
<transition name="slide">
<div class="foo">...</div>
</transition>
`
}

const Bar = {
template: `
<transition name="fade">
<div class="bar">...</div>
</transition>
`
}

3.还可以根据当前路由和目标路由之间的关系设计动态过渡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<template>
<div>
<div>
<router-link to="subviewa">subviewa</router-link><br>
<router-link to="subview-b">subview-b</router-link><br>
<p>Home</p>
<transition :name="transitionName">
<router-view></router-view>
</transition>
</div>
</div>
</template>

<script>
export default {
name: "Home",
data() {
return {
transitionName: 'slide-left'
}
},
beforeRouteUpdate (to, from, next) {
this.transitionName = to.path < from.path ? 'slide-right' : 'slide-left'
next()
},
}
</script>

<style scoped>
.slide-left-enter, .slide-right-leave-active {
opacity: 0;
transform: translateX(10px);
}
.slide-left-leave-active, .slide-right-enter {
opacity: 0;
transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
</style>

数据获取

在实践中,很多时候在进入一个新的路由页面时,我们都是需要从服务器请求数据的,有两种方式:

  • 在导航之后获取:首先导航到新的页面,在导航的生命周期钩子中(比如created方法)做数据获取。
  • 在导航之前获取:在路由导航进入警卫之前获取数据,数据获取完成之后执行导航。

技术上都可以实现,用哪种取决于用户体验的目标。

导航之后获取数据

使用这种方式,我们会立即导航到新的页面,渲染组件,在组件的created钩子中渲染组件。当获取数据是我们可以展示一个loading的状态,并且每一页可以有自己的loading视图。

我们看一个代办事项的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<template>
<div>
<div v-if="loading">
Loading
</div>
<div v-if="error">
{{ error }}
</div>
<div v-if="list" >
<div v-for=" item in list" :key="item.id">
<p>{{ item.title }}</p>
</div>
</div>
</div>
</template>

<script>
import TodoAPI from '../api/todo';
export default {
name: "TodoList",
data () {
return {
loading: false,
list: null,
error: null
}
},
created () {
// 组件创建时获取数据
this.fetchData()
},
watch: {
// 路由改变的时候再次调用方法
'$route': 'fetchData'
},
methods: {
fetchData () {
this.error = this.list = null
// 模拟网络请求
TodoAPI.getTodoList(
(todolist) => {
this.loading = false,
this.list = todolist
},
(error)=>{
this.loading = false,
this.error = error
})
}
}
}
</script>

// 网络请求API -- todo.js
const _todoList = [
{"id": 1, "title": "购物"},
{"id": 2, "title": "回复邮件" }
]

export default ({
getTodoList (cb,errorCb) {
setTimeout(() => {
Math.random() > 0.5
? cb(_todoList)
: errorCb("数据请求失败")
},1000)
}
})

滚动行为

当我们使用vue-router的时候,我们可能想要打开新页再在一个位置,或使返回历史页保持在上次浏览的位置。vue-router允许我们自定义导航行为。

注意
这个功能仅仅在浏览器支持history.pushState的时候可以用。

当我们创建一个路由实例的时候,我们可以创建一个scrollBehavior函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const scrollBehavior = function (to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
const postion = {}

if (to.hash) {
postion.selector = to.hash
if (to.hash === '#anchor2') {
postion.offset = {y: 100 }
}
if (/^#\d/.test(to.hash) || document.querySelector(to.hash)) {
return postion
}
return false
}

return new Promise(resolve => {
if (to.matched.some(m => m.meta.scrollToTop)) {
postion.x = 0
postion.y = 0
}

this.app.$root.$once('triggerScroll', () => {
resolve(postion)
})
})
}
}

const router = new VueRouter({
mode: 'history',
scrollBehavior,
routes,
})

这里面接收to,from两个路由对象,第三个参数savedPosition,第三个参数仅仅在popstate导航中可用(浏览器的向前/后退按钮被触发的时候)。

这个对象返回滚动位置对象,格式如:

  • {x: nubmer, y:number }
  • {selector: string, offset? : { x: number, y: number}}(offset 仅仅被支持在2.6.0+以上)

如果是空值或无效值,则不会发出滚动。

1
2
3
4
5
6
7
scrollBehavior (to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { x: 0, y: 0 }
}
}

如果返回savedPosition,和我们使用浏览器的向前向后按钮一下,如果有历史滚动页,会返回到历史页查看的位置,如果返回{ x: 0, y: 0 }则滚动到顶部。

在开始的例子里,我们可以这样模拟滚动到某处的行为:

1
2
3
4
5
6
7
8
9
if (to.hash) {
postion.selector = to.hash

if (to.hash === '#anchor2') {
postion.offset = {y: 100 }
}

// ...
}

以下我们返回了一个Promise,来返回滚动的位置

1
2
3
4
5
6
7
8
9
10
11
12
return new Promise(resolve => {
// 如果匹配scrollToTop,我们就返回顶部
if (to.matched.some(m => m.meta.scrollToTop)) {
postion.x = 0
postion.y = 0
}

// 如果空值或无效值,返回当前滚动位置
this.app.$root.$once('triggerScroll', () => {
resolve(postion)
})
})

懒加载路由

当使用一个捆绑器构建应用时,JavaScript包会变得非常大,因此影响页面加载的时间。如果我们可以把每一个路由组件分离单独的块,等浏览页面时再加载他们会效率比较高。

Vue的异步组件功能webpack的代码分离功能可以很容易的做到这一点。

首先,可以将异步组件定义为返回Promise的工厂函数(应解析为组件本身):

1
const Foo = () => Promise.resolve({ /* component definition */ })

其次,在webpack2,我们可以使用动态导入语法去表示代码分离点:

1
import('./Foo.vue') // returns a Promise

注意
如果你使用Babel,你需要添加syntax-dynamic-import以便Babel可以正确解析。

把这两个步骤联合起来,我们就可以通过webpack自定定义一个异步的代码分离组件,所以我们可以把我们的路由配置修改为:

1
2
3
{
path: '/lazy-route', component: () => import('./components/lazy-route/LazyHome')
}

在同一个块中分组组件

有时,我们想要把所有组件分组到相同的异步块中,为了实现这个,在webpack > 2.4中,我们可以使用特殊的注释语法提供命名块:

1
2
3
4
5
6
{
path: '/lazyTwo', component: () => import(/* webpackChunkName: "lazy" */ './components/lazy-route/LazyTwo'),
children: [
{path: 'lazyThree', component: () => import (/* webpackChunkName: "lazy" */ './components/lazy-route/LazyThree')}
]
},

webpack将使用相同块名称的任何异步模块分组到相同的异步块中。


参考:Vue Router