08-Vue基础-组件详解
组件基础
1. 组件复用
组件可以全局注册,也可以局部注册。全局注册后,任何Vue实例都可以使用,如下所示:
Vue.component("my-component", {
template: `
<div>hello</div>
`
});
my-component是组件名,下一个参数是组件的一些选项参数,包含模板template,还可以有data、methods等选项。然后在父组件中调用子组件就可以了。完整示例如下:
<body>
<div id="app">
<!-- 组件调用 -->
<my-component></my-component>
</div>
<script>
// 全局组件
Vue.component("my-component", {
template: `
<select>
<option>html</option>
<option>js</option>
<option>css</option>
</select>
`
});
// 根组件
var app = new Vue({
el: "#app"
});
</script>
</body>
在Vue实例中,使用components选项可以局部注册组件,注册后的组件只有在改实例作用域下有效。组件中也可以使用 components 选项来注册组件,是组件嵌套的基础,示例如下:
<body>
<div id="app">
<!-- 组件调用 -->
<my-child></my-child>
</div>
<script>
// 局部组件
var Child = {
template: "<div>局部注册组件的内容</div>"
};
// 根组件
var app = new Vue({
el: "#app",
// 注册组件
components: {
"my-child": Child
}
});
</script>
</body>
除了template选项外,组件中还可以项Vue实例那样使用其他选项,比如data,computed,methods等,但是,在使用data时,和实例稍有区别,然后将数据return出去,例如:
<body>
<div id="app">
<!-- 组件调用 -->
<my-component></my-component>
</div>
<script>
// 全局组件
Vue.component("my-component", {
template: `
<div> {{ message }} </div>
`,
data: function() {
return {
message: "组件内容"
};
}
});
// 根组件
var app = new Vue({
el: "#app"
});
</script>
</body>
2. 父组件传值
组件不仅仅是要把模板的内容进行复用,有时候还需要组件间进行通讯。通常父组件的模板中包含子组件,父组件向子组件传递参数或数据,子组件接收到参数或数据进行不同的渲染。父组件向子组件传递参数是通过props来实现的。
在组件中,使用选项props来声明需要从父级接收的数据,props的值可以是两种,一种是字符串数组,一种是对象。如下示例:
<body>
<div id="app">
<!-- 组件调用 -->
<my-component message="来自父组件的数据"></my-component>
</div>
<script>
// 全局组件
Vue.component("my-component", {
props: ["message"],
template: `
<div> {{ message }} </div>
`
});
// 根组件
var app = new Vue({
el: "#app"
});
</script>
</body>
props中声明的数据与组件data函数return的数据主要区别就是props的来自父级,而data中的是组件自己的数据,作用域是组件自己本身,这两种数据都可以在模板和计算属性以及方法选项上使用。
注意:DOM模板中接收的props名称为驼峰命名法时,需要转为短横线分隔命名。例如:
<div id="app">
<my-component warning-text="提示消息"></my-component>
</div>
父组件传递的数据也可动态绑定,可以使用v-bind属性。例如:
<body>
<div id="app">
<input type="text" v-model="parentMessage" />
<my-component :message="parentMessage"></my-component>
</div>
<script>
// 全局组件
Vue.component("my-component", {
props: ["message"],
template: `
<div> {{ message }} </div>
`
});
// 根组件
var app = new Vue({
el: "#app",
data: {
parentMessage: ""
}
});
</script>
</body>
一般传递给组件的数据都是需要验证的,谁也不清楚传递给子组件的数据是否符合预期效果,所以对传递的数据进行校验就显得有必要了。示例如下:
<script>
// 全局组件
Vue.component("my-component", {
props: {
// 必须是数字类型
propA: Number,
// 必须是字符串或数字类型
propB: [String, Number],
// 布尔值类型,没有定义,默认为true
propC: {
type: Boolean,
default: true
},
// 数字,而且是必传项
prodD: {
type: Number,
required: true
},
// 如果是数组或对象,默认值必须是一个函数来返回
propE: {
type: Array,
default: function() {
return [];
}
},
// 自定义一个函数验证
propF: {
validator: function(value) {
return value > 10;
}
}
}
});
</script>
验证的type类型可以是:
- String
- Number
- Boolean
- Object
- Array
- Function
type也可以是一个自定义构造器,使用instanceof检测。
当prop验证失败时,开发版本Vue会在浏览器控制台会抛出一条警告。
3. 组件通信
组件关系可以分为父子组件通信、兄弟组件通信、非父子组件通讯。
3.1. 自定义事件
当子组件需要向父组件传递数据时,就要用到自定义事件。v-on指令除了监听DOM事件外,还可以用于组件之间的自定义事件。
如果你了解JavaScript的设计模式--观察者模式,一定知道dispatchEvent和addEventListener这两个方法。Vue组件也有类似的模式,子组件用$emit()来触发事件,父组件用$on()来监听子组件的事件。
父组件也可以直接在子组件上自定义标签上使用v-on来监听子组件触发的自定义事件,示例如下:
<body>
<div id="app">
<p>总数: {{ total }}</p>
<my-component
@increase="handleGetTotal"
@reduce="handleGetTotal"
></my-component>
</div>
<script>
// 全局组件
Vue.component("my-component", {
template: `
<div>
<button @click="handleIncrease">+1</button>
<button @click="handleReduce">-1</button>
</div>
`,
data: function() {
return {
counter: 0
};
},
methods: {
handleIncrease: function() {
this.counter++;
this.$emit("increase", this.counter);
},
handleReduce: function() {
this.counter--;
this.$emit("reduce", this.counter);
}
}
});
// 根组件
var app = new Vue({
el: "#app",
data: {
total: 0
},
methods: {
handleGetTotal: function(total) {
this.total = total;
}
}
});
</script>
</body>
上面的示例中,子组件有两个按钮,分别是实现加一和减一的效果,在改变组件data的counter变量的值后,通过$emit()方法再把它传递给父组件,父组件再使用v-on指令接收参数。$emit()方法的第一个参数是自定义事件的名称,如示例的increase和reduce,后边的参数是传递的数据,可以填写多个或者不填写。
3.2. 使用 v-model
在自定义组件上可以使用v-model指令:
<body>
<div id="app">
<p>总数: {{ total }}</p>
<my-component v-model="total"></my-component>
</div>
<script>
// 全局组件
Vue.component("my-component", {
template: `
<div>
<button @click="handClick">+1</button>
</div>
`,
data: function() {
return {
counter: 0
};
},
methods: {
handClick: function() {
this.counter++;
this.$emit("input", this.counter);
}
}
});
// 根组件
var app = new Vue({
el: "#app",
data: {
total: 0
}
});
</script>
</body>
v-model还可以用来创建自定义表单输入组件,进行双向数据绑定,例如:
<body>
<div id="app">
<p>总数: {{ total }}</p>
<my-component v-model="total"></my-component>
<button @click="handleReduce">-1</button>
</div>
<script>
// 全局组件
Vue.component("my-component", {
props: ["value"],
template: `
<input :value="value" @input="updateValue">
`,
methods: {
updateValue: function() {
this.$emit("input", event.target.value);
}
}
});
// 根组件
var app = new Vue({
el: "#app",
data: {
total: 0
},
methods: {
handleReduce: function() {
this.total--;
}
}
});
</script>
</body>
实现这样一个具有双向数据绑定的v-model组件要满足下面连个要求:
- 接收一个
value属性 - 在有新的
value时触发input事件
3.3. 非父子组件通信
在实际业务中,除了父子组件通讯,还可能存在非父子组件通讯的场景,非父子组件一般有两种,兄弟组件和跨多级组件。
Vue1.x中,除了$emit()方法外,还有$dispatch()和$broadcast()这两个方法。$dispatch()用于向上级派发事件,只要是它的父级,都可以在Vue实例的events选项中接收,示例代码如下:
<!-- 注意:该示例需使用Vue.js 1.x的版本 -->
<body>
<div id="app">
{{ message }}
<my-component></my-component>
</div>
<script>
// 全局组件
Vue.component("my-component", {
template: '<button @click="handleDispatch">派发事件</button>',
methods: {
handleDispatch: function() {
this.$dispatch("on-message", "来自内部组件的数据");
}
}
});
// 根组件
var app = new Vue({
el: "#app",
data: {
message: ""
},
events: {
"on-message": function(msg) {
this.message = msg;
}
}
});
</script>
</body>
这两种方法看起来很好用,但是在Vue 2.x中都弃用了。Vue 2.x中推荐使用一个空的事件总线bus,即设置一个中介。他是消息传递的中间件。
示例代码如下:
<body>
<div id="app">
{{ message }}
<component-a></component-a>
</div>
<script>
// bus 事件总线
var bus = new Vue();
// 全局组件
Vue.component("component-a", {
template: `
<button @click="handleEvent">传递事件</button>
`,
methods: {
handleEvent: function() {
bus.$emit("on-message", "来自组件component-a的内容");
}
}
});
// 根组件
var app = new Vue({
el: "#app",
data: {
message: ""
},
mounted: function() {
var _this = this;
// 在实例初始化时,监听来自 bus 实例的事件
bus.$on("on-message", function(msg) {
_this.message = msg;
});
}
});
</script>
</body>
分析案例:
首先创建了一个Vue空实例,并赋值为bus变量;然后定义了全局组件component-a;最后创了Vue根实例app,在app初始化时,也就是在生命周期mounted钩子函数里监听了来自bus事件中间件的on-message事件,而在组件component-a中,点击按钮会通过on-message事件,将消息发出去,此时,app会接收来自bus的事件,进而在回调里完成自己的业务逻辑。
这种方法巧妙而轻量地实现了任何组件间的通信,包括父子组件、兄弟组件、非父子组件、跨级组件等。如果深入使用,可以扩展bus实例,给他添加data、methods、computed等选项,这些都是可以共用的,在业务中,尤其是协同开发非常有用,因为经常需要共享一些通用的信息,比如用户登录的昵称、性别、邮箱等,甚至是用户的授权token信息。只需要在初始化时让bus获取一次,任何时间、任何组件都可以监听获取其数据,在单页面富应用(SPA)中会很实用。
当然,如果你的项目更大的话,推荐使用状态管理解决方案vuex,在进阶教程,我们会继续讨论。
除了中央事件总线bus,还有两种方法可以实现组件间通信:父链和子组件索引。
- 父链
在子组件中,使用this.$parent可以直接访问该组件的父实例或组件,父组件也可以通过this.$children访问它所有的子组件,而且可以递归向上或向下无限访问,直到根实例或最内层组件。示例代码如下:
<body>
<div id="app">
{{ message }} <br />
<component-a></component-a>
</div>
<script>
// 全局组件
Vue.component("component-a", {
template: `
<button @click="handleEvent">通过父链直接修改数据</button>
`,
methods: {
handleEvent: function() {
// 访问父链后,可以直接操作数据,比如修改
this.$parent.message = "来自子组件component-a的内容";
}
}
});
// 根组件
var app = new Vue({
el: "#app",
data: {
message: "我是父组件的内容"
}
});
</script>
</body>
注意:尽管Vue允许这样操作,但是在业务中,子组件应该尽可能地避免依赖父组件的数据,更不应该主动修改它的数据,因为这会使得父子组件紧密耦合,只看父组件,很难理解父组件的状态,因为它可能随时随地就被其他子组件修改,理想情况下是,只有组件自己修改自己的数据。推荐父子组件通信通过props和$emit。
- 子组件索引
当子组件较多时,通过this.$children来一一遍历出我们需要的一个组件是比较困难的,尤其在组件进行动态渲染时,他们的序列是不固定的。Vue提供了子组件索引的方法,用特殊的属性ref来为子组件指定一个索引名称。示例如下:
<body>
<div id="app">
<button @click="handleRef">通过<strong>ref</strong>获取子组件实例</button>
<component-a ref="comA"></component-a>
</div>
<script>
// 全局组件
Vue.component("component-a", {
template: `
<div>我是子组件</div>
`,
// 注意哦,这里data是一个方法
data: function() {
return {
message: "来自子组件的内容"
};
}
});
// 根组件
var app = new Vue({
el: "#app",
data: {},
methods: {
handleRef: function() {
// 通过 $refs 来访问指定的实例
var msg = this.$refs.comA.message;
document.write(msg);
}
}
});
</script>
</body>
注意,$ref只在组件渲染完成后才填充,并且它是响应式的,它仅仅作为一个直接访问子组件的应急方案,应当避免在模板或计算属性中使用$ref。
4. 使用 solt 分发内容
4.1. 什么是 solt
- 开发背景
我们先设计一个组件化网站布局案例,它的结构可能是这样的:
<app-content>
<!-- 一级导航菜单 -->
<menu-main></menu-main>
<!-- 二级导航菜单 -->
<menu-sub></menu-sub>
<div class="contanier">
<!-- 侧边栏 -->
<menu-left></menu-left>
<!-- 主体内容 -->
<menu-content></menu-content>
</div>
<app-footer></app-footer>
</app-content>
当需要让组件组合使用,混合组件的内容和子组件的模板时,就会用到solt,这个过程叫做内容分发(transclusion),有时也叫插槽。
- 编译的作用域
父组件模板的内容是在父组件作用域内编译,子组件模板的内容是在子组件作用域内编译。注意slot分发的内容,作用域是父组件上的。
4.2. solt 用法
- 单个
solt
在子组件内使用特殊的<slot>元素就可以为这个子组件开启一个slot插槽,在父组件模板里,插入子组件标签内的所有内容将替代子组件的<slot>标签以及它的内容。示例代码如下:
<body>
<div id="app">
<children-component>
<p>分发的内容</p>
<p>更多分发的内容</p>
</children-component>
</div>
<script>
// 全局组件
Vue.component("children-component", {
template: `
<div>
<slot>
<p>若父组件没有插入内容,我将作为默认出现</p>
</slot>
</div>
`
});
// 根组件
var app = new Vue({
el: "#app"
});
</script>
</body>
分析案例:
子组件children-component的模板内定义了一个<slot>元素,并且用一个<p>作为默认的内容,在父组件没有使用slot时,会渲染这段默认的文本;如果写入了slot,那就会替换整个<slot>。以上案例渲染结果为:
<div id="app">
<div>
<p>分发的内容</p>
<p>更多分发的内容</p>
</div>
</div>
注意,子组件<slot>内的备用内容,它的作用域是子组件本身。
- 具名 slot
给<slot>元素指定一个name后可以分发多个内容,具名slot可以与多个slot共存。如下示例:
<body>
<div id="app">
<children-component>
<h2 slot="header">标题</h2>
<p>分发的内容</p>
<p>更多分发的内容</p>
<div slot="footer">底部信息</div>
</children-component>
</div>
<script>
// 全局组件
Vue.component("children-component", {
template: `
<div class="container">
<div class="header">
<slot name="header"></slot>
</div>
<div class="main">
<slot></slot>
</div>
<div class="footer">
<slot name="footer"></slot>
</div>
</div>
`
});
// 根组件
var app = new Vue({
el: "#app"
});
</script>
</body>
子组件内声明了 3 个<slot>元素,其中在<div class="main"></div>内的<slot>没有使用name特性,它将作为默认的slot出现,父组件没有使用slot特性的元素与内容都将出现在这里。
slot通俗的理解就是占坑,在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中<slot>位置),当插槽也就是坑<slot name="mySlot">有命名时,组件标签中使用属性slot="mySlot"的元素就会替换该对应位置内容。
- 作用域插槽
作用域插槽是一种特殊的slot,使用一个可以复用的模板替换已渲染的元素。我们先来一个简单的示例看下:
<body>
<div id="app">
<children-component>
<template scope="props">
<p>来自父组件的内容</p>
<p>{{ props.msg }}</p>
</template>
</children-component>
</div>
<script>
// 全局组件
Vue.component("children-component", {
template: `
<div class="container">
<slot msg="来自子组件的内容"></slot>
</div>
`
});
// 根组件
var app = new Vue({
el: "#app"
});
</script>
</body>
上面案例父组件使用了<template>标签,而且标签拥有一个scope="props"的特性,props是一个临时变量,通过临时变量props可以调用子组件插槽的数据msg。
作用域插槽更具代表性的用例是列表组件,允许组件自定义应该如何渲染列表每一项。示例代码如下:
<body>
<div id="app">
<my-list :books="books">
<!-- 作用域插槽也可以是具名 slot -->
<template slot="book" scope="props">
<li>{{ props.bookName }}</li>
</template>
</my-list>
</div>
<script>
// 全局组件
Vue.component("my-list", {
props: {
books: {
type: Array,
// 设置默认值
default: function() {
return [];
}
}
},
template: `
<ul>
<!-- 具名插槽 -->
<slot name="book" v-for="book in books" :book-name="book.name"></slot>
</ul>
`
});
// 根组件
var app = new Vue({
el: "#app",
data: {
books: [
{ name: "《Vue.js 实战》" },
{ name: "《JavaScript 实战》" },
{ name: "《CSS 实战》" }
]
}
});
</script>
</body>
仔细查看以上案例,会发现,直接使用v-for指令简单多了,此处作用只是为介绍作用域插槽。
- 访问 slot
如果我们想要访问某个具名slot该怎么办呢?其实,Vue.js也提供了相关方法,通过$slot就可以访问某个具名slot,this.$slot.default包含了所有没有被包含的具名slot中的节点。
未完待续。。。
当你的才华撑不起自己的野心,那就努力学习吧!
浙公网安备 33010602011771号