Vue3手册译稿 - 基础 - 组件基础

基础示例

一个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组件了解更多内容。

组件可以根据组件名在一个实例里复用,这个示例中就是button-counter。可以当成一个定制组件在根节点下使用:

<div id="components-demo">
  <button-counter></button-counter>
</div>
app.mount('#components-demo')

因为组件是可复用的实例,它可以使用根实例相同的选项属性,例如data,computed,watch,methods和生命周期勾子。

可复用组件

组件可以根据你的需要多次复用:

<!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是分离的。这是因为每次使用一个组件,就相当于创建一个新的实例。

组件组织

通用作法是把组件组织成嵌套的组件树(组件套组件):
image
例如,一个应用你可能需要一个页对组件、导航栏组件、内容组件,每个组件内部又包含导航链接、博客列表组件等。


使用组件前必须在Vue里先注册。有全局本地两种注册方式。刚才我们是使用`component选项在当前实例中注册了一个全局组件。在应用内部可以反复调用。
如果你对组件注册仍然有点疑惑,建议阅读组件注册章节。

使用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注册成功,你就可以像定制属性一样传递数据给它了,示例:

<!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>

运行结果如下:
image

上面示例展示了,通过v-bind(缩写:)传递动态数据给子组件。
如果还是有点不理解,建议阅读pros章节。

监听子组件事件

上面我们定义了一个blog的组件,有些功能需要与父容器进行通讯。不用官网的例子了吧,我们来实现一个删除博客的功能。子组件调用父容器的methods,要用$emit发射出来,组件实例的地方使用属性接收,就可以调用父容器任何methods了:

<!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一起使用的定制输入控件:

<input v-model="searchText" />

一样样的:

<input :value="searchText" @input="searchText = $event.target.value" />

使用该组件时,v-model被替换成这样:

<custom-input
  :model-value="searchText"
  @update:model-value="searchText = $event"
></custom-input>

[waring]警告
注意到这里model-value使用了短横线命名法是因为我们使用在内联 DOM模板中。你可以在DOM模板解析警告章节中发现短横线和驼峰命名法详细解释。
要想让这个组件正常使用,组件内的<input>必须要:

  • 绑定value属性到modelValueprops
  • 在input上要把新的录入内容使用update:modelValue发射出来
    看一个完整的例子吧:
<!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的方法是使用计算属性,为计算属性定义一个getsetget方法返回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)
      }
    }
  }
})

看完上面仍有疑惑,读下定制事件吧!

使用插槽实现内容分布

像HTML标签一样,我们有时候需要传递内容到组件内部:

<alert-box>
  出现了一些错误
</alert-box>

自定义组件内部的自定义内容像是有一个占位符,从组件实例里传递进来,替换占位符的内容。这就是插槽<slot>

app.component('alert-box', {
  template: `
    <div class="demo-alert-box">
      <strong>Error!</strong>
      <slot></slot>
    </div>
  `
})

如有困惑,请阅读slot章节。
插槽部分让我们稍微延伸下,制作一个APP的头部(使用了具名插槽):
image

<!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 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>等只能出现在某些特定的元素内。
在这些有严格限制的元素同组件一起使用时,会导致一些问题:

<table>
  <blog-post-row></blog-post-row>
</table>

这个定制组件<blog-post-row>在渲染时因错误提升到外部。我们有一个v-is指令变通方法:

<table>
  <tr v-is="'blog-post-row'"></tr>
</table>

[warning] v-is被当作JavaScript表达式,所以组件名需要放到引号内:

<!-- 不正确,不会渲染任何内容 -->
<tr v-is="blog-post-row"></tr>
<!-- 正确 -->
<tr v-is="'blog-post-row'"></tr>

同时,HTML标签不区分大小写,所以浏览器会将大写解释为小写。这意味着你使用内联DOM模板,props和处理事件参数用驼峰命名会与短横线命相对就:

// JavaScript中驼峰命名
app.component('blog-post', {
  props: ['postTitle'],
  template: `
    <h3>{{ postTitle }}</h3>
  `
})
<!-- HTML中使用短横线 -->
<blog-post post-title="hello!"></blog-post>

需要注意的是,使用字符串模板时以下几种情况下这些限制是不存在的:

本节示例代码:

https://github.com/zhouyu629/vue3-demo/tree/main/compent-basic

posted on 2021-03-10 11:32  zhouyu  阅读(123)  评论(0编辑  收藏  举报

导航