vue组件开发思路整理

613 阅读7分钟

开篇

组件是前端开发必不可少的利器,当下流行的Vue就是组件框架。

在现有element-ui框架的加持上,Vue项目的开发似乎变得更加的便捷和清爽。然而实际现状究竟如何?看到客户提供的设计稿总会发现,与现有的UI框架提供的组件差异还不小。

框架现状

流行越广的框架越没有针对性。为了更大范围的吸引来自不同行业的用户,因此必须牺牲行业的特殊性而保留普遍性。因而流行越广的框架是无法直接完成行业开发需求。独立维护一套框架对自身和公司来说,消耗的成本和精力也是巨大的。

解决措施

面对不同行业的差异性需求开发,只能在开发团队内部弥补。在现有基础组件之上,二次开发或者独立开发应用于行业的组件变得尤为重要。本文整理了笔者在对elmenet-ui组件库的理解分析基础之上对组件开发思路的收集。

base组件

base组件指的是基础的HTML控件或者被二开的框架组件,本文着重讲解基于HTML控件开发,组件二开的思路也是同样。
<input>为例:

  • 触发事件并传递给父组件
  • 接受来自父组件的参数

事件触发

如下代码所示,监听input的change事件

<input @change="handleChange" >

@是v-on的缩写,绑定事件监听器。事件类型由参数指定。用在普通元素上,只能监听原生DOM事件。用在自定义元素组件上时,也可以监听子组件触发的自定义事件
上面的代码也可以写成这样

<input v-on:change="handleChange" >

看到这里有读者可能会问input控件并没有change事件,v-on:change监听从何而来?

这里提一个js的基础知识概念:dom事件

dom事件

先来看下面的代码

<input id="btn" type="button" onclick="console.log('You clicked the button!');" value="Click" />

上面我们定义了一个按钮,并在按钮被点击后在控制台输出了一句话。代码效果我们不做赘述。
这就是所谓的dom0级的事件处理。
常用的另一种js操作写法如下:

document.getElementById('btn').onclick = function() {
            console.log('you clicked the button secondly!');
        };

同样是定义了一个按钮,并在按钮被点击后在控制台输出了一句话。代码效果同样不做赘述。
两种方式定义的事件处理,input标签js。但是实际的操作之后就会发现,控制台只输出了一句话。说明dom0级事件处理,后定义的事件会覆盖前定义的事件

下面再看dom2级的事件处理方式:

document.getElementById('btn').addEventListener('click', function() {
         console.log('I am clicked by dom2!');
 });       
document.getElementById('btn').addEventListener('click', function() {
         console.log('I am clicked by dom2 secondly!!');
});

代码执行结果是,两段文字都被打印。说明dom2级事件重复事件的定义并没有被覆盖掉。
上面简单介绍了dom事件的分类以及常用区别,然而Vuedom元素绑定事件正是采用dom2级事件的处理方式-事件监听addEventListener.

addEventListener()方法

设定一个事件监听器,当某一事件发生通过设定的参数执行操作。语法是:

addEventListener(event, function, useCapture)

  • 参数 event 是必须的,表示监听的事件,例如 click, touchstart 等,就是之前不加前缀 on 的事件。
  • 参数 function 也是必须的,表示事件触发后调用的函数,可以是外部定义函数,也可以是匿名函数。
  • 参数 useCapture 是选填的,填true或者false,用于描述事件是冒泡还是**捕获,true表示捕获,默认的false表示冒泡。

到这里,上面提出的监听问题也就已经说明白了。

<div @click="func">

实际上相当于

el.addEventListener('click', func)

事件传递

利用Vue自身提供的api$emit就可以完成
下面请看节选自element-ui组件库的部分源码

<input
    @focus="handleFocus"
    @change="handleChange"
>
...
handleFocus(event) {
        this.focused = true;
        this.$emit('focus', event);//事件传递
},
handleChange(event) {
  this.$emit('change', event.target.value);//值传递
}

v-on:在自定义元素组件上,通过监听子组件触发的自定义事件,获取event或者value

参数接收 与传递

这里的参数,不仅仅指的是常规意义上的参数,还可能是函数、dom 遵循Vue单向数据的设计标准,父组件向子组件传递props,但是子组件不能修改父组件传递来的props子组件只能通过事件通知父组件进行数据更改。
对于开发组件来说,根据不同的业务场景,需要依赖不同的传递参数来控制不同的控件属性,以达到我们封装组件的目的。

子组件传递 父组件接收

父组件通过监听子组件传递出的自定义事件从而获得子组件传递的event或者value

<!-- 子组件传递 -->
<input @change="handleChange" >
this.$emit('change', event.target.value);

<!-- 父组件接收 -->
<el-input @change="handleChange"></el-input>
...
handleChange(value){
    console.log(value)
}

父组件传递 子组件接收

父组件通过props传递参数给子组件,子组件接收响应参数的变化,修改自身属性。

// 父组件传递参数
<el-input
    type="number"
    v-model="model"
    :minlength="minVal"
    :maxlength="maxVal"
    :placeholder="label"/>
    ...
    data(){
        return {
            model:'',
            minVal:20,
            maxVal:80
            label:'请输入内容'
        }
    }

:v-bind的缩写,动态地绑定一个或多个attribute,或一个组件prop到表达式。
v-model在表单控件或者组件上创建双向绑定。它会根据控件类型自动选取正确的方法来更新元素。尽管有些神奇,但v-model本质上不错是语法糖。
v-model 默认会利用名为 value 的 prop 和名为 input 的事件。

//子组件接收参数
<input v-bind="$attrs" >
...
props: {
      value: [String, Number]
    }

看到这里有些读者或许会产生疑惑,父组件传递下来5个属性,子组件props只接收1个,并且在input标签里,也完全没有体现?

vm.$attrs

2.4.0新增

  • 类型:{[key:string]:string}
  • 只读
  • 详细:
    包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。

$attrs通俗的讲就是子组件继承父组件不作为prop传递的属性值,直接可以被子组件input标签识别,所以在input标签里,不存在自身属性值的prop双向绑定。而props中的value,正是利用了v-model的特性:v-model 默认会利用名为 value 的 prop 和名为 input 的事件。

vm.$listeners

2.4.0新增

  • 类型:{ [key: string]: Function | Array<Function> }

  • 只读

  • 详细: 包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用。

$listeners通俗的讲就是子组件继承父组件不含.native修饰器的事件监听 在子组件接收到参数的变化后,dom元素动态的响应数据修改。

内容分发

内容分发是实现组件开发必不可少的模块。正常来讲,组件的样式内容填充可能不是固定的,这时就需要把带有特殊标记的内容放在组件特定的位置处。slot应运而生。

  • Props: name-string,用于命名插槽。
  • Usage: <slot>元素作为组件模板之间的内容分发插槽。<slot>元素自身将被替换。
// 父组件通过带有名字的slot传递内容给子组件
<el-input
    placeholder="请输入内容"
    v-model="input4">
    <i slot="prefix" class="el-input__icon el-icon-search"></i>
</el-input>
//子组件通过`$slots.prefix`判断是否显示该插槽所在元素
//子组件通过具名插槽获取父组件传递的内容并替换自身
<span class="el-input__prefix" v-if="$slots.prefix || prefixIcon">
        <slot name="prefix"></slot>
        <i class="el-input__icon"
           v-if="prefixIcon"
           :class="prefixIcon">
        </i>
</span>

自定义组件

自定义组件涉及的内容上文已经介绍完毕,下面来总结自定义组件的开发流程,更确切的说是一个标准的自定义组件对外暴露那些内容?

  • Attributes

    对外:通过Attributes控制组件样式、组件内容显隐、局部事件触发
    对内:原有基础控件的基础属性+特殊业务场景下添加的属性

  • Slots -可以不存在

    对外:通过不同名称在组件内部分发不同内容
    对内:形成自身作用域

  • Events

    对外:继承基础控件Events+特殊业务封装事件
    对内:传递事件或者值

  • Methods

    对外:通过ref属性调用自定义组件内部函数,调用基础控件内部函数
    对内:自定义组件内部函数可以被外部调用。基础控件内部原有函数也可被调用
    整理上述内容如下图所示: 自定义组件开发思路整理

Web Components

谷歌公司主推的浏览器原生组件Web Components API。相比第三方框架,原生组件简单直接,符合直觉,不用加载任何外部模块,代码量小。

它通过标准化非侵入式的方式封装一个组件,每个组件有自身的HTML结构、CSS样式,JavaScript代码,它通过Shadow DOM在文档中创建一个完全独立于其他元素的DOM树,主文档和基于shadow DOM创建的独立组件之间互不影响。

  1. 创建web component
class UserCard extends HTMLElement {
  constructor() {
    super();
    var shadow = this.attachShadow( { mode: 'closed' } );
    
    var templateElem = document.getElementById('userCardTemplate');
    var content = templateElem.content.cloneNode(true);
    content.querySelector('img').setAttribute('src', this.getAttribute('image'));
    content.querySelector('.container>.name').innerText = this.getAttribute('name');
    content.querySelector('.container>.email').innerText = this.getAttribute('email');

    shadow.appendChild(content);
  }
}
window.customElements.define('user-card', UserCard);
  1. 引用
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
<user-card
  image="https://semantic-ui.com/images/avatar2/large/kristy.png"
  name="User Name"
  email="yourmail@some-email.com"
></user-card>
  
<template id="userCardTemplate">
  <style>
   :host {
     display: flex;
     align-items: center;
     width: 450px;
     height: 180px;
     background-color: #d4d4d4;
     border: 1px solid #d5d5d5;
     box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
     border-radius: 3px;
     overflow: hidden;
     padding: 10px;
     box-sizing: border-box;
     font-family: 'Poppins', sans-serif;
   }
   .image {
     flex: 0 0 auto;
     width: 160px;
     height: 160px;
     vertical-align: middle;
     border-radius: 5px;
   }
   .container {
     box-sizing: border-box;
     padding: 20px;
     height: 160px;
   }
   .container > .name {
     font-size: 20px;
     font-weight: 600;
     line-height: 1;
     margin: 0;
     margin-bottom: 5px;
   }
   .container > .email {
     font-size: 12px;
     opacity: 0.75;
     line-height: 1;
     margin: 0;
     margin-bottom: 15px;
   }
   .container > .button {
     padding: 10px 25px;
     font-size: 12px;
     border-radius: 5px;
     text-transform: uppercase;
   }
  </style>
  
  <img class="image">
  <div class="container">
    <p class="name"></p>
    <p class="email"></p>
    <button class="button">Follow John</button>
  </div>
</template>

</body>
</html>

以上web component代码示例来自阮一峰老师Web Components 入门实例教程,Web Component被称为能真正实现组件化开发的技术,Web Component可以取代你的前端框架吗?也曾在知乎热议。不过对于开发者来说,对示例代码理解的基础上深入内部分析理解其原理,掌握其开发思路,奔着一颗学习的心态去研究才最为重要,至于未来谁取代谁,时间会告诉我们答案。

总结

本文整理的是自定义组件的开发思路,使用的也都是基础的数据传递方法、事件监听处理方式。
文章内容有限,对部分细节也没有过多的展开,感兴趣的读者可以自行查阅相关资料获取。
读者如发现问题或者错误,欢迎随时指正。
文章参考链接:
MDN Web 文档
Vue官网
element-ui
Web Components 入门实例教程