上篇回顾:Vue基础
Vue Router
概述
上一期介绍了Vue的基本功能,这期着重说一下Vue的重点之一:路由。
Vue的路由,将以往我们原生界面的跳转整理在了一起,供我们可以集合起来进行调配。
而且,配合路由,我们可以很容易地将我们的应用变成单页应用。
再添加上一些页面切换效果,甚至可以作为H5嵌套到APP里面模拟出原生应用的效果。
1、起步
要使用路由,在引入vue.js
的基础上,还要引入vue-router.js
。
下面先来看一个示例:
(界面部分)
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
<div id="app">
<h1>Hello App!</h1>
<p>
<!-- 使用 router-link 组件来导航. -->
<!-- 通过传入 `to` 属性指定链接. -->
<!-- <router-link> 默认会被渲染成一个 `<a>` 标签 -->
<router-link to="/foo" tag=“span”>Go to Foo</router-link>
<router-link to="/bar">Go to Bar</router-link>
</p>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</div>
如上,界面的路由主要由两部分组成:
<router-link>
:导航(类似跳转按钮),设置跳转路径。
默认显示为<a>
标签,也可以通过tag=“span”
进行指定。
to="[路径]"
指定链接的地址。<router-view>
:跳转界面的展示区域。
用于加载to="[路径]"
链接界面的内容
(js部分)
// 0. 如果使用模块化机制编程,导入Vue和VueRouter,要调用 Vue.use(VueRouter)
// 1. 定义(路由)组件。
// 可以从其他文件 import 进来
var Foo = { template: '<div>foo</div>' }
var Bar = { template: '<div>bar</div>' }
// 2. 定义路由
// 每个路由应该映射一个组件。 其中"component" 可以是
// 通过 Vue.extend() 创建的组件构造器,
// 或者,只是一个组件配置对象。
// 我们晚点再讨论嵌套路由。
var routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]
// 3. 创建 router 实例,然后传 `routes` 配置
// 你还可以传别的配置参数, 不过先这么简单着吧。
var router = new VueRouter({
routes // (缩写)相当于 routes: routes
})
// 4. 创建和挂载根实例。
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
var app = new Vue({
router
}).$mount('#app')
// 现在,应用已经启动了!
以上,为一个简单的路由js配置,分为四个步骤:
- 声明路由组件,定义需要跳转的界面,如:
var Foo = { template: '<div>foo</div>' }
- 定义路由配置,将 路由路径 与 路由组件 匹配起来,如:
var routes = [ { path: '/foo', component: Foo } ]
- 创建路由的 router 实例,进行一些基本的配置,如:
var router = new VueRouter({ routes: routes })
- new Vue() 并加入路由配置,如:
new Vue({ router }).$mount('#app')
2、动态路径参数
Vue路由地址有几种携带参数的方式:
- 在
/
后用:
来声明,后面拼接上参数名,如:{ path: '/user/:idtest', component: User }
通过{{ $route.params.idtest }}
在跳转后的内容中获得参数值。 在写实际链接地址的时候,忽略:
,直接写入参数值,如:<router-link to="/user/foo">/user/foo</router-link>
<router-link to="/user/bar">/user/bar</router-link>
以上会进入同一个页面,而
foo、bar就是各自携带的参数,会被设置到this.$route.params
,可以通过{{ $route.params.idtest }}
获得。 - 传递多个参数,也可以这样:
/user/:username/post/:post_id
- 其他参数,如果带查询参数,可以这样:
// 带查询参数,变成 /user?plan=private
{ path: '/user', component: User, query: {plan: 'private' }})
- 动态参数还可以使用 命名路由 的形式来赋值:
声明时,name
指路由名称。
在界面上调用时,直接name
引用名称,params
记录参数即可。
示例:
{ path: '/user/:idtest', name:'test', component: User }
<router-link :to="{name:'test', params:{idtest: '11'}}">/user/foo</router-link>
- 监控路由跳转
watch: {
'$route' (to, from) {
//to:目的地
//from:当前地址
}
}
- 同一个路径可以匹配多个路由,此时,匹配的优先级就按照路由的定义顺序:谁先定义的,谁的优先级就最高。
3、嵌套路由
即在原来的路由配置下,加上children
属性,children
的配置也一样是原来的路由配置方式。
而对应有children
的路由页面,也加入<router-view>
以供加载子路由。
注:子路由继承父路径,所以子路由的path
只需设置后面的内容,且不用/
开头。
示例:
var User = {
template:
`<div class="user">
<h2>User {{ $route.params.id }}</h2>
<router-view></router-view>
</div>`
}
var 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
}
]
}
]
})
4、编程式的导航
路由导航跳转的原理:点击 <router-link>
,内部会调用router.push(...)
。
所以,除了在页面声明<router-link>
,也可以在js使用router.push(location)
来进行跳转页面。
该方法的参数可以是一个字符串路径,或者一个描述地址的对象。
如:
- 字符串:
router.push('home')
- 对象:
router.push({ path: 'home' })
- 命名的路由:
router.push({ name: 'user', params: { userId: 123 }})
- 带查询参数:
router.push({ path: 'register', query: { plan: 'private' }})
a、 router.push
方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的 URL。
b、 router.replace(location)
跟 router.push
很像,唯一的不同就是,它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录。
声明方式 <router-link :to="..." replace>
c、 router.go(n)
参数是一个整数,意思是在 history 记录中向前或者后退多少步,类似 window.history.go(n)
。(正数前进,负数返回)
5、命名的路由
在路由的设置里面,加入字段name
,作为路由的名称。
界面上就可以用路由的名称来调用路由了。
var 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
路径。
注: 是params
, 不是param
。
6、命名视图
对于想要同时(同级)展示多个视图的,就需要为每个视图命名,以进行区分。
当 router-view
没有设置名字时,默认为 default
。
当需要设置名字时,直接加属性name
即可,如下:
<router-view class="view one"></router-view>
<router-view class="view two" name="a"></router-view>
一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件。
在js上配置多个视图,使用components
进行配置(区别于单个视图的component
,多个需要加s
)。示例如下:
var router = new VueRouter({
routes: [{
path: '/',
components: {
default: Foo,
a: Bar,
b: Baz
}
}]
});
7、重定向和别名
- 重定向:当用户访问
/a
时,URL 将会被替换成/b
,然后匹配路由为/b
,如:
{ path: '/a', redirect: '/b' }
{ path: '/a', redirect: { name: 'foo' }}
{ path: '/a', redirect: to => {
// 方法接收 目标路由 作为参数
// return 重定向的 字符串路径/路径对象
}}
- 别名:
/a
的别名是/b
,意味着,当用户访问/b
时,URL 会保持为/b
,但是路由匹配则为/a
,就像用户访问/a
一样,如:
{ path: '/a', component: A, alias: '/b' }
8、导航钩子
主要用来拦截导航,让它完成跳转或取消。可以在跳转前、后进行一些自定义操作。
-
全局beforeEach钩子:
在导航跳转开始之前调用,按这些钩子创建的顺序进行调用。
调用完根据最终返回结果,确定导航是否进行跳转。
在执行钩子的过程中,导航会一直处于等待中。var router = new VueRouter({ ... }) router.beforeEach((to, from, next) => { // ... })
参数说明:
to
: 即将要进入的目标 路由对象
from
: 当前导航正要离开的路由
next
: 是一个function,一定要调用该方法来最终决定这个钩子的执行结果。可选的内容如下:next()
: 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是confirmed
(确认的)。next(false)
: 中断当前的导航。如果浏览器的 URL 改变了(可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到from
路由对应的地址。next('/')
或者next({ path: '/' })
: 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。
你可以向next()
传递任意位置对象,且允许设置诸如replace: true
、name: 'home'
之类的选项以及任何用在<router-link>
的to prop
或router.push()
中的选项。next(error)
: (2.4.0+) 如果传入next
的参数是一个 Error 实例,则导航会被终止且该错误会被传递给router.onError()
注册过的回调。
注:确保要调用 next 方法,否则钩子就不会被 resolved。
-
全局解析钩子beforeResolve (2.5.0+ 版本新增)
与router.beforeEach
类似,区别是在导航被确认之前,同时在所有组件内钩子和异步路由组件被解析之后,它才会被调用。 -
全局的after钩子:
在导航完成后调用。与before钩子类似,但没有next
方法,因为是在导航完成后执行,所以不能改变导航:
router.afterEach((to, from)=> { // ... })
-
某个路由独享的钩子:
你可以在路由配置上直接定义 beforeEnter 钩子:
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
注:这些钩子与全局 before 钩子的方法参数是一样的。
- 组件内的钩子:
直接以例子说明:
var Foo = {
template: `...`,
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当钩子执行前,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}
注:beforeRouteEnter
虽然因为组件还未创建所以不能调用this
,但在组件创建后会执行next
回调方法,所以可以在next
的回调函数里调用this
。
完整的导航解析流程:
- 导航被触发。
- 在失活的组件里调用离开钩子。
- 调用全局的
beforeEach
钩子。 - 在重用的组件里调用
beforeRouteUpdate
钩子 (2.2+)。 - 在路由配置里调用
beforeEnter
。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter
。 - 调用全局的
beforeResolve
钩子 (2.5+)。 - 导航被确认。
- 调用全局的
afterEach
钩子。 - 触发 DOM 更新。
- 用创建好的实例调用
beforeRouteEnter
钩子中传给next
的回调函数。
9、router-link
<router-link>
的两种写法:- 直接
to
,后面带跳转链接; - 绑定的
to
,后面链接(或者别名)需要特别加上''
, 如:v-bind:to="'home'"
、:to="'home'"
- 直接
<!-- 写法示例 -->
<!-- 1、字符串 -->
<router-link to="home">Home</router-link>
<!-- 渲染结果 -->
<a href="home">Home</a>
<!-- 2、使用 v-bind 的 JS 表达式 -->
<router-link v-bind:to="'home'">Home</router-link>
<!-- 3、不写 v-bind 也可以,就像绑定别的属性一样 -->
<router-link :to="'home'">Home</router-link>
<!-- 4、同上 -->
<router-link :to="{ path: 'home' }">Home</router-link>
- 设置
append
属性后,则在当前(相对)路径前添加基路径。例如,我们从/a
导航到一个相对路径b
,如果没有配置append
,则路径为/b
,如果配了append
,则为/a/b
,例如:
我们的当前路径为/link
,
<router-link :to="{ path: 'relative/path'}"></router-link>
:指向路径为/relative/path
<router-link :to="{ path: 'relative/path'}" append></router-link>
:指向路径为/link/relative/path
- 有时候想要
<router-link>
渲染成某种标签,例如<li>
。 可以使用tag
进行指定,如:tag="li"
active-class
: 设置 链接激活时使用的 CSS 类名。默认值可以通过路由的构造选项linkActiveClass
来全局配置
10、路由元信息meta
在配置路由的时候,直接加入字段meta
即可进行配置:
{ path: 'bar', component: Bar, meta: { requiresAuth: true } }
使用的时候,遍历$route.matched
来检查路由记录
中的 meta
字段。
为什么是遍历?
首先,我们称呼 routes
配置中的每个路由对象为路由记录
。
路由记录
可以是嵌套的,因此,当一个路由匹配成功后,他可能匹配多个路由记录。例如,根据上面的路由配置,/foo/bar
这个 URL 将会匹配父路由记录/foo
以及子路由记录/bar
两个路由记录
。
一个路由匹配到的所有路由记录会暴露为$route
对象的 $route.matched
数组,所以我们需要遍历 $route.matched
来检查路由记录
中的 meta
字段
11、界面特效
vue还提供了路由过渡的界面效果,和页面滚动效果。配合这些,可以让前端设计得更人性化。在移动端单页面的应用下,能更加贴近App的使用效果。
其他补充
- data参数监控:
vm.$watch(expOrFn, callBack, [options])
参数:
expOrFn
--> String 或 Function
callBack
--> 回调函数
options
--> 对象
options
取值:
deep
:boolean型。深度监视,为了发现对象内部值的变化,可以在选项参数中指定deep: true
。注意监听数组的变动不需要这么做。
immediate
:boolean型。在选项参数中指定immediate: true
将立即以表达式的当前值触发回调。
用法:观察 Vue 实例变化的一个表达式或计算属性函数,回调函数得到的参数为新值和旧值,表达式只接受监督的键路径。对于更复杂的表达式,用一个函数取代。
var vm = new Vue({
el:'#app',
data:{
a:123,
b:321
}
});
//监听数据变化
vm.$watch('a',function(){
alert('数据a 和 数据b 发生变化了');
this.b = this.a + 100;
},{deep:true});
-
限定样式仅限当前组件使用,使用
scoped
:在style中写入scoped
,如:<style scoped></style>
-
$nextTick
:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
即:在某控件发生变动后执行的方法。
下面引用一段话说明什么时候需要用到Vue.nextTick()
:
你在Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中。原因是什么呢,原因是在created()钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作无异于徒劳,所以此处一定要将DOM操作的js代码放进Vue.nextTick()的回调函数中。与之对应的就是mounted钩子函数,因为该钩子函数执行时所有的DOM挂载和渲染都已完成,此时在该钩子函数中进行任何DOM操作都不会有问题 。
在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的DOM结构的时候,这个操作都应该放进Vue.nextTick()的回调函数中。
原因是,Vue是异步执行dom更新的,一旦观察到数据变化,Vue就会开启一个队列,然后把在同一个事件循环 (event loop) 当中观察到数据变化的 watcher 推送进这个队列。如果这个watcher被触发多次,只会被推送到队列一次。这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和DOm操作。而在下一个事件循环时,Vue会清空队列,并进行必要的DOM更新。
当你设置 vm.someData = ‘new value’,DOM 并不会马上更新,而是在异步队列被清除,也就是下一个事件循环开始时执行更新时才会进行必要的DOM更新。如果此时你想要根据更新的 DOM 状态去做某些事情,就会出现问题。
为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback)
。这样回调函数在 DOM 更新完成后就会调用。
示例:
watch:{
data: function(){
this.$nextTick(function(){
//TODO:
});
}
}