企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
# 组件基础 ## 基础示例 一个Vue组件示例: ~~~ // 创建一个Vue应用 const app = Vue.createApp({}) // 定义一个叫`button-counter`的全局组件 app.component('button-counter', { data() { return { count: 0 } }, template: ` <button @click="count++"> 你点击我 {{ count }} 次. </button>` }) ~~~ >[info]提示 这是一个简单的组件示例。在典型的 Vue应用程序中,组件一般都是在独立的文件中。你可以通过[单文件Vue组件](https://v3.vuejs.org/guide/single-file-component.html)了解更多内容。 组件可以根据组件名在一个实例里复用,这个示例中就是`button-counter`。可以当成一个定制组件在根节点下使用: ~~~html <div id="components-demo"> <button-counter></button-counter> </div> ~~~ ~~~ app.mount('#components-demo') ~~~ 因为组件是可复用的实例,它可以使用根实例相同的选项属性,例如`data`,`computed`,`watch`,`methods`和生命周期勾子。 ## 可复用组件 组件可以根据你的需要多次复用: ~~~html <!DOCTYPE html> <html lang="en-US"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>resuing component</title> <script src="https://unpkg.com/vue@next"></script> </head> <body> <div id="app"> <button-counter></button-counter> <button-counter></button-counter> <button-counter></button-counter> <button-counter></button-counter> <button-counter></button-counter> <button-counter></button-counter> </div> </body> <script type="text/javascript"> const data = { data() { return { } }, methods: { } } const app = Vue.createApp(data) app.component('button-counter',{ data() { return { count: 0 } }, template: ` <button @click="count++">你点击了我{{ count }} 次</button> ` }) app.mount("#app") </script> </html> ~~~ 注意每次点击按钮,每个按钮只增加自己的点击次数,count是分离的。这是因为每次使用一个组件,就相当于创建一个新的实例。 ## 组件组织 通用作法是把组件组织成嵌套的组件树(组件套组件): ![](https://box.kancloud.cn/b5c08269dfc26ae6d7db3801e9efd296_1406x544.png) 例如,一个应用你可能需要一个页对组件、导航栏组件、内容组件,每个组件内部又包含导航链接、博客列表组件等。 <br /> 使用组件前必须在Vue里先注册。有`全局`和`本地`两种注册方式。刚才我们是使用`component选项在当前实例中注册了一个全局组件。在应用内部可以反复调用。 如果你对组件注册仍然有点疑惑,建议阅读[组件注册](https://v3.vuejs.org/guide/component-registration.html)章节。 ## 使用props向子组件传递数据 前面我们提到一个博客列表的组件,但我们需要传递博客标题、内容等我们想展示的字段给这个组件。因此`props`粉墨登场。 `props`是一个可以注册到组件的定制属性, 是一个数组,可以接收多个参数。比如我们要传递博客标题到子组件,我们只需要把`title`包含在`props`属性列表里即可: ~~~ const app = Vue.createApp({}) app.component('blog-post', { props: ['title'], template: `<h4>{{ title }}</h4>` }) app.mount('#blog-post-demo') ~~~ 一旦你把一个值传递给子组件,这个值就是子组件的一个属性,这个属性可以在子组件模板里同其他属性一样访问。 一个组件可以有多个`props`,默认可以传递任何值给`props`。 一旦`props`注册成功,你就可以像定制属性一样传递数据给它了,示例: ~~~html <!DOCTYPE html> <html lang="en-US"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>resuing component</title> <script src="https://unpkg.com/vue@next"></script> </head> <body> <div id="app"> <blog v-for="(item,index) in blogs" :key="index" :title="item.title" :date="item.date" > </blog> </div> </body> <script type="text/javascript"> const data = { data() { return { blogs: [ {"title":"这是博客标题1",date:"2021年1月1日"}, {"title":"这是博客标题2",date:"2021年1月2日"}, {"title":"这是博客标题3",date:"2021年1月3日"}, {"title":"这是博客标题4",date:"2021年1月4日"}, {"title":"这是博客标题5",date:"2021年1月5日"} ] } } } const app = Vue.createApp(data) app.component('blog',{ props: ['title','date'], template: ` <div class="title">{{ title }}</div> <div class="date">发表于:{{ date }}</div> <hr /> ` }) app.mount("#app") </script> <style type="text/css"> .title {font-size:25px;font-weight:bold;} .date {font-size:18px;color:#cccccc;padding-left:10px;} </style> </html> ~~~ 运行结果如下: ![](https://img.kancloud.cn/6d/45/6d453796c57b85ffb30fd5f56126de7f_519x442.png) 上面示例展示了,通过`v-bind`(缩写`:`)传递动态数据给子组件。 如果还是有点不理解,建议阅读[pros](https://v3.vuejs.org/guide/component-props.html)章节。 ## 监听子组件事件 上面我们定义了一个`blog`的组件,有些功能需要与父容器进行通讯。不用官网的例子了吧,我们来实现一个删除博客的功能。子组件调用父容器的`methods`,要用`$emit`发射出来,组件实例的地方使用属性接收,就可以调用父容器任何`methods`了: ~~~html <!DOCTYPE html> <html lang="en-US"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>resuing component</title> <script src="https://unpkg.com/vue@next"></script> </head> <body> <div id="app"> <blog v-for="(item,index) in blogs" :key="index" :title="item.title" :date="item.date" :index="index" @del="delblog($event)" > </blog> </div> </body> <script type="text/javascript"> const data_and_methods = { data() { return { blogs: [ {"title":"这是博客标题1",date:"2021年1月1日"}, {"title":"这是博客标题2",date:"2021年1月2日"}, {"title":"这是博客标题3",date:"2021年1月3日"}, {"title":"这是博客标题4",date:"2021年1月4日"}, {"title":"这是博客标题5",date:"2021年1月5日"} ] } }, methods: { delblog(item_index) { if(confirm("确实要删除这条博客吗?")){ this.blogs.splice(item_index,1) } } } } const app = Vue.createApp(data_and_methods) app.component('blog',{ props: ['title','date','index'], template: ` <div class="title">{{ title }}</div> <div class="date">发表于:{{ date }}</div> <button @click="$emit('del',index)">删除</button> <hr /> ` }) app.mount("#app") </script> <style type="text/css"> .title {font-size:25px;font-weight:bold;} .date {font-size:18px;color:#cccccc;padding-left:10px;} </style> </html> ~~~ 这个示例把后面的参数传递也包含了,后面内容不译了。 ### 组件中使用`v-model` 记住,定制事件也可以用来创建一个与`v-model`一起使用的定制输入控件: ~~~html <input v-model="searchText" /> ~~~ 一样样的: ~~~html <input :value="searchText" @input="searchText = $event.target.value" /> ~~~ 使用该组件时,`v-model`被替换成这样: ~~~html <custom-input :model-value="searchText" @update:model-value="searchText = $event" ></custom-input> ~~~ >[waring]警告 注意到这里`model-value`使用了短横线命名法是因为我们使用在内联 DOM模板中。你可以在[DOM模板解析警告](https://v3.vuejs.org/guide/component-basics.html#dom-template-parsing-caveats)章节中发现短横线和驼峰命名法详细解释。 要想让这个组件正常使用,组件内的`<input>`必须要: * 绑定`value`属性到`modelValue`props * 在input上要把新的录入内容使用`update:modelValue`发射出来 看一个完整的例子吧: ~~~html <!DOCTYPE html> <html lang="en-US"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>component v-model</title> <script src="https://unpkg.com/vue@next"></script> </head> <body> <div id="app"> <custom-input v-model="searchText"></custom-input> <p>{{ searchText }}</p> </div> </body> <script type="text/javascript"> const data_and_methods = { data() { return { searchText: '' } }, methods: { } } const app = Vue.createApp(data_and_methods) app.component('custom-input',{ props: ['modelValue'], emits: ['update:modelValue'], template: ` <input :value="modelValue" @input="$emit('update:modelValue',$event.target.value)" /> ` }) app.mount("#app") </script> </html> ~~~ 另一种实现组件`v-model`的方法是使用计算属性,为计算属性定义一个`get`和`set`。`get`方法返回`modelValue`属性,`set`方法负责发射方法。 ~~~ app.component('custom-input', { props: ['modelValue'], emits: ['update:modelValue'], template: ` <input v-model="value"> `, computed: { value: { get() { return this.modelValue }, set(value) { this.$emit('update:modelValue', value) } } } }) ~~~ 看完上面仍有疑惑,读下[定制事件](https://v3.vuejs.org/guide/component-custom-events.html)吧! ## 使用插槽实现内容分布 像HTML标签一样,我们有时候需要传递内容到组件内部: ~~~html <alert-box> 出现了一些错误 </alert-box> ~~~ 自定义组件内部的自定义内容像是有一个占位符,从组件实例里传递进来,替换占位符的内容。这就是插槽`<slot>`。 ~~~ app.component('alert-box', { template: ` <div class="demo-alert-box"> <strong>Error!</strong> <slot></slot> </div> ` }) ~~~ 如有困惑,请阅读[slot](https://v3.vuejs.org/guide/component-slots.html)章节。 插槽部分让我们稍微延伸下,制作一个APP的头部(使用了具名插槽): ![](https://img.kancloud.cn/f7/4d/f74d740312692fe4aa6f8dc7b6ee9311_475x669.png) ~~~html <!DOCTYPE html> <html lang="en-US"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>component slot</title> <link href="https://cdn.bootcss.com/font-awesome/5.13.0/css/all.css" rel="stylesheet"> <script src="https://unpkg.com/vue@next"></script> </head> <body> <div id="app"> <custom-header> <template v-slot:leftcont><i class="fa fa-reply"></i></template> <template v-slot:title>博客</template> <template v-slot:rightcont><i class="fa fa-plus"></i></template> </custom-header> </div> </body> <script type="text/javascript"> const data = { data() { return { } }, methods: { } } const app = Vue.createApp(data) app.component('custom-header',{ template: ` <div class="cust-container"> <div class="cust-left"><slot name="leftcont"></slot></div> <div class="cust-mid"><slot name="title"></slot></div> <div class="cust-right"><slot name="rightcont"></slot></div> </div> ` }) app.mount("#app") </script> <style type="text/css"> body {margin:0px;padding:0px;color:#fff;} .cust-container {display:flex;display: -webkit-flex;flex-direction:row;justify-content:space-between;background: gold;transition: 600ms ease-in-out;box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); border-radius: 5px; } .cust-container div {padding:10px;} .cust-mid {font-weight:bold;font-size:18px;} </style> </html> ~~~ ## 动态组件 有时为不同组件设置一个动态开关是有用处的,类似tab页切换: ~~~html <html lang="en-US"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>vue 动态组件</title> <script src="https://unpkg.com/vue@next"></script> </head> <body> <div id="app" class="demo"> <button v-for="tab in tabs" :key="tab" :class="['tab-button',{active:currentTab === tab}]" @click="currentTab = tab" >{{ tab }}</button> <component :is="currentTabComponent" class="tab" ></component> </div> <script type="text/javascript"> const app = Vue.createApp({ data() { return { currentTab: 'Home', tabs: ['Home','Posts','Archive'] } }, computed: { currentTabComponent(){ return 'tab-' + this.currentTab.toLowerCase() } } }) app.component('tab-home',{ template: ` <div class="demo-tab">Home component</div> ` }) app.component('tab-posts',{ template: ` <div class="demo-tab">Posts component</div> ` }) app.component('tab-archive',{ template: ` <div class="demo-tab">Archive component</div> ` }) app.mount('#app') </script> <style type="text/css"> .demo {font-family: sans-serif;border: 1px solid #eee;border-radius: 2px;padding: 20px 30px;margin-top: 1em;margin-bottom: 40px;user-select: none;overflow-x: auto;} .tab-button {padding: 6px 10px;border-top-left-radius: 3px;border-top-right-radius: 3px;border: 1px solid #ccc;cursor: pointer;background: #f0f0f0;margin-bottom: -1px;margin-right: -1px;} .tab-button:hover {background: #e0e0e0;} .tab-button.active {background: #e0e0e0;} .demo-tab {border: 1px solid #ccc;padding: 10px;} </style> </body> </html> ~~~ ## DOM模板解析提示 有些HTML标签如`<ul>`,`<table>`,`<ol>`,`<select>`对于他内部的元素有严格的限制,而有些元素如,`<li>`,`<tr>`,`<option>`等只能出现在某些特定的元素内。 在这些有严格限制的元素同组件一起使用时,会导致一些问题: ~~~html <table> <blog-post-row></blog-post-row> </table> ~~~ 这个定制组件`<blog-post-row>`在渲染时因错误提升到外部。我们有一个`v-is`指令变通方法: ~~~html <table> <tr v-is="'blog-post-row'"></tr> </table> ~~~ >[warning] `v-is`被当作JavaScript表达式,所以组件名需要放到引号内: >~~~html ><!-- 不正确,不会渲染任何内容 --> ><tr v-is="blog-post-row"></tr> ><!-- 正确 --> ><tr v-is="'blog-post-row'"></tr> >~~~ > 同时,HTML标签不区分大小写,所以浏览器会将大写解释为小写。这意味着你使用内联DOM模板,`props`和处理事件参数用驼峰命名会与短横线命相对就: ~~~js // JavaScript中驼峰命名 app.component('blog-post', { props: ['postTitle'], template: ` <h3>{{ postTitle }}</h3> ` }) ~~~ ~~~html <!-- HTML中使用短横线 --> <blog-post post-title="hello!"></blog-post> ~~~ 需要注意的是,使用字符串模板时以下几种情况下这些限制是不存在的: * 字符串模板 (e.g.`template: '...'`) * [独立文件 (`.vue`) 组件](https://v3.vuejs.org/guide/single-file-component.html) * `<script type="text/x-template">`