基于原生webComponent封装组件

3,513 阅读5分钟

组件在当今在前端使用非常频繁,前端组件化开发最早源于react,有了组件化开发之后让前端开发如同堆积木一样开发应用,某种程度上提高了组件的复用性及可扩展性。那什么是组件内呢?组件其实就是数据和方法的简单封装。其实前端组件多数是把视图、逻辑、样式做对应的封装。

确定组件调用风格

  • 无论是组件也好还是库也罢,其实最想做的一件事情就是做封装,只是他们的封装会针对于一些特定功能。比如函数是对代码块的封装,js库是对于某些功能的封装,组件封装集成化会更强。言而总之,都是对于逻辑的抽象。对于封装其实都会遵循设计原则里的开闭原则(Open Closed Principle)。开闭原则是一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭。

  • 组件在设计之初,我应该关注2件事情。1.组件内部尽量做到封闭 。2.为了让组件在使用层面灵活,应该尽可能提供更多的api及调用方式,让组件使用起来更加顺畅。例如juery会对外暴露一个 $符号。上面会提供各种属性及方法供外部使用。同样前后端交互中axios库,为了让开发人员在开发中更灵活的使用,对外暴露了 axios(config) 同样也提供了 axios.getaxios.postaxios.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真香!!各位老铁,点赞关注来一波。谢谢给位老铁 !!!!!