vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > vue组件及父子组件通信

vue组件以及父子组件通信方式

作者:刘乙江

这篇文章主要介绍了Vue组件化的基本概念,包括什么是组件化、Vue的组件化思想以及如何在Vue中注册和使用组件,文章还详细讲解了如何进行父子组件之间的通信,包括父组件传递数据给子组件和子组件通过自定义事件将数据传递给父组件,文章最后通过一个综合练习来巩固所学知识

vue组件及父子组件通信

一. 认识组件化

1.1. 什么是组件化?

人面对复杂问题的处理方式:

任何一个人处理信息的逻辑能力都是有限的

所以,当面对一个非常复杂的问题时,我们不太可能一次性搞定一大堆的内容。

但是,我们人有一种天生的能力,就是将问题进行拆解。

如果将一个复杂的问题,拆分成很多个可以处理的小问题,再将其放在整体当中,你会发现大的问题也会迎刃而解。

组件化也是类似的思想:

我们需要通过组件化的思想来思考整个应用程序:

1.2. Vue的组件化

组件化是Vue、React、Angular的核心思想,也是现在前端框架的重点内容(包括以后实战项目):

组件化思想的应用:

接下来,我们来学习一下在Vue中如何注册一个组件,以及之后如何使用这个注册后的组件。

二. 注册一个组件

2.1. 注册全局组件

如果我们现在有一部分内容(模板、逻辑等),我们希望将这部分内容抽取到一个独立的组件中去维护,这个时候如何注册一个组件呢?

我们先从简单的开始谈起,比如下面的模板希望抽离到一个单独的组件:

<h2>{{title}}</h2>
<p>{{message}}</p>

注册组件分成两种:

我们先来学习一下全局组件的注册:

  <template id="my-cpn">
    <h2>我是组件标题</h2>
    <p>我是组件内容,哈哈哈哈</p>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const app = Vue.createApp(App);

    // 注册全局组件(使用app)
    app.component("my-cpn", {
      template: "#my-cpn"
    });

    app.mount('#app');
  </script>

之后,我们可以在App组件的template中直接使用这个全局组件:

  <template id="my-app">
    <my-cpn></my-cpn>
    <my-cpn></my-cpn>
    <my-cpn></my-cpn>
    <my-cpn></my-cpn>
  </template>

当然,我们组件本身也可以有自己的代码逻辑:

// 注册全局组件(使用app)
app.component("my-cpn", {
  template: "#my-cpn",
  data() {
    return {
      title: "我是标题",
      message: "我是内容, 哈哈哈哈"
    }
  },
  methods: {
    btnClick() {
      console.log("btnClick");
    }
  }
});

2.2. 组件的名称

在通过app.component注册一个组件的时候,第一个参数是组件的名称,定义组件名的方式有两种:

方式一:使用kebab-case(短横线分割符)

当使用 kebab-case (短横线分隔命名) 定义一个组件时,你也必须在引用这个自定义元素时使用 kebab-case,例如 ;

app.component('my-component-name', {
  /* ... */
})

方式二:使用PascalCase(驼峰标识符)

当使用 PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。也就是说 和 都是可接受的;

app.component('MyComponentName', {
  /* ... */
})

import 导入进来的组件最好加.vue

2.3. 注册局部组件

全局组件往往是在应用程序一开始就会全局组件完成,那么就意味着如果某些组件我们并没有用到,也会一起被注册:

所以在开发中我们通常使用组件的时候采用的都是局部注册:

接下来,我们看一下局部组件是如何注册的:

<div id="app"></div>

  <template id="my-app">
    <component-a></component-a>
    <component-b></component-b>
  </template>

  <template id="component-a">
    <h2>{{title}}</h2>
    <p>{{message}}</p>
  </template>


  <template id="component-b">
    <h2>{{title}}</h2>
    <p>{{message}}</p>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const ComponentA = {
      template: "#component-a",
      data() {
        return {
          title: "我是ComponentA标题",
          message: "我是ComponentA内容, 哈哈哈哈"
        }
      }
    }

    const ComponentB = {
      template: "#component-b",
      data() {
        return {
          title: "我是ComponentB标题",
          message: "我是ComponentB内容, 呵呵呵呵"
        }
      }
    }

    const App = {
      template: '#my-app',
      components: {
        'component-a': ComponentA,
        'component-b': ComponentB,
      },
      data() {
        return {
          message: "Hello World"
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>

在开发中通常我们都会注册局部组件,而不是全局组件。

三. Vue的开发模式

3.1. Vue的开发模式

目前我们使用vue的过程都是在html文件中,通过template编写自己的模板、脚本逻辑、样式等。

但是随着项目越来越复杂,我们会采用组件化的方式来进行开发:

所以在真实开发中,我们可以通过一个后缀名为 .vue 的single-file components (单文件组件) 来解决,并且可以使用webpack或者vite或者rollup等构建工具来对其进行处理。

在这个组件中我们可以获得非常多的特性:

代码的高亮;

3.2. 如何支持SFC

如果我们想要使用这一的SFC的.vue文件,比较常见的是两种方式:

我们最终,无论是后期我们做项目,还是在公司进行开发,通常都会采用Vue CLI的方式来完成。

3.3 css作用域污染问题

app.vue是helloworld.vue的父组件

如果在app.vue上写了一个h2.{color:red}

那么不光app.vue的对应字体颜色会变化,helloworld.vue对应字体颜色也会变化。

解决方法:

四. 认识组件的嵌套

4.1. App单独开发

前面我们是将所有的逻辑放到一个App.vue中:

我们来分析一下下面代码的嵌套逻辑,假如我们将所有的代码逻辑都放到一个App.vue组件中:

<template>
  <div>
    <h2>Header</h2>
    <h2>NavBar</h2>
  </div>
  <div>
    <h2>Banner</h2>
    <ul>
      <li>商品列表1</li>
      <li>商品列表2</li>
      <li>商品列表3</li>
      <li>商品列表4</li>
      <li>商品列表5</li>
    </ul>
  </div>
  <div>
    <h2>Footer</h2>
    <h2>免责声明</h2>
  </div>
</template>

<script>
export default {

};
</script>

<style scoped></style>

我们会发现,将所有的代码逻辑全部放到一个组件中,代码是非常的臃肿和难以维护的。并且在真实开发中,我们会有更多的内容和代码逻辑,对于扩展性和可维护性来说都是非常差的。

所以,在真实的开发中,我们会对组件进行拆分,拆分成一个个功能的小组件。

4.2. 组件的拆分

我们可以按照如下的方式进行拆分:

Header.vue组件

<template>
  <div>
    <h2>Header</h2>
    <h2>NavBar</h2>
  </div>
</template>

Main.vue组件:

<template>
  <div>
    <banner></banner>
    <product-list></product-list>
  </div>
</template>

Banner.vue组件:

<template>
  <h2>Banner</h2>
</template>

ProductList组件:

<template>
  <ul>
    <li>商品列表1</li>
    <li>商品列表2</li>
    <li>商品列表3</li>
    <li>商品列表4</li>
    <li>商品列表5</li>
  </ul>
</template>

Footer.vue组件:

<template>
  <div>
    <h2>Footer</h2>
    <h2>免责声明</h2>
  </div>
</template>

按照如上的拆分方式后,我们开发对应的逻辑只需要去对应的组件编写就可。

4.3. 组件的通信

上面的嵌套逻辑如下,它们存在如下关系:

在开发过程中,我们会经常遇到需要组件之间相互进行通信:

总之,在一个Vue项目中,组件之间的通信是非常重要的环节,所以接下来我们就具体学习一下组件之间是如何相互之间传递数据的;

五. 父子组件的相互通信

5.1. 父组件传递给子组件

在开发中很常见的就是父子组件之间通信,比如父组件有一些数据,需要子组件来进行展示:

什么是Props呢?

Props有两种常见的用法:

5.1.1. props的数组用法

封装ShowMessage.vue组件:

<template>
  <div>
    <h2>组件展示的title:{{title}}</h2>
    <p>组件展示的content: {{content}}</p>
  </div>
</template>

<script>
  export default {
    props: ["title", "content"]
  }
</script>

通过App.vue传递给组件数据:

<template>
  <div>
    <show-message title="哈哈哈" content="我是哈哈哈"></show-message>
    <show-message title="呵呵呵" content="我是呵呵呵"></show-message>
  </div>
</template>

<script>
  import ShowMessage from './ShowMessage.vue';

  export default {
    components: {
      ShowMessage
    }
  }
</script>

当然,我们也可以将data中的数据传递给子组件:

<template>
  <div>
    <show-message :title="title1" :content="content1"></show-message>
    <show-message :title="title2" :content="content2"></show-message>
  </div>
</template>

<script>
  import ShowMessage from './ShowMessage.vue';

  export default {
    components: {
      ShowMessage
    },
    data() {
      return {
        title1: "哈哈哈",
        content1: "我是哈哈哈",
        title2: "呵呵呵",
        content2: "我是呵呵呵"
      }
    }
  }
</script>

当然,我们也可以直接传递一个对象:

<template>
  <div>
    <show-message :title="message.title" :content="message.content"></show-message>
    <show-message v-bind="message"></show-message>
  </div>
</template>

<script>
  import ShowMessage from './ShowMessage.vue';

  export default {
    components: {
      ShowMessage
    },
    data() {
      return {
        message: {
          title: "嘿嘿嘿",
          content: "我是嘿嘿嘿"
        }
      }
    }
  }
</script>
5.1.2. props的对象用法

数组用法中我们只能说明传入的attribute的名称,并不能对其进行任何形式的限制,接下来我们来看一下对象的写法是如何让我们的props变得更加完善的。

ShowMessage.vue的props对象写法:

<template>
	<div>
		<h2>组件展示的title:{{title}}</h2>
		<p>组件展示的content: {{content}}</p>
	</div>
</template>
<script>
	export default {
		props: { // 指定类型:
		title: String,      
		// 指定类型,同时指定是否必选、默认值:      
		content: { type: String,require: true,default: "哈哈哈"      }    }  }
</script>

细节一:那么type的类型都可以是哪些呢?

细节二:对象类型的其他写法

<script>
	export default {
		props: { // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)      
			propA: Number,
			// 多个可能的类型      
			propB: [String, Number],
			// 必填的字符串      
			propC: {
				type: String,
				required: true
			},
			// 带有默认值的数字      
			propD: {
				type: Number,
				default: 100
			},
			// 带有默认值的对象      
			propE: {
				type: Object, // 对象或数组默认值必须从一个工厂函数获取        
				default () {
					return {
						message: 'hello'
					}
				}
			},
			// 自定义验证函数      
			propF: {
				validator(value) { // 这个值必须匹配下列字符串中的一个          
					return ['success', 'warning', 'danger'].includes(value)
				}
			},
			// 具有默认值的函数      
			propG: {
				type: Function, // 与对象或数组默认值不同,这不是一个工厂函数 —— 这是一个用作默认值的函数        
				default () {
					return 'Default function'
				}
			}
		},
	}
</script>

细节三:Prop 的大小写命名(camelCase vs kebab-case)

ShowMessage.vue组件:

<template>
	<div>
		<p>{{messageInfo}}</p>
	</div>
</template>
<script>
	export default {
		props: {
			messageInfo: String,
		}
	}
</script>

App.vue组件中传入:

<template>
	<div>
		<show-message messageInfo="哈哈哈"></show-message>
		<show-message message-info="哈哈哈"></show-message>//推荐这个
	</div>
</template>

重申一次,如果你使用.vue文件写,那么这个限制就不存在,因为messageInfo是由vueloader编译的,他认识。如果使用template:xxx,那么必须使用kebab-case

5.1.3. 非Prop的Attribute

什么是非Prop的Attribute呢?

Attribute继承

当组件有单个根节点时,非Prop的Attribute将自动添加到根节点的Attribute中:

禁用Attribute继承

如果我们不希望组件的根元素继承attribute,可以在组件中设置 inheritAttrs: false:

<template>
	<div> 我是NotPropAttribue组件 <h2 :class="$attrs.class"></h2>
	</div>
</template>
<script>
	export default {
		inheritAttrs: false
	}
</script>

多个根节点的attribute

多个根节点的attribute如果没有显示的绑定,那么会报警告,我们必须手动的指定要绑定到哪一个属性上:

<template>
	<div :class="$attrs.class">我是NotPropAttribue组件1</div>
	<div>我是NotPropAttribue组件2</div>
	<div>我是NotPropAttribue组件3</div>
</template>

5.2. 子组件传递给父组件

什么情况下子组件需要传递内容到父组件呢?

我们如何完成上面的操作呢?

5.2.1. 自定义事件的流程

我们封装一个CounterOperation.vue的组件:

<template>
	<div> <button @click="increment">+1</button> <button @click="decrement">-1</button> </div>
</template>
<script>
	export default {
		emits: ["addOne", "subOne"],
		methods: {
			increment() {
				this.$emit("addOne");
			},
			decrement() {
				this.$emit("subOne");
			}
		}
	}
</script>

我们可以在App.vue中来监听自定义组件发出的事件:

<template>
	<div>
		<h2>当前计数: {{counter}}</h2>
		<counter-operation @addOne="add" @subOne="sub"></counter-operation>
	</div>
</template>
<script>
	import CounterOperation from './CounterOperation.vue';
	export default {
		components: {
			CounterOperation
		},
		data() {
			return {
				counter: 0
			}
		},
		methods: {
			add() {
				this.counter++
			},
			sub() {
				this.counter--;
			}
		}
	}
</script>
5.2.2. 自定义事件的参数

自定义事件的时候,我们也可以传递一些参数给父组件:

<template>
	<div> <button @click="increment">+1</button> <button @click="incrementTen">+10</button> <button @click="decrement">-1</button>
	</div>
</template>
<script>
	export default {
		methods: {
			incrementTen() {
				this.$emit("addTen", 10)
			}
		}
	}
</script>
<style scoped></style>
5.2.3. 自定义事件的验证

在vue3当中,我们可以对传递的参数进行验证:

<template>
	<div> <button @click="incrementTen">+10</button> </div>
</template>
<script>
	export default {
		emits: {
			addTen: function(payload) {
				if (payload === 10) {
					return true
				}
				return false;
			}
		},
		methods: {
			incrementTen() {
				this.$emit("addTen", 10)
			}
		}
	}
</script>

5.3. 组件间通信案例练习

我们来做一个相对综合的练习:

5.3.1. TabControl实现

TabControl.vue的实现代码:

<template>
	<div class="tab-control"> <template v-for="(item, index) in titles" :key="item">
			<div class="tab-control-item" @click="itemClick(index)" :class="{active: index === currentIndex}"> <span class="underline">{{ item }}</span>
			</div>
		</template> </div>
</template>
<script>
	export default {
		props: {
			titles: {
				type: Array,
				default () {
					return [];
				},
			},
		},
		emits: ["titleClick"],
		data() {
			return {
				currentIndex: 0
			}
		},
		methods: {
			itemClick(index) {
				this.currentIndex = index;
				this.$emit("titleClick", index);
			},
		},
	};
</script>
<style scoped>
	.tab-control {
		display: flex;
		justify-content: space-between;
	}

	.tab-control-item {
		flex: 1;
		text-align: center;
		height: 40px;
		line-height: 40px;
	}

	.tab-control-item.active {
		color: red;
	}

	.tab-control-item.active span {
		display: inline-block;
		border-bottom: 4px solid red;
		padding: 0 10px;
	}
</style>
5.3.2. App中的使用

我们在App中的使用过程如下:

<template>
	<div>
		<tab-control :titles="titles" @titleClick="titleClick"></tab-control>
		<h2>{{contents[currentIndex]}}</h2>
	</div>
</template>
<script>
	import TabControl from './TabControl.vue';
	export default {
		components: {
			TabControl
		},
		data() {
			return {
				titles: ["衣服", "鞋子", "裤子"],
				contents: ["衣服页面", "鞋子页面", "裤子页面"],
				currentIndex: 0
			}
		},
		methods: {
			titleClick(index) {
				this.currentIndex = index;
			}
		}
	}
</script>

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文