由于很多视频和教程在谈及 Vue 的时候,总是习惯于从 Vue-cli 谈起,而组件化的开发方式相比 jQuery ,在思路上有着很大的不同。此外,这种开发方式往往会连带着会使用很多其他的技术,容易使得初学者感到不适。这里依据自己学习 Vue 的过程,帮助大家通览 Vue 学习中可能会使用的一些技术和知识。
Why Vue?
这里谈谈自己目前的看法:
当我们调用 API 从后台拉取数据后,如果想将这些数据渲染到页面中,就不得不使用循环,将拼接的 DOM 对象插入页面某一元素之中。在这一过程中,我们仅仅想操纵数据,却不得不为了渲染,而加入大量的标签拼接和 DOM 元素操作。这一点在进行复杂的 DOM 交互的时候体现的更为明显。
我们操作 DOM 元素,目的之一即是改变 DOM 的一些属性(HTML 属性、CSS 样式、子元素等)。而这些属性抽象来看都是数据,如果我们能够把对 DOM 的操作变为对数据的操作,而把数据与 DOM 之间的关联交给另一种机制处理,那么,很多需求都会得以优雅的实现。
使用 Vue 可以很好的解决上述问题,帮助开发者把时间更多地投入在核心业务实现上。
PS:当然,这不是 Vue 的唯一优点,但已经足够吸引我们去学习它。
Vue 的简单应用
引入 Vue
和其他 js 文件的引入方式相同,下载 vue.js 文件后,使用以下方式即可:
<script src="./vue.js"></script>
当然也可以使用 CDN 方式引入:
<script src="https://cdn.bootcss.com/vue/2.3.3/vue.js"></script>
注意,官方提供的 Vue 下载方式分为开发版本和生产版本,前者可以提供更为全面的报错信息。Vue 文件可以从以下网址中下载:
https://cn.vuejs.org/v2/guide/installation.html
Vue 实例
每个 Vue.js 应用都是
通过构造函数 Vue 创建一个 Vue 的根实例 启动的。
解释下官方文档的意思:Vue.js 起作用的所有代码都是以 Vue 实例为入口的(或者说都会汇集到 Vue 实例之中)。而得到一个 Vue 实例只需使用以下代码:
<script src="./vue.js"></script>
<script>
var app = new Vue({
// 配置选项
})
</script>
注:Vue 代码需要在引入 vue.js 之后书写。
Vue 作用范围
在 Vue 实例化的代码中,我们可以传入一个 {}
对象作为配置选项。而这其中最为重要的是 el
,他标示着 Vue 代码作用的 DOM 元素范围。
<div id="app">
</div>
<script>
var app = new Vue({
el: '#app'
});
</script>
通过设定 el : '#app'
,使得 Vue 会监控 id 为 app 的 DOM 元素,从而使得 Vue 起到作用。
这也就意味着,除了这个 id 为 app 的 div 之外的元素,Vue 是无法作用的。
注意,这里的 el
最终只会定位到一个 DOM 元素。也就是说,即使这里写成 .app
或 div
,也只会定位到第一个符合的 DOM。
data 配置和数据绑定
另一个重要的配置是 data
,标示 Vue 作用域之中可以使用的变量,如下:
<div id="app">
{{ msg }}
</div>
<script>
var app = new Vue({
el: '.app',
data: {
msg: 'hello'
}
});
</script>
使用 moustache 语法(双花括号),Vue 会按照 {{ msg }}
寻找 data
配置中的 msg
变量,并将其与 {{ msg }}
替换。注意,这里的 msg 是处于 data
配置中的变量。如果需要使用字符串,需要写成 {{ 'string' }}
。
此外,data
配置对象中的每一项都会暴露出来,也就意味着我们可以使用以下方式得到 msg
的值:
var app = new Vue({
el: '.app',
data: {
msg: 'hello'
}
});
console.log(app.msg);
而对于其他的配置项,想要使用他们,就需要使用以下方式:
console.log(app.$el);
这也就意味着,想要得到 msg
,我们还可以使用:
console.log(app.$data.msg);
打通了这一步,我们已经可以做很多事情了。在上面的代码中,我们将 data
中的 msg
渲染为文本,这实际上是 div#app
的 innerHTML
属性。那么,其他的属性是不是也能照本宣科地替换呢:
<div id="app">
<p title="{{ title }}"></p>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
title: 'p-title'
}
});
</script>
打开浏览器,在审查元素中,我们发现 title
属性并没有被替换。而在控制台中会显示警告:
Interpolation inside attributes has been removed. Use v-bind or the colon shorthand instead.
这表明,我们不能直接套用之前的方式来操作 DOM 的 HTML 属性。想要这么做的话,我们需要使用指令。
基础指令
v-bind
为了能够操作 HTML 属性,我们需要使用如下方式:
<div id="app">
<p v-bind:title="title"></p>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
title: 'p-title'
}
});
</script>
这种 v-
的方式被称为指令,v-bind
为非常常用的一种。v-bind:title="title"
可以将 HTML 属性 title 替换为 data
中的 title
。注意,这里我们无需再使用双花括号形式,写在指令中的值都会被认为是变量而从 data
中寻找。
想要使用字符串的话,可以加上单引号:
<div id="app">
<p v-bind:title="'string title'"></p>
</div>
同理,我们可以类比上例来改变其他属性:
<div id="app">
<p v-bind:id="id" v-bind:class="myclass" v-bind:title="title"></p>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
title: 'p-title',
id: 'p-id',
myclass: 'p-class'
}
});
</script>
注意,这里使用 myclass
是为了防止与 class
关键字冲突。
v-text & v-html
除了使用之前 {{ xxx }}
的方式向 DOM 中插入文本值以外,我们也可以通过指令的方式做到这一点:
<div id="app">
<div v-text="text"></div>
<div v-html="html"></div>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
text: '<h1> H1 TEXT </h1>',
html: '<h1> H1 HTML </h1>'
}
});
</script>
从字面意思也能看出,使用 v-text
插入的值会以文本形式展现,其中的 HTML 标签会被转义,而 v-html
则会直接以 HTML 代码的形式加入页面之中。
v-if & v-show
在前端业务中,控制某些元素的显隐是很常见的功能,我们可以使用 v-if
进行控制:
<div id="app">
<div v-if="canShow">SHOW</div>
<div v-if="!canShow">HIDE</div>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
canShow: true,
}
});
</script>
打开浏览器,在审查元素中我们可以看到,SHOW 这一 div 会存在于页面之中,而另一个则不会。注意,这不是简单的 display:none
,而是在 DOM 树中删除了这一节点。
如果我们想通过 display
控制显隐,可以使用 v-show
:
<div id="app">
<div v-show="canShow">SHOW</div>
<div v-show="!canShow">HIDE</div>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
canShow: true;
}
});
</script>
打开审查元素可以看到,这里的隐藏是通过 display:none
进行控制的。
注意,这里我们使用了非运算符 !canShow
。在属性绑定之中,单运算符是可以使用的,而对于如条件语句等相对复杂的计算,我们可以使用其他的方式,见后续 computed
配置内容。
v-for
提到前端应用,循环渲染也是经常使用的,最常见的便是表格渲染:
<div id="app">
<table v-for="rows in tables" style="border: 1px solid #000;">
<thead>
<tr>
<th>序号</th>
<th>姓名</th>
<th>年龄</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in rows">
<td>{{ index + 1 }}</td>
<td>{{ row.name }}</td>
<td>{{ row.age }}</td>
</tr>
</tbody>
</table>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
tables: [
[
{
name: 'foo1',
age: 'age1'
},
{
name: 'foo1',
age: 'age1'
}
],
[
{
name: 'foo2',
age: 'age2'
}
]
],
}
});
</script>
这里注意,循环写在了循环项之中,而不是循环项的父级元素,需要与其他模板框架作区分。
v-bind 指令的简写
形如 v-bind:xxx
的指令中,xxx
成为指令的参数,而对于 v-bind
这种指令,参数是一定会有的,每次都写 v-bind
显得很是繁琐。Vue 为其提供了简写形式,使得我们可以使用 :
代替 v-bind:
书写这类指令:
<div id="app">
<button :title="title" :id="id">
测试按钮
</button>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
title: 'btn-text',
id: 'btn-id',
}
});
</script>
双向绑定
在以上指令的例子中,数据单向的由 Vue 实例输出到 DOM 之中。在 vue.js 中,我们还可以使用 v-model
指令用以实现数据从 DOM 向 Vue 实例的输出。
事实上,用户能在浏览器 DOM 中手动改变的属性也就是 value 值,最直观的便是 input 标签:
<div id="app">
<input type="text" v-model="username">
<p v-text="username"></p>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
username: 'default-username',
}
});
</script>
这里,v-model
实现了数据的双向绑定,从而把 input, p 标签和 data.username
绑定在了一起。
同样的,具有 value 属性的元素都可以通过 v-model
实现双向绑定。
texearea
<div id="app">
<textarea name="name" rows="8" cols="80" v-model="description"></textarea>
<p v-text="description"></p>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
description: 'default-description',
}
});
</script>
radio
<div id="app">
<label for=""><input type="radio" name="gender" value="male" v-model="gender"> 男 </label>
<label for=""><input type="radio" name="gender" value="female" v-model="gender"> 女 </label>
<p v-text="gender"></p>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
gender: 'male',
}
});
</script>
checkbox
<div id="app">
<label for=""><input type="checkbox" name="holiday" value="Monday" v-model="holiday"> 周一 </label>
<label for=""><input type="checkbox" name="holiday" value="Tuesday" v-model="holiday"> 周二 </label>
<label for=""><input type="checkbox" name="holiday" value="Wednesday" v-model="holiday"> 周三 </label>
<p v-text="holiday"></p>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
holiday: [],
}
});
</script>
注意,多选框中需要把 holiday
设置为 []
。
单选 select
<div id="app">
<select v-model="holiday" style="width: 50px">
<option value="Monday">Monday</option>
<option value="Tuesday">Tuesday</option>
<option value="Wednesday">Wednesday</option>
</select>
<p v-text="holiday"></p>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
holiday: null,
}
});
</script>
多选 select
<div id="app">
<select v-model="holiday" multiple style="width: 50px">
<option value="Monday">Monday</option>
<option value="Tuesday">Tuesday</option>
<option value="Wednesday">Wednesday</option>
</select>
<p v-text="holiday"></p>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
holiday: [],
}
});
</script>
计算属性
有时,我们可能需要对 data
中的一些值进行简单的计算,从而得到一个可能会复用的计算后的值。在 Vue 中,可以通过 computed
配置实现:
<div id="app">
<h1>{{ info }}</h1>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
name: 'my-name',
age: 'my-age',
},
computed: {
info: function(){
return 'Name: ' + this.name + ', Age: ' + this.age;
}
}
});
</script>
在这里,我们通过 computed
配置中的 info
计算了 name
和 age
的值。而对于计算属性 info
,它的使用和 data
是相同的。
info
会监听 name
和 age
的变化,当他们的值改变时,info
都会改变:
<div id="app">
<input type="text" v-model="name">
<input type="text" v-model="age">
<h1>{{ info }}</h1>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
name: 'my-name',
age: 'my-age',
},
computed: {
info: function(){
return 'Name: ' + this.name + ', Age: ' + this.age;
}
}
});
</script>
事件
在前端应用中,事件机制是必不可少的,Vue 中使用 v-on:xxx
来为元素绑定事件。而在事件触发机制中,事件被配置在 methods
之中:
<div id="app">
<button v-on:click="add">增加</button>
<h1>{{ count }}</h1>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
count: 0,
},
methods: {
add: function(){
this.count++;
}
}
});
</script>
在这里,我们通过 v-on:click="add"
为 button 添加了一个点击时间,并使其在点击后触发 methods
之中的 add
方法。
此外,我们也经常遇到检测键盘按键事件的需求,比如检测回车键:
<div id="app">
<input type="text" v-on:keydown="keydown">
</div>
<script>
var app = new Vue({
el: '#app',
data: {
count: 0,
},
methods: {
keydown: function(e){
if(13 === e.keyCode){
console.log('Enter pressed.');
}
}
}
});
</script>
Vue 中提供了一种更为简便的方式来针对性地检测按键类别:
<div id="app">
<input type="text" v-on:keydown.enter="keydown">
</div>
<script>
var app = new Vue({
el: '#app',
data: {
count: 0,
},
methods: {
keydown: function(e){
console.log('Enter pressed.');
}
}
});
</script>
这种形如 v-on:keydown.enter
的写法被称为修饰符。在 Vue 中,可以使用的修饰符有很多,能够大大简化我们的代码。
与 v-bind:xxx
相同,v-on:xxx
也比较常用。在 Vue 中,我可以使用如下方式简写这一指令:
<div id="app">
<input type="text" @keydown.enter="keydown">
</div>
组件
局部组件和全局组件
在很多前端框架中,我们可以使用 class="navbar"
来得到一个具有导航样式的 DOM 元素。试想,如果我们可以使用 <Navbar></Navbar>
来创建导航元素,那么在语义层面会显得更容易理解。页面的布局也会显得更为清晰(事实上,在 HTML5 中,也新增了一些语义化的标签)。
想要实现这一点,我们就需要使用 Vue 中的组件。关于组件有很多东西要说,这里先简单的介绍下组件的简单用法。
<div id="app">
<navbar></navbar>
</div>
<script>
var app = new Vue({
el: '#app',
components: {
'navbar' : {
template: '<div> --- hello navbar --- </div>'
}
}
});
</script>
我们使用 components
配置了一个名为 navbar
的组件。而后 Vue 会根据配置,将对应的标签替换为 template
中的内容。
注意,这种组件注册的方式为局部注册,即该组件只能在当前 Vue 实例的作用域(el
配置的 DOM 元素下)才有效果。除此之外,我们也可以在全局注册组件:
<div id="app">
<navbar></navbar>
</div>
<script>
Vue.component('navbar', {
template: '<div> --- global component --- </div>',
});
var app = new Vue({
el: '#app',
});
</script>
我们可以通过多个 Vue.component('component-name', { /*config*/ })
在全局注册多个组件。
所谓全局,也就是说,在任意 Vue 实例中都是起作用的:
<div id="app">
<navbar></navbar>
</div>
<div id="foo">
<navbar></navbar>
</div>
<script>
Vue.component('navbar', {
template: '<div> --- global component --- </div>',
});
var app = new Vue({
el: '#app',
});
var foo = new Vue({
el: '#foo',
})
</script>
组件的另一种引用方式
直接在 template
配置中以字符串的形式写入标签显得很突兀,Vue 还提供了另一种引入模板的方式:
<div id="app">
<navbar></navbar>
</div>
<div id="foo">
<navbar></navbar>
</div>
<script type="text/x-template" id="my-template">
<div> --- global component --- </div>
</script>
<script>
Vue.component('navbar', {
template: '#my-template',
});
var app = new Vue({
el: '#app',
});
var foo = new Vue({
el: '#foo',
})
</script>
注意,x-template
脚本务必写在 Vue 代码之上,不然会因为引入顺序的问题导致 Vue 找不到模板。
组件命名约定
如文档中所说,在注册组件的时候,对命名的方式是没有限制的,但在使用时却需要使用短横线形式:
当注册组件(或者 props)时,可以使用短横线(kebab-case) ,小驼峰(camelCase) ,或大驼峰(TitleCase) 。Vue 不关心这个。
而在 HTML 模版中,请使用 kebab-case 形式。
这也就是说,形如下列形式的组件命名:
components: {
'kebab-cased-component': { /* ... */ },
'camelCasedComponent': { /* ... */ },
'TitleCasedComponent': { /* ... */ }
}
在使用时,都需要变为短横线形式:
<kebab-cased-component></kebab-cased-component>
<camel-cased-component></camel-cased-component>
<title-cased-component></title-cased-component>
组件插值
在全局注册方式之下,我们可以通过以下方式引入 data
值:
<div id="app">
<navbar></navbar>
</div>
<script>
Vue.component('navbar', {
template: '<div> {{ count }} </div>',
data: {
count: 1,
}
});
var app = new Vue({
el: '#app',
});
</script>
注意,这里的 data
值写在了全局的 component
之中,而非 Vue 实例。
当我们这么写时,浏览器会报错:
The "data" option should be a function that returns a per-instance value in component definitions.
借用文档中的方法,我们可以使用如下方式使 data 返回函数,从而跳过报错:
let data = {
count: 1
};
Vue.component('navbar', {
template: '<div> {{ count }} </div>',
data: function(){
return data;
}
});
var app = new Vue({
el: '#app',
});
但当我们这样书写代码时,就会发现出现了问题:
<div id="app">
<navbar></navbar>
<navbar></navbar>
<navbar></navbar>
</div>
<script>
let data = {
count: 1
};
Vue.component('navbar', {
template: '<button @click="count = count + 1"> {{ count }} </button>',
data: function(){
return data;
},
});
var app = new Vue({
el: '#app',
});
</script>
这是官方文档中的一个例子,在这一例子中,点击任意一个 navbar
组件,都会使得 count
值加 1,这意味着组件间共用了 count
变量。
有时我们确实需要组件间共享变量,但更多的需求是各个组件独享,这时我们便需要将 data
写为如下形式:
<div id="app">
<navbar></navbar>
<navbar></navbar>
<navbar></navbar>
</div>
<script>
Vue.component('navbar', {
template: '<button @click="count = count + 1"> {{ count }} </button>',
data: function(){
return {
count: 1
};
}
});
var app = new Vue({
el: '#app',
});
</script>
乍一看,这种方式和之前的写法似乎是一样的(只是把 let data 替换掉而已),可是
return {
count: 1
};
的写法实际相当于:
return new Object({
count: 1
});
如果读者在之前有了解过深拷贝和浅拷贝的知识的话,就会很容易理解这一点了。
let data = {
count: 1
};
Vue.component('navbar', {
template: '<button @click="count = count + 1"> {{ count }} </button>',
data: function(){
return data;
},
});
这种方式使得每个组件操作的数值都是同一个 data
,而组件触发事件后所修改的值都是同一个 data
的 count
键,自然会导致之前的结果。
而当代码写成以下形式时:
return {
count: 1
};
每次的返回值都是一个新的 Object
,这样就可以使得各个组件独享数据了。
但是问题又来了,当需要各个组件共享某一变量时该怎么做呢?参照上述的内容,我们可以使用如下方式实现:
<div id="app">
<navbar></navbar>
<navbar></navbar>
<navbar></navbar>
</div>
<script>
let store = {
globalCount : 0,
}
Vue.component('navbar', {
template: '<div><button @click="localCount += 1"> Local: {{ localCount }} </button> <button @click="store.globalCount += 1"> Global: {{ store.globalCount }} </button></div>',
data: function(){
return {
localCount: 1,
store: store,
};
}
});
var app = new Vue({
el: '#app',
});
</script>
类比深拷贝和浅拷贝的知识,上例会比较容易理解。在使用中,共享和独享数据需要根据需求针对选择。
注意,这里我们把 template
写成了:
<div>
<button @click="localCount += 1"> Local: {{ localCount }} </button>
<button @click="store.globalCount += 1"> Global: {{ store.globalCount }} </button>
</div>
是因为组件必须只有一个元素作为根元素,直接写成两个 button
标签会出现如下错误:
Component template should contain exactly one root element. If you are using v-if on multiple elements, use v-else-if to chain them instead.
组件注意事项
如官方文档所说:
当使用 DOM 作为模版时(例如,将 el 选项挂载到一个已存在的元素上), 你会受到 HTML 的一些限制,因为 Vue 只有在浏览器解析和标准化 HTML 后才能获取模版内容。尤其像这些元素 <ul> ,<ol>,<table> ,<select> 限制了能被它包裹的元素, 而一些像 <option> 这样的元素只能出现在某些其它元素内部。
在某些时候,我们需要使用:
<table>
<tr is="my-row"></tr>
</table>
来代替:
<my-row></my-row>
组件传参
大家应该会注意到,以上我们在使用数据时,data
字段是写于 Vue.component
之中而非 Vue 实例中的。这是因为,组件是不能直接访问全局数据的,这一机制实际上防止了组件数据的污染。当我们写成如下形式时:
<div id="app">
<navbar></navbar>
</div>
<script>
Vue.component('navbar', {
template: '<div> Local: {{ count }} </div>',
});
var app = new Vue({
el: '#app',
data: function(){
return {
count: 1,
};
}
});
</script>
会有报错信息,提示 count
值找不到:
[Vue warn]: Property or method "count" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
当组件需要使用全局数据时,需要通过传参的形式:
<div id="app">
<navbar :counter="count"></navbar>
</div>
<script>
Vue.component('navbar', {
template: '<div> Local: {{ counter }} </div>',
props: [
'counter'
]
});
var app = new Vue({
el: '#app',
data: function(){
return {
count: 1,
};
}
});
</script>
上例中,Vue.component
配置中的 props
规定了该组件可以传递的参数。之后便可以在通过 <navbar :counter="count"></navbar>
实现参数传递。
我们把这种传参方式抽象来看,这其实形成了父组件到子组件的数据流。
这里务必注意以下两种方式的区别:
<navbar :counter="count"></navbar>
<navbar counter="count"></navbar>
根据之前数据绑定的内容,前者中的 counter
相当于 v-bind:counter
,这里的 count
为变量;而后者的 count
只相当于字符串 'count'
。
这里引出另一个问题:在 v-bind:xxx="foo"
中出现的 foo
其实是变量名,如果想输入字符串,需要写成:
v-bind:xxx="'foo'" # 注意单引号
这里涉及到字面量值的问题,比如当我们需要传递数字 1 的时候,使用 counter="1"
实际传入字符串 '1'
。我需要通过 v-bind:counter="1"
才能正确地传入数字 1;
事件传递
现在我们知道,父组件可以通过 props
向子组件传递参数,数据流实现了自根向叶的传递,那么子组件如何向父组件传递信息呢?
在 Vue 中,我们需要通过自定义事件来实现数据的反向流动:
<div id="app">
<navbar @my-event="pop" ></navbar>
</div>
<script>
Vue.component('navbar', {
template: '<button @click="clicker">POP</button>',
methods: {
clicker: function(){
this.$emit('my-event');
}
}
});
var app = new Vue({
el: '#app',
methods: {
pop: function(){
alert('A child component pops.');
}
}
});
</script>
在上例中,子组件捕捉 clicker
事件后,通过 this.$emit('my-event')
向上传递了 my-event
事件。而在 <navbar @my-event="pop" ></navbar>
中,捕获了 my-event
这一自定义事件,从而打通了反向的通路。
可是似乎我们只是打通了通路,数据应该怎么传递呢?在 Vue 中,我们只需在事件函数中添加参数即可:
Vue.component('navbar', {
template: '<button @click="clicker">POP</button>',
methods: {
clicker: function(){
let paramA = 'hello';
let paramB = 'vue';
this.$emit('my-event', paramA, paramB);
}
}
});
var app = new Vue({
el: '#app',
methods: {
pop: function(pA, pB){
alert('A child component pops with params: ' + pA + ', ' + pB);
}
}
});
原生事件传递
使用自定义事件固然可以做到业务逻辑的反向流动,但面对一些开源的组件库,直接修改源码显然不显示。如果此时我们希望对一些组件绑定诸如点击等事件,就需要使用到 native
修饰符实现。
如下代码中,直接对组件绑定点击事件是无效的:
<div id="app">
<child @click='clicker'></child>
</div>
<script src="./vue.js"></script>
<script>
Vue.component('child', {
template: '<button>POP</button>'
});
var app = new Vue({
el: '#app',
methods: {
clicker: function() {
alert('A child component click.');
}
}
});
</script>
面对这种情况,我们需要使用 native
修饰符:
<div id="app">
<child @click.native='clicker'></child>
</div>
<script src="./vue.js"></script>
<script>
Vue.component('child', {
template: '<button>POP</button>'
});
var app = new Vue({
el: '#app',
methods: {
clicker: function() {
alert('A child component click.');
}
}
});
</script>
渲染函数
在之前已经说过,我们可以通过以下方式实现组件渲染:
<div id="app">
<navbar></navbar>
</div>
<script>
Vue.component('navbar', {
template: '<h1>template...</h1>',
});
var app = new Vue({
el: '#app',
});
</script>
在 Vue 中,采用这种 template
的方式(包括 <script type='text/x-template'></script>
)后,Vue 会将其编译为 render
函数。形如以下写法:
<div id="app">
<navbar></navbar>
</div>
<script>
Vue.component('navbar', {
render: function(createElement){
return createElement('h1', 'template..');
},
});
var app = new Vue({
el: '#app',
});
</script>
在这里演示了简单的 render
函数应用,createElement('h1', 'some content')
相当于 <h1>some content</h1>
。
对于更为复杂的模板,我们可以使用如下方式:
<div id="app">
<navbar :proptitle="mytitle"></navbar>
</div>
<script>
Vue.component('navbar', {
render: function(createElement) {
return createElement('div', {
domProps: {
innerText: 'content..',
},
attrs: {
id: 'my-id',
title: this.proptitle
},
props: ['proptitle']
});
},
});
var app = new Vue({
el: '#app',
data: function() {
return {
mytitle: 'my-title'
};
}
});
</script>
上例中,我们使用 domProps
设置了 v-text
;使用 attrs
设置了标签的 HTML 属性。其中,title: this.proptitle
将 title
属性与 proptitle
关联了起来。
采用 template
的写法相当于:
<div id="app">
<navbar :proptitle="mytitle"></navbar>
</div>
<script>
Vue.component('navbar', {
template: '<div id="my-id" title="proptitle">content</div>',
props: ['proptitle']
});
var app = new Vue({
el: '#app',
data: function() {
return {
mytitle: 'my-title'
};
}
});
</script>
可以看出,render
函数相当于实现了标签到对象的编译过程。
createElement 别名
根据文档中所说,我们可以使用 h
作为 createElement
的别名,从而遵守 Vue 社区的规范。
将 h 作为 createElement 的别名是 Vue 生态系统中的一个通用惯例,实际上也是 JSX 所要求的,如果在作用域中 h 失去作用, 在应用中会触发报错。
即写为如下形式:
Vue.component('App', {
render: function(h) {
return h('div', {
});
},
});
运行时构建和独立构建
在 Vue 中,运行过程可以分为编译为 render
函数,以及渲染函数被调用两步。
我们可以把这两部都放在浏览器中进行,这种方式被称之为运行时构建。由于运行时构建不会先产生编译后的 render
函数,因而文件比较小。
除了运行时构建,我们还可以将编译为 render
函数的过程放在服务端(非浏览器端)进行,比如采用 vue-loader
一类的工具实现进行编译(如进行 vue 单文件组件开发时)。而这一类便为称为独立构建。可想而知,独立构建会产生不少 render
函数,从而使得其文件体积较大。
字符串模板与非字符串模板
由于 HTML 中并无标签和属性的大小写区别,所以 mytitle="xxx"
和 myTitle="xxx"
是一样的。这也就意味着,当我们需要使用一些属性向模板传值的时候,小驼峰(camelCased )的命名形式需要转为短横线(kebab-case)形式:
<div id="app">
<navbar :prop-title="mytitle"></navbar>
</div>
<script type="text/x-template" id="template">
<h1 :title="propTitle">{{propTitle}}</h1>
</script>
<script>
Vue.component('navbar', {
template: '#template',
props: ['propTitle']
});
var app = new Vue({
el: '#app',
data: function() {
return {
mytitle: 'title_content'
};
}
});
</script>
当我们采用驼峰形式作为属性向模板中传值时:
<navbar :propTitle="mytitle"></navbar>
实际接收到的属性为:proptitle
,而在定义中,注册的属性变量为:propTitle
,因而不能正确的传递信息。此时,Vue 会在控制台给出提示:
[Vue tip]: Prop "proptitle" is passed to component <Navbar>, but the declared prop name is "propTitle". Note that HTML attributes are case-insensitive and camelCased props need to use their kebab-case equivalents when using in-DOM templates. You should probably use "prop-title" instead of "propTitle".
在上例中,我们使用 <navbar :prop-title="mytitle"></navbar>
的形式在 #app
作用域下使用了模板。如之前所说,这种形式会受制于 HTML 无视大小写的问题,为了规避这一问题,除了上述所说的短横线形式之外,我们还可以使用其他调用方式:
<div id="app">
<demo></demo>
</div>
<script>
Vue.component('navbar', {
template: '<h1 :title="propTitle">{{propTitle}}</h1>',
props: ['propTitle']
});
Vue.component('demo', {
template: '<navbar :prop-title="mytitle"></navbar>',
data: function() {
return {
mytitle: 'title_content'
};
}
});
var app = new Vue({
el: '#app'
});
</script>
注:这里写成短横线形式也是可以的。
在本例中,我们使用 demo
标签调用 demo
组件,进而调用 navbar
,注意,由于 <navbar :prop-title="mytitle"></navbar>
并没有直接以 HTML 的形式存在,而是以字符串的方式出现在 template
的配置中。如上述关于渲染函数的
介绍,这种形式可以规避 HTML 本身大小写不敏感而导致的问题。
同理,之前提到的组件命名约定所需保证的短横线引用形式,在字符串模式中也是无需遵守的。
注:文档中采用字符串模板和非字符串模板来表示二者的区分,个人觉得这样的描述反而容易引起歧义。实际上,无论是采用上述写法或是用渲染函数(又或是后续会提及的单文件组件),只要能够跳过 HTML 的直接形式,都可以避免大小写不敏感而导致的问题。
Vue 生命周期
文档中提到了 Vue 实例的生命周期,以及与之相关的生命周期钩子函数。所谓钩子函数,即是在进入生命周期的各个阶段时触发的函数。为了正确的使用这些生命周期钩子函数,我们首先需要了解 Vue 到底有哪些生命周期。
一个 Vue 示例的生命周期大体上为:data 的创建,DOM 节点的创建,data 的更新,以及实例的销毁。
根据 Vue 实例在各个阶段的任务,Vue 提供了以下生命周期钩子函数,用于在进入具体的某一阶段时,自定义地执行一些操作:
beforeCreate & created
二者相对 Vue 实例中的 data
而言。
在 beforeCreate
阶段,data
还未创建,即配置在 Vue data
中的数据还处于 undefined
状态。
在 created
阶段,data
按照配置被赋予相应的值。
beforeMount & mounted
二者相对于 DOM 而言。
在 beforeMount
阶段,最终的 DOM 以虚拟 DOM 的方式存在。
而在 mounted
阶段,DOM 被创建,且 DOM 中引用的 Vue 中的 data 被替换成相应的值。
beforeUpdate & updated
二者相对于 data
的更新而言。
在 Vue 生命周期之中,data
会不断地发生变化,每一次变化都会一次触发 beforeUpdate
和 updated
。
beforeDestroy & destroyed
二者相对于 Vue 实例的销毁而言。
示例
我们通过以下代码进行测试:
<div id="app">
{{ username }}
</div>
<script>
var app = new Vue({
el: '#app',
data: {
username: 'dailybird',
created_at : '2017.6.12'
},
beforeCreate: function(){
console.log(this.username);
console.log(this.$el);
},
created: function(){
console.log(this.username);
console.log(this.$el);
},
beforeMount: function(){
console.log(this.username);
console.log(this.$el);
},
mounted: function(){
console.log(this.username);
console.log(this.$el);
},
beforeUpdate: function(){
console.log('before update');
},
updated: function(){
console.log('updated');
},
beforeDestroy(){
console.log('before destroy');
},
destroyed(){
console.log('destroyed');
}
});
app.username = 'dailybirdo';
setTimeout('app.$destroy()', 100);
</script>
浏览器控制台中输出的结果为:
注意,这里在调用 app.$destroy()
之前延迟了一段时间,是防止 Vue 实例销毁之时,数据的更新还没有完成的情况。
用法
那么,这些生命周期钩子函数应该用于什么场景和需求呢?
对于一些网页应用而言,数据的获取是网页在浏览器中加载时异步进行的。也就是说,页面框架和数据渲染不是同时推送到浏览器中的。参考于 Vue 实例的两个重要阶段:created
和 mounted
,二者分别表示 data
和 DOM 的创建完成时机。
如果我们在 mounted
之时进行数据接口调用,而此时 DOM 元素已经渲染,就会出现页面元素中的 data
值先填充为 Vue 实例中配置的默认 data
,然后被后台返回的真实数据替换的情况。
因而,这里推荐在 created
阶段进行数据获取 API 的调用。尽管由于异步原因,仍可能出现数据返回之时,Vue 实例已经进入 mounted
阶段,但这已是相对合适的调用时机了。
如果我们选择在 beforeCreated
阶段调用 API,此时 data
还没有被创建,如果 API 返回的速度很快,早于 created
,就会出现真实的后台数据被 Vue 配置中的 data
覆盖的情况。
为了优化由于异步调用方式而可能造成问题,我们可以在 beforeCreated
阶段展示一个加载框,或使用合适的默认值如:正在加载数据...
来提升用户体验。
关于生命周期钩子的详细应用可以参考:Vue 实例中的生命周期钩子详解。
需要准备的 ES6 知识
作为新的 JavaScript 标准,ES6 引入了很多新的特性和语法,为开发带来了便捷。然而现在还有很多浏览器不支持 ES6,这一点的解决方法我们会在之后谈及 Webpack 和 Bebel 时提到。
以下介绍一些在 Vue 开发中必备的 ES6 知识,至于全部的 ES6 内容,大家可以参考 ECMAScript 6 入门。
let
let
关键字改变了 JavaScript 语言没有块级作用域的情况。
在原先使用 var
关键字的情况下:
var i = 0;
while(true){
i = 2;
break;
}
console.log(i); // i = 2
而改为 let
后:
let i = 0;
while(true){
let i = 2;
break;
}
console.log(i); // i = 0
还有非常经典的闭包问题:
for (var i = 0; i < 4; i++) {
setTimeout(function(){
console.log(i);
}, 1000 * i);
}
// 每隔一秒输出一个 4
之前的解决方式是使用函数级作用域形成闭包:
for(var index = 0; index < 4; index++){
(function(i){
setTimeout(function(){
console.log(i);
}, 1000 * i);
})(index)
}
// 每隔一秒依次输出 0 - 3
而现在,我们可以使用:
for (let i = 0; i < 4; i++) {
setTimeout(function(){
console.log(i);
}, 1000 * i);
}
// 每隔一秒依次输出 0 - 3
有关闭包和作用域问题可以参考之前的撰文:谈谈 setTimeout 这道经典题目。
为了保持与其他语言在变量作用域上的统一,以免跳进大坑,建议使用 let
关键字。
对象
对象和 JSON的区别
对象和 JSON 看上去很像,都是使用花括号包裹,都是键值对的形式。但二者仍有很多不同:
对象中键可以不加引号,但 JSON 中键必须用双引号包裹(不能使用单引号);
对象的值可以为函数,而 JSON 的键值都必须为字符串;
JSON 中,最后一项的末尾不能加逗号,而对象无此限制。
键值对的简化写法
一般,我们可以使用如下方式定义一个对象:
let obj = {
keyA: "valueA",
keyB: "valueB"
};
console.log(obj);
有时,当键值同名时,我们仍然不得不写成如下形式:
let keyA = "keyA";
let keyB = "keyB";
let obj = {
keyA: keyA,
keyB: keyB
};
console.log(obj);
而现在,我们可以使用以下方式简化写法:
let keyA = "keyA";
let keyB = "keyB";
let obj = {
keyA,
keyB
};
console.log(obj);
值为函数的简化写法
当我们需要在一个对象中将键与一个函数对应时,通常会写成如下写法:
let obj = {
name: 'My name',
funcA: function(){
console.log(this.name);
}
}
obj.funcA();
现在我们可以简化成如下写法:
let obj = {
name: 'My name',
funcA(){
console.log(this.name);
}
}
obj.funcA();
以上两点在 Vue 的组件化开发之中非常常用。
解构传参
有时,我们会向函数中传入对象的值,比如表单数据,如下:
let obj = {
username: 'my-username',
password: 'my-password',
};
function destruct(username, password){
console.log(username, password);
}
let username = obj.username;
let password = obj.password;
destruct(username, password);
现在,我们可以通过解构的方式重新定义形参:
let obj = {
username: 'my-username',
password: 'my-password',
};
function destruct({username, password});{
console.log(username, password);
}
destruct(obj);
这样可以简化一定的代码。
常量函数名
我们可以使用常量在对象中定义函数:
let obj = {
funcname(){
console.log('xxx');
}
}
可以写成:
let FUNC_NAME = 'funcname';
let obj = {
[FUNC_NAME](){
console.log('xxx');
}
}
将函数名称提取成常量方式可以一定程度上减少函数名修改导致的副作用。
箭头函数
通常,我们使用如下方式定义并调用函数:
function func(name, age){
console.log(name, age);
}
func('dailybird', 22);
现在,我们可以简化它:
(name, age) => {
console.log(name, age);
}
大家可以注意到,采用箭头函数的写法更适合于匿名函数的场景。在 JavaScript 中,匿名函数出现最为频繁的场景即是回调函数。如异步 API 调用后的数据获取:
_.post((response) => {
let data = response.data;
})
此外,箭头函数还可以明确 this
指向问。在以下代码中:
let obj = {
name: 'dailybird',
getName: function(){
setTimeout(function(){
console.log(this.name);
}, 1000);
}
}
obj.getName();
getName
并不能得到 name
值,这是因为套在 setTimeout
之中匿名函数的 this
指向全局空间。
我们可以使用 bind
进行 this
的绑定:
let obj = {
name: 'dailybird',
getName: function(){
setTimeout(function(){
console.log(this.name);
}.bind(this), 1000);
}
}
obj.getName();
也可以使用 self = this
避免 this
指向被调换:
let obj = {
name: 'dailybird',
getName: function(){
// 这里的 let 和 var 并无区别
let self = this;
setTimeout(function(){
console.log(self.name);
}, 1000);
}
}
obj.getName();
现在,我们也能通过箭头函数来解决这一问题:
let obj = {
name: 'dailybird',
getName: function(){
setTimeout(() => {
console.log(this.name);
}, 1000);
}
}
obj.getName();
在 Vue 应用中,会大量出现调用接口后,在回调函数中对所属实例的数据进行修改的情况。这时,妥善处理 this
指向问题就显得极为重要。
对象展开符
Node.js
Node.js 是一个比较大的话题,这里只说一些与 Vue 开发有关的内容。
后续的内容会提到:我们可以使用高阶的 JavaScript 语法和代码规范进行项目开发,然后使用类似「编译」的方式「开发模式」的代码「编译」成大多数浏览器可以运行的「发布模式」代码。
而为了提供支持这一过程的环境,我们需要 Node.js 及其包管理工具 npm
。
通过 Node.js 环境,我们可以在非浏览器环境使用 JavaScript 代码,输出大多数「浏览器」支持的 JavaScript。
安装
Node.js 的安装相对傻瓜,从 官方网站 下载后,双击安装即可,这里推荐使用最新版本。
测试
安装完成后,在命令行执行以下代码可以查看安装的 Node.js
版本,以及气包管理工具 npm
的版本:
# Node.js 版本
node -v
# Node.js 的包管理工具 npm 的版本
npm -v
完成这一步之后,我们便可以开始之后的内容了。
Babel
尽管 ES6 提供了大量新语法,可以简化编码、提高开发效率,但浏览器兼容性问题一直都是 B/S 模式的通病。直至目前为止,各个浏览器对 ES6 的兼容程度都不乐观。这也就意味着,即使是最新的主流浏览器,都无法保证可以正常运行 ES6 语法的 JavaScript 代码,更不用提那些老版本的浏览器了。
为了使我们既能使用 ES6 的新语法,又不会受制于浏览器版本问题,我们需要考虑一种类似「编译」的过程,将原有的 ES6 代码「编译」为 ES5。这样一来,我们既可以在开发环境中使用简洁、语义化更强的新语法,又可以通过「编译」后的低版本代码兼容更多的浏览器。
Babel 在线编译
我们可以使用 Babel 在线编译快速体验这一过程,访问 Babel 首页展示 或 Babel 在线编译 均可。
下面我们就尝试将之前提到的 ES6 语法进行转义。
let 关键字
当不涉及到作用域问题时,let
直接编译为 var
。
下面我们尝试对之前提到的块级作用域问题进行编译:
可以看到,Bebel 面对这一问题的解决思路和我们之前的想法是一致的。
键值对的简写方法
值为函数的简写方法
解构传参
可以看到,这里编译的结果显示解构传参实际上就是语法帮助我们简化了步骤。
注意,正因为这种写法,在函数体内是不对 obj
产生直接修改的。
如果需要修改 obj
的值,需要使用非解构方式,通过 obj.username = 'xxx'
实现修改。
常量函数名
代码会被编译为:
箭头函数
之前提到,箭头函数可以改善 this
指向问题,我们看看 Babel 的解决方案:
可以看出,Babel 也是使用将 this
关键字使用局部变量进行保存的方式改善了指向问题。
在项目中使用 Babel
初始化
首先,我们在项目目录下执行:
npm init
一路回车确认后,可看到项目下出现了 package.json
文件,其中记录了当前项目的依赖管理配置。为了使得 Bebel 能够正常起到作用,现在执行:
npm install --save-dev babel-preset-es2015
可以看到项目中出现了一个新的文件夹 node_modules
。这个文件夹类似于项目的依赖库。此外,执行之后,在 package.json
之中,会增加这一句配置:
"devDependencies": {
"babel-preset-es2015": "^6.24.1"
}
注意到,安装时添加了 --save-dev
的参数,这一参数的意义在于将所安装项目定义为开发时使用的(因为 Babel 的作用只是在代码运行前进行编译)。
相比之下,对于一些需要在运行时使用的库,如 jQuery
、Angular
或 Vue
等,安装时需要添加的参数即为 --save
而非 --save-dev
了。如下:
npm install some-component --save
在这句代码执行完毕之后,package.json
中会增加:
"dependencies": {
"some-component": "^version"
}
这之后,我们还需要在全局安装 Babel 转码工具。执行以下代码:
npm install --global babel-cli
这里我们使用了另一个参数 --global
,这一参数表示本次安装会在全局起作用。
.babelrc 文件
想要使用 Babel 进行编译,除了进行上一步骤的安装之外,还需要对编译进行一些配置,而这些配置需要存于 .babelrc
文件中。
该文件对转码规则和用到的插件进行了定义,如下:
{
"presets": [
"es2015"
],
"plugins": []
}
这里我们定义了转码规则为 es2015
,即 ES6
。更多的转码规则可以参考 Babel 入门教程。
编译测试
下面我们创建一个文件 origin.js
,书写一些带有 ES6 新语法的代码,如之前提到的作用域问题:
// origin.js
for (let i = 0; i < 4; i++) {
setTimeout(function(){
console.log(i);
}, 1000 * i);
}
然后执行以下指令(注意,.babelrc 文件需要事先创建):
babel origin.js
可以看到命令行中打印出了编译后的结果。
可是我们需要把编译后的结果输出到一个文件之中,此时需要执行:
babel origin.js -o output.js
这样,我们就可以在 output.js
中看到编译的结果了。
关于 Babel 的话题就谈到这里,更多深入的内容可以参考其他文档。
webpack
Babel 帮助我们解决了 ES6 代码浏览器兼容性的问题。为了实现前端工程化开发,我们还需要一个功能的支持,即在 JavaScript 代码中引入其他的 JavaScript 代码。
只有完成这一点,我们才能将 JavaScript 代码依据逻辑进行拆分,然后通过某种方式组合起来,从而实现组件化、模块化的开发。
由于 webpack 的内容和使用方式很多,对 webpack 更深入的了解可以参考其他博客。这里只介绍最为重要的部分,即 import 和 export。
安装
webpack 作为一个指令,建议通过全局的方式安装,这样一来,我们就可以全局使用 webpack
指令了:
npm init
npm install -g webpack
报错解决
在安装过程中,可能会出现以下报错信息:
npm ERR! Refusing to install webpack as a dependency of itself
这是因为 package.json
的 name
值也被写成了 webpack
,修改即可,可参考:局部安装webpack提示无法依赖。
配置
为了能够正常使用 webpack,我们需要对其进行一些配置。当然,webpack 的配置项很多,这里只介绍最为重要的一部分,即 export
和 import
,更多有关 webpack 的知识可以参考这一套视频教程:【DevOpen.Club 出品】Webpack 2 视频教程。
我们在项目根目录下新建 webpack.config.js
文件,这是 webpack 配置文件的默认名。内容如下:
const config = {
entry: __dirname+'/app/entry.js',
output: {
path: __dirname+'/dist',
filename: 'bundle.js'
},
module: {
rules: [
]
}
};
module.exports = config;
这一配置文件指定了入口文件,即 /app/entry.js
,以及打包后的输出文件 /dist/bundle.js
。
import & export
在模块化开发之中,通常会将 JavaScript 代码分散到多个文件之中。这时,我们需要使用 import
和 export
关键字关联起 JavaScript 代码,从而提高代码的复用能力。这一点在 Vue 的组件化开发之中体现地尤为明显。借助 webpack,我们可以从一个入口文件开始,通过导入导出,实现代码的组合,以及项目的工程化。
首先,我们在 app
目录下新建 entry.js
文件:
import API from './componentA'
console.dir(API);
这里引入了 componentA.js
,注意,文件后缀是可以省略的。
之后,我们新建这一文件:
const GET_PRODUCTS = 'www.xxx.com/products';
export default {
GET_PRODUCTS
}
即 entry.js
引入了 componentA.js
中的输出,从而引入了其中的 GET_PRODUCTS
变量。
现在,我们在项目根目录下执行 webpack
,可以看到提示信息:
提示我们已经根据配置,生成了 dist/bundle.js
。
而后,我们在根目录下新建 index.html
并引入 bundle.js
:
<script src="./dist/bundle.js"></script>
打开后,可以在浏览器控制台中看到输出效果:
报错解决
ERROR in Entry module not found: Error: Can't resolve 'xxx/webpack/app' in 'xxx/webpack'
这是因为 webpack.config.js
的 entry 只能指定为单个 JavaScript 文件,当指定为目录时,默认为该路径下的 index.js
文件。如果入口文件名不是 index.js
,会抛出错误。
export 和 export default
在输出文件中,可以有两种形式,下面我们介绍这两种形式的使用方式:
export
导出方式:
const GET_PRODUCTS = 'www.xxx.com/products';
export GET_PRODUCTS;
导入方式:
import {GET_PRODUCTS} from './componentA'
console.log(GET_PRODUCTS);
注:一个输出文件中可以有多个 export
。
export default
导出方式:
const GET_PRODUCTS = 'www.xxx.com/products';
export default {
GET_PRODUCTS
}
导入方式:
import API from './componentA'
console.log(API.GET_PRODUCTS);
注:一个输出文件中只能有一个 export default
。
组件开发
Vue-cli
进行 Vue 组件化开发需要 webpack 及一系列插件的支持,自行配置 package.json
和 webpack 相对麻烦,为了简化这些繁琐的重复工作,我们可以使用 Vue 脚手架工具来快速构建项目结构。
执行以下指令,全局安装 Vue-cli:
npm install --global vue-cli
之后我们可以执行以下指令,创建一个 Vue 工程:
vue init webpack project_name
这条指令中,webpack 表示该 vue 工程使用 webpack 的方式进行打包。而 project_name 表示自定义地项目名字。
执行之后,会依次询问一些信息,如项目名称、项目描述、作者等会作为项目信息存于 package.json
之中。构建方式、vue-router。
? Project name (xxx) # 项目名称 (随意填写)
? Project description (A Vue.js project) # 项目描述 (随意填写)
? Author (yangning <yangning3@jd.com>) # 作者 (随意填写)
? Vue build (Use arrow keys) # 构建方式 (回车即可)
? Install vue-router? (Y/n) # 是否使用 vue-router (可选,有关 vue 路由会在后续小节中提到)
? Use ESLint to lint your code? (Y/n) # 是否使用 ESlint 规范代码 (建议 N)
? Setup unit tests with Karma + Mocha? # 是否使用 Karma + Mocha 测试框架(建议 N)
? Setup e2e tests with Nightwatch? (Y/n) # 是否使用 Nightwatch 端到端测试框架(建议 N)
这之后,我们需要使用以下指令实现安装(可以在 package.json
中找到依赖的库):
npm install
以下对 Vue 工程中的部分文件进行介绍。
package.json
在 package.json
中可以找到如下信息:
"scripts": {
"dev": "node build/dev-server.js",
"start": "node build/dev-server.js",
"build": "node build/build.js"
},
根据之前所说,scripts
中的配置可以作为指令,以 npm run xxx
的方式执行。
在这里,使用 scripts
中定义的指令的效果如下:
npm run dev # 启动热部署,为开发模式
npm run build # 同 npm run dev
npm run build # 使用发布模式编译,生成 /dist 文件作为编译结果
Vue 项目中的 webpack 比较复杂,这里对重要的地方进行介绍。
webpack.base.conf.js
这是 webpack 的基础配置,其中在 module
中使用了 vue-loader
,babel-loader
进行处理。
在 resolve
中,可以看到如下配置:
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src')
}
},
其中,alias
中定义了 @
符号用以指向项目根目录下的 src
文件夹。
路径问题
在 Vue 项目中,如何正确找到如图片、字体文件、第三方库文件等是相对重要的事情。以下提供几种正确使用路径的方法。
第三方库文件引入
在 Vue 中,经常需要引入第三方库,比如在 /src/main.js
文件中,使用以下方式引入了 Vue.js:
import Vue from 'vue'
采用这种方式引入的库文件,会在 /node_modules
中进行寻找。
静态资源文件
前端页面中不可避免地会使用到很多静态资源文件,引用这些静态文件可以通过以下方式:
相对路径
采用相对路径当然可行,但对于那些层级较深的文件而言,引用较低层级的静态资源就比较麻烦了。而且,由于字体文件等编译后与 js 文件的相对路径会发生改变,所以也不适合使用相对路径引用。使用
@
符号
如上所说,@
符号被定位到了/src
中,所以我们可以使用@/dir/file.png
定位到/src/dir/file.png
。
注意,由于 @
符号的解析只会默认在 JavaScript 中生效,若要在 HTML 中使用 @
,我们需要使用 ~@
,如:
<script src="~@/dir/file.png"></script>
使用
/
在 Vue 项目中,/
被指向/static
目录,通常用于字体文件及图片等资源的引入。
启动项目
我们可以使用以下指令开启热部署模式以进行开发调试:
npm run dev
执行后浏览器会自动打开一个窗口,并访问:
localhost:8080/#/
由于热部署的存在,我们对代码的修改都会立刻同步到浏览器中。
Vue 组件模板
在 Vue 项目中,我们可以新建 .vue
后缀的文件,作为一个单独的组件。该组件的模板如下:
<style scoped>
</style>
<template>
<div>
</div>
</template>
<script>
export default {
}
</script>
在 .vue
文件中,包含了一个组件的三个部分,即 HTML 结构部分(写于 template
之中);CSS 样式部分(写于 style
之中);以及 Vue 实例对象部分(写于 export default{ }
之中)。
在这里,export default{ }
实际相当于 export default new Vue({ })
。即一个 .vue
文件实际提供了一个组件样式,及以此组件为作用域的 Vue 实例。
注意,在 JavaScript 代码中,我们可以省略 el: xxx
,即对作用域的声明。在 vue
文件中,其作用域为 template
标签下的第一层标签。这也就是说,下面这两种种写法是不正确的:
<template>
<div>
</div>
<div>
</div>
</template>
<template>
<div v-for="item in list">
</div>
</template>
使用这种写法时,会抛出如下错误:
Component template should contain exactly one root element. If you are using v-if on multiple elements, use v-else-if to chain them instead.
需要保证 template
标签下只有一个父级标签,循环和多 if 语句这种可能导致出现多个标签的行为也不允许。
使用组件构建页面
在很多站点中,页面都会配备首部导航条、页脚,而页面间的切换一般只会导致中心内容区的变动。
采用传统方式开发的页面存在以下两个问题:
在不使用后端模板引擎的情况下,需要在每个页面中重复书写首部导航条和页脚;
即使使用了后端模板引擎,每一次页面跳转也都会导致页面的全部刷新,而非内容区的局部刷新。
第二个问题涉及到前端路由和单页应用,我们会在后续的 Vue Router 中讨论。
为了解决第一个问题,我们可以考虑把后端模板引擎的工作放到前端完成,也就是 Vue 组件。
现在,我们在 /src/components
文件夹下新建两个文件,分别是:Navbar.vue
和 Footbar.vue
,其中代码如下:
// Navar.vue
<style scoped>
div{
background-color: tan;
}
</style>
<template>
<div>
头部导航条
{{ msg }}
</div>
</template>
<script>
export default {
data () {
return {
msg: 'navbar'
}
}
}
</script>
// Footbar.vue
<style scoped>
div{
background-color: deepskyblue;
}
</style>
<template>
<div>
底部导航条
{{ msg }}
</div>
</template>
<script>
export default {
data () {
return {
msg: 'navbar'
}
}
}
</script>
这样,我们就新建了两个 Vue 组件,分别作为首部导航条和页脚;
然后我们修改 /src/App.vue
文件,内容如下:
<style>
div{
background-color: aquamarine;
}
</style>
<template>
<div>
<Navbar></Navbar>
中心内容
<Footbar></Footbar>
</div>
</template>
<script>
import Navbar from './components/Navbar'
import Footbar from './components/Footbar'
export default {
components: {
Navbar, Footbar
}
}
</script>
这里,我们引入了之前所写的两个 Vue 组件。对比前几节中提到的 Vue 组件,这里的不同只不过是把每个组件都放到了一个单独的 .vue
文件中而已。
创建、修改文件并点击保存后,由于之前启动了 npm run dev
,即热部署,此时,浏览器会自动刷新并显示如下效果:
这里可以注意到两点:
虽然
Navbar
和Footbar
中均有msg
这一数据,但二者并不冲突;Navbar
、Footbar
、App
这三个组件中均对div
标签定义了背景颜色,但三者并未在样式上相互覆盖。
这表明一个 Vue 组件的样式、数据等均是与其他组件隔离的。
组件间数据传递
以下演示 App
组件向 Navbar
组件传参的方法:
修改 Navbar.vue
代码为:
<style scoped>
div{
background-color: tan;
}
</style>
<template>
<div>
头部导航条
{{ msg }}
{{ navbarMsg }}
</div>
</template>
<script>
export default {
props: [
'navbarMsg'
],
data () {
return {
msg: 'navbar'
}
}
}
</script>
修改 App.vue
的代码为:
<style>
div{
background-color: aquamarine;
}
</style>
<template>
<div>
<Navbar navbar-msg="params"></Navbar>
中心内容
<Footbar></Footbar>
</div>
</template>
<script>
import Navbar from './components/Navbar'
import Footbar from './components/Footbar'
export default {
components: {
Navbar, Footbar
}
}
</script>
这里的传参方式和之前提到的相同。注意,navbar-msg
这里涉及到字面量问题,大家可以翻看之前的内容。
保存后,页面会更新为:
组件间事件传递
以下演示 Footbar
中想让传递事件的方法:
修改 Footbar.vue
为:
<style scoped>
div{
background-color: deepskyblue;
}
</style>
<template>
<div @click="clicker">
底部导航条
{{ msg }}
</div>
</template>
<script>
export default {
data () {
return {
msg: 'navbar'
}
},
methods: {
clicker(){
console.log('component click');
this.$emit('navbar-click', 'params1', 'params2');
}
}
}
</script>
修改 App.vue
为:
<style>
div{
background-color: aquamarine;
}
</style>
<template>
<div>
<Navbar navbar-msg="params"></Navbar>
中心内容
<Footbar @navbar-click="trigger"></Footbar>
</div>
</template>
<script>
import Navbar from './components/Navbar'
import Footbar from './components/Footbar'
export default {
components: {
Navbar, Footbar
},
methods: {
trigger(param1, param2){
console.log('get event with', param1, param2);
}
}
}
</script>
这样,当我们点击底部导航条后,控制台会输出:
component click
get event with params1 params2
这里要注意两点:
子组件抛出 ( emit ) 事件时,并不考虑父组件会不会捕获该事件。相当于只提供接口,而不依赖它;
父组件捕获事件时并不会干涉到子组件的执行,使用
return false;
也不可以。
组件传参和事件传递中的问题
组件传参和事件传递实际构成了组件间数据传递的正反线路。但从以上的使用方式中,可以感觉到采用这两种方式进行数据传递的局限性。
全局状态难以维护;
同级组件间数据难以传输;
存在大量只起到传输数据作用的事件;
API 调用过程缺少管理方案,会存在重复的 API 请求代码;
调用 API 之类的异步请求和修改数据的同步操作糅合在一起。
而 Vue 本身是推荐在项目中使用数据驱动运行的。面对组件间的复杂关系,我们需要一种全局的数据管理工具来帮助我们方便的获取数据,并基于此满足一系列的需求。
此外,针对不可避免的 API 请求,我们也需要一种机制进行管理。
所以,我们需要一种状态管理器来帮助我们完成这些功能,这就是 Vuex。
Vuex
如上所说,Vuex 是一个状态管理机制,那么,状态是指哪些呢?
在这里,Vuex 需要帮我们维护的内容分为三类:
存储数据的 state;
同步修改数据的 mutations;
异步获取数据的 actions;
Vuex 安装和引入
按下 CTRL + C
退出热部署,执行以下指令进行 vuex 的安装:
npm install vuex --save
然后修改 main.js
:
在文件首部添加:
import Vuex from 'vuex'
Vue.use(Vuex);
然后实例化一个 Vuex.Store 对象:
let store = new Vuex.Store({
state: {
navbar: {
title: 'navbar-title'
}
},
mutations: {},
actions: {}
});
注意,这里包含了 state
, mutations
和 actions
。
然后在 new Vue({})
中加入 store
:
new Vue({
el: '#app',
store,
template: '<App/>',
components: {App}
})
注意,这里加入 store
意味着我们可以其他 Vue 实例中使用 this.$store
来得到实例化的 Vuex.Store
对象。
自此,我们便可以通过修改 store
变量中的 state
,mutations
和 actions
来实现状态管理了。
state
如上所示,我们已经在 state
中添加了 navbar
对象,相当于将其交给 Vuex 进行管理:
let store = new Vuex.Store({
state: {
navbar: {
title: 'navbar-title'
}
},
mutations: {},
actions: {}
});
然后,我们可以在 Vue 组件中得到该值。比如 Navbar
组件(这里省略了 style
部分):
<template>
<div>
头部导航条 {{ title }}
</div>
</template>
<script>
export default {
data () {
return {
}
},
computed: {
title(){
return this.$store.state.navbar.title;
}
}
}
</script>
注意,这里我们使用 computed
来得到 state
中管理的值,是方便在 state
中的值发生变化时同步更新。有关 computed
和 data
在 Vuex 中的使用可以参考 「获取 vuex state 中的值一定要使用 computed 而不能使用 data 吗?」。
然后我们可以通过以下方式对其进行修改,这里我们通过一个点击事件实现对 state
中值的修改:
// Navbar.vue
<template>
<div @click="trigger">
头部导航条 {{ title }}
</div>
</template>
<script>
export default {
data () {
return {
}
},
computed: {
title(){
return this.$store.state.navbar.title;
}
},
methods: {
trigger(){
this.$store.state.navbar.title = 'after modified';
}
}
}
</script>
当然,在 Vuex 中,并不推荐直接使用这种方式对 state
中管理的内容进行修改,从规范上来说,一切的修改操作都应该在 mutations
中进行。
mutations
我们修改 main.js
为其添加 mutations
如下:
let store = new Vuex.Store({
state: {
navbar: {
title: 'navbar-title'
}
},
mutations: {
setNavbarTitle(state, value){
state.navbar.title = value;
}
},
actions: {}
});
注意,mutations
中的方法接收两个参数,第一个参数即为 store.state
,第二个是附带的值,一般为修改的目标值(在官方文档中,这一参数被称为载荷 ( payload ) )。注意,mutations
中的方法只能接收两个参数。
调用 mutations
中的方法可以使用 commit
进行,commit
同样需要传递两个参数,第一个参数为所需调用的 mutations
中的方法名,第二个参数为目标值,即等同于 mutations
方法中的第二个参数。
我们修改 Navbar.vue
中的 trigger
如下:
methods: {
trigger(){
this.$store.commit('setNavbarTitle', 'after modified');
}
}
这样一来,此后所有涉及到对 state
中 navbar.title
内容的修改都可以使用这一方式进行。
actions
在网页开发中,大量的需求都伴随着 API 的调用,相比于 mutations
对 state
中数据的同步修改,此类 API 的调用过程属于异步。整个过程为:先异步调用后台接口获得数据,再同步将数据更新到 store.state
中。
在 Vuex 中,后者使用 mutations
进行操作,而前者则是使用 actions
进行。由于调用后台接口的过程涉及到一些其他的库和跨域问题,我们这里暂且使用伪代码,仅为了理清程序的逻辑。
首先,我们修改 store
的 actions
:
let store = new Vuex.Store({
state: {
navbar: {
title: 'navbar-title'
}
},
mutations: {
setNavbarTitle(state, value){
state.navbar.title = value;
}
},
actions: {
loadNavbarTitle(context, appendix){
// 调用后台 API 的伪代码
let title = _.get('http://api.xxx.com/navbar/title');
context.commit('setNavbarTitle', title);
}
}
});
然后修改 Navbar.vue
中的 trigger
:
trigger(){
this.$store.dispatch('loadNavbarTitle', 'test');
}
类比 mutations
,我们使用 dispatch
调用 actions
,其中第一个参数为 actions
的方法名,第二个为附带参数。
而在 actions
中,会存在两个参数,第一个为 context
,即为 store
(注意区别 mutations
的第一个参数),第二个为附带参数。
actions 的另一使用场景
有时,我们可能想要把对数据的更新或 API 的调用切分成更小的部分,即从一个入口 mutations
调用多个颗粒度小的 mutations
,或者是一个 actions
调用多个其他的 actions
。
如果我们想要在 mutations
或 actions
中继续调用其他的 mutations
或 actions
,只能试图通过方法中的参数或 this
进行。
让我们观察一下 mutations
中获得的参数:第二个参数为传递的附加参数,自然不能做些什么;第一个参数为 store.state
,通过这一值已经无法再调用 mutations
或 actions
了。
那么 this
关键字呢?当我们在其中使用 console.log(this)
时,发现输出为 undefined
。事实上,this
指针已经无法再定位到 store
了。
现在我们考虑 actions
,之前已经说过,actions
的第一个参数为 store
,而通过这一参数,我们可以继续使用 commit
或 dispatch
操作 mutations
或 actions
,或者直接操作 state
。
基于以上,我们发现 actions
还具有另一种使用场景,即通过 actions
继续分发多个 mutations
(或 actions
),从而更灵活的组织代码,以减少代码的冗余。
参考以下示例:
let store = new Vuex.Store({
state: {
navbar: {
title: 'navbar-title'
}
},
mutations: {
setNavbarTitle(state, value){
state.navbar.title = value;
},
test1(state){
console.log('test1');
},
test2(state){
console.log('test2');
},
},
actions: {
loadNavbarTitle(context){
// 分发到两个 mutations 之中
context.commit('test1');
context.commit('test2');
}
},
func(){
console.log('func');
}
});
官方数据流
通过以上铺垫,我们便可以祭出官方给出的状态管理架构图:
根据之前给出的代码,我们可以得到 Vuex 的一般使用流程:
组件捕获事件,调用
dispatch
触发actions
;在
actions
中调用后台 API 获取数据;调用
commit
触发mutations
更新存储于state
之中的数据;组件中的
computed
更新到组件的视图中。
以上过程形成了闭环。也是官方标准的数据流。但在使用中,如果部分过程无需调用后台 API,也可以由组件直接调用 commit
触发 mutations
,从而跳过不必要的 actions
。
目录结构
从之前的介绍中可以隐约感觉到,如果我们将所有的 state
,mutations
,actions
都放在 Vuex.Store({})
中的话,随着项目规模的扩大,其中的内容会越来越多,这样会导致代码难以维护。为了避免这个问题,我们需要将不同职责的 store
代码进行拆分。
官方给出了推荐的项目结构:
在这一目录结构中,state
中的值被拆分为各个模块(modules),并连同 actions
、mutations
一起在 index.js
进行汇集。
这其中,actions
和 mutations
包含全局性质的操作。而把每个小模块的 state
,mutations
,actions
都写在单独的 JavaScript 代码中并在 modules
中汇集。
有关 modules
和官方目录结构还有很多东西可以谈及,留待之后再撰文。
Vue-Resource
在 vue 中,我们可以使用一些库实现后台 API 的调用,比较常用的由 vue-resource
和 axios
,这里简单介绍 vue-resource
。
安装
和其他的库一样,使用 npm
安装即可:
npm install vue-resource --save
使用
类似于 Vuex 的使用,我们需要在 main.js
文件中进行修改,添加以下两句:
import VUeResource from 'vue-resource'
Vue.use(VUeResource);
然后,我们可以在组件中进行使用。这里提供一个测试用的 API 地址,来自 百度 APIStore:
http://apis.baidu.com/apistore/iplookup/iplookup_paid?ip=117.89.35.58
由于这些 API 服务需要购买才可以返回正确的信息。这里只用于走通形式,不在乎返回数据的正确与否。
以下对最常用的 GET 和 POST 请求进行介绍,更多的使用方法可以参考 API 文档。
GET 请求
我们在 Footbar.vue
的 script
中使用以下方式发起 GET 请求。
export default {
created(){
let api = 'http://apis.baidu.com/apistore/iplookup/iplookup_paid?ip=117.89.35.58';
this.$http.get(api).then(response => {
console.log('接口调用成功');
console.log(response);
}, response => {
console.log('接口调用错误');
});
}
}
这里在 create()
生命周期中触发了 GET 方法。可以看到浏览器中输出的结果。
以上示例使用了 this.$http
发起请求,需要在 Vue 组件之中。如果不借助 this
,我们可以通过如下方式调用:
let api = 'http://apis.baidu.com/apistore/iplookup/iplookup_paid?ip=117.89.35.58';
Vue.http.get(api).then(response => {
console.log('接口调用成功');
console.log(response);
}, response => {
console.log('接口调用错误');
});
注意,这里的 http
是没有 $
符号的。
POST 请求
GET 和 POST 请求的使用方式稍有不同,如下:
let api = 'http://apis.baidu.com/apistore/iplookup/iplookup_paid?ip=117.89.35.58';
this.$http.post(api, {
username: 'xxx'
}).then(response => {
console.log('接口调用成功');
console.log(response);
}, response => {
console.log('接口调用错误');
});
由于测试 API 不支持 POST 方式,控制台会打印出接口调用错误的提示,同时会抛出一个很重要的错误信息:
XMLHttpRequest cannot load http://apis.baidu.com/apistore/iplookup/iplookup_paid?ip=117.89.35.58. Request header field Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.
即是跨域问题。
跨域问题
这是由于我们通过 A 域名的 JavaScript 代码请求 B 域名的 API 时,在不做任何处理的情况下,会违背 浏览器同源政策 而被浏览器拒绝。
这会在前后端分离架构中导致两个致命的问题:
API 请求无法发送;
Cookie 信息无法传递;
解决这个问题的方案通常有两类:jsonp 或是添加响应头。这里提供一些解决方法以供读者参考:
Vue Router
基于 Vue 的组件开发方式可以让我们在切换页面时只替换某一部分的组件,从而实现局部刷新。想要实现这一点,需要由前端拦截请求,通过 JavaScript 代码根据访问的 URL 替换不同的组件从而实现局部刷新效果。
在介绍这一点前,我们先提一下 iframe
的方式。采用 iframe
标签,通过修改其 src
属性,可以实现页面局部(即 iframe
DOM 元素)刷新。但这样的局部刷新存在一些问题:
内部
iframe
的 URL 变化无法在浏览器中显示出来。也就是说,当iframe
局部刷新的时候,浏览器上访问的 URL 是不变化的,这就使得用户无法通过赋值 URL 的方式再次打开同样的页面。这一点在很多应用中是致命的;外部无法捕获内部
iframe
的变化。当用户在iframe
中的页面进行跳转或类似操作时,外部无法得知。又因为浏览器同源政策的存在,使得内外部的 DOM、数据等是绝缘的。这样的方式在很多场景下极其受限。
安装
同之前所说的其他 Vue 插件一样,我们通过 npm 进行安装:
npm install vue-router --save
使用
首先,我们在 src
目录下新建一个 router
目录,在其中创建 index.js
文件,内容如下:
import Vue from 'vue'
import Router from 'vue-router'
import ContentA from '../components/ContentA';
import ContentB from '../components/ContentB';
Vue.use(Router)
export default new Router({
routes: [
{
path: '/a',
name: 'ContentA',
component: ContentA
},
{
path: '/b',
name: 'ContentB',
component: ContentB
}
]
})
这个文件相当于 Vue Router 的路由配置文件。注意到,在最上方,我们引入了两个组件。现在我们创建这两个组件。
在 src/components
文件夹下新建 ContentA.vue
和 ContentB.vue
两个文件,并在其中随便写入一些内容:
// `ContentA.vue`
<style scoped>
</style>
<template>
<div>
这里是 A 号组件
</div>
</template>
<script>
export default {
data () {
return {
msg: 'index content'
}
}
}
</script>
// `ContentB.vue`
<style scoped>
</style>
<template>
<div>
这里是 B 号组件
</div>
</template>
<script>
export default {
data () {
return {
msg: 'index content'
}
}
}
</script>
这之后需要修改 main.js
文件,在首部添加以下内容,即引入 src/router/index.js
:
import router from './router/index'
最后,我们需要对 App.vue
进行修改:
<style>
div{
background-color: aquamarine;
}
</style>
<template>
<div>
<Navbar></Navbar>
<router-view></router-view>
<Footbar></Footbar>
</div>
</template>
<script>
import Navbar from './components/Navbar'
import Footbar from './components/Footbar'
export default {
components: {
Navbar, Footbar
},
methods: {
}
}
</script>
注意到,我们在 template
标签中加入了 <router-view></router-view>
标签。作为 Vue Router 填充组件的标志。
根据配置,当我们访问 /a
时,<router-view></router-view>
会被替换为 ContentA
组件;当访问 /b
时,其会被替换为 ContentB
。
现在我们做一下尝试,在执行了 npm run dev
之后,访问 http://localhost:8080/#/a
和 http://localhost:8080/#/b
,查看效果。
当访问这两个 URL 时,可以在控制台的网络中进行监控,已验证网页确实没有重新请求。
History
可以注意到,我们访问的 URL 中存在一个 #
,官方的解释如下:
vue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。
如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。
此时我们需要修改 router/index.js
文件:
export default new Router({
mode: 'history',
routes: [...]
})
但是,这样一来,当我们直接输入 localhost:8080/xxx
而非从 localhost:8080/
跳转过去的时候,Vue Router 无法正常完成功能。这是由于,当我们访问 localhost:8080/xxx
时,后端会在其配置中寻找该 URL 应该转发的文件,而不是交给 /index.html
再由其中引入的 JavaScript 代码进行路由控制。
为了能够支持这一需求,我们需要修改服务器端的配置,可以参考官方说明中的 后端配置例子。
如在 Nginx 中:
location / {
try_files $uri $uri/ /index.html;
}
可以看到,这即是将请求都导向 index.html 从而使得 JavaScript 获得了对请求的路由控制。
编程式导航
在组件中,我们可以通过以下方式进行跳转:
this.$router.push({
name: 'a'
})
注意,这里的 name
即为我们在 router/index.js
中配置的 name
值。
导航钩子
有时,我们想要在 URL 跳转时做一些处理,比如根据当前状态判断此次跳转能否执行。此时就需要使用到导航钩子。我们可以在 router/index.js
中配置一个导航钩子:
let router = new Router({
routes: [...]
})
router.beforeEach((to, from, next) => {
console.log(to);
console.log(from);
next();
})
在该导航钩子中,对跳转前、跳转后的元信息进行了打印,我们可以通过它们获得路由配置的信息,从而根据实际需求进行相应的控制。
this.$router
和 this.$route
我们可以通过 this.$router
进行跳转。但当我们想要获得如当前 URL 的一些信息(如含参路由)时,需要通过 this.$route
获得。上面提到的导航钩子中的 to
和 from
都是 this.$route
的形式。
注意事项
Vue Router 的引入会使得打包的 JavaScript 文件已经包含了所有的组件和逻辑代码。随着项目的扩大,JavaScript 文件会越来越大,使得初次加载的时间变长。应对这一问题,我们可以考略在业务层面进行拆分,并将从属于某一业务的多个组件放在一起打包,从而降低加载文件的大小;
在配置了钩子函数之后,当页面刷新时,会自动触发一次从首页到访问页的跳转事件。即 from = 首页,to = 访问页。在使用钩子函数进行跳转权限判定时需要注意。