从vue组件三大核心概念出发,写好一个组件【理论篇】

一个适用性良好的组件,一种是可配置项不少,另外一种就是容易覆写,从而扩展功能css

Vue 组件的 API 来自三部分——prop、事件和插槽:html

  • prop 容许外部环境传递数据给组件
  • event 容许从组件内触发外部环境的反作用
  • slot 容许外部环境将额外的内容组合在组件中

prop

组件具备自身状态,当没有相关 porps 传入时,使用自身状态完成渲染和交互逻辑;当该组件被调用时,若是有相关 props 传入,那么将会交出控制权,由父组件控制其行为前端

仅一个值传入组件

  • 若是该组件设计上支持双向绑定,可以使用v-model将该参数传入组件,减小记忆成本(毕竟 vue 官方的语法糖,不用白不用)
<my-component v-model="foo" />
复制代码
  • 若是该组件能够独立运行,不依赖父组件时,仍是给这个值起个名字吧
<component-no-sync :childNeed="foo" />
复制代码

不少值须要传入组件

好比当一个组件有诸多配置项,且当没有传入配置项取用组件内部默认项的时候,咱们原先的父组件写法:vue

<child-component :prop1="var1" :prop2="var2" :prop="var3" ... />
复制代码

其实能够在父组件上直接使用v-bind={子组件props集合}git

可是为了方便覆写子组件的内部配置项,不妨使用一个对象将配置收集到一块儿,可是这种作法不能利用 props 验证对象里面每一个的值类型github

<child-component v-model="text" :setting="{color:'bule'}" />

// 子组件内部读取配置,经过扩展运算符替换掉默认配置
const setting ={
  ...defaultSetting,
  ...this.setting
}
复制代码

computed 属性

vue 的 computed 属性默认是只读的,你能够提供一个 setter。它能够优化我写组件的逻辑,适用于父组件处理的值和子组件处理的值是同一个的状况web

<template>
  <el-select v-model="email">
    <el-option v-for="item in adminUserOptions" :key="item.email" :label="item.email" :value="item.email" />
  </el-select>
</template>
复制代码
export default {
  props: {
    value: {}
  },
  computed: {
    email: {
      get() {
        return this.value
      },
      set(val) {
        this.$emit('input', val)
        this.$emit('change', val)
      }
    }
  }
}
复制代码

灵活的 prop

咱们常看到一些优秀的组件库,传入的值既能够是一个 String/Number,也能够是一个函数。element-ui

好比ElementUITable组件,当你想要显示树形数据的时候,必须传入row-key。看它的介绍就知道是有多灵活:微信

row-key的做用:行数据的 Key,用来优化 Table 的渲染;在使用 reserve-selection 功能与显示树形数据时,该属性是必填的。类型为 String 时,支持多层访问:user.info.id,但不支持 user.info[0].id,此种状况请使用 Function数据结构

处理 rowKey 生成 RowIdentity 的函数源码:

//https://github.com/ElemeFE/element/blob/dev/packages/table/src/util.js
export const getRowIdentity = (row, rowKey) => {
  if (!row) throw new Error('row is required when get row identity')
  // 行数据的key
  if (typeof rowKey === 'string') {
    if (rowKey.indexOf('.') < 0) {
      return row[rowKey]
    }
    // 支持多层访问:user.info.id
    let key = rowKey.split('.')
    let current = row
    for (let i = 0; i < key.length; i++) {
      current = current[key[i]]
    }
    return current
    // 经过函数自定义
    // 我处理过父和子id可能相同的状况,只好经过Function自定义
    // 不能够经过时间或者随机字符串生成ID
  } else if (typeof rowKey === 'function') {
    return rowKey.call(null, row)
  }
}
复制代码

因为业务场景多变,组件的设计者很难考虑彻底,不妨设计灵活的 prop,由开发者自行定义

其余

当组件有 prop 传入的时候,尽可能考虑一下,当 prop 变化的时候,组件是否可以响应 prop 的变化。也就是说使用 prop,是否使用了计算属性,或者 watch 了 props 的值。

事件

emit/on

读者确定知道 emit/on 如何使用,我就简单说一下 vue 的 v-modelsync的语法糖,咱们能够利用这些语法糖,帮助咱们写出简洁的代码(父组件能够少写监听子组件的事件,好比你不用写@input)

v-model

看一下下面的代码示例,就能懂这句话了。v-model 会忽略全部表单元素的 value、checked、selected 特性的初始值而老是将 Vue 实例的数据做为数据来源。你应该经过 JavaScript 在组件的 data 选项中声明初始值

<input v-model="searchText" />

<input v-bind:value="searchText" v-on:input="searchText = $event.target.value" />

// 当把v-model用在组件上

<custom-input v-bind:value="searchText" v-on:input="searchText = $event" ></custom-input>
复制代码

为了让它正常工做,这个组件内的 <input> 必须:将其 value 特性绑定到一个名叫 value 的 prop 上在其 input 事件被触发时,将新的值经过自定义的 input 事件抛出,即this.$emit('input',changedValue)

自定义 v-model

为啥要自定义组件的 v-model 呢,由于数据不符合要求呗。你的输入值不可能老是 value ,你的事件不可能老是 input,具体详见文档

sync(双向绑定语法糖)

vue 真的是方便了开发者不少,站在开发者的角度考虑,很大的提高开发效率

以  update:myPropName  的模式触发事件取代双向绑定this.$emit('update:title', newTitle),具体详见文档

Function 经过 prop 传入

原本想放在 prop 部分的,可是我的以为其实它和 emit/on 更有关系一点

有读者可能会问,为何不能把子组件里面的事件 emit 出来,经过父组件处理?而后传入一个控制子组件的 prop 属性。

我想说的是,能够,可是这样真的很麻烦,子组件内部的状态却要依赖父组件传值。

该组件内部的状态,咱们须要把它暴露出来嘛?我以为不须要,组件内部的状态就让它处于组件内部

可是能够经过传入 function(你能够理解为一个钩子),参与组件状态变动的行为。好比很好用的拖拽库,Vue.Draggable控制元素是否被拖动的行为。

Vue.Draggable能够传入一个 move 方法,咱们看一下它如何处理的。

onDragMove(evt, originalEvent) {
      const onMove = this.move;
      // 若是没有传入move,那么返回true,能够移动
      if (!onMove || !this.realList) {
        return true;
      }

      const relatedContext = this.getRelatedContextFromMoveEvent(evt);
      const draggedContext = this.context;
      const futureIndex = this.computeFutureIndex(relatedContext, evt);
      Object.assign(draggedContext, { futureIndex });
      const sendEvt = Object.assign({}, evt, {
        relatedContext,
        draggedContext
      });
      // 组件行为由传入的move函数控制
      return onMove(sendEvt, originalEvent);
}
复制代码

这样作的好处,就是组件内部自由一套运行逻辑,可是我能够经过传入 function 来干预。我没有直接修改组件内部状态,而是经过函数(你能够称它为钩子)去触发,方便调试组件,使得组件行为具备可预测性

父组件直接操做子组件

不多有这样的骚操做,可是因为数据和操做的复杂性,当数据结构复杂,嵌套过深的状况下,父组件很难对于子组件的数据的精细控制

所以,若是不得已而为之,请在文档里,把子组件能够调用的方法暴露出来,供使用者使用。使用这种组件比较麻烦,得去看文档,没有文档的只好去看源码

ElementUItree组件提供了不少方法,用于父组件去操做子组件。

eg:this.$refs.tree.setCheckedKeys([]);

插槽

HTML <slot> element 是 Web Components 技术的一部分,是自定义 web 组件的占位符,vue 里面的 slot 的灵感来自 Web Components 规范草案,具体见文档

默认插槽

能用默认插槽就不要使用具名插槽,我真的不想使用你这个组件的时候还去翻看你的插槽叫什么名字

以前我司一个网页模板 三个插槽,header,body,footer,我用的是真的难受,每次都记不得,看似三个单词都挺熟悉的,可是其实 head,content,foot 这些单词也都行啊,谁知道用啥(可能我老了吧,组件若是不是必要尽可能不要让人有记忆成本)。

后备内容

就是给组件里面的插槽定义默认值,它只会在没有提供内容的时候被渲染。建议用上插槽就给它添加默认内容

封装他人组件

有些时候咱们多是对他人的组件进行封装,这里强烈推荐使用v-bind="$attrs" 和 v-on="$listeners"vm.$attrs 是一个属性,其包含了父做用域中不做为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。这些未识别的属性能够经过 v-bind="$attrs" 传入内部组件。未识别的事件可经过v-on="$listeners"传入

举个例子,好比我建立了个人按钮组件myButton,封装了 element-ui 的 el-button 组件(其实什么事情都没作),在使用组件 <my-button />时,就能够直接在组件上使用 el-button 的属性,不被 prop 识别的属性会传入到 el-button 元素上去

<template>
  <div>
    <el-button v-bind="$attrs">导出</el-button>
  <div>
</template>
// 父组件使用
<my-button type='primary' size='mini'/>
复制代码

组件命名

这里推荐遵循 vue 官方指南,值得一看

咱们构建组件的时候一般会将其入口命名为 index.vue ,引入的时候,直接引入该组件的文件夹便可。

可是这样作会有一个问题,当你编辑多个组件的时候,全部的组件入口都叫作index.vue,容易糊涂

vscode 显然意识到了这个问题,因此当文件名相同的文件被打开时,它会在文件名旁边显示文件夹名

如何解决呢,咱们能够把 index.js 看成一个单纯的入口,不承担任何逻辑。仅仅负责引入component-name-container以及export default component-name-container

my-app
└── src
        └── components
                └── component-name
                    ├── component-name.css
                    ├── component-name-container.vue
                    └── index.js
复制代码

tips(我的喜爱)

  • template,把一个<template> 元素当作不可见的包裹元素,并在上面使用 v-if。最终的渲染结果将不包含 <template> 元素
  • 能用 computed 计算属性的,尽可能就不用 watch
  • 模板里面写太多 v-if 会让你的模板很难看,v-else-if尽可能仍是别用了吧。一长串的 if else,在模板里面看的很乱

关于我

一个一年小前端,关注个人微信公众号,和我一块儿交流,我会尽我所能,而且看看我能成长成什么样子吧。

微信公共号


你有什么写组件的独特技巧,不妨在评论区告诉我吧