如何构建出色的Vue组件?

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


 1. 实现 v-model 兼容性
 2. 对事件透明
 3. 为恰当的元素赋予属性
 4. 拥抱浏览器标准,实现键盘导航功能
 5. 优先选择事件,而不是回调
 6. 限制组件内样式的使用

实现 v-model 兼容性

某些组件主要是为表单字段所设计的,包括搜索自动完成、日期选择字段,或是为简单的字段添加额外的功能,使组件的使用者能够添加数据属性。为了使所设计的组件符合使用标准,最重要的一种方式就是支持 v-model。
举例来说,假设开发者打算为输入框封装,则需要通过 value 这个 prop 对输入框进行初始化,并在选中状态下实现一个 input 事件,以下面的代码为例:

子组件:
<template>
    <input :value="value" @input="change" />
</template>
<script>
export default {
    
    
    name: "myStar",
    props: {
    
    
        value: {
    
    
            // 必须是value属性,名字不能变
            type: [Number, String],
            required: true,
        },
    },
    methods: {
    
    
        change(e) {
    
    
            this.$emit("input", e.target.value); // 必须是input事件
        },
    },
};
</script>
父组件:
<template>
    <div class="parent">
        <users v-model="name"></users>
        <div>子传给父的:{
    
    {
    
    name}}</div>
    </div>
</template>

<script>
import users from "@/components/users";
export default {
    
    
    name: "test",
    components: {
    
    
       users,
    },
    data() {
    
    
        return {
    
    
            name: '王一博',
        };
    },
    watch: {
    
    
        name(val) {
    
    
            console.log('父:',val);
        },
    },
};
</script>

编写的组件内部还有一个使用v-model的组件,只需要把"v-model"替换为":value",这样功能即可正常使用
子组件也可以结合model

    // 组件内使用model更新值
    model: {
    
    
        prop: 'value',//父组件传下来的值与props相对应
        event: 'input',//事件:触发这个事件,从而改变父组件的v-model的值
    },

对事件透明

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

A组件与 C 组件之间的通信: (跨多级的组件嵌套关系)
在这里插入图片描述

A 组件
<template>
    <div class="app">
        <div>A组件:</div>
        <b-child :b="childB" :c="childC" @bfn="onTest1" @cfn="onTest2">
        </b-child>
    </div>
</template>
<script>
import BChild from "./BChild.vue";
export default {
    
    
    data() {
    
    
        return {
    
    
            childB: "b",
            childC: "C",
        };
    },
    components: {
    
     BChild },
    methods: {
    
    
        onTest1(val) {
    
    
            console.log("A组件接受:", val);
        },
        onTest2(val) {
    
    
            console.log("A组件接受:", val);
        },
    },
};
</script>
<style scoped>
.app {
    
    
    width: 200px;
    height: 300px;
    font-size: 20px;
    color: #f00;
    background: #000;
}
</style>

BChild.vue
<template>
    <div class="child-b">
        <p>在B组件:</p>
        <p>props: {
    
    {
    
     b }}</p>
        <p>$attrs: {
    
    {
    
     $attrs }}</p>
        <hr />
        <!-- C组件中能直接触发test的原因在于 B组件调用C组件时 使用 v-on 绑定了$listeners 属性 -->
        <!-- 通过v-bind 绑定$attrs属性,C组件可以直接获取到A组件中传递下来的props(除了B组件中props声明的) -->
        <c-child v-bind="$attrs" v-on="$listeners"></c-child>
    </div>
</template>
<script>
import CChild from "./CChild.vue";
export default {
    
    
    name:"BChild",
    props: ["b"],
    data() {
    
    
        return {
    
    };
    },
    inheritAttrs: false,
    components: {
    
     CChild },
    mounted() {
    
    
        this.$emit("bfn",'B传数据');
    },
};
</script>
<style scoped>
.child-b{
    
    
    background: coral;
    padding: 20px;
    margin: 20px;
}
</style>

CChild.vue:
<template>
    <div class="child-c">
        <p>在c组件:</p>
        <p>props: {
    
    {
    
     c }}</p>
        <hr />
    </div>
</template>
<script>
export default {
    
    
    props: ["c"],
    data() {
    
    
        return {
    
    };
    },
    inheritAttrs: false,
    mounted() {
    
    
        this.$emit("cfn", "c传数据");
    },
};
</script>
<style scoped>
.child-c {
    
    
    background: #fff;
}
</style>

因此我们可以将监听方法赋予适当的对象,即 $listeners 对象。原因就会不言自明:访问某个组件的事件监听者的功能,

为恰当的元素赋予属性

在默认情况下,Vue 会识别出添加在组件上的属性,并将其应用在组件的根元素上。
为了实现这一点,开发者需要告诉组件不要使用默认的方式添加属性**,而是通过 $attrs 对象直接为目标元素添加属性**。按以下方式编写 JavaScript:

inheritAttrs: false,

然后在模板中这样写:

<c-child v-bind="$attrs" ></c-child>

案例如上

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

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

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

W3C 官网上可以找到为通用组件实现键盘导航功能的完整建议。如果开发者能够遵循这些建议,所构建的组件就能够适用于任何类型的应用程序,尤其适用于那些关心可访问性的用户。

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

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

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

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

3. 这种方式更符合标准习惯,Vue 的示例代码与文档都倾向于在组件与父组件进行通信时使用事件。
 my-custom.vue
export default {
    
    
  methods() {
    
    
    handleAction() {
    
    
      this.$emit('action-happened', data);
    }
  }
}
其父组件则对应进行修改:
<my-custom @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,或是完全从头开始编写样式。

猜你喜欢

转载自blog.csdn.net/gao_xu_520/article/details/121144040