vue3之组件
组件
概念:html、css、js的集合体,为该集合体命名,用该名字复用html、css与js组成的集合体(复用性)。
组件分类:
- 根组件:new vue()生成的组件
- 局部组件:组件名 = {},{}内部采用的是vue语法
- 全局组件:Vue.component('组件名', {}),{}内部采用的是vue语法
全局组件使用范围:可以在页面中任何位置使用
局部组件使用范围:只能在定义它的el中使用,不能再其他位置使用,否则就无法生效
组件的特点:
- 每一个组件都是一个vue实例
- 根组件是最顶层父组件,局部与全局组件作为子组件,也可作为其他局部或全局组件的父组件。
- 每个组件均具有自身的模板template,根组件的模板就是挂载点
- 每个组件模板只能拥有一个根标签
- 子组件的数据具有作用域,以达到组件的复用
- 局部组件必须注册后才能使用,全局组件不需要注册,提倡使用局部组件
- 局部、全局、根组件都是vue实例,一个实例对应一套html、css、js结构,实例就是组件
根组件
<div id="app">
<h1>{{ msg }}</h1>
</div>
<script type="text/javascript">
// 通过new Vue创建的实例就是根组件(实例与组件一一对应,一个实例就是一个组件)
// 每个组件组件均拥有模板,template
var app = new Vue({
// 根组件的模板就是挂载点
el: "#app",
data : {
msg: "根组件"
},
// 模板: 由""包裹的html代码块,出现在组件的内部,赋值给组件的$template变量
// 显式书写模块,就会替换挂载点,但根组件必须拥有挂载点
template: "<div>显式模板</div>"
})
// app.$template
</script>
总结:根组件,可以不明确template,template默认采用挂载点页面结构;如果设置的template,挂载点内部的内容无效。
局部组件
局部组件需要在根组件中注册才能使用。
步骤:声明组件、注册组件、渲染组件
<div id="app">
<local-tag></local-tag>
<local-tag></local-tag>
</div>
<script>
var localTag = {
data () {
return {
count: 0
}
},
template: '<button @click="btnAction">局部{{ count }}</button>',
methods: {
btnAction () {
this.count ++
}
}
}
new Vue({
el: "#app",
components: {
'local-tag': localTag
}
})
</script>
全局组件
<div id="app">
<global-tag></global-tag>
<global-tag></global-tag>
</div>
<script>
Vue.component('global-tag', {
data () {
return {
count: 0
}
},
template: '<button @click="btnAction">全局{{ count }}</button>',
methods: {
btnAction () {
this.count ++
}
}
})
new Vue({
el: "#app"
})
</script>
组件的注册
组件名
在注册一个组件的时候,我们始终需要给它一个名字。比如在全局注册的时候我们已经看到了:
Vue.component('my-component-name', { /* ... */ })
该组件名就是 Vue.component
的第一个参数。
你给予组件的名字可能依赖于你打算拿它来做什么。当直接在 DOM 中使用一个组件 (而不是在字符串模板或单文件组件) 的时候,我们强烈推荐遵循 W3C 规范中的自定义组件名 (字母全小写且必须包含一个连字符)。这会帮助你避免和当前以及未来的 HTML 元素相冲突。
你可以在风格指南中查阅到关于组件名的其它建议。
组件名大小写
定义组件名的方式有两种:
使用 kebab-case
Vue.component('my-component-name', { /* ... */ })
当使用 kebab-case (短横线分隔命名) 定义一个组件时,你也必须在引用这个自定义元素时使用 kebab-case,例如 ``。
使用 PascalCase
Vue.component('MyComponentName', { /* ... */ })
当使用 PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。也就是说 和
都是可接受的。注意,尽管如此,直接在 DOM (即非字符串的模板) 中使用时只有 kebab-case 是有效的。
全局注册
到目前为止,我们只用过 Vue.component
来创建组件:
Vue.component('my-component-name', {
// ... 选项 ...
})
这些组件是全局注册的。也就是说它们在注册之后可以用在任何新创建的 Vue 根实例 (new Vue
) 的模板中。比如:
Vue.component('component-a', { /* ... */ })
Vue.component('component-b', { /* ... */ })
Vue.component('component-c', { /* ... */ })
new Vue({ el: '#app' })
<div id="app">
<component-a></component-a>
<component-b></component-b>
<component-c></component-c>
</div>
在所有子组件中也是如此,也就是说这三个组件在各自内部也都可以相互使用。
局部注册
全局注册往往是不够理想的。比如,如果你使用一个像 webpack 这样的构建系统,全局注册所有的组件意味着即便你已经不再使用一个组件了,它仍然会被包含在你最终的构建结果中。这造成了用户下载的 JavaScript 的无谓的增加。
在这些情况下,你可以通过一个普通的 JavaScript 对象来定义组件:
var ComponentA = { /* ... */ }
var ComponentB = { /* ... */ }
var ComponentC = { /* ... */ }
然后在 components
选项中定义你想要使用的组件:
new Vue({
el: '#app',
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
})
对于 components
对象中的每个属性来说,其属性名就是自定义元素的名字,其属性值就是这个组件的选项对象。
注意局部注册的组件在其子组件中*不可用*。例如,如果你希望 ComponentA
在 ComponentB
中可用,则你需要这样写:
var ComponentA = { /* ... */ }
var ComponentB = {
components: {
'component-a': ComponentA
},
// ...
}
或者如果你通过 Babel 和 webpack 使用 ES2015 模块,那么代码看起来更像:
import ComponentA from './ComponentA.vue'
export default {
components: {
ComponentA
},
// ...
}
注意在 ES2015+ 中,在对象中放一个类似 ComponentA
的变量名其实是 ComponentA: ComponentA
的缩写,即这个变量名同时是:
- 用在模板中的自定义元素的名称
- 包含了这个组件选项的变量名
模块系统
如果你没有通过 import
/require
使用一个模块系统,也许可以暂且跳过这个章节。如果你使用了,那么我们会为你提供一些特殊的使用说明和注意事项。
在模块系统中局部注册
如果你还在阅读,说明你使用了诸如 Babel 和 webpack 的模块系统。在这些情况下,我们推荐创建一个 components
目录,并将每个组件放置在其各自的文件中。
然后你需要在局部注册之前导入每个你想使用的组件。例如,在一个假设的 ComponentB.js
或 ComponentB.vue
文件中:
import ComponentA from './ComponentA'
import ComponentC from './ComponentC'
export default {
components: {
ComponentA,
ComponentC
},
// ...
}
现在 ComponentA
和 ComponentC
都可以在 ComponentB
的模板中使用了。
础组件的自动化全局注册
可能你的许多组件只是包裹了一个输入框或按钮之类的元素,是相对通用的。我们有时候会把它们称为基础组件,它们会在各个组件中被频繁的用到。
所以会导致很多组件里都会有一个包含基础组件的长列表:
import BaseButton from './BaseButton.vue'
import BaseIcon from './BaseIcon.vue'
import BaseInput from './BaseInput.vue'
export default {
components: {
BaseButton,
BaseIcon,
BaseInput
}
}
而只是用于模板中的一小部分:
<BaseInput
v-model="searchText"
@keydown.enter="search"
/>
<BaseButton @click="search">
<BaseIcon name="search"/>
</BaseButton>
幸好如果你使用了 webpack (或在内部使用了 webpack 的 Vue CLI 3+),那么就可以使用 require.context
只全局注册这些非常通用的基础组件。这里有一份可以让你在应用入口文件 (比如 src/main.js
) 中全局导入基础组件的示例代码:
import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'
const requireComponent = require.context(
// 其组件目录的相对路径
'./components',
// 是否查询其子目录
false,
// 匹配基础组件文件名的正则表达式
/Base[A-Z]\w+\.(vue|js)$/
)
requireComponent.keys().forEach(fileName => {
// 获取组件配置
const componentConfig = requireComponent(fileName)
// 获取组件的 PascalCase 命名
const componentName = upperFirst(
camelCase(
// 获取和目录深度无关的文件名
fileName
.split('/')
.pop()
.replace(/\.\w+$/, '')
)
)
// 全局注册组件
Vue.component(
componentName,
// 如果这个组件选项是通过 `export default` 导出的,
// 那么就会优先使用 `.default`,
// 否则回退到使用模块的根。
componentConfig.default || componentConfig
)
})
记住全局注册的行为必须在根 Vue 实例 (通过 new Vue
) 创建之前发生。这里有一个真实项目情景下的示例。
组件注册实例:
<head>
<meta charset="UTF-8">
<title>子组件</title>
<style>
body, h2 {
margin: 0;
}
.wrap {
width: 880px;
margin: 0 auto;
}
.wrap:after {
content: '';
display: block;
clear: both;
}
.box {
width: 200px;
border-radius: 10px;
overflow: hidden;
background-color: #eee;
float: left;
margin: 10px;
}
.box img {
width: 100%;
}
.box h2 {
text-align: center;
font-weight: normal;
font-size: 20px;
}
</style>
</head>
<body>
<div id="app">
<div class="wrap">
<local-tag></local-tag>
<local-tag></local-tag>
<local-tag></local-tag>
<local-tag></local-tag>
<global-tag></global-tag>
<global-tag></global-tag>
<global-tag></global-tag>
<global-tag></global-tag>
</div>
</div>
</body>
<script src="js/vue.js"></script>
<script>
// 声明局部组件:局部组件要在其父组件中注册才能使用
// 1、声明组件 2、注册组件 3、渲染组件 => 全局组件注册
let localTag = {
template: `
<div class="box" @click="fn">
<img src="img/001.jpg" alt="">
<h2>图片1</h2>
</div>
`,
methods: {
fn() {
console.log(this)
}
}
};
Vue.component('global-tag', {//全局注册(声明并注册)
template: `
<div class="box" @click="fn">
<img src="img/002.jpg" alt="">
<h2>图片2</h2>
</div>
`,
methods: {
fn() {
console.log(this)
}
}
});
new Vue({
el: '#app',
data: {},
components: { // 局部组件的注册
localTag:localTag,
}
})
</script>
组件化
局部或全局组件,一个组件可能会被复用多次,每个组件都应该有自己独立的变量名称空间,当一个组件被多次复用时如果我们把组件的数据写死那么每次复用就没有区别了,如同python中类实例化对象,有些属性值是对象自己的,需要在对象实例化的时候传入类中。
例子:页面中显示多张图片,当点击每张图片的时候显示该图片被点击的次数。
为了解决每点击一张照片,其余照片也同步计数的问题,这里组件化的处理是定义一个函数将计数结果返回出来,由于函数有自己独立的名称空间,在组件实例化时,实例化组件也有独立的变量空间来存储自己的计数,从而解决了数据之间相互影响的问题。
<head>
<meta charset="UTF-8">
<title>组件化</title>
<style>
body, h2 {
margin: 0;
}
.wrap {
width: 880px;
margin: 0 auto;
}
.wrap:after {
content: '';
display: block;
clear: both;
}
.box {
width: 200px;
border-radius: 10px;
overflow: hidden;
background-color: #eee;
float: left;
margin: 10px;
}
.box img {
width: 100%;
}
.box h2 {
text-align: center;
font-weight: normal;
font-size: 20px;
}
</style>
</head>
<body>
<div id="app">
<div class="wrap">
<local-tag></local-tag>
<local-tag></local-tag>
<local-tag></local-tag>
<local-tag></local-tag>
</div>
</div>
</body>
<script src="js/vue.js"></script>
<script>
let localTag = {
template: `
<div class="box" @click="fn">
<img src="img/001.jpg" alt="">
<h2>图片被点击了{{ count }}下</h2>
</div>
`,
data() { // 局部或全局组件,一个组件可能会被复用多次,每个组件都应该有自己独立的变量名称空间
return {
count: 0,
}
}, // 数据需要组件化,作为方法的返回值(方法执行后会产生一个局部作用域)
methods: {
fn() {
console.log(this);
this.count++;
}
}
};
new Vue({
el: '#app',
data: {},
components: {
localTag,
}
});
组件间数据的传递
父组件传递数据给子组件
-
通过绑定属性的方式进行数据传递
1)子组件可以通过props自定义组件属性(采用反射机制,需要填写字符串,但是使用时可以直接作为变量)
2)子组件会在父组件中渲染,渲染时,将父组件的变量绑定给子组件的自定义属性,可以将变量值传递给子组件
<div id="app">
<global-tag :sup_data1='sup_data1' :supData2='sup_data2'></global-tag>
//将父组件的变量绑定给子组件的自定义属性
</div>
<script type="text/javascript">
Vue.component('global-tag', {
props:['sup_data1', 'supdata2'],//自定义字符串
template: '<div>{{ sup_data1 }} {{ supdata2 }}</div>'
})
new Vue({
el: '#app',
data: {
sup_data1: '数据1',
sup_data2: '数据2'
}
})
</script>
父组件传递数据给子组件例子
<head>
<meta charset="UTF-8">
<title>父传子</title>
<style>
body, h2 {
margin: 0;
}
.wrap {
width: 880px;
margin: 0 auto;
}
.wrap:after {
content: '';
display: block;
clear: both;
}
.box {
width: 200px;
border-radius: 10px;
overflow: hidden;
background-color: #eee;
float: left;
margin: 10px;
}
.box img {
width: 200px;
height: 240px;
}
.box h2 {
text-align: center;
font-weight: normal;
font-size: 20px;
}
</style>
</head>
<body>
<div id="app">
<div class="wrap">
<local-tag v-for="dog in dogs" :dog="dog" def="12345" :xyz="dog.name"></local-tag>
</div> <!-- 将父组件的变量绑定给子组件的自定义属性 -->
</div>
</body>
<script src="js/vue.js"></script>
<script>
let dogs = [
{
name: '二哈1号',
img: 'img/100.jpg',
},
{
name: '二哈2号',
img: 'img/200.jpg',
},
{
name: '二哈3号',
img: 'img/300.jpg',
},
{
name: '二哈4号',
img: 'img/400.jpg',
},
{
name: '二哈1号',
img: 'img/100.jpg',
},
{
name: '二哈2号',
img: 'img/200.jpg',
},
{
name: '二哈3号',
img: 'img/300.jpg',
},
{
name: '二哈4号',
img: 'img/400.jpg',
}
];
// 1)子组件可以通过props自定义组件属性(采用反射机制,需要填写字符串,但是使用时可以直接作为变量)
// 2)子组件会在父组件中渲染,渲染时,将父组件的变量绑定给子组件的自定义属性,将可以将变量值传递给子组件
let localTag = {
props: ['dog', 'def', 'xyz'],//自定义字符串
template: `
<div class="box" @click="fn">
<img :src="dog.img" alt="">
<h2>捶{{ dog.name }}{{ count}}下</h2>
<!--<h3>{{ abc }}</h3>-->
<!--<h3>{{ def }}</h3>-->
<!--<h3>{{ xyz }}</h3>-->
</div>
`,
data() {
return {
count: 0,
}
},
methods: {
fn() {
console.log(this.dog);
this.count++;
}
}
};
new Vue({
el: '#app',
data: {
dogs,
},
components: {
localTag,
}
});
</script>
子组件传递数据给父组件
-
通过发送事件请求的方式进行数据传递
自定义子组件标签的事件,子组件在父组件中渲染并绑定事件方法,所以事件方法由父组件来实现。子组件通过this.$emit('自定义事件名',‘触发事件的回调参数们’),子组件触发自定义事件,携带出子组件的内容,在父组件中实现自定义事件的方法,拿到子组件传递给父组件的内容。
<div id="app">
<global-tag @send_action='receiveAction'></global-tag>
</div>
<script type="text/javascript">
Vue.component('global-tag', {
data () {
return {
sub_data1: "数据1",
sub_data2: '数据2'
}
},
template: '<div @click="clickAction">发生</div>',
methods: {
clickAction () {
this.$emit('send_action', this.sub_data1, this.sub_data2)
//vm.$emit( eventName, […args] )触发当前实例上的事件。附加参数都会传给监听器回调。
}
}
})
new Vue({
el: '#app',
methods: {
receiveAction (v1, v2) {
console.log(v1, v2)
}
}
})
</script>
子组件传数据给父组件例子
<script>
let tag = {
template: `
<div>
<input type="text" v-model="t1">
<input type="text" v-model="t2">
<button @click="changeTitle">修改标题</button>
</div>
`,
data() {
return {
t1: '',
t2: '',
}
},
methods: {
changeTitle() {
if (this.t1 && this.t2) {
// console.log(this.t1, this.t2);
this.$emit('action', this.t1, this.t2);
this.t1 = '';
this.t2 = '';
}
}
}
};
let tag2 = {
template: `
<div>
主标题内容:<input type="text" v-model="t1" @input="t1Fn">
子标题内容:<input type="text" v-model="t2">
</div>
`,
data() {
return {
t1: '',
t2: '',
}
},
methods: {
t1Fn() {
this.$emit('h1a', this.t1);
}
},
watch: {
t2 () {
this.$emit('h3a', this.t2);
}
}
};
new Vue({
el: '#app',
data: {
h1: '主标题',
h3: '子标题'
},
components: {
tag,
tag2,
},
methods: {
actionFn(a, b, c) {
// console.log('触发了', a, b, c);
this.h1 = a;
this.h3 = b;
},
aFn1(a) {
if (!a) {
this.h1 = '主标题';
return;
}
this.h1 = a;
},
aFn3(a) {
if (!a) {
this.h3 = '子标题';
return;
}
this.h3 = a;
},
}
})
</script>
父子组件实现todoList
<div id="app">
<div>
<input type="text" v-model="val">
<button type="button" @click="submitMsg">提交</button>
</div>
<ul>
<!-- <li v-for="(v, i) in list" :key="i" @click="removeMsg(i)">{{ v }}</li> -->
<todo-list v-for="(v, i) in list" :key="i" :v="v" :i="i" @delect_action="delect_action"></todo-list>
</ul>
</div>
<script type="text/javascript">
Vue.component("todo-list", {
template: "<li @click='delect_action'><span>第{{ i + 1 }}条: </span><span>{{ v }}</span></li>",
props: ['v', 'i'],
methods: {
delect_action () {
this.$emit("delect_action", this.i)
}
}
})
new Vue({
el: "#app",
data: {
val: "",
list: []
},
methods: {
submitMsg () {
// 往list中添加input框中的value
if (this.val) {
this.list.push(this.val);
this.val = ""
}
},
delect_action(index) {
this.list.splice(index, 1)
}
}
})
</script>