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是分离的。这是因为每次使用一个组件,就相当于创建一个新的实例。
组件组织
通用作法是把组件组织成嵌套的组件树(组件套组件):
例如,一个应用你可能需要一个页对组件、导航栏组件、内容组件,每个组件内部又包含导航链接、博客列表组件等。
使用组件前必须在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>
运行结果如下:
上面示例展示了,通过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
属性到modelValue
props - 在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
的方法是使用计算属性,为计算属性定义一个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)
}
}
}
})
看完上面仍有疑惑,读下定制事件吧!
使用插槽实现内容分布
像HTML标签一样,我们有时候需要传递内容到组件内部:
<alert-box>
出现了一些错误
</alert-box>
自定义组件内部的自定义内容像是有一个占位符,从组件实例里传递进来,替换占位符的内容。这就是插槽<slot>
。
app.component('alert-box', {
template: `
<div class="demo-alert-box">
<strong>Error!</strong>
<slot></slot>
</div>
`
})
如有困惑,请阅读slot章节。
插槽部分让我们稍微延伸下,制作一个APP的头部(使用了具名插槽):
<!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>
需要注意的是,使用字符串模板时以下几种情况下这些限制是不存在的:
- 字符串模板 (e.g.
template: '...'
) - 独立文件 (
.vue
) 组件 <script type="text/x-template">
本节示例代码:
https://github.com/zhouyu629/vue3-demo/tree/main/compent-basic