对于通用类型组件,我们要求它尽可能的短小精悍,调用起来更为简单,所以不能设计太多的参数。基础组件库不能符合这个要求,
主要是因为基础组件库需要尽可能增加普适性,不会因为没有某个常用的属性,导致该组件需要复制一份重写,再加上日积月累的 pull request,
属性和参数必然会越来越多。而我们在业务中使用,完全不需要这么多的配置,如果有重大差别,重新复制一份,对于后续的维护反而更方便。
所以是否新增加属性还是拷贝一份,是根据后续该组件是否会产生比较大的发展方向差异来决定的。
Vue 组件与 React 组件有比较大的区别,模板的设计更偏向于 HTML,所以要实现类似 react 的高阶组件的需求通常比较少,
而高阶组件集成度过高,对于业务来说,当业务越来越复杂,组件内部逻辑将拆分困难,未必是件好事,所以我们只讨论普通的组件设计。
组件设计是考虑组件通讯方式,主要分为以下几个方面:向下传值,向上传值,伪双向绑定,方法调用。
向下传值就是父级传给子级数据。前面已经提到了,在 props 传值尽量对传入数据进行类型校验,保证尽快发现问题。除此之外,也有一些注意事项。
传值类型如果是引用类型的 Object 类型,那么尽量给它默认值,防止 undefined。
其次,父级在赋值时,不要使用 a=newData 这种写法,而是使用 Object.assign 来保证能准确触发组件更新。
还有另外一种方式,但不方便声明所有对象内的数据时,可以使用 this.$set(this, 'key', newData),保证对象一定会被监听到。
Vue 2.0 需要使用 $emit 进行事件向上冒泡, 父组件进行事件的监听就可以进行处理。
伪双向绑定Vue 2.0 提供了语法糖,支持双向绑定,使得Vue 进行双向传递数据极为方便,不需要既向上传值又向下传值。
当然它不是真正的绑定,而是封装了之前提到的向下传值和向上传值,简单的语法糖。它分为两类:v-model 和 .sync 修饰符
数据传递支持各种类型,不过建议传递的数据使用数组而不要使用对象类型,对象类型可能会出现渲染监听失败的问题。
v-model 使用的是 value 属性和 input 事件,父组件会自动把 input 事件的值赋给对应的变量。
在设计组件中,如果有双向的数据传递,且符合组件设计目的,应该优先使用 v-model 来实现数据的控制,
这样的组件更符合 Vue 组件的标准。
要注意的是,如果是自行写 render 函数,双向绑定要自己实现。
sync.sync 修饰符和 v-model 比较类似,不过它的 props 可以是自定义的,而向上传值时方式为:
this.$emit('update:propsName', val)本质上和 v-model 是类似的。sync 修饰符相比于 v-model,语义化更好,用起来更方便
方法调用有了 props 和 emit ,我们已经基本能够实现大部分功能了,但总有些子组件的层次控制或者数据控制无法通过这种方式实现,
这个时候,组件间的交互就需要使用子组件的 Methods 来定义,使用 this.$refs.组件ref 来调用它的方法。
比如说 el-tree 组件,设置选中和非选中,只靠数据传递,无法保证设计选中状态,所以它提供了一些方法来进行手动选择。
在设计组件时,使用方法进行控制应该是最后才考虑的,因为我们通常无法一眼看出某个方法是否应该支持外部调用,
只能通过看文档才能得知相关的方法
除组件外,Vue 提供了一些机制用于减少项目中的代码重复率。
使用插件或者 mixins 实现插件机制需要在 Vue 初始化的时候引入。看下 vue-meta 的插件入口写法:
/** * Plugin install function. * @param {Function} Vue - the Vue constructor. */ export default function VueMeta (Vue, options = {}) { // set some default options const defaultOptions = { keyName: VUE_META_KEY_NAME, contentKeyName: VUE_META_CONTENT_KEY, metaTemplateKeyName: VUE_META_TEMPLATE_KEY_NAME, attribute: VUE_META_ATTRIBUTE, ssrAttribute: VUE_META_SERVER_RENDERED_ATTRIBUTE, tagIDKeyName: VUE_META_TAG_LIST_ID_KEY_NAME } // combine options options = assign(defaultOptions, options) // bind the $meta method to this component instance Vue.prototype.$meta = $meta(options) // store an id to keep track of DOM updates let batchID = null // watch for client side component updates Vue.mixin({ beforeCreate () { // Add a marker to know if it uses metaInfo // _vnode is used to know that it's attached to a real component // useful if we use some mixin to add some meta tags (like nuxt-i18n) if (typeof this.$options[options.keyName] !== 'undefined') { this._hasMetaInfo = true } // coerce function-style metaInfo to a computed prop so we can observe // it on creation if (typeof this.$options[options.keyName] === 'function') { if (typeof this.$options.computed === 'undefined') { this.$options.computed = {} } this.$options.computed.$metaInfo = this.$options[options.keyName] } }, created () { // if computed $metaInfo exists, watch it for updates & trigger a refresh // when it changes (i.e. automatically handle async actions that affect metaInfo) // credit for this suggestion goes to [Sébastien Chopin](https://github.com/Atinux) if (!this.$isServer && this.$metaInfo) { this.$watch('$metaInfo', () => { // batch potential DOM updates to prevent extraneous re-rendering batchID = batchUpdate(batchID, () => this.$meta().refresh()) }) } }, activated () { if (this._hasMetaInfo) { // batch potential DOM updates to prevent extraneous re-rendering batchID = batchUpdate(batchID, () => this.$meta().refresh()) } }, deactivated () { if (this._hasMetaInfo) { // batch potential DOM updates to prevent extraneous re-rendering batchID = batchUpdate(batchID, () => this.$meta().refresh()) } }, beforeMount () { // batch potential DOM updates to prevent extraneous re-rendering if (this._hasMetaInfo) { batchID = batchUpdate(batchID, () => this.$meta().refresh()) } }, destroyed () { // do not trigger refresh on the server side if (this.$isServer) return // re-render meta data when returning from a child component to parent if (this._hasMetaInfo) { // Wait that element is hidden before refreshing meta tags (to support animations) const interval = setInterval(() => { if (this.$el && this.$el.offsetParent !== null) return clearInterval(interval) if (!this.$parent) return batchID = batchUpdate(batchID, () => this.$meta().refresh()) }, 50) } } }) }