对组件功能的封装,可以像搭积木一样开发网页。
Vue官方的示例图对组件化开发的形象展示。左边是一个网页,可以按照功能模块抽象成很多组件,这些组件就像积木一样拼接成网页。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aS7Zk8tp-1686298456553)(null)]
1 什么是组件化开发
1.1 浏览器封装好的组件
在页面的源码里写出的button标签,会在前端页面中显示如下样式:
这button就是个组件,这样前端页面在显示上会加上边框和鼠标悬停样式,还可使用click事件触发函数等。只不过这是浏览器封装好的组件,编码只需使用如下代码。
<button> 按钮 </button>
1.2 Vue自定义组件
把一个功能的模板(template)封装在一个.vue文件。如下图,把每个组件的逻辑和样式,即JS和CSS封装在一起,方便在项目中复用整个组件:
项目有导航、侧边栏、表格、弹窗等组件,并且也会引入Element3组件库。也会定制业务相关组件,最终通过这些组件,积木式搭建页面。
Vue组件化机制很好用,只需在其基础上,掌握和学习组件化在使用上的设计理念,以实现高效的代码复用,开发中把组件分成:
- 通用型组件
- 业务型组件
通用型组件就是各大组件库的组件风格,包括按钮、表单、弹窗等通用功能。业务型组件包含业务的交互逻辑,包括购物车、登录注册等,和我们不同的业务强绑定。
设计组件的要点,先选择一个简单的组件。
全局组件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>TodoList</title> <script src="../vue.js"></script> </head> <body> <div id="app"> <input type="text" v-model="todoValue"/> <button @click="submit">提交</button> <ul> <!-- <li v-for="item of list">{{item}}</li> --> <!-- 这里的 list 代表new Vue.data里的 list --> <!-- :就是 v-bind,将值传递给组件 --> <todo-item v-for="item of list" :item="item"> </todo-item> </ul> </div> <script> // 全局组件 Vue.component('TodoItem', { // 通过 :item="item"接收其值 props: ["item"], template: '<li>{{item}}</li>' }); var app = new Vue({ el: '#app', data: { list: [] }, methods: { submit: function () { this.list.push(this.todoValue) this.todoValue = '' } } }) </script> </body> </html>
效果:
局部组件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>局部组件</title> <script src="../vue.js"></script> </head> <body> <div id="app"> <input type="text" v-model="todoValue"/> <button @click="submit">提交</button> <ul> <todo-item v-for="item of list" :item="item"> </todo-item> </ul> </div> <script> // 局部组件 var TodoItem = { props: ["item"], template: '<li>{{item}}</li>' } var app = new Vue({ el: '#app', components:{ TodoItem }, data: { list: [] }, methods: { submit: function () { this.list.push(this.todoValue) this.todoValue = '' } } }) </script> </body> </html>
效果:
2 组件间传值
2.1 父组件 -> 子组件
刚才的全局组件案例,其实就包含父组件向子组件传值。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>TodoList</title> <script src="../vue.js"></script> </head> <body> <div id="root"> <div> <input type="text" v-model="todoValue"/> <button @click="handleBtnClick">提交</button> </div> <ul> <!-- 通过 v-bind,将父组件的 item 值传给子组件了--> <todo-item :content="item" :index="index" v-for="(item, index) in list" @delete="handleItemDelete"> </todo-item> </ul> </div> <script> // 定义了一个名为TodoItem的组件 // new Vue的子组件 // 在代码中,通过使用</todo-item>标签,用到了该子组件 var TodoItem = { // 该组件接受内容和索引作为属性 // 并在列表中显示 props: ['content', 'index'], template: "<li @click='handleItemClick'>{{content}}</li>", methods: { handleItemClick: function () { this.$emit("delete", this.index); } } } // new Vue属于全局组件 // 在本 demo 中,也属于最外层的父组件 // 整个root div 区域也就是该父组件的模板 // 定义了一个名为app的Vue实例 var app = new Vue({ el: "#root", components: { TodoItem: TodoItem }, // 该实例具有 data: { // 一个todoValue数据属性 todoValue: "", // 一个list数组属性 list: [] }, methods: { // 当用户点击提交按钮时 handleBtnClick: function () { // 应用程序将todoValue添加到list数组 this.list.push(this.todoValue) // 并将todoValue重置为空字符串 this.todoValue = "" }, // 当用户单击列表中的项目时 // 应用程序将该项目从列表中删除 handleItemDelete: function (index) { this.list.splice(index2, 1) } } }) </script> </body> </html>
2.2 子组件 -> 父组件
若现在要实现,点击待办项,能将其删除,就涉及子组件传值给父组件了。
先实现一个click功能:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>子组件传值给父组件</title> <script src="../vue.js"></script> </head> <body> <div id="root"> <div> <input type="text" v-model="todoValue"/> <button @click="handleBtnClick">提交</button> </div> <ul> <!-- 通过 v-bind,将父组件的 item 值传给子组件了--> <todo-item :content="item" :index="index" v-for="(item, index) in list" > </todo-item> </ul> </div> <script> // 定义了一个名为TodoItem的组件,new Vue的子组件 // 在代码中,通过使用</todo-item>标签,用到了该子组件 var TodoItem = { // 该组件接受内容和索引作为属性 // 并在列表中显示 props: ['content', 'index'], // v-on:click 简写成 @click template: "<li @click='handleItemClick'>{{content}}</li>", methods: { handleItemClick: function () { alert("click") } } } // new Vue属于全局组件 // 在本 demo 中,也属于最外层的父组件 // 整个root div 区域也就是该父组件的模板 // 定义了一个名为app的Vue实例 var app = new Vue({ el: "#root", components: { TodoItem: TodoItem }, // 该实例具有 data: { // 一个todoValue数据属性 todoValue: "", // 一个list数组属性 list: [] }, methods: { // 当用户点击提交按钮时 handleBtnClick: function () { // 应用程序将todoValue添加到list数组 this.list.push(this.todoValue) // 并将todoValue重置为空字符串 this.todoValue = "" } } }) </script> </body> </html>
效果:
现在考虑,把子组件数据传递到父组件,由父组件决定子组件到底显示哪些值。
所以要实现删除,就要将子组件内容传给父组件,父组件来改变数据,父组件的数据变化了,子组件的数据自然就会变更。
在Vue.js中,可以通过在子组件中触发一个自定义事件并传递数据来实现将子组件数据传递到父组件。父组件可以监听子组件的自定义事件,并在事件处理程序中接收传递的数据并更新父组件的数据。这样,父组件的数据变化会自动更新子组件的数据,从而实现删除功能。
父组件的数据变化为啥会自动更新子组件的数据
在Vue.js中,当父组件的数据更新时,它会重新渲染所有子组件。这是因为Vue.js使用了响应式数据绑定的机制,当父组件的数据变化时,所有依赖于该数据的子组件都会自动更新。这个机制是通过Vue.js内部实现的虚拟DOM和数据依赖追踪来实现的。因此,当父组件的数据变化时,子组件的数据也会自动更新,从而实现了数据的同步。
那就来发事件吧:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>子组件传值给父组件</title> <script src="../vue.js"></script> </head> <body> <div id="root"> <div> <input type="text" v-model="todoValue"/> <button @click="handleBtnClick">提交</button> </div> <ul> <!-- 通过 v-bind,将父组件的 item 值传给子组件 --> <!-- 在父组件里创建子组件的同时,就能监听子组件发出的事件 一旦子组件被触发了,就会执行父组件的 handleItemDelete --> <todo-item :content="item" :index="index" v-for="(item, index) in list" @delete="handleItemDelete" > </todo-item> </ul> </div> <script> // 定义了一个名为TodoItem的组件,new Vue的子组件 // 在代码中,通过使用</todo-item>标签,用到了该子组件 var TodoItem = { // 该组件接受内容和索引作为属性 // 并在列表中显示 props: ['content', 'index'], // v-on:click 简写成 @click template: "<li @click='handleItemClick'>{{content}}</li>", methods: { handleItemClick: function () { // alert("JavaEdge 666") // 点击子组件时,子组件对外发出事件 this.$emit("delete") } } } // new Vue属于全局组件 // 在本 demo 中,也属于最外层的父组件 // 整个root div 区域也就是该父组件的模板 // 定义了一个名为app的Vue实例 var app = new Vue({ el: "#root", components: { TodoItem: TodoItem }, // 该实例具有 data: { // 一个todoValue数据属性 todoValue: "", // 一个list数组属性 list: [] }, methods: { // 当用户点击提交按钮时 handleBtnClick: function () { // 应用程序将todoValue添加到list数组 this.list.push(this.todoValue) // 并将todoValue重置为空字符串 this.todoValue = "" }, handleItemDelete: function () { alert("delete") } } }) </script> </body> </html>
效果:
看来发出的事件生效了,那么再小改一处,让父组件把元素清空:
handleItemDelete: function () { // alert("delete") this.list = []; }
现在,考虑只删除点击的那一项,而不是清空呢?父组件给子组件传个 index,子组件必须接收它:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>子组件传值给父组件</title> <script src="../vue.js"></script> </head> <body> <div id="root"> <div> <input type="text" v-model="todoValue"/> <button @click="handleBtnClick">提交</button> </div> <ul> <!-- 通过 v-bind,将父组件的 item 值传给子组件 --> <!-- 在父组件里创建子组件的同时,就能监听子组件发出的事件 一旦子组件被触发了,就会执行父组件的 handleItemDelete --> <todo-item :content="item" :index="index" v-for="(item, index) in list" @delete="handleItemDelete"> </todo-item> </ul> </div> <script> // 定义了一个名为TodoItem的组件,new Vue的子组件 // 在代码中,通过使用</todo-item>标签,用到了该子组件 var TodoItem = { // 该组件接受内容和索引作为属性 // 并在列表中显示 props: ['content', 'index'], // v-on:click 简写成 @click template: "<li @click='handleItemClick'>{{content}}</li>", methods: { handleItemClick: function () { // alert("JavaEdge 666") // 点击子组件时,子组件对外发出事件 // this.$emit("delete") // 点击子组件时,子组件对外发出事件,还顺带一个参数也发出去,那么监听事件的handleItemDelete就能拿到 index this.$emit("delete",this.index) } } } // new Vue属于全局组件 // 在本 demo 中,也属于最外层的父组件 // 整个root div 区域也就是该父组件的模板 // 定义了一个名为app的Vue实例 var app = new Vue({ el: "#root", components: { TodoItem: TodoItem }, // 该实例具有 data: { // 一个todoValue数据属性 todoValue: "", // 一个list数组属性 list: [] }, methods: { // 当用户点击提交按钮时 handleBtnClick: function () { // 应用程序将todoValue添加到list数组 this.list.push(this.todoValue) // 并将todoValue重置为空字符串 this.todoValue = "" }, handleItemDelete: function (index) { // alert("delete") // this.list = []; alert(index) } } }) </script> </body> </html>
效果:当我点击 index=0 的 java 时,弹窗顺势而出
最终实现删除指定项:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>子组件传值给父组件</title> <script src="../vue.js"></script> </head> <body> <div id="root"> <div> <input type="text" v-model="todoValue"/> <button @click="handleBtnClick">提交</button> </div> <ul> <!-- 通过 v-bind,将父组件的 item 值传给子组件 --> <!-- 在父组件里创建子组件的同时,就能监听子组件发出的事件 一旦子组件被触发了,就会执行父组件的 handleItemDelete --> <todo-item :content="item" :index="index" v-for="(item, index) in list" @delete="handleItemDelete"> </todo-item> </ul> </div> <script> // 定义了一个名为TodoItem的组件,new Vue的子组件 // 在代码中,通过使用</todo-item>标签,用到了该子组件 var TodoItem = { // 该组件接受内容和索引作为属性 // 并在列表中显示 props: ['content', 'index'], // v-on:click 简写成 @click template: "<li @click='handleItemClick'>{{content}}</li>", methods: { handleItemClick: function () { // alert("JavaEdge 666") // 点击子组件时,子组件对外发出事件 // this.$emit("delete") // 点击子组件时,子组件对外发出事件,还顺带一个参数也发出去,那么监听事件的handleItemDelete就能拿到 index this.$emit("delete",this.index) } } } // new Vue属于全局组件 // 在本 demo 中,也属于最外层的父组件 // 整个root div 区域也就是该父组件的模板 // 定义了一个名为app的Vue实例 var app = new Vue({ el: "#root", components: { TodoItem: TodoItem }, // 该实例具有 data: { // 一个todoValue数据属性 todoValue: "", // 一个list数组属性 list: [] }, methods: { // 当用户点击提交按钮时 handleBtnClick: function () { // 应用程序将todoValue添加到list数组 this.list.push(this.todoValue) // 并将todoValue重置为空字符串 this.todoValue = "" }, handleItemDelete: function (index) { // alert("delete") // this.list = []; // alert(index) /** * 这段代码的作用是从列表中删除指定索引的元素: * 1. list是一个列表对象。 * 2. splice()是一个JavaScript数组方法,它用于在数组中添加或删除元素 * 第一个参数:要删除或添加元素的起始索引 * 第二个参数:要删除的元素数量 * 3. 因此,这行代码将从list列表中删除指定索引的元素 */ this.list.splice(index, 1) } } }) </script> </body> </html>
总结
为精简代码,注意 v-bind:index="index"
可简写成 :index="index"
。