开篇
组件是前端开发必不可少的利器,当下流行的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事件的分类以及常用区别,然而Vue为dom元素绑定事件正是采用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创建的独立组件之间互不影响。
- 创建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);
- 引用
<!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 入门实例教程