Vue 组件设计

Vue 组件设计

Vue 做为 MVVM 框架一员,不论是写业务仍是基础服务,都少不了书写组件。本文总结一下书写业务组件的一些心得。javascript

为何要写组件?

咱们知道,只要是组件,就须要在引用的时候与 view 或者其余组件进行相关的交互,即 props 传值,$emit 触发事件,
使用 $refs 调用组件方法等,与写在同一个文件相比,耗费的精力明显更多。那为何须要拆分出组件呢?我认为有两种目的:
复用和隔离。html

复用

在业务代码中,会有大量相似的界面,保证交互惟一,即便咱们有了相似 element-ui 或者 iview 这种基础组件库,
咱们一样须要为这些基础组件添加 props 或者 events,只有一处使用时,没有任何问题,当你的业务中出现两次、三次甚至更多时,
代码中会出现大量重复的代码,并且这些代码在线上可能会慢慢露出一些深层次的 bug,要修复这些 BUG,就须要 n 倍的时间去写一样的代码,
让人抓狂。因此咱们页面一样须要像 js 抽出公用的方法同样抽出公用组件,这就是复用的目的。vue

隔离

复用针对的是代码重复问题,而隔离则是针对代码逻辑过于复杂的问题。一般咱们要实现一个复杂的逻辑,它是一个扁平化的多逻辑并行问题,
人脑对于同时思考是有必定限制的,过于复杂就很难一下考虑全面。
首先须要抽象出它的目的,而后对实现进行分层,让每一层只解决一个简单的问题,这些层合起来造成一个完整的解决方案;
或者将问题拆分红几块,每块之间具备必定的联系,每次思考时只须要考虑局部的逻辑便可。
不论是分层、分块仍是混合式,它的目的是对进行隔离,从而简化问题。若是某个页面 js + template 行数很是多(1k+),
这个时候就能够考虑是否是要对部分功能拆解,便于在后续添加新功能,或者修改 BUG 的时候更为方便定位到问题的代码,
不会出现改错函数的问题。java

须要注意的是,虽然复用和隔离是让逻辑更为清晰,但使用本身写的组件会让项目的入手难度提升,须要先了解总体的设计,
才能针对性的修改代码或者添加新的功能,得失各半。node

组件设计的一些理念

网上有关于组件设计的基本原则:http://www.fly63.com/article/detial/996
内容比较多,下面进行一些经常使用的原则概括。react

单一职责

以前提到的组件拆分目的:复用与隔离,对于隔离的类型,组件业务必然很重,此时虽然要保证组件尽量简单,
而复用类型的,通用性更强,因此功能越单一,使用起来就越方便。咱们知道 react 有一个概念:container/component,
即 component 只是渲染组件,而 container 才是产生业务的组件,咱们 Vue 也能够依照这个理念进行设计。
即把数据处理等带有反作用的工做放在父组件中,而子组件只进行展现或操做,经过事件的方式让父组件进行处理,
保证逻辑归一,后续维护也更为方便。或者使用 slot 等相似高阶组件的方式来简化当前组件的内容。git

无反作用/引用透明

和纯函数相似,设计的一个组件不该该对父组件产生反作用,从而达到引用透明(引用屡次不影响结果)。
数据操做前必须进行复制。好比须要添加额外的键值,或者须要对数组类型的数据进行操做,会对原始数据产生影响,
须要使用解构的方式进行复制:github

const newData = { ...oldData }
const newList = [...oldList]

注:引用类型的 props 千万不要直接修改对象,虽然可以达到传递数据的目的,但会产生反作用,若是有其余地方用到该数据,可能产生未知的影响。vue-cli

入口和出口正确性检查

Vue 提供了类型检查工具,只在 dev 状况下生效,虽然和 JSON Schema 相比功能比较少,但可以作基本的类型检查了,
咱们只须要在 props 时不使用字符串型,而是为它定义详细的类型, 并为它设置默认值(vue-cli 的 eslint 严格模式已经强制要求):element-ui

['name1'] // 不规范写法
{
  name1: {
    type: String,
    default: undefined
  }
}

组件划分颗粒度

组件拆分出来以后,拆成几层或者是拆成几块,影响文件的数量。若是层级比较多,各类 props 传递,事件传递,维护成本比较高。
举例:若是是一个二级的列表,即有多个一级列表,一级列表各有一级列表,这个时候应该怎么拆分呢?
按单一原则,咱们可能须要拆分红如下几个:一级列表卡片自己,二级列表卡片,二级列表承载组件,一级列表承载组件。
这种划分,组件是三级,两块,数据的传递就会比较困难。若是一级卡片列表不复杂,咱们能够将几个 v-for 与组件自己合并,
即一级列表承载组件+一级列表卡片+二级列表卡片,二级列表卡片。这种处理方式保证全部的数据处理在第一层上,二级卡片只作渲染,
保证逻辑处理集中在一个组件,维护也比较方便。固然,若是一级卡片很是复杂,或者数据须要大量的处理,须要根据状况把最细的进行合并。

新功能下添加新属性/新文件

对于通用类型组件,咱们要求它尽量的短小精悍,调用起来更为简单,因此不能设计太多的参数。基础组件库不能符合这个要求,
主要是由于基础组件库须要尽量增长普适性,不会由于没有某个经常使用的属性,致使该组件须要复制一份重写,再加上日积月累的 pull request,
属性和参数必然会愈来愈多。而咱们在业务中使用,彻底不须要这么多的配置,若是有重大差异,从新复制一份,对于后续的维护反而更方便。
因此是否新增长属性仍是拷贝一份,是根据后续该组件是否会产生比较大的发展方向差别来决定的。

Vue 组件之间的交互设计

Vue 组件与 React 组件有比较大的区别,模板的设计更偏向于 HTML,因此要实现相似 react 的高阶组件的需求一般比较少,
而高阶组件集成度太高,对于业务来讲,当业务愈来愈复杂,组件内部逻辑将拆分困难,未必是件好事,因此咱们只讨论普通的组件设计。
组件设计是考虑组件通信方式,主要分为如下几个方面:向下传值,向上传值,伪双向绑定,方法调用。

数据流转

向下传值

向下传值就是父级传给子级数据。前面已经提到了,在 props 传值尽可能对传入数据进行类型校验,保证尽快发现问题。除此以外,也有一些注意事项。
传值类型若是是引用类型的 Object 类型,那么尽可能给它默认值,防止 undefined。

default: () => ({})

其次,父级在赋值时,不要使用 a=newData 这种写法,而是使用 Object.assign 来保证能准确触发组件更新。
还有另一种方式,但不方便声明全部对象内的数据时,可使用 this.$set(this, 'key', newData),保证对象必定会被监听到。

向上传值

Vue 2.0 须要使用 $emit 进行事件向上冒泡, 父组件进行事件的监听就能够进行处理。

伪双向绑定

Vue 2.0 提供了语法糖,支持双向绑定,使得Vue 进行双向传递数据极为方便,不须要既向上传值又向下传值。
固然它不是真正的绑定,而是封装了以前提到的向下传值和向上传值,简单的语法糖。它分为两类:v-model 和 .sync 修饰符
数据传递支持各类类型,不过建议传递的数据使用数组而不要使用对象类型,对象类型可能会出现渲染监听失败的问题。

v-model

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)
      }
    }
  })
}

要的本质是使用 prototype 设置独立变量,而后使用 mixins 注入相关的方法。能够看到,基本上每一个生命周期都会处理到。
mixin 不只使用在插件中,直接使用也是能够的。关于 mixins 可看官方文档:https://cn.vuejs.org/v2/guide/mixins.html.

事件与属性透传

以前提到组件尽量参数少,但参数过少,组件没法实现某些定制化的要求,而咱们组件可能有多个层次,
这种状况下咱们须要将当前组件的父组件的其余属性透传给子组件,将父组件其余事件监听给子组件,写法以下:

<div name="main">
  <input v-on='$listeners' v-bind="$attrs" />
</div>

其余注意事项

DOM 操做

正常状况下是不推荐业务组件直接操做 DOM 的,但有时候要写组件监听事件,这种状况下必定要注意在 destroyed 时候进行 removeEventListener。

相关文章
相关标签/搜索