微信号:frontshow

介绍:InfoQ大前端技术社群:囊括前端、移动、Node全栈一线技术,紧跟业界发展步伐。

如何构建出色的Vue组件?

2018-07-04 17:47 邵思华 译
作者|KEVIN BALL
译者|邵思华
编辑|覃云
很少有开发人员在上手编写 Vue 组件时,就考虑到将组件开源化的问题。大部分开发者都是为了自己的项目编写组件,在某些场景下决定通过编写组件的方式处理问题。但另一方面,由于大部分组件都是从某个特定场景开始逐步演化出来的,因此许多组件在 Vue 生态系统中表现并不“出色”。作者将通过本文为读者介绍构建优秀 Vue 组件的最佳实践。

从一方面来说,这是种理想的方式,它意味着 Vue 开发者能够使用越来越多的开源组件(在 npmjs.com 上搜索“vue”,会查到 12000 多个包,点击 https://www.npmjs.com/search?q=vue)。

但另一方面,由于大部分组件都是从某个特定场景开始逐步演化出来的,而且并不是每个开发者都具备设计可跨平台重用的组件的经验,因此许多组件在 Vue 生态系统中表现并不“出色”。

那么,“出色”的定义是什么呢?从较高层次来说,这意味着 Vue 开发者在使用组件时感觉自然,易于扩展,并且能够与各种应用进行集成。

通过对大量的开源组件进行调查后,我认为一个出色的 Vue 组件需要实现以下几点:

  1. 实现 v-model 兼容性

  2. 对事件透明

  3. 为恰当的元素赋予属性

  4. 拥抱浏览器标准,实现键盘导航功能

  5. 优先选择事件,而不是回调

  6. 限制组件内样式的使用

实现 v-model 兼容性

某些组件主要是为表单字段所设计的,包括搜索自动完成、日期选择字段,或是为简单的字段添加额外的功能,使组件的使用者能够添加数据属性。为了使所设计的组件符合使用标准,最重要的一种方式就是支持 v-model。

根据 Vue 组件开发指南(链接:https://vuejs.org/v2/guide/components.html#Using-v-model-on-Components),实现组件的 v-model 本质上只需传递一个 value 属性,并提供一个 input 事件处理器。

举例来说,假设开发者打算为输入框实现一个日期选择器的封装,则需要通过 value 这个 prop 对日期选择器进行初始化,并在选中状态下实现一个 input 事件,以下面的代码为例:

import datepicker from 'my-magic-datepicker';

export default {
  props: ['value'],
  mounted() {
    datepicker(this.$el, {
          date: this.value,
          onDateSelected: (date) => {
            this.$emit('input', date);
      },
    });
  }
}
对事件透明

为了支持 v-model,组件必须实现 input 事件。但其他事件,例如单击、键盘输入又该如何处理呢?原生的事件会基于 HTML 元素进行冒泡,而 Vue 的事件处理默认不会产生冒泡行为。

举例来说,如果不做某些特殊处理,以下代码是无法正常工作的:

<my-textarea-wrapper @focus="showFocus">

showFocus 事件处理器在正常情况下不会被调用,除非在封装组件的代码中暴露 focus 事件。不过,Vue 为开发者提供了一种以编程方式访问某个组件的事件监听者的功能,因此我们可以将监听方法赋予适当的对象,即 $listeners 对象。

只需稍稍思考一下,原因就会不言自明:这种方式能够让开发者在组件中的合适位置传递事件监听器。比方说,在 textarea 封装组件中可以这样写:

<div class="my-textarea-wrapper">
  <textarea v-on="$listeners" ></textarea>
</div>

这样一来,在 textarea 中产生的事件就能够正常传递了。

为恰当的元素赋予属性

开发者应当如何处理属性呢?例如 textarea 的 rows,或是为任意的元素添加一个 title 属性以显示提示。

在默认情况下,Vue 会识别出添加在组件上的属性,并将其应用在组件的根元素上。这种行为在大部分情况下符合开发者的期望,但也有例外。如果我们再回顾一下上文所展示的 textarea 封装组件,就会发现更自然的处理方式是将属性赋予 textare 本身,而不是 div 元素。

为了实现这一点,开发者需要告诉组件不要使用默认的方式添加属性,而是通过 $attrs 对象直接为目标元素添加属性。按以下方式编写 JavaScript:

export default {
  inheritAttrs: false,
}

然后在模板中这样写:

<div class="my-textarea-wrapper">
  <textarea v-bind="$attrs"></textarea>
</div>
拥抱浏览器标准,实现键盘导航功能

在 web 开发过程中,最容易被忽略的部分就是页面的可访问性和键盘导航功能。如果你希望写出能够完美贴合 web 生态的组件,这也是最重要的一点。

本质上说,这就意味着你需要确保组件符合浏览器标准:可以使用 tab 键选择表单字段,通常也可以使用回车键激活某个按钮或链接。

在 W3C 官网(链接:https://www.w3.org/TR/wai-aria-practices/#aria_ex)上可以找到为通用组件实现键盘导航功能的完整建议。如果开发者能够遵循这些建议,所构建的组件就能够适用于任何类型的应用程序,尤其适用于那些关心可访问性的用户。

优先选择事件,而不是回调

在组件与父组件进行数据通信以及用户交互时,通常有两种选择:在 props 中添加回调函数,或是使用事件。由于 Vue 的自定义事件不会像原生的浏览器事件一样产生冒泡,因此这两种选择在功能上是一致的。但是从组件重用性的角度来说,我始终优先推荐事件而不是回调。原因如下:

在 Fullstack Radio 的某期节目(链接: http://www.fullstackradio.com/87)中,Vue 的核心团队成员 Chris Fritz 给出了以下理由:

  1. 通过使用事件,使父组件能够了解的信息变得非常明确。它清晰地划分了“从父组件中获取的信息”和“发送给父组件的信息”这两种概念。

  2. 在某些简单的场景中,可以选择在事件处理器中直接使用表达式,以精简代码。

  3. 这种方式更符合标准习惯,Vue 的示例代码与文档都倾向于在组件与父组件进行通信时使用事件。

幸运的是,如果某个组件已经选择了在 props 中定义回调的方式,将其修改为暴露事件的方式也非常简单。使用回调的组件代码看起来像这样:

// my-custom-component.vue
export default {
  props: ['onActionHappened', ...]
  methods() {
    handleAction() {
      ... // your custom code
      if (typeof this.onActionHappened === 'function') {
        this.onActionHappened(data);
      }
    }
  }
}

然后以类似于这样的方式进行嵌入:

<my-custom-component :onActionHappened="actionHandler" />

开发者可参照以下代码将其修改为基于事件的方式:

// my-custom-component.vue
export default {
  methods() {
    handleAction() {
      ... // your custom code
      this.$emit('action-happened', data);
    }
  }
}

其父组件则对应进行修改:

<my-custom-component @action-happened="actionHandler" />
限制组件内样式的使用

Vue 定义的单文件组件结构允许开发者在组件中直接嵌入样式,尤其在结合了组件应用范围的情况下,使开发者能够创建出完全封装的,具备完整样式的组件。并且这种组件不会影响到应用的其他部分。

由于这一系统的强大能力,开发者会不自觉地选择将所有样式都封装在组件中,构建出一个包含全部样式的组件。问题在于,没有任何应用的样式是完全相同的,这种组件或许在你的应用中表现得非常美观,但在其他应用中就显得一团糟。而且组件的样式往往是在全局样式表之后才开始加载,想要覆盖这些样式简直是一场恶梦。

为了避免这种情况的发生,我的建议是,如果某些 CSS 在结构上对于组件来说不是必需的(例如 color,border,shadow 等等),则应当从组件文件中去除,或是至少能够关闭这些样式。可以选择提供一个能够自定义的 SCSS partial,让使用者能够按照其意愿进行自定义。

不过,如果仅仅提供一个 SCSS 文件,这种方式仍然有一个缺陷。组件的使用者不得不在样式表的编译过程中引入这个 SCSS,否则就无法看到组件的样式。为了克服这一缺点,开发者可以通过一个 class 为样式提供控制范围。如果 SCSS 使用了 mixin 的结构,开发者就能够按照与使用者相同的方式利用这个 SCSS partial,以实现更多的自定义样式。

<template>
  <div :class="isStyledClass">
    <!-- my component -->
  </div>
</template>

随后在 JavaScript 中这样实现:

export default {
  props: {
    disableStyles: {
      type: Boolean,
      default: false
    }
  },
  computed: {
    isStyledClass() {
    if (!this.disableStyles) {
      return 'is-styled';
    }
  },
}

接下来:

@import 'my-component-styles';
.is-styled {
  @include my-component-styles();
}

通过这种方式,组件自带的样式仍然按照开发者的想法呈现,如果使用者需要对其进行自定义的修改,也无需通过更高优先级的选择器进行覆盖。只需将 disableStyles 这个 prop 设置为 true 即可关闭默认的样式,随后选择按照自己的设置使用预定义的 mixin,或是完全从头开始编写样式。

  原文链接

https://vuejsdevelopers.com/2018/06/18/vue-components-play-nicely

 课程推荐


前端之巅

「前端之巅」是 InfoQ 旗下关注大前端技术的垂直社群。紧跟时代潮流,共享一线技术,欢迎关注。

 
前端之巅 更多文章 百度新发布的智能小程序是什么东西? 28个顶级的React UI组件库,请查收! 这10个优质技术公众号,你都关注了吗? 为什么说WebAssembly让Web开发的未来更美好? Udacity也弃用React Native了!
猜您喜欢 预告 · 2050 | 我们要在这场科技大会上聊开源 美美的商务范儿——ggplot2蝴蝶图 Swan,另一个长期运行任务的调度器(转给Mesos\/Marathon爱好者) 教你如何自己写一个微信小游戏「跳一跳」外挂 一种基于Lucene的实时搜索方案