组件基础

组件基础:

  • 了解组件的概念
  • 掌握组件的定义
  • 了解组件的组织方式
  • 掌握组件通信

组件 (Component) 是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码。

Component Tree

通过 Element 感受组件的威力

Element 是基于 Vue 开发的一个知名的第三方组件库,它能帮助我们更加快速的构建应用。

Element 官网

安装:

  1. npm i element-ui

Hello World:

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <!-- 引入样式 -->
  6. <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
  7. </head>
  8. <body>
  9. <div id="app">
  10. <el-button @click="visible = true">按钮</el-button>
  11. <el-dialog :visible.sync="visible" title="Hello world">
  12. <p>欢迎使用 Element</p>
  13. </el-dialog>
  14. </div>
  15. </body>
  16. <!-- 先引入 Vue -->
  17. <script src="https://unpkg.com/vue/dist/vue.js"></script>
  18. <!-- 引入组件库 -->
  19. <script src="https://unpkg.com/element-ui/lib/index.js"></script>
  20. <script>
  21. new Vue({
  22. el: '#app',
  23. data: function() {
  24. return { visible: false }
  25. }
  26. })
  27. </script>
  28. </html>

使用组件

组件的定义方式分为两种,全局定义和局部定义:

  • 全局组件定义在全局,在任意组件中都可以直接使用
  • 局部组件定义在组件内部,只能在当前组件使用
  • 建议把通用的组件定义在全局,把不通用的组件定义在局部

全局注册

注册:

  1. Vue.component('my-component', {
  2. template: '<div>A custom component!</div>'
  3. });
  4. // 创建根实例
  5. new Vue({
  6. el: '#example'
  7. });

在模板中使用组件:

  1. <div id="example">
  2. <my-component></my-component>
  3. </div>

渲染结果:

  1. <div id="example">
  2. <div>A custom component!</div>
  3. </div>

局部注册

你不必把每个组件都注册到全局。你可以通过某个 Vue 实例/组件的实例选项 components 注册仅在其作用域中可用的组件:

注册:

  1. new Vue({
  2. // ...
  3. components: {
  4. // <my-component> 将只在父组件模板中可用
  5. 'my-component': {
  6. template: '<div>A custom component!</div>'
  7. }
  8. }
  9. });

使用:

  1. <div id="example">
  2. <div>A custom component!</div>
  3. </div>

组件的模板

  • DOM 模板
  • 字符串模板
  • .vue 单文件组件中的 template 模板

组件的 data 必须是函数

构造 Vue 实例时传入的各种选项大多数都可以在组件里使用。只有一个例外:data 必须是函数。

  1. Vue.component('simple-counter', {
  2. template: '<button v-on:click="counter += 1">{{ counter }}</button>',
  3. data: function () {
  4. return { counter: 0 }
  5. }
  6. });
  7. new Vue({
  8. el: '#example-2'
  9. });

组件的作用域是独立的

  • 组件无法访问外部作用域成员
  • 外部作用域也无法访问组件内部成员

组件组合

组件设计初衷就是要配合使用的,最常见的就是形成父子组件的关系:组件 A 在它的模板中使用了组件 B。它们之间必然需要相互通信:父组件可能要给子组件下发数据,子组件则可能要将它内部发生的事情告知父组件。然而,通过一个良好定义的接口来尽可能将父子组件解耦也是很重要的。这保证了每个组件的代码可以在相对隔离的环境中书写和理解,从而提高了其可维护性和复用性。
在 Vue 中,父子组件的关系可以总结为 prop 向下传递,事件向上传递。父组件通过 prop 给子组件下发数据,子组件通过事件给父组件发送消息。看看它们是怎么工作的。

prop 向下传递,事件向上传递


组件化构建 TodoMVC

todomvc-component.png


组件通信

组件基础 - 图4

在树形结构里面,组件之间有几种典型的关系:父子关系、兄弟关系、没有直接关系。

相应地,组件之间有以下几种典型的通讯方案:

  • 直接的父子关系
  • 直接的父子关系
  • 没有直接关系
  • 利用 cookie 和 localstorage 进行通讯。
  • 利用 session 进行通讯。
  • 父传子 Props Down
  • 子通知父亲 Events Up

  • 通过 ref 父亲直接访问子组件

    • 给子组件起个 ref
    • 然后在父组件中通过 this.$refs.子组件ref名
  • 子组件可以在内部通过 this.$parent 直接访问父组件
  • 非父子关系
    • 事件通信 Events Bus
    • Global Bus
  • 集中式状态管理 Vuex

父子组件通信:Props Down

1. 在父组件中通过子组件标签属性传递数据

  1. <child message="hello!"></child>

2. 在子组件显式地用 props 选项声明它预期的数据并使用

  1. Vue.component('child', {
  2. // 声明 props
  3. props: ['message'],
  4. // 就像 data 一样,prop 也可以在模板中使用
  5. // 同样也可以在 vm 实例中通过 this.message 来使用
  6. template: '<span>{{ message }}</span>'
  7. });

camelCase vs. kebab-case

HTML 特性是不区分大小写的。所以,当使用的不是字符串模板时,camelCase (驼峰式命名) 的 prop 需要转换为相对应的 kebab-case (短横线分隔式命名)。

  1. Vue.component('child', {
  2. // 在 JavaScript 中使用 camelCase
  3. props: ['myMessage'],
  4. template: '<span>{{ myMessage }}</span>'
  5. });
  1. <!-- 在 HTML 中使用 kebab-case -->
  2. <child my-message="hello!"></child>

如果你使用字符串模板,则没有这些限制。

动态 Prop

与绑定到任何普通的 HTML 特性相类似,我们可以用 v-bind 来动态地将 prop 绑定到父组件的数据。每当父组件的数据变化时,该变化也会传导给子组件:

  1. <div>
  2. <input v-model="parentMsg">
  3. <br>
  4. <child v-bind:my-message="parentMsg"></child>
  5. </div>

你也可以使用 v-bind 的缩写语法:

  1. <child :my-message="parentMsg"></child>

字面量语法 vs 动态语法

初学者常犯的一个错误是使用字面量语法传递数值:

  1. <!-- 传递了一个字符串 "1" -->
  2. <comp some-prop="1"></comp>

因为它是一个字面量 prop,它的值是字符串 “1” 而不是一个数值。如果想传递一个真正的 JavaScript 数值,则需要使用 v-bind,从而让它的值被当作 JavaScript 表达式计算:

  1. <!-- 传递真正的数值 -->
  2. <comp v-bind:some-prop="1"></comp>

单向数据流

Prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是反过来不会。这是为了防止子组件无意间修改了父组件的状态,来避免应用的数据流变得难以理解。

另外,每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop。如果你这么做了,Vue 会在控制台给出警告。

在两种情况下,我们很容易忍不住想去修改 prop 中数据:

  1. Prop 作为初始值传入后,子组件想把它当作局部数据来用
  2. Prop 作为原始数据传入,由子组件处理成其它数据输出

对这两种情况,正确的应对方式是:

1. 定义一个局部变量,并用 prop 的值初始化它:

  1. props: ['initialCounter'],
  2. data: function () {
  3. return { counter: this.initialCounter }
  4. }

2. 定义一个计算属性,处理 prop 的值并返回:

  1. // ...
  2. props: ['size'],
  3. computed: {
  4. normalizedSize: function () {
  5. return this.size.trim().toLowerCase()
  6. }
  7. },

!> 注意在 JavaScript 中对象和数组是引用类型,指向同一个内存空间,如果 prop 是一个对象或数组,在子组件内部改变它会影响父组件的状态。

Prop 验证

我们可以为组件的 prop 指定验证规则。如果传入的数据不符合要求,Vue 会发出警告。这对于开发给他人使用的组件非常有用。
要指定验证规则,需要用对象的形式来定义 prop,而不能用字符串数组:

  1. Vue.component('example', {
  2. props: {
  3. // 基础类型检测 (`null` 指允许任何类型)
  4. propA: Number,
  5. // 可能是多种类型
  6. propB: [String, Number],
  7. // 必传且是字符串
  8. propC: {
  9. type: String,
  10. required: true
  11. },
  12. // 数值且有默认值
  13. propD: {
  14. type: Number,
  15. default: 100
  16. },
  17. // 数组/对象的默认值应当由一个工厂函数返回
  18. propE: {
  19. type: Object,
  20. default: function () {
  21. return { message: 'hello' }
  22. }
  23. },
  24. // 自定义验证函数
  25. propF: {
  26. validator: function (value) {
  27. return value > 10
  28. }
  29. }
  30. }
  31. });

type 可以是下面原生构造器:

  • String
  • Number
  • Boolean
  • Function
  • Object
  • Array
  • Symbol

type 也可以是一个自定义构造器函数,使用 instanceof 检测。

当 prop 验证失败,Vue 会抛出警告 (如果使用的是开发版本)。
注意 prop 会在组件实例创建之前进行校验,所以在 default 或 validator 函数里,诸如 data、computed 或 methods 等实例属性还无法使用。


父子组件通信:Events Up

我们知道,父组件使用 prop 传递数据给子组件。但子组件怎么跟父组件通信呢?这个时候 Vue 的自定义事件系统就派得上用场了。

1. 在子组件中调用 $emit() 方法发布一个事件

  1. Vue.component('button-counter', {
  2. template: '<button v-on:click="incrementCounter">{{ counter }}</button>',
  3. data: function () {
  4. return {
  5. counter: 0
  6. }
  7. },
  8. methods: {
  9. incrementCounter: function () {
  10. this.counter += 1
  11. // 发布一个名字叫 increment 的事件
  12. this.$emit('increment')
  13. }
  14. },
  15. });

2. 在父组件中提供一个子组件内部发布的事件处理函数

  1. new Vue({
  2. el: '#counter-event-example',
  3. data: {
  4. total: 0
  5. },
  6. methods: {
  7. incrementTotal: function () {
  8. this.total += 1
  9. }
  10. }
  11. });

3. 在使用子组件的模板的标签上订阅子组件内部发布的事件

  1. <div id="counter-event-example">
  2. <p>{{ total }}</p>
  3. <!--
  4. 订阅子组件内部发布的 increment 事件
  5. 当子组件内部 $commit('increment') 发布的时候,就会调用到父组件中的 incrementTotal 方法
  6. -->
  7. <button-counter v-on:increment="incrementTotal"></button-counter>
  8. </div>

给组件绑定原生事件

有时候,你可能想在某个组件的根元素上监听一个原生事件。可以使用 v-on 的修饰符 .native。例如:

  1. <my-component v-on:click.native="doTheThing"></my-component>

.sync 修饰符

在一些情况下,我们可能会需要对一个 prop 进行“双向绑定”。
就是当一个子组件改变了一个带 .sync 的 prop 的值时,这个变化也会同步到父组件中所绑定的值。

在使用子组件的时候加上 .sync 修饰符:

  1. <comp :foo.sync="bar"></comp>

在子组件内部更新 foo 的值时,显示的触发一个更新事件:

  1. this.$emit('update:foo', newValue);

非父子组件通信:Event Bus

有时候,非父子关系的两个组件之间也需要通信。在简单的场景下,可以使用一个空的 Vue 实例作为事件总线:

  1. var bus = new Vue();
  1. // 触发组件 A 中的事件
  2. bus.$emit('id-selected', 1);
  1. // 在组件 B 创建的钩子中监听事件
  2. bus.$on('id-selected', function (id) {
  3. // ...
  4. });

专业组件通信大杀器:Vuex

在复杂的情况下,我们应该考虑使用专门的 状态管理模式


使用插槽分发内容


组件其它