本文同步在我的博客shymean.com上,欢迎关注javascript
写Vue有很长一段时间了,除了常规的业务开发以外,也应该思考和反思一下封装组件的正确方式。以弹窗组件为例,一种实现是在须要模板中引入须要弹窗展现的组件,而后经过一个flag变量来控制弹窗的组件,在业务代码里面会充斥着冗余的弹窗组件逻辑,十分不优雅。css
本文整理了开发Vue组件的一些技巧,包含大量代码示例。html
vue-cli3提供了很是方便的功能,能够快速编写一些测试demo,是开发组件必备的环境。下面是安装使用步骤vue
// 全局安装vue-cli3
npm install -g @vue/cli
vue -V // 查看版本是否为3.x
// 安装扩展,此后能够快速启动单个vue文件
npm install -g @vue/cli-service-global
// 快速启动demo文件
vue serve demo.vue
复制代码
若是须要scss,则还须要在目录下安装sass-loader等。java
下面是使用vue-cli3可能会碰见的几个问题,更多使用教程能够参考:一份超级详细的Vue-cli3.0使用教程[赶忙来试试!]node
自定义入口文件git
若是须要(好比须要开发移动端的组件),能够在使用vue serve
时自定义html入口文件,在根目录下编写index.html
,并确保页面包含#app
的dom便可。github
引入公共混合文件web
经过style-resources-loader
在每一个文件引入公共样式混合等,参考自动化导入vuex
须要访问Vue全局对象
在某些时候须要放问全局Vue对象,如开发全局指令、插件时
import Vue from "vue"
import somePlugin from "../src/somePlugin"
Vue.use(somePlugin)
复制代码
上面这种写法并不会生效,这是由于vue serve xxx.vue
仅仅只能做为快速原型开发的方案,使用的Vue与 import引入的Vue不是同一个对象。一种解决办法是手动指定vue serve
的入口文件
// index.js
import Vue from "../node_modules/vue/dist/vue.min"
import placeholder from "../src/placeholder/placeholder"
Vue.use(placeholder)
new Vue({
el: "#app",
template: ``,
created(){},
})
复制代码
Vue组件的API主要包含三部分:prop、event、slot
参考:单向数据流-官方文档。
父级 prop 的更新会向下流动到子组件中,可是反过来则不行
单向数据流是Vue组件一个很是明显的特征,不该该在子组件中直接修改props的值
从源码/src/core/vdom/create-component.js
和/src/core/vdom/helpers/extract-props.js
里能够看见,在处理props的取值时,首先从
function extractPropsFromVNodeData(){
const res = {}
const { attrs, props } = data
// 执行浅拷贝
checkProp(res, props, key, altKey, true) || checkProp(res, attrs, key, altKey, false)
return res
}
复制代码
在子组件修改props,却不会修改父组件,这是由于extractPropsFromVNodeData
中是经过浅复制将attrs传递给props的。
浅复制意味着在子组件中对对象和数组的props进行修改仍是会影响父组件,这就违背了单向数据流的设计。所以须要避免这种状况出现。
这里能够参考:vue组件通讯全揭秘,写的比较全面
此外,若是须要跨组件或者兄弟组件之间的通讯,能够经过eventBus或者vuex等方式来实现。
考虑下面场景:父组件将数据经过prop形式传递给子组件,子组件进行相关操做并修改数据,须要修改父组件的prop值(一个典型的例子是:购物车的商品数量counter组件)。
根据组件单向数据流和和事件通讯机制,须要由子组件经过事件通知父组件,并在父组件中修改原始的prop数据,完成状态的更新。在子组件中修改父组件的数据的场景在业务中也是比较常见的,那么有什么办法能够“绕开”单向数据流的限制呢?
能够参考React的状态提高,直接经过props将父元素的数据处理逻辑传入子组件,子组件只作数据展现和事件挂载便可
<template>
<div class="counter">
<div class="counter_btn" @click="onMinus">-</div>
<div class="counter_val">{{value}}</div>
<div class="counter_btn" @click="onPlus">+</div>
</div>
</template>
<script> export default { props: { value: { type: Number, default: 0 }, onMinus: Function, onPlus: Function }, }; </script>
复制代码
而后在调用时传入事件处理函数
<template>
<div>
<counter :value="counter2Val" :on-minus="minusVal" :on-plus="plusVal"></counter>
</div>
</template>
<script> export default { data() { return { counter2Val: 0, } }, methods: { minusVal(){ this.counter2Val-- }, plusVal(){ this.counter2Val++ } } } </script>
复制代码
很明显,因为在每一个父组件中都须要实现on-minus
和on-plus
,所以状态提高并无从根本上解决问题。
Vue内置了v-model
指令,v-model 是一个语法糖,能够拆解为 props: value 和 events: input。就是说组件只要提供一个名为 value 的 prop,以及名为 input 的自定义事件,知足这两个条件,使用者就能在自定义组件上使用 v-model
<template>
<div>
<button @click="changeValue(-1)">-1</button>
<span>{{currentVal}}</span>
<button @click="changeValue(1)">+1</button>
</div>
</template>
<script> export default { props: { value: { type: Number // 定义value属性 } }, data() { return { currentVal: this.value }; }, methods: { changeVal(val) { this.currentVal += parseInt(val); this.$emit("input", this.currentVal); // 定义input事件 } } }; </script>
复制代码
而后调用的时候只须要传入v-model指令便可
<counter v-model="counerVal"/>
复制代码
使用v-model,能够很方便地在子组件中同步父组件的数据。在2.2以后的版本中,能够定制v-model
指令的prop和event名称,参考model配置项
export default {
model: {
prop: 'value',
event: 'input'
},
// ...
}
复制代码
在开发组件中,获取组件实例是一个很是有用的方法。组件能够经过$refs
、$parents
、$children
等方式得到vm实例引用
$refs
在组件(或者dom上)增长ref属性便可
$parents
获取子组件挂载的父组件节点
$children
,获取组件的全部子节点
这些接口返回的都是vnode,能够经过vnode.componentInstance
得到对应的组件实例,而后直接调用组件的方法或访问数据。虽然这种方式多多少少有些违背组件的设计理念,增长了组件之间的耦合成本,但代码实现会更加简洁。
一般状况下,表单验证是表单提交前一个十分常见的应用场景。那么,如何把表单验证的功能封装在组件内部呢?
下面是一个表单组件的示例,展现了经过得到组件的引用来实现表单验证功能。
首先定义组件的使用方式,
xm-form
接收model
和rule
两个prop
model
表示表单绑定的数据对象,最后表单提交的就是这个对象rule
表示验证规则策略,表单验证可使用async-validator插件xm-form-item
接收的prop
属性,对应form组件的model和rule的某个key值,根据该key从model上取表单数据,从rule上取验证规则下面是使用示例代码
<template>
<div class="page">
<xm-form :model="form" :rule="rule" ref="baseForm">
<xm-form-item label="姓名" prop="name">
<input v-model="form.name"/>
</xm-form-item>
<xm-form-item label="邮箱" prop="email">
<input v-model="form.email"/>
</xm-form-item>
<xm-form-item>
<button @click="submit">提交</button>
</xm-form-item>
</xm-form>
</div>
</template>
<script> import xmForm from "../src/form/form" import xmFormItem from "../src/form/form-item" export default { components: { xmForm, xmFormItem, }, data() { return { form: { name: "", email: "" }, rule: { name: [ {required: true, message: '用户名不能为空', trigger: 'blur'} ], email: [ {required: true, message: '邮箱不能为空', trigger: 'blur'}, {type: 'email', message: '邮箱格式不正确', trigger: 'blur'} ], } } }, methods: { submit() { // 调用form组件的validate方法 this.$refs.baseForm.validate().then(res => { console.log(res) }).catch(e => { console.log(e) }) } } } </script>
复制代码
接下来让咱们实现form-item组件,其主要做用是放置表单元素,及展现错误信息
<template>
<label class="form-item">
<div class="form-item_label">{{label}}</div>
<div class="form-item_mn">
<slot></slot>
</div>
<div class="form-item_error" v-if="errorMsg">{{errorMsg}}</div>
</label>
</template>
<script> export default { name: "form-item", props: { label: String, prop: String }, data() { return { errorMsg: "" } }, methods: { showError(msg) { this.errorMsg = msg } } } </script>
复制代码
而后让咱们来实现form组件
calcFormItems
获取每一个xm-form-item
的引用,保存在formItems中prop
属性,处理对应的error信息<template>
<div class="form">
<slot></slot>
</div>
</template>
<script> import AsyncValidator from 'async-validator'; export default { name: "xm-form", props: { model: { type: Object }, rule: { type: Object, default: {} } }, data() { return { formItems: [] } }, mounted() { this.calcFormItems() }, updated() { this.calcFormItems() }, methods: { calcFormItems() { // 获取form-item的引用 if (this.$slots.default) { let children = this.$slots.default.filter(vnode => { return vnode.tag && vnode.componentOptions && vnode.componentOptions.Ctor.options.name === 'form-item' }).map(({componentInstance}) => componentInstance) if (!(children.length === this.formItems.length && children.every((pane, index) => pane === this.formItems[index]))) { this.formItems = children } } }, validate() { let validator = new AsyncValidator(this.rule); let isSuccess = true let findErrorByProp = (errors, prop) => { return errors.find((error) => { return error.field === prop }) || "" } validator.validate(this.model, (errors, fields) => { this.formItems.forEach(formItem => { let prop = formItem.prop let error = findErrorByProp(errors || [], prop) if (error) { isSuccess = false } formItem.showError(error && error.message || "") }) }); return Promise.resolve(isSuccess) } } } </script>
复制代码
这样咱们就完成了一个通用的表单验证组件。从这个例子中能够看出获取组件引用,在组件开发中是一个很是有用的方法。
一些组件如提示框、弹出框等,更适合单独的API调用方式,如
import MessageBox from '@/components/MessageBox.vue'
MessageBox.toast('hello)
复制代码
如何实现制这种不须要手动嵌入模板里面的组件呢?原来,除了在经过在模板中嵌入组件到children挂载组件,Vue还为组件提供了手动挂载的方法$mount
let component = new MessageBox().$mount()
document.getElementById('app').appendChild(component.$el)
复制代码
经过这种方式,咱们就是能够封装API形式调用组件,下面是一个alert消息提示的接口封装
一个消息组件就是在页面指定绘制展现提示消息的组件,下面是简单实现
<template>
<div class="alert">
<div class="alert-main" v-for="item in notices" :key="item.name">
<div class="alert-content">{{ item.content }}</div>
</div>
</div>
</template>
<script> let seed = 0; function getUuid() { return 'alert_' + (seed++); } export default { data() { return { notices: [] } }, methods: { add(notice) { const name = getUuid(); let _notice = Object.assign({ name: name }, notice); this.notices.push(_notice); // 定时移除,单位:秒 const duration = notice.duration; setTimeout(() => { this.remove(name); }, duration * 1000); }, remove(name) { const notices = this.notices; for (let i = 0; i < notices.length; i++) { if (notices[i].name === name) { this.notices.splice(i, 1); break; } } } } } </script>
复制代码
下面来实现消息组件挂载到页面的逻辑,并对外暴露展现消息的接口
// alert.js
import Vue from 'vue';
// 具体的组件
import Alert from './alert.vue';
Alert.newInstance = properties => {
const props = properties || {};
// 实例化一个组件,而后挂载到body上
const Instance = new Vue({
data: props,
render (h) {
return h(Alert, {
props: props
});
}
});
const component = Instance.$mount();
document.body.appendChild(component.$el);
// 经过闭包维护alert组件的引用
const alert = Instance.$children[0];
return {
// Alert组件对外暴露的两个方法
add (noticeProps) {
alert.add(noticeProps);
},
remove (name) {
alert.remove(name);
}
}
};
// 提示单例
let messageInstance;
function getMessageInstance () {
messageInstance = messageInstance || Alert.newInstance();
return messageInstance;
}
function notice({ duration = 1.5, content = '' }) {
// 等待接口调用的时候再实例化组件,避免进入页面就直接挂载到body上
let instance = getMessageInstance();
instance.add({
content: content,
duration: duration
});
}
// 对外暴露的方法
export default {
info (options) {
return notice(options);
}
}
复制代码
而后就可使用API的方式来调用弹窗组件了
import alert from './alert.js'
// 直接使用
alert.info({content: '消息提示', duration: 2})
// 或者挂载到Vue原型上
Vue.prototype.$Alert = alert
// 而后在组件中使用
this.$Alert.info({content: '消息提示', duration: 2})
复制代码
高阶组件能够看作是函数式编程中的组合。能够把高阶组件看作是一个函数,他接收一个组件做为参数,并返回一个功能加强的组件。
高阶组件是一个接替Mixin实现抽象组件公共功能的方法,不会由于组件的使用而污染DOM(添加并不想要的div标签等)、能够包裹任意的单一子元素等等
在React中高阶组件是比较经常使用的组件封装形式,在Vue中如何实现高阶组件呢?
在组件的render函数中,只须要返回一个vNode数据类型便可,若是在render函数中提早作一些处理,并返回this.$slots.default[0]
对应的vnode,就能够实现高阶组件。
Vue内置了一个高阶组件keep-alive
,查看源码能够发现其实现原理,就是经过维护一个cache,并在render函数中根据key返回缓存的vnode,来实现组件的持久化。
节流是web开发中处理事件比较常见的需求。常见的场景有及时搜索框避免频繁触发搜索接口、表单按钮防止在短暂时间误重复提交等
首先来看看Throttle组件的使用方式,接收两个props
time
表示节流的时间间隔events
表示须要处理的事件名,多个事件用逗号分隔在下面的例子中,经过Throttle组件来控制其内部button的点击事件,此时连续点击屡次,触发clickBtn的次数要比点击的次数小(节流函数经过一个定时器进行处理)。
<template>
<div>
<Throttle :time="1000" events="click">
<button @click="clickBtn">click {{count}}</button>
</Throttle>
</div>
</template>
复制代码
下面是具体实现,实现高阶组件的主要功能是在render函数中对当前插槽中的vnode进行处理
const throttle = function (fn, wait = 50, ctx) {
let timer
let lastCall = 0
return function (...params) {
const now = new Date().getTime()
if (now - lastCall < wait) return
lastCall = now
fn.apply(ctx, params)
}
}
export default {
name: 'throttle',
abstract: true,
props: {
time: Number,
events: String,
},
created() {
this.eventKeys = this.events.split(',')
this.originMap = {}
this.throttledMap = {}
},
// render函数直接返回slot的vnode,避免外层添加包裹元素
render(h) {
const vnode = this.$slots.default[0]
this.eventKeys.forEach((key) => {
const target = vnode.data.on[key]
if (target === this.originMap[key] && this.throttledMap[key]) {
vnode.data.on[key] = this.throttledMap[key]
} else if (target) {
// 将本来的事件处理函数替换成throttle节流后的处理函数
this.originMap[key] = target
this.throttledMap[key] = throttle(target, this.time, vnode)
vnode.data.on[key] = this.throttledMap[key]
}
})
return vnode
},
}
复制代码
咱们还能够进一步封装,经过debounce函数来实现Debounce组件,可见高阶组件的做用,就是为了加强某个组件而存在的。关于高阶组件的其余应用,能够参考HOC(高阶组件)在vue中的应用。
本文整理了几种实现Vue组件的技巧
在了解Vue的API以后,理解上面的概念都比较轻松,封装组件,除了对于API的熟练度以外,更多地是考察JavaScript基础。Vue入门十分轻松,可是要写好优雅的Vue代码,也是一份不小的学问。