组件化
vue组件系统提供了一种抽象,让我们可以使用独立可复用的组件来构建大型应用,任意类型的应用界面都可以抽象为一个组件树。组件化能提高开发效率、方面重复使用、简化调试步骤、提升项目可维护性,便于多人协同开发
组件通信常用方式
- props
$emit
/$on
- event bus
- vuex
边界情况
- $parent
- $children
- $root
- $refs
- provide/inject
- 非props特性
- $attrs
- $listeners
组件通信
props
父给子传值
// child
props:{msg: String}
//parent
<HelloWorld msg="Hello World!" />
自定义事件
子给父传值
// child
this.$emit('add', good)
//parent
<Cart @add="cartAdd($event)" />
事件总线
任意两个组件之间传值常用事件总线或vuex的方式
// Bus:事件派发、监听和回调管理
class Bus{
constructor(){
this.callbacks = [];
}
$on(name, fn){
this.callbacks[name] = this.callbacks[name] || [];
this.callbacks[name].push(fn)
}
$emit(name, args){
if(this.callbacks[name]){
this.callbacks[name].forEach(cb=>cb(args))
}
}
}
// main.js
Vue.prototype.$bus = new Bus()
// child1
this.$bus.$on('foo', handle)
// child2
this.$bus.$emit('foo')
实践中通常用Vue代替Bus,因为Vue已经实现了
$on
和$emit
vuex
创建唯一的全局数据管理者store,通过它管理数据并通知组件状态更新
$attrs/$listeners
包含了父作用域中不作为prop被识别的(且获取)的特性绑定(class和style除外)。当一个组件没有声明任何prop时,这里会包含所有父作用域的绑定,并且可以通过v-bind="$attrs"传入内部组件——在创建高级别的组件时非常有用
// child:并未在props中声明foo
<p>{{$attrs.foo}}</p>
// parent
<HelloWorld foo="foo" />
// 给Grandson隔代传值
<Child2 msg="foo" @some-event="onSomeEvent" />
// Child2做展开
<Grandson v-bind="$attrs" v-on="$listeners" />
// Grandson使用
<div @click="$emit('some-evnet', 'msg from grandson')">
{{msg}}
</div>
provide/inject
能够实现祖先和后代之间传值
// ancestor
provide(){
return {
yc: 'yichan'
}
}
// descendant
inject: ['yc']
插槽
插槽语法是Vue实现内容分发API,用于复合组件开发。该技术在通用组件库开发中有大量应用
匿名插槽
// comp1
<div>
<slot></slot>
</div>
// parent
<comp>hello</comp>
具名插槽
将内容分发到自组件指定位置
// comp2
<div>
<slot></slot>
<slot name="content"></slot>
</div>
//parent
<Comp2>
<!-- 默认插槽⽤default做参数 -->
<template v-slot:default>具名插槽</template>
<!-- 具名插槽⽤插槽名做参数 -->
<template v-slot:content>内容...</template>
</Comp2>
作用域插槽
分发内容要用到子组件中的数据
// comp3
<div>
<slot :foo="foo"></slot>
</div>
// parent
<Comp3>
<!-- 把v-slot的值指定为作⽤域上下⽂对象 -->
<template v-slot:default="slotProps">
来⾃⼦组件数据:{{slotProps.foo}}
</template>
</Comp3>
组件化实战
通用表单组件
收集数据、校验数据并提交
需求:
- 表单KForm
- 载体,输入数据model,校验规则rules
- 校验validate - 表单项KFormItem
- label标签添加
- 载体,输入项包起来
- 校验执行者,显示错误 - 输入框KInput
- 双绑
- 图标、反馈
KInput.vue
<template>
<div>
<!-- 1.双绑 -->
<input :type="type" :value="value" @input="onInput" v-bind="$attrs" />
</div>
</template>
<script>
export default {
// 特性继承关掉,div上就不会出现$attrs
inheritAttrs: false,
props: {
value: {
type: String,
default: "",
},
type: {
type: String,
default: "text",
},
},
methods: {
onInput(e) {
this.$emit("input", e.target.value);
// 触发校验
// 源码中实现了一个this.dispatch('k-form-item', 'validate')
this.$parent.$emit("validate");
},
},
};
</script>
<style scoped>
</style>
KFormItem.vue
<template>
<div>
<!-- 1.labelasyncValidator -->
<label v-if="label">{{ label }}</label>
<!-- 2.内部使用 -->
<slot></slot>
<!-- 3.校验结果 -->
<p v-if="error">{{ error }}</p>
<!-- <p>{{ form.rules[this.prop] }}</p> -->
</div>
</template>
<script>
import Validator from "async-validator";
export default {
inject: ["form"],
props: {
label: {
type: String,
default: "",
},
prop: {
type: String,
default: "",
},
},
data() {
return {
error: "",
};
},
mounted() {
// 老爹挂载之后,孩子一定已挂载,KInput已挂载,可以监听校验事件
this.$on("validate", () => {
this.validate();
});
},
methods: {
validate() {
// 获取校验规则
const rules = this.form.rules[this.prop];
// 当前值
const value = this.form.model[this.prop];
const validator = new Validator({ [this.prop]: rules });
// 执行校验,返回一个promise
return validator.validate({ [this.prop]: value }, (errors) => {
if (errors) {
// 显示错误信息
this.error = errors[0].message;
} else {
// 校验通过清除错误
this.error = "";
}
});
},
},
};
</script>
<style scoped>
</style>
KForm.vue
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {
props: {
model: {
type: Object,
required: true,
},
rules: Object,
},
methods: {
// 全局校验
validate(cb) {
// 1.执行全部Item校验
// 结果[Promise...]
const results = this.$children
.filter((item) => item.prop)
.map((item) => item.validate());
// 2.判断校验结果
Promise.all(results)
.then(() => cb(true))
.catch(() => cb(false));
},
},
};
</script>
<style scoped>
</style>
组合上面的表单组件
index.vue
<template>
<div>
<!-- <ElementTest></ElementTest> -->
<!-- KInput -->
<k-form :model="model" :rules="rules" ref="loginForm">
<k-form-item label="用户名" prop="username">
<k-input v-model="model.username" placeholder="用户名" />
<!-- 等效于 -->
<!-- <k-input :value="model.username" @input="model.username=$event" /> -->
</k-form-item>
<k-form-item label="密码" prop="password">
<k-input v-model="model.password" placeholder="密码" />
</k-form-item>
<k-form-item>
<button @click="onLogin">登录</button>
</k-form-item>
</k-form>
</div>
</template>
<script>
import KForm from "./KForm.vue";
import KFormItem from "./KFormItem.vue";
// import ElementTest from "@/components/form/ElementTest.vue";
import KInput from "./KInput.vue";
export default {
provide() {
// 隔代传参给KFormItem
return {
form: this,
};
},
components: {
// ElementTest
KInput,
KFormItem,
KForm,
},
data() {
return {
model: {
username: "tom",
password: "",
},
rules: {
username: [{ required: true, message: "请输⼊⽤户名" }],
password: [{ required: true, message: "请输⼊密码" }],
},
};
},
methods: {
onLogin() {
// 全局校验
this.$refs.loginForm.validate((isValid) => {
if (isValid) {
console.log("submit login");
} else {
alert("校验失败");
}
});
},
},
};
</script>
<style scoped>
</style>
实现弹窗组件
弹窗类组件的特点是它们在当前Vue实例之外独立存在,通常挂载于body;它们是通过JS动态创建的,不需要在任何组件中声明。
通知组件:Notice.vue
<template>
<div class="box" v-if="isShow">
<h3>{{ title }}</h3>
<p class="box-content">{{ message }}</p>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
default: "",
},
message: {
type: String,
default: "",
},
duration: {
type: Number,
default: 3000,
},
},
data() {
return {
isShow: false,
};
},
methods: {
show() {
this.isShow = true;
setTimeout(this.hide, this.duration);
},
hide() {
this.isShow = false;
this.remove();
},
},
};
</script>
<style scoped>
.box {
position: fixed;
width: 100%;
top: 16px;
left: 0;
text-align: center;
pointer-events: none;
background-color: #fff;
border: grey 3px solid;
box-sizing: border-box;
}
.box-content {
width: 200px;
margin: 10px auto;
font-size: 14px;
padding: 8px 16px;
background: #fff;
border-radius: 3px;
margin-bottom: 8px;
}
</style>
动态使用工具方法:utils/create.js
import Vue from "vue";
// 1.create(Comp):动态创建Comp组件实例
// 2.执行挂载获取其dom元素
// 3.追加到body中
export default function create(Component, props) {
// 创建组件实例
// 方案1:extends
/* const Ctor = Vue.extends(Component);
new Ctor({ propsData: props }); */
// 方案2:借鸡生蛋
const vm = new Vue({
render: h => h(Component, { props })
}).$mount(); // 只挂载,不指定宿主,依然一颗得到dom
// 手动追加
document.body.appendChild(vm.$el);
// 获取组件实例
// vm.$root // 根实例
const comp = vm.$children[0]; // 根组件实例
// 给一个淘汰方法
comp.remove = () => {
document.body.removeChild(vm.$el);
vm.$destroy();
};
// 返回一个组件实例
return comp;
}
封装成一个插件 plugins/notice.js
import Notice from "@/components/Notice.vue";
import create from "@/utils/create";
export default {
// 把Notice和create封装为一个插件
install(Vue) {
Vue.prototype.$notice = props => {
const comp = create(Notice, props);
comp.show();
return comp;
};
}
};
在main.js中使用Vue.use()
// 导入封装插件
import notice from "@/plugins/notice";
Vue.use(notice);
测试代码 index.vue
// 用封装插件形式
this.$notice({
title: "测试",
message: isValid ? "过" : "不过",
});
实现Table组件
一个简单的表格组件KTable,有基本实现、插槽以及排序功能
模板代码:index.vue
<template>
<div>
<!-- KTable -->
<k-table :data="tableData">
<k-table-column sortable prop="date" label="日期"></k-table-column>
<k-table-column sortable prop="name" label="姓名"></k-table-column>
<k-table-column prop="address" label="地址"></k-table-column>
<k-table-column label="操作">
<template v-slot:default="scope">
<button @click="handleEdit(scope.$index, scope.row)">编辑</button>
<button @click="handleDelete(scope.$index, scope.row)">删除</button>
</template>
</k-table-column>
</k-table>
</div>
</template>
<script>
import KTable from "./KTable.vue";
import KTableColumn from "./KTableColumn.vue";
export default {
components: { KTable, KTableColumn },
data() {
return {
tableData: [
{
date: "2016-05-02",
name: "小徐",
address: "上海市",
},
{
date: "2017-05-03",
name: "小虎",
address: "上海市",
},
{
date: "2018-05-20",
name: "小刘",
address: "上海市",
},
{
date: "2021-01-02",
name: "小李",
address: "上海市",
},
],
};
},
};
</script>
<style scoped>
</style>
表格组件:KTable.vue
<script>
export default {
props: {
data: {
type: Array,
required: true,
},
},
data() {
return {
orderFiled: "",
orderBy: "desc",
};
},
computed: {
columns() {
// 由于不一定有prop属性,内部如果出现了默认作用域插槽,则就按照它来执行渲染
return this.$slots.default
.filter((vnode) => vnode.tag)
.map(({ data: { attrs, scopedSlots } }) => {
const column = { ...attrs };
if (scopedSlots) {
// 自定义模板
column.renderCell = (row, i) => (
<div>{scopedSlots.default({ row, $index: i })}</div>
);
} else {
// 设置prop的情况
column.renderCell = (row) => <div>{row[column.prop]}</div>;
}
return column;
});
},
},
created() {
this.columns.forEach((column) => {
// 如果存在sortable列,则头一个作为默认排序字段
if (column.hasOwnProperty("sortable")) {
if (column.prop && !this.orderFiled) {
this.sort(column.prop, this.orderBy);
}
}
});
},
methods: {
sort(filed, by) {
this.orderFiled = filed;
this.orderBy = by;
this.data.sort((a, b) => {
const v1 = a[this.orderFiled];
const v2 = b[this.orderFiled];
if (typeof v1 === "number") {
return this.orderBy === "desc" ? v2 - v1 : v1 - v2;
} else {
return this.orderBy === "desc"
? v2.localeCompare(v1)
: v1.localeCompare(v2);
}
});
},
toggleSort(field) {
const by = this.orderBy === "desc" ? "asc" : "desc";
this.sort(field, by);
},
},
// 实现一个渲染函数JSX
render() {
return (
<table>
<thead>
<tr>
{this.columns.map((column) => {
if (column.hasOwnProperty("sortable") && column.prop) {
let orderArrow = "↑↓";
if (this.orderFiled === column.prop) {
orderArrow = this.orderBy === "desc" ? "↓" : "↑";
}
return (
<th
key={column.label}
onClick={() => this.toggleSort(column.prop)}
>
{column.label} <span>{orderArrow}</span>
</th>
);
} else {
return <th key={column.label}>{column.label}</th>;
}
})}
</tr>
</thead>
<tbody>
{this.data.map((row, rowIndex) => (
<tr key={rowIndex}>
{this.columns.map((column, columnIndex) => (
<td key={columnIndex}>{column.renderCell(row, rowIndex)}</td>
))}
</tr>
))}
</tbody>
</table>
);
},
};
</script>
<style scoped>
table {
width: 50%;
margin: 0 auto;
}
</style>
插槽组件:KTableColumn.vue
<template>
<div></div>
</template>
<script>
export default {};
</script>
<style scoped>
</style>