1. 组件注册
1.1 组件的构建(子组件)
在项目源码目录下的./component目录下创建组件文件MyComponent.vue
<template>
<div>
{{ msg }}
</div>
</template>
<script>
export default {
name: "MyComponent",
/* prop属性从父组件传递赋值 */
props: {
msg: String,
},
};
</script>
<style scoped>
/* some style code*/
</style>
1.2 引用组件(在父组件中)
在./app.vue文件中引用MyComponent组件
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png" />
<my-component msg="Welcome to Your Vue.js App" />
</div>
</template>
<script>
/* 引用MyComponent */
import MyComponent from "./components/MyComponent.vue";
export default {
name: "App",
components: {
MyComponent,
},
};
</script>
<style>
/* some style code*/
</style>
1.3 自动化全局注册基础组件
有的基础组件会在多个子组件文件中反复使用,如果反复注册,将会显得代码冗余,因此使用自动化全局注册组件。
假设我们有以Base开头的三个基础组件BaseButton、BaseIcon、BaseInput,我们可以这样在main.js文件中全局注册组件。
import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'
const requireComponent = require.context(
// 其组件目录的相对路径
'./components',
// 是否查询其子目录
false,
// 匹配基础组件文件名的正则表达式
/Base[A-Z]\w+\.(vue|js)$/
)
requireComponent.keys().forEach(fileName => {
// 获取组件配置
const componentConfig = requireComponent(fileName)
// 获取组件的 PascalCase 命名
const componentName = upperFirst(
camelCase(
// 获取和目录深度无关的文件名
fileName
.split('/')
.pop()
.replace(/\.\w+$/, '')
)
)
// 全局注册组件
Vue.component(
componentName,
// 如果这个组件选项是通过 `export default` 导出的,
// 那么就会优先使用 `.default`,
// 否则回退到使用模块的根。
componentConfig.default || componentConfig
)
})
需要注意的是,全局注册行为必须发生在根vue实例创建之前(new Vue()之前)
2. Prop
2.1 prop的作用
简言之,prop的作用就是:
- 子组件提供一些参数
- 父组件可以通过这些参数向子组件传递值,用于子组件的渲染
例如,在以上案例中,msg作为prop的属性,被预定义为String类型。父组件可以像使用html原生属性般使用msg属性。
我们不仅可以像msg传入一个基本数据类型,甚至还可以传入一个数组,一个对象。只要在子组件中经过合理的使用,就会达到满意的效果。
2.2 单向数据流
针对prop,我们需要引入一个非常重要的思想,单向数据流:
-
父级prop的更新会导致子组件的prop的更新,使得子组件的相应内容重新渲染。
-
而子组件的prop更新不会导致父组件的prop更新。
-
因此,我们不应该在子组件中更新prop属性!!!
如果子组件想更新自己的prop,最好定义一个私有的data或者计算属性
- 定义一个counter,并将initialCounter作为它的初始值。
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
- 定义一个normalizedSize,它会随着size的prop更新而更新。
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}
2.3 prop的检查验证
我们可以为prop指派默认类型,例如它必须是一个字符串。
我们也可以为prop指定默认值。
props: {
// 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
propA: Number,
// 多个可能的类型
propB: [String, Number],
// 必填的字符串
propC: {
type: String,
required: true
},
// 带有默认值的数字
propD: {
type: Number,
default: 100
},
// 带有默认值的对象
propE: {
type: Object,
// 对象或数组默认值必须从一个工厂函数获取
default: function () {
return { message: 'hello' }
}
},
// 自定义验证函数
propF: {
validator: function (value) {
// 这个值必须匹配下列字符串中的一个
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
}
如果不希望父组件继承子组件的属性,可以使用inheritAttrs: false禁用继承。
3. 自定义事件
3.1 自定义事件基础
自定义事件,顾名思义,就是在js原生的事件基础上,增加一些自己的事件。
通常来说,对于一个事件,有两个关键点:如何触发该事件、触发后会发生什么。
在vue中,自定义的事件,对于程序员来说,主要需要处理以上两件事。
3.1.1 自定义事件的触发
this.$emit('my-event', param)
只要在想要触发事件的时候,调用上述函数,事件即可触发,是不是很简单?
需要注意的是,我们推荐全程采用kebab-case的事件名,这关系到事件的监听。
3.1.2 自定义事件的监听
假设我们需要在组件my-component中绑定监听事件
<my-component v-on:my-event="doSomething"></my-component>
由于html不区分大小写,因此我们上面的事件名推荐使用kebab-case命名法。当然,在vue以后的版本里,我认为可能会增加驼峰命名法的解析方法。
3.2 自定义组件的v-model
自定义组件如下:
Vue.component('base-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
template: `
<input
type="checkbox"
v-bind:checked="checked"
v-on:change="$emit('change', $event.target.checked)"
>
`
})
我们如此使用该组件
<base-checkbox v-model="lovingVue"></base-checkbox>
此处的lovingVue的值会传入名为checked的prop。当base-checkbox触发一个change事件,并附带一个值时,这个lovingVue的值会被更新。效果类似双向绑定。
3.3 将原生事件绑定到子组件
通常在html中,存在一种被称为“事件冒泡”的现象,当子元素被点击时,父元素也会发生响应。
然而在子组件中使用原生事件却出现了问题。例如,当我们如下调用元素时,采用.native后缀来绑定原生的focus事件。
<base-input v-on:focus.native="onFocus"></base-input>
但事实上,base-input是一个这样的组件。input存在于根元素label的内部。因此,我们在这里绑定的focus事件,并不会监听input的focus事件。
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
</label>
vue提供了一个**$listeners**对象来解决这个问题。我们可以采用如下方法来使得input的focus事件被监听。
Vue.component('base-input', {
inheritAttrs: false,
props: ['label', 'value'],
computed: {
inputListeners: function () {
var vm = this
// `Object.assign` 将所有的对象合并为一个新对象
return Object.assign({},
// 我们从父级添加所有的监听器
this.$listeners,
// 然后我们添加自定义监听器,
// 或覆写一些监听器的行为
{
// 这里确保组件配合 `v-model` 的工作
input: function (event) {
vm.$emit('input', event.target.value)
}
}
)
}
},
template: `
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on="inputListeners"
>
</label>
`
})
子组件经过如此封装后,就是一个完全透明的包裹器了,完全可以像一个普通的input元素一样使用了,也不必再使用.native监听器了。
3.4 .sync修饰符实现双向绑定
vue遵循单向数据流原则,只有父组件可以向子组件传递数据。然而有时我们确实希望,子组件的值的变更会带来父组件的值的变更。通常我们使用如下方式。
<text-document
v-bind:title="doc.title"
v-on:update:title="doc.title = $event"
></text-document>
然后如下调用自定义事件update
this.$emit('update:title', newTitle)
在组件中,v-bind实现的是父组件向子组件传递数据,而v-on:update:title通过自定义事件模拟子组件向父组件传递数据。两者实现了双向绑定。而这种写法通常比较固定,因此我们在vue的2.3.0版本中引入了.sync修饰符
<text-document v-bind:title.sync="doc.title"></text-document>
是不是方便了很多?注意,带有 .sync 修饰符的v-bind 不能和表达式一起使用,不然子组件向谁传递数据嘞?
4. Slot
4.1 Slot的基本使用方法
组件的使用:
<navigation-link url="/profile">
Your Profile
</navigation-link>
组件的模板:
<a
v-bind:href="url"
class="nav-link"
>
<slot></slot>
</a>
这样的组件,最终被渲染为:
<a
v-bind:href="url"
class="nav-link"
>
Your Profile
</a>
值得注意的是,Your Profile可以含有HTML代码,甚至可以含有其他组件。
4.2 Slot的变量编译作用域
我们通过如下形式使用slot。由于url是子组件中的property,因此此处访问不到,会是undefine。只有在父组件中的property才能被渲染。
<navigation-link url="/profile">
Clicking here will send you to: {{ url }}
</navigation-link>
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
4.3 设置Slot内部的默认值
假设我们有一个组件,模板代码如下。那么,当我们使用该组件时,不填写组件内部的"Your Profile",这时,子组件会自动渲染"提交"二字。如果填写"Your Profile",那么就不会自动渲染"提交"二字。
总而言之,“提交”就是子组件slot的默认值。
<button type="submit">
<slot>提交</slot>
</button>
4.4 单个组件多Slot的处理
对于一个组件,我们可能同时需要多个slot。因此,我们需要讨论多个slot情形下的命名、传值的情况。
4.4.1 slot的命名
具有名字是slot,叫做具名插槽。比如,我们有如下所示的三slot组件。组件的名字依次为header、default、footer。值得注意的是,未指定name属性的slot会被默认命名为default。
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
我们可以如下使用该组件。不难发现,不同slot的内容被包裹于不同的<template>
中,template需要通过形如v-slot:header的方式命名。如果不包裹于<template>
中,则被渲染到不带名字的<slot>
中。当然,我们也可以为其添加<template v-slot:default>
标签。
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>
它最终会被渲染为如下。
<div class="container">
<header>
<h1>Here might be a page title</h1>
</header>
<main>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</main>
<footer>
<p>Here's some contact info</p>
</footer>
</div>
在vue2.6.0的更新中,我们可以使用动态指令参数。
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
</base-layout>
4.4.2 slot的传值
有时,我们需要在组件调用中使用子组件的属性值。语法格式如下。其中,slotProps是子组件的props对象。当然,你也可以不让它叫“slotProps”,随便起啥名字都可以的。
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
<template v-slot:other="otherSlotProps">
...
</template>
</current-user>
如果不想使用全部的子组件props,只想用其中的一个对象user,则可以像这样调用。
<current-user v-slot="{ user }">
{{ user.firstName }}
</current-user>
我们甚至还可以让user在这里重命名为person。
<current-user v-slot="{ user: person }">
{{ person.firstName }}
</current-user>
还可以为它设置默认值。
<current-user v-slot="{ user = { firstName: 'Guest' } }">
{{ user.firstName }}
</current-user>
4.5 v-slot的强化
v-slot的使用犹如v-on,v-bind一样广泛,因此它也有对应的缩写。
完整指令 | 缩写 |
---|---|
v-on:action | @action |
v-bind:user | :user |
v-slot:default | #default |
下面是一个例子。
<current-user #default="{ user }">
{{ user.firstName }}
</current-user>
5. 动态组件
有时,我们需要一个可以动态确定类型的组件。Vue提供了如下语法,通过is来构造一个动态组件。组件的类型取决于currentTabComponent变量,当currentTabComponent发生变化时,组件的类型也会发生变化。
- 当currentTabComponent == Post时,组件的类型是Post。
- 当currentTabComponent == MyHeader时,组件的类型是MyHeader。
这种方式的操作,实现了动态组件的解耦。如果没有这种方式,那么在Post与MyHeader之间发生切换时,可能要涉及到很多的Vue指令的复杂操作,来让一个单独是组件实现两种形态,却不能将一个组件拆分成两个组件。
<component v-bind:is="currentTabComponent"></component>
在使用这种结构的动态组件一段时间后,不难发现,它不会对组件过去的状态做保存。即当我们在Post组件下打钩了某些checkbox。当我们切换成MyHeader,然后再切换回Post组件时,发现那些所有的checkbox都没有被打钩,即元素被重新渲染了。这会产生极大的性能开销,以及不好的用户体验。
keep-alive标签应运而生,被keep-alive标签所包裹的元素,即使在通过is attribute切换状态后,仍能保存之前的状态。
当然,我们要求keep-alive包裹下的切换元素都有自己的name属性,例如Post和MyHeader都要求有自己的属性。
<!-- 失活的组件将会被缓存!-->
<keep-alive>
<component v-bind:is="currentTabComponent"></component>
</keep-alive>
6. 异步组件
异步组件,即不随主DOM加载而加载的组件。一般来说,我们希望异步组件,在我们用到它时再加载,以节约网络传输。
一个最简单的异步组件的写法如下。’./my-async-component’组件代表加载的原型组件的路径。
Vue.component('async-webpack-example', function (resolve) {
// 这个特殊的 `require` 语法将会告诉 webpack
// 自动将你的构建代码切割成多个包,这些包
// 会通过 Ajax 请求加载
require(['./my-async-component'], resolve)
})
// 或者
Vue.component(
'async-webpack-example',
// 这个动态导入会返回一个 `Promise` 对象。
() => import('./my-async-component')
)
当异步组件加载时,我们需要处理器加载时的状态。我们可以在后一种形式的lambda表达式中返回如下对象。
const AsyncComponent = () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: import('./MyComponent.vue'),
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000
})
7. 处理极端情况
7.1 逆向访问元素&组件
在大部分情况下,我们应遵循单向数据流原则:只有父组件可以向子组件传递数据,逆着却不行。然而,有时存在极端情况,使得我们不得不去违背单向数据流原则,来实现某些功能。Vue给我们提供了这样的机会,但并不推荐总是使用。
访问元素 | 使用 |
---|---|
在子组件中访问根实例 | this.$root.foo等 |
在子组件中访问父组件 | this.$father.foo等 |
在父组件中访问子组件 | this.$refs.refAttribute.foo等 |
向多级子元素中传递父级元素方法(依赖注入) | provide和inject |
需要详细介绍的是后两种情况:
7.1.1 在父组件中访问子组件
一个子组件实例,只可能有一个父组件实例。而一个父组件实例中,却可能有多个子组件实例。因此,父组件要访问子组件,需要给子组件取名字。在这里,我们通过添加ref属性来给子组件取名字。例如,我们将base-input组件取名为usernameInput,可以像下面一样。
<base-input ref="usernameInput"></base-input>
然后这样访问:
this.$refs.usernameInput.focus()
如果来点更狠的,我们想在父组件中访问base-input组件中的元素,例如input元素,那么,我们可以给base-input中的input元素添加ref属性,如下所示。
<base-input ref="usernameInput"></base-input>
然后这样访问:
this.$refs.input.focus()
7.1.2 向多级子元素传递父级元素方法(依赖注入)
给任意级别的子元素,传递指定的方法,而不是一股脑儿把整个父级实例传递给子元素,防止了越权,降低了耦合性。
我们可以在父组件中定义provide属性,provide属性所返回的方法,可以被子组件通过inject属性接收到。使用方法如下所示。
在父组件中,我们设定了一个getMap方法,可供子元素访问。
provide: function () {
return {
getMap: this.getMap
}
}
在子组件中,我们可以通过inject接收来自provide的传递。
inject: ['getMap']
可以发现,这种操作就好比范围更加大的prop,它有如下特点:
- 祖先组件不需要知道哪些后代组件使用它提供的 property
- 后代组件不需要知道被注入的 property 来自哪里
7.2 程序化的事件侦听器
所谓程序化的时间侦听器,即它不写在html中,也就是不通过v-on的方式被侦听,而是通过js代码的方式被处理。事件侦听器有如下三种:
方法 | 效果 |
---|---|
$on(eventName, eventHandler) | 侦听一个事件 |
$once(eventName, eventHandler) | 一次性侦听一个事件 |
$off(eventName, eventHandler) | 停止侦听一个事件 |
下面我们以$once为例,展示它对程序解耦合的效果。
我们需要在某一个组件中使用多个Pikaday对象,当该组件被销毁时,所有的Pikaday对象也要被销毁。根据我们之前所了解的生命周期钩子,很容易想到,我们应该在beforeDestory中调用所有Pikaday的destory方法。然而,根据低耦合的思路,Pikaday的摧毁应该交给Pikaday自己的生命周期钩子去Handle,而不是交给父组件去Handle。因此,使用$once方法,就会带来这样的好处。
mounted: function () {
this.attachDatepicker('startDateInput')
this.attachDatepicker('endDateInput')
},
methods: {
attachDatepicker: function (refName) {
var picker = new Pikaday({
field: this.$refs[refName],
format: 'YYYY-MM-DD'
})
this.$once('hook:beforeDestroy', function () {
picker.destroy()
})
}
}
但说到底,其实我们还是推荐创建一个可复用的input-datepicker组件来实现模块化。
7.3 循环引用
7.3.1 递归组件
要知道,组件是可以调用其自身的,这就给组件的递归创造了可能性。下面的stack-overflow组件就是一个典型的例子。
name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'
然后组件会导致"max stack size exceeded"的错误。因此,如果要递归调用,请保证会最终得到一个v-if="false"的结果。
7.3.2 组件间的循环引用
7.4 通过script定义模板
<script type="text/x-template" id="hello-world-template">
<p>Hello hello hello</p>
</script>
Vue.component('hello-world', {
template: '#hello-world-template'
})
x-template需要定义在vue所属的DOM元素之外。
7.5 控制更新
7.5.1 强制更新
有时候,数组或对象中属性或元素的改变,并不会被vue的响应式系统检测到,因为它们的引用并没有发生改变。此时,我们需要调用this.$forceUpdate来强制更新。
7.5.2 创建静态组件
静态组件,即一旦被创建,就不再是响应式元素,这使得系统开销降低了,但同时也会使得模板有时变得更加费解。向下面这个例子中,我们添加一个v-once属性,代表该组件是静态组件。
Vue.component('terms-of-service', {
template: `
<div v-once>
<h1>Terms of Service</h1>
... a lot of static content ...
</div>
`
})
关于Vue的组件部分,就到此为止。组件作为Vue中的核心内容,需要熟练掌握。通常一种操作可以有两种及以上的实现方式。在不必要时,我们尽量应遵循单向数据流原则,来提高项目的可维护性。