Vue正式篇(一)组件化

组件化
Vue组件系统提供了一种抽象,让我们可以使用独立可复用的组件来构建大型应用,任意类型的应用界面都可以抽象为一个组件树。组件化能提高开发效率,利于复用,简化调试步骤,提升项目可维护性,便于多人协同开发

组件通信常用方式
props
eventbus
vuex
自定义事件:

  • 边界情况
    – $parent
    – $children
    – $root
    – $refs
    – provide/inject
  • 非prop特性
    – $attrs
    – $listeners
    注:事件的派发和监听是同一个。

props
父传给子

// parent
<HelloWorld msg="Welcome to Your Vue.js App">

// child
props: [msg: String]

子给父传值

this.$emit('add', good)

// parent
<Cart @add="cartAdd($event)" />

事件总线
任意两个组件之间传值常用事件总线 或 vuex的方式,用Bus模拟Vue

// 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')

vuex
创建唯一的全局数据管理者store,通过它管理数据并通知组件状态变更。
vuex详情可见:Vuex相关笔记

$parent/$root
兄弟组件之间通信可以通过共同祖辈,$parent$root

// brother1
this.$parent.$on('foo', handle)

// broter2
this.$parent.$emit('foo')

$children
父组件可以通过$children访问子组件实现父子通信。

// parent
this.$children[0].xx = 'xxx'

注:$children不能保证子元素顺序

$attrs/$listeners
包含了父作用域中不作为prop被识别(且获取)的特性绑定(class和style除外)。当一个组件没有声明任何prop时,这里会包含所有父作用域的绑定(class和style除外),并且可以通过v-bind="$attrs"传入内部组件——在创建高级别的组件时非常有用。
v-bind="$attrs"展开$attrs
inheritAttrs: false // 设置为false避免设置到根元素上

// parent
<HelloWorld foo="foo" />

// child: 并未在prop中声明foo
<p>{{$attrs.foo}}</p>

refs
获取子节点引用

// parent
<HelloWorld ref="hw">

mounted() {
	this.$refs.hw.xx = 'xxx'
}

provide/inject
能够实现祖先和后代之间传值

provide() {
	return { foo: 'foo' }
}

// descendant
inject: ['foo']

插槽
插槽语法是Vue实现的内容分发API,用于符合组件开发。该技术在通用组件库开发中有大量应用。
匿名插槽

// comp1
<div>
	<slot></slot>
</div>

// parent
<comp>hello</comp>

具名插槽
将内容非法到子组件指定位置

// comp2
<div>
	<slot></slot>
	<slot></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
    • 指定数据、校验规则
  • KFormItem
    • 执行校验
    • 显示错误信息
  • KInput
    • 维护数据

KInput
创建components/form/KInput.vue

<template>
	<div>
		<input :value="value" @input="onInput" v-bind="$attrs">
	</div>
</template>
<script>
export default {
	inheritArrs: false,		// 不希望组件根元素继承特性
	props: {
		value: {
			type: String,
			default: ''
		}
	},
	methods: {
		onInput(e) {
			this.$emit('input', e.target.value)
		}
	}
}
</script>

使用KInput
创建components/form/index.vue,添加如下代码:

<template>
	<div>
		<h3>KForm表单</h3>
		<hr>
		<k-input v-model="model.username"></k-input>
		<k-input type="password" v-model="model.password"></k-input>
	</div>
</template>
<script>
import KInput from './KInput'

export default {
	components: {
		KInput
	},
	data() {
		return {
			model: { username: 'name', password: '' },
		}
	}
}
</script>

实现KFormItem
创建components/form/KFormItem.vue

<template>
    <div>
        <label v-if="label">{{label}}</label>
        <slot></slot>
        <p v-if="error">{{error}}</p>
    </div>
</template>
<script>
export default {
	props: {
		label: {	// 输入项标签
			type: String,
			default: ''
		},
		prop: {		// 字段名
			type: String,
			default: ''
		}
	},
	data() {
		return {
			error: ''	// 校验错误
		}
	}
}
</script>

使用KFormItem
components/form/index.vue,添加基础代码:

<template>
    <div>
        <h3>KForm表单</h3>
        <hr>
        <k-form-item label="用户名" prop="username">
            <k-input v-model="model.username"></k-input>
        </k-form-item>
        <k-form-item label="确认密码" prop="password">
            <k-input type="password" v-model="model.password"></k-input>
        </k-form-item>
    </div>
</template>

实现KForm

<template>
    <form>
        <slot></slot>
    </form>
</template>
<script>
export default {
	provide() {
		return {
			form: this	// 将组件实例作为提供者,子代组件可方便获取
		}
	},
	props: {
		model: { type: Object, required: true },
		rules: { type: Object }
	}
}
</script>

使用KForm

<template>
    <div>
        <h3>KForm表单</h3>
        <hr>
        <k-form :model='model' :rules='rules' ref='loginForm'>
            ...
        </k-form>
    </div>
</template>
<script>
import KForm from './KForm'

export default {
	components: {
		KForm,
	},
	data() {
		return {
			rules: {
				username: [{ required: true, message: "清输入用户名" }],
				password: [{ required: true, message: "清输入密码" }]
			}
		}
	},
	methods: {
		submitForm() {
			this.$refs['loginForm'].validate(valid => {
				if(valid) {
					alert('请求登录')
				} else {
					alert('校验失败')
				}
			})
		}
	}
}
</script>

数据校验
input通知校验

onInput(e) {
	// ...
	// $parent指FormItem
	this.$parent.$emit('validate')
}

FormItem监听校验通知,获取规则并执行校验

inject: ['form'],	// 注入
mounted() {	// 监听校验事件
	this.$on('validate', () => { this.validate() })
},
methods: {
	validate() {
		// 获取对应FormItem校验规则
		console.log(this.form.rules[this.prop]);
		// 获取校验值
		console.log(this.form.model[this.prop])
	}
}

安装async-validator: npm i async-validator -S

// KFormItem.vue
import Schema from 'async-validator'

validate() {
	// 获取对应FormItem校验规则
	const rules = this.form.rules[this.prop];
	// 获取校验值
	const value = this.form.model[this.prop];
	// 校验描述对象
	const descriptor = { [this.prop]: rules };
	// 创建校验器
	const schema = new Schema(descriptor);
	// 返回Promise,没有触发catch就说明验证通过
	return schema.validate({ [this.prop]: value }, errors => {
		if(errors) {
			// 将错误信息显示
			this.error = errors[0].message;
		} else {
			// 校验通过
			this.error = '';
		}
	})
}

表单全局验证,为Form提供validate方法

// KForm.vue
validate(cb) {
	// 调用所有含有prop属性的子组件的validate方法并得到Promise数组
	const tasks = this.$children
		.filter(item => item.prop)
		.map(item => item.validate())
	// 所有任务必须全部成功才算校验通过,任一失败则校验失败
	Promise.all(tasks)
		.then(() => cb(true))
		.catch(() => cb(false))
}

实现弹窗组件
弹窗这类组件的特点是它们在当前Vue实例之外独立存在,通常挂载与body;它们是通过JS动态创建的,不需要再任何组件中声明。
常见使用:

this..$create(Notice, {
	title: '通知标题',
	message: '提示信息',
	duration: 1000
}).show()

create函数

import Vue from 'vue'

// 创建函数接收要创建组件定义
function create(Component, props) {
	// 创建一个vue实例
	const vm = new Vue({
		render(h) {
			// render函数将传入组件配置对象转换为虚拟dom
			return h(Component, { props })
		}
	}).$mount();	// 执行挂载函数,但未指定挂载目标,表示只执行初始化工作

	// 将生成dom元素追加至body
	document.body.appendChild(vm.$el)

	// 给组件实例添加销毁方法
	const comp = vm.$children[0]
	comp.remove = () => {
		document.body.removeChild(vm.$el);
		vm.$destroy();
	}
	return comp
}
// 暴露调用接口
export default create;

注:另一种创建组件实例的方式:Vue.extent(Component)

通知组件
通知组件,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: 1000
		}
	},
	data() {
		return {
			isShow: false
		}
	},
	methods: {
		show() {
			this.isShow = true;
			setTimeout(this.hide, this.duration);
		},
		hide() {
			this.isShow = false;
			this.remove()
		}
	}
}
</script>
<style>
.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>

使用create.api
测试,components/form/index.vue

import create from '@/utils/create'
import Notice from '@/components/Notice'

export default {
	methods: {
		submitForm(form) {
			this.$refs[form].validate(valid => {
				const notice = create(Notice, {
					title: '通知标题',
					message: valid ? '请求登录!' : '校验失败!',
					duration: 100
				})
				notice.show()
			})
		}
	}
}

优化
1、修正input中$parent写法的问题
- mixin emitter
- 声明componentName
- dispatch()
2、使用Vue.extent方式实现create方法
- Vue.extent

	const comp = { data: {}, props: {} }
	const Ctor = Vue.extent(comp)
	new Ctor({propsData: {}})

1、修正input中$parent写法问题
在src下新建mixins文件夹,使用vue源码中的emitter

// emitter.js
// 广播:自上而下派发事件
function broadcast(componentName, eventName, params) {
	// 遍历所有的子元素,如果子元素componentName和传入的相同则派发事件
	this.$children.forEach(child => {
		var name = child.$options.componentName;
		if(name === componentName) {
			child.$emit.apply(child, [eventName].concat(params))
		} else {
			broadcast.apply(child, [componentName, eventName].concat([params]))
		}
	})
}
export default {
	methods: {
		// 冒泡查找componentName相同组件并派发事件
		dispatch(componentName, eventName, params) {
			var parent = this.$parent || this.$root;
			var name = parent.$options.componentName

			// 向上查找直到找到相同名称的组件
			while(parent && (!name || name !== componentName)) {
				parent = parent.$parent

				// 向上查找直到找到相同名称的组件
				while(parent && (!name || name !== componentName)) {
					parent = parent.$parent
					if(parent) {
						name = parent.$options.componentName
					}
				}
				// 如果找到就派发事件
				if(parent) {
					parent.$emit.apply(parent, [eventName].concat(params))
				}
			},
			broadcast(componentName, eventName, params) {
				broadcast.call(this, componentName, eventName, params)
			}
		}
	}
}

KFormItem.vue中增加componentName

componentName: 'KFormItem'

KInput.vue中变化

// this.$parent.$emit('validate')改为:
this.dispatch('KFormItem', 'validate')

解耦$children
在KFormItem.vue中混入emitter

// 派发事件通知KForm,新增一个KFormItem实例
this.dispatch('KForm', 'kkb.form.addField', [this])

在KForm中混入emitter

created() {
	this.fields = []
	this.$on('kkb.form.addField', item => {
		this.fields.push(item)
	})
}
// 替换原有tasks
const tasks = this.fields.map(item => item.validate())

2、使用Vue.extent方式实现create方法(组件实例创建的另一种解决方案)

// create.js
import Vue from 'vue'

function create(Component, props) {
	// 组件构造函数如何获取?
	// 1.Vue.extent()
	const Ctor = Vue.extent(Component)
	// 创建组件实例
	const comp = new Ctor({propsData: props})
	comp.remove = function() {
		document.body.removeChild(comp.$el)
		comp.$destroy()
	}
	// 2.render
	const vm = new Vue({
		// h是createElement,返回VNode,是虚拟dom
		// 需要挂载才能变成真实dom
		render:h => h(Component, {props}),
	}).$mount()		// 不能指定宿主元素,则会创建真实dom,但是不会追加操作

	// 获取真实dom
	document.body.appendChild(vm.$el)
	const comp = vm.$children[0]

	// 删除
	comp.remove = function() {
		document.body.removeChild(vm.$el)
		vm.$destroy()
	}
	return comp
}
export default create

3、使用插件进一步封装便于使用,create.js

import Notice from '@/components/Notice.vue'
// ...
export default {
	install(Vue) {
		Vue.prototype.$notice = function(options) {
			return create(Notice, options)
		}
	}
}
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值