组件在当今在前端使用非常频繁,前端组件化开发最早源于react,有了组件化开发之后让前端开发如同堆积木一样开发应用,某种程度上提高了组件的复用性及可扩展性。那什么是组件内呢?组件其实就是数据和方法的简单封装。其实前端组件多数是把视图、逻辑、样式做对应的封装。
确定组件调用风格
-
无论是组件也好还是库也罢,其实最想做的一件事情就是做封装,只是他们的封装会针对于一些特定功能。比如函数是对代码块的封装,js库是对于某些功能的封装,组件封装集成化会更强。言而总之,都是对于逻辑的抽象。对于封装其实都会遵循设计原则里的开闭原则(Open Closed Principle)。开闭原则是一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭。
-
组件在设计之初,我应该关注2件事情。1.组件内部尽量做到封闭 。2.为了让组件在使用层面灵活,应该尽可能提供更多的api及调用方式,让组件使用起来更加顺畅。例如juery会对外暴露一个
$
符号。上面会提供各种属性及方法供外部使用。同样前后端交互中axios库,为了让开发人员在开发中更灵活的使用,对外暴露了axios(config)
同样也提供了axios.get
、axios.post
、axios.put
等等多种方式的调用。vue也是如此,如要通过new Vue(config)
来进行调用,内部组件通过自定义标签形式调用。 -
至于到底要如何确定调用风格,没有绝对的定义。原则是能够让使用者更方便调用你对外暴露的api就好了。今天我们以element ui里的MessageBox 弹框为例用原生js模拟封装。调用风格我们确定为:1、
new MessaegBox(config)
的形式来调用。 2.通过类似vue自定义标签形式来进行调用如<message-box></message-box>
。长的样子如下:
根据需求确定配置
- 以调用为入口,根据需求来确定目前我们要写的messageBox需要有哪些配置。比如当前我的messagebox我想实现点击弹出、点击确定按钮关闭弹框且调用确定的回调、自定义标题、自定义内容等等,这里的配置当然是根据需求来定,也是要看你要把组件写的灵活程度了。根据上述需求,配置列表如下:
{
width: "30%",
height: "250px",
title: "测试标题",
content: "测试内容",
dragable: true, //是否可拖拽
maskable: true, //是否有遮罩
isCancel:false //是否有取消
}
定义组件类合并配置
- 确定好了组件调用方式及配置项之后可以抽离组件为messageBox类来封装组件逻辑。传入配置。由于组件目前调用方式通过实例化形式:
//调用messageBox组件
new MessageBox({
width: "30%",
height: "250px",
title: "测试标题",
content: "测试内容",
dragable: true, //是否可拖拽
maskable: true, //是否有遮罩
isCancel:false //是否有取消
})
在外部调用过程中,为了让调用更加灵活,作为用户配置可以传也可以不传入所有配置。故在内部接收到配置后我们需要给一个defaultOpts 且需要将 defaultOpts 和 外部传入配置来进行合并。如下:
export default class MessageBox {
constructor(opts) {
let defaultOpts = {
width: "30%",
height: "250px",
title: "测试标题",
content: "测试内容",
dragable: true, //是否可拖拽
maskable: true, //是否有遮罩
isCancel: false //是否有取消
}
this.opts = Object.assign(defaultOpts, opts);
}
}
如此就实现了配置的合并
创建组件dom结构及样式
- 由于要封装html及样式,当然我们可以通过js动态生成dom及css结构。但是有了webcomponent之后可以很方便的通过原生办到。如下,定义 MessageBox相关dom结构及css样式:
const template = document.createElement('template');
template.innerHTML = `
<style>
.k-dialog {
width: 30%;
z-index: 2001;
display: block;
position: absolute;
background: #fff;
border-radius: 2px;
box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
margin: 0 auto;
top: 15vh;
left:30%;
}
.k-wrapper {
position: fixed;
left: 0px;
top: 0px;
bottom: 0px;
right: 0px;
background: black;
opacity: 0.4;
z-index: 2000;
}
.k-header {
padding: 20px 20px 10px;
}
.k-header .k-title {
line-height: 24px;
font-size: 18px;
color: #303133;
float: left;
}
.k-body {
padding: 30px 20px;
color: #606266;
font-size: 14px;
}
.k-footer {
padding: 10px 20px 30px;
text-align: right;
}
.k-close {
color: #909399;
font-weight: 400;
float: right;
cursor: pointer;
}
.k-cancel {
color: #606266;
border: 1px solid #dcdfe6;
text-align: center;
cursor: pointer;
padding: 12px 20px;
font-size: 14px;
border-radius: 4px;
font-weight: 500;
margin-right: 10px;
}
.k-cancel:hover {
color: #409eff;
background: #ecf5ff;
border-color: #c6e2ff;
}
.k-primary {
border: 1px solid #dcdfe6;
text-align: center;
cursor: pointer;
padding: 12px 20px;
font-size: 14px;
border-radius: 4px;
font-weight: 500;
background: #409eff;
color: #fff;
margin-left: 10px;
}
.k-primary:hover {
background: #66b1ff;
}
.k-input{
width: 100%;
margin-left: 20px;
margin-bottom: 20px;
}
.input-inner {
-webkit-appearance: none;
background-color: #fff;
background-image: none;
border-radius: 4px;
border: 1px solid #dcdfe6;
box-sizing: border-box;
color: #606266;
display: inline-block;
font-size: inherit;
height: 40px;
line-height: 40px;
outline: none;
padding: 0 15px;
transition: border-color .2s cubic-bezier(.645, .045, .355, 1);
width: 100%;
margin-top: 20px;
}
</style>
<div class="k-wrapper"></div>
<div class="k-dialog">
<div class="k-header">
<span class="k-title">提示</span><span class="k-close">X</span>
</div>
<div class="k-body">
<span>这是一段文本</span>
<input class="input-inner" type="text" />
</div>
<div class="k-footer">
<span class="k-cancel">取消</span>
<span class="k-primary">确定</span>
</div>
</div>
`;
定义自定义组件,创建自定义组件:
class MessageBoxele extends HTMLElement{
constructor(){
super();
console.log(this);
this._shadowRoot = this.attachShadow({ mode: 'open' });
this._shadowRoot.appendChild(template.content);
}
}
customElements.define("message-boxele", MessageBoxele);
通过MessageBox类createDom方法来创建dom结构,通过open来控制messagebox显示:
export default class MessageBox {
constructor(opts) {
let defaultOpts = {
width: "30%",
height: "250px",
title: "测试标题",
content: "测试内容",
dragable: true, //是否可拖拽
maskable: true, //是否有遮罩
isCancel: false //是否有取消
}
this.opts = Object.assign(defaultOpts, opts);
this.createDom();
}
createDom(){
let MessageBoxEle = document.createElement("message-boxele");
document.body.appendChild(MessageBoxEle);
MessageBoxEle.style.display = "none";
this.MessageBoxEle = MessageBoxEle;
}
open(){
this.MessageBoxEle.style.display = "block";
}
}
根据配置动态显示自定义标题及内容
通过自定义组件属性来进行配置的传入,传入自定义属性:
createDom(){
let MessageBoxEle = document.createElement("message-boxele");
MessageBoxEle.style.display = "none";
// 将参数配置以属性形式传入自定义组件内部
MessageBoxEle.width=this.opts.height;
MessageBoxEle.title = this.opts.title;
MessageBoxEle.content = this.opts.content;
MessageBoxEle.isCancel = this.opts.isCancel;
this.MessageBoxEle = MessageBoxEle;
document.body.appendChild(MessageBoxEle);
}
在messageBox自定义组件内部设置各种属性,如下:
set width(newValue) {
this._shadowRoot.querySelector(".k-dialog").style.width = newValue;
}
set height(newValue) {
this._shadowRoot.querySelector(".k-dialog").style.height = newValue;
}
set title(newValue) {
this._shadowRoot.querySelector(".k-title").innerHTML = newValue;
}
set content(newValue) {
this._shadowRoot.querySelector(".k-body span").innerHTML = newValue;
}
set isCancel(newValue) {
if (!newValue) {
this._shadowRoot.querySelector(".k-footer").removeChild(this._shadowRoot.querySelector(".k-cancel"))
}
}
set maskable(newValue) {
console.log(newValue);
if (newValue) {
this._shadowRoot.querySelector(".k-wrapper").style.display = "block";
} else {
this._shadowRoot.querySelector(".k-wrapper").style.display = "none";
}
}
设置组件是否可以拖拽
canDragable() {
let dailog = this._shadowRoot.querySelector(".k-dialog");
dailog.onmousedown = e => {
let x = e.clientX - dailog.offsetLeft;
let y = e.clientY - dailog.offsetTop;
dailog.onmousemove = e => {
let xx = e.clientX;
let yy = e.clientY;
dailog.style.left = xx - x + "px";
dailog.style.top = yy - y + "px";
}
}
document.onmouseup = function () {
dailog.onmousemove = "";
}
}
set dragable(newValue) {
if (newValue) {
this.canDragable();
}
}
最后添加组件对应的事件
可以通过观察者模式添加及触发事件,由于HTMLElement已经继承了EventTarget类所以我们可以直接观察事件及触发事件 , 将事件委托给 k-dailog容器。如下:
this._shadowRoot.querySelector(".k-dialog").addEventListener("click", e => {
switch (e.target.className) {
case 'k-close':
this.dispatchEvent(new CustomEvent("close"))
break;
case 'k-cancel':
this.dispatchEvent(new CustomEvent("cancel"));
break;
case 'k-primary':
let value = this._shadowRoot.querySelector(".input-inner").value;
this.dispatchEvent(new CustomEvent("primary", {
detail: value
}));
break;
}
})
事件绑定就可以在MessageBox类里:
addEvent() {
this.MessageBoxEle.addEventListener("close", e => {
this.MessageBoxEle.style.display = "none";
})
this.MessageBoxEle.addEventListener("cancel", e => {
this.MessageBoxEle.style.display = "none";
})
this.MessageBoxEle.addEventListener("primary", e => {
console.log(e.detail);
this.opts.success(e.detail);
this.MessageBoxEle.style.display = "none";
})
}
至此,一个基础webComponent的原生组件封装完毕。框架盛行的年代框架虽然香,但回头 看看原生js,多数情况下依旧可以给我们解决问题 。 总而言之,言而总之,js真香!!各位老铁,点赞关注来一波。谢谢给位老铁 !!!!!