怎么用web Components撸一个组件

怎么用web Components撸一个组件

技术杂谈小彩虹2021-08-20 10:01:11190A+A-

前提概要

了解和学习原生Web Components不仅可以掌握未来的组件化标准,还能帮助我们理解现有的前端框架。

Web Components学习总结

  • 如何基于Web Components调用实现组件
  • 如何根据实际需求配置封装一个组件
  • 如何做到根据自定义配置实现数据的动态实现
  • 如何给组件添加自定义事件处理

Web Components基础知识

  • Custom Elements
  • Html templates
  • shadow Dom
  • 存取器属性的学习与实战
  • EventTarget的自定义事件处理

必备基础知识回顾

理解存取器属性getter/setter

  • 下方案例一中当想要获取person.name的时候会调用相应的getter方法
  • 此例会在控制台上打印出this.val,但因为getter返回undefined,所以name:undefined
//案例一:
//在对象初始化之前可以通过这种方式
var person = {
  val: '名字',
  get name() {console.log(this.val);}, 
  set name(name) {this.val = name;} 
};

person.name

  • 案例二:
//在对象初始化之前可以通过这种方式
var attr={
        _x:10,
        get x() {
            return this._x+1;
        },
        set x(val) {
            this._x=val+2;
        }
    }
    
    //会调用getter x()
    console.log(attr.x); //11
	
    //会调用setter x()
    attr.x=21;

    console.log(attr.x);//24

  • 而ES5的对象原型的属性 __defineGetter__ __defineSetter__用来给对象已经定义之后,给对象的属性绑定新的get和set方法
//案例三
//!!!一定是在对象已经定义之后
//对象初始化之后可以这样添加属性的setter/getter
var person = {
  val: '名字'
}

person.__defineGetter__('name',function(){return this.val;});
person.__defineSetter__('name',function(name){this.val = name;})
console.log(person.name);  //名字
person.name = '新名字';     
console.log(person.name);  //新名字

  • 案例四:通过defineProperty给对象的属性绑定新的get和set方法
//对象初始化之后可以这样添加属性的setter/getter
var stu={
     _age:20,
     editor:1
}
Object.defineProperty(stu,"age",{
    get:function(){
        return this._age;
    },
    set:function(newage){
        this._age=newage;
        this.editor++;
    }
})

//调用setter方法
stu.age=200;
console.log(stu)

  • 案例五:
class Person{
        set name(value){
                console.log(value)
        }
};

//会触发setter
Person.name='xxxx'

var p=new Person();

//会触发setter
p.name='xx'

Web Components基础必备知识之Custom elements

两种custom elements(Autonomous custom elements和Customized built-in elements )

  • Autonomous custom elements 我称之为自定义自命名组件,是个独立的元素,它不继承其他内建的HTML元素。你可以直接把它们写成(像)HTML标签的形式来在页面上使用。例如<message-boxele></message-boxele> 或者是document.createElement('message-boxele')这样来创建使用。
  • Customized built-in elements 继承自基本的HTML元素。在创建时,你必须指定所需扩展的元素。使用时,需要先写出基本的元素标签,并通过is属性指定custom element的名称。例如<img src="#" is="my-img">或者 document.createElement("img", { is: my-img" })来创建使用。

接下来分别展示两种组件的基本使用模板方式

  • Customized built-in elements的使用
//html代码

<!-- 我称之为内建组件 -->
<img src="#" is="my-img">

//js代码

//第一步:
class Myimg extends HTMLImageElement{
    constructor(){
        
        //`Myimg`就是自定义元素的类
        //这个类的父类是`HTMLImageElement`
        //因此继承了 `HTML` 元素的特性。
        super() //必写

        console.log(this)
        //<img src="#" is="my-img">
        //this指向组件节点

        setTimeout(()=>{
          this.src='https://dss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3892521478,1695688217&fm=26&gp=0.jpg'
        },100)
    }
}



//第二步:

//使用浏览器原生的`customElements.define()`方法,
//告诉浏览器`<my-img>`元素与这个类关联。
window.customElements.define('my-img',Myimg,{extends:'img'});


  • Autonomous custom elements (自定义命名组件)的使用
//html页面代码

<body>
    <!-- 自命名组件 -->
    <my-com message='改变了内容'></my-com>
</body>

//js代码



//自定义命名组件可以统一继承HTMLElement
//第一步:
class MyCom extends HTMLElement{
    constructor(){
        super();

        //在哪里调用就在哪里生成
        //this指向<my-com>xx</my-com>
        console.log(this)


        //直接往自定义命名组件里添加dom结构是有局限的可以引入shadow root
        // this.innerHTML=`<button>点击</button>`


        //创建一个 shadow root
        //我们首先会将shadow root附加到custom element上
        //mode可以是open或者是closed,这定义了shadow root的内部实现是否可以被js访问及修改
        let _sd=this.attachShadow({mode:'open'});


        //然后通过一系列DOM操作创建custom element的内部影子DOM结构
        let button=document.createElement('div');

        button.innerHTML=`<button>点击</button>`;

		
        //再将其附加到 shadow root上
        _sd.appendChild(button);
        
        //获取自定义属性message的内容
        let message=this.getAttribute('message');
        
        //根据自定义属性message的内容改变<button>的值
        _sd.querySelector('button').innerHTML=message
    }
}


//第二步:
//第一个参数为自命名组件名,第二个参数为类对象
window.customElements.define('my-com',MyCom);
  • 改变前的<button>内容

  • 改变后的<button>内容

  • 拓展custom elements的四个生命周期
 class MyCom extends HTMLElement{
 	constructor(){
    	super()
    }
    
    
  connectedCallback(){ //建议在这里发送数据请求
              console.log('connectedCallback首次被插入到文档DOM时被调用')
  }
  disconnectedCallback(){
      console.log('disconnectedCallback当custom element从文档DOM删除时调用')
  }
  adoptedCallback(){
      console.log('adoptedCallback当custom element被移动到新文档时被调用')
  }
  attributeChangedCallback(){
      console.log('attributeChangedCallback当custom element增加、移除或更改自身属性时被调用')
  }
  
  
 }

Web Components基础必备知识之HTML templates

HTML templates的基本模板

<template>
	<style>
    	省略css代码
    <style>
    <div>
    	省略html代码
    <div>
</template>

组件基本实现:
let template=document.createElement('template');
template.innerHTML=`
    <style>
    	省略css代码
    <style>
    <div>
    	省略html代码
    <div>
`;

//html templates配合Shadow DOM的基本实现逻辑


class MessageBoxele extends HTMLElement{
  constructor(){
      super();

      // console.log(this) 
      //<message-boxele></message-boxele>

      //将shadow root附加到custom element上
      this._shadowRoot=this.attachShadow({mode:'open'});


      // console.log(this._shadowRoot)
      // 将html templates插入到这个Shadow DOM里面
      //获取`<template>`节点以后,克隆了它的所有子元素,
      //这是因为可能有多个自定义元素的实例,这个模板还要留给其他实例使用
      //所以不能直接移动它的子元素。
      this._shadowRoot.appendChild(template.content.cloneNode(true))
  }
}

window.customElements.define('message-boxele',MessageBoxele);

  • 该组件的html templates代码如下:
let 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>
`;

Web Components基础必备知识之巧用存取器属性

  • 1.我们先创建一个继承至HTMLElement的自定义自命名组件
  • 2.我们根据上文提到到内容templateshoadow dom创建出一个自定义组件
  • 3.思考题(下方'./test.js'会用到):什么情况下会触发自定义自命名组件中的setter呢?
class MessageBoxeles extends HTMLElement{
  constructor(){
        super();        
        this._shadowRoot=this.attachShadow({mode:'open'});
        this._shadowRoot.appendChild(template.content);      
  }
  
  
  
  /*思考在什么情况下会触发这里的setter呢?
  思路:MessageBoxeles 和 <message-boxeles></message-boxeles>
  已经关联到了一起,例如在改变 <message-boxeles> 的宽度时候会
  触发MessageBoxeles的set width()方法吗???
  
  
   set width(newValue){
        //拿到要设置的width后要根据这个值添加上
        // console.log(newValue,'setWidth')
        this._shadowRoot.querySelector(".k-dialog").style.width = newValue;
    }
    set title (newValue){
        this._shadowRoot.querySelector(".k-title").innerHTML = newValue;
    }
    set content(newValue){
        this._shadowRoot.querySelector(".k-body span").innerHTML= newValue;
    } 
  */
  
  
}

window.customElements.define('message-boxeles',MessageBoxeles);

  • 到这里为止在浏览器中已经可以直接通过添加<message-boxeles></message-boxeles>组件标签的方式呈现出组件了
  • 但是我们不是通过这种方式来把组件添加到页面上,而是通过let MessageBoxEles=document.createElement('message-boxeles');和document.body.appendChild(MessageBoxEles);的方式往页面添加组件。
  • 我们为了能够让组件动态的实现数据呈现我们可以定义一个名为MessageBoxs的类(通过export default导出),这个类的工作是接受一个对象格式的数据配置,根据这个配置来实现组件数据动态的实现。
  • 我们在相应的地方引入(import MessageBoxs from 'xx.js')这个名为MessageBoxs的类,通过new运算符实例化类并传入配置改变组件的数据

  • 第一步:引入这个名为MessageBoxs的类要如何调用这个类
'./test.js'文件中定义着这个导出类

import MessageBoxs from './test.js';



/*
这里是默认配置
在用户不传入配置或者缺少的时候需要合并使用

let defaultOpts={
    width: "30%",
    height: "250px",
    title: "测试标题",
    content: "测试内容",
    dragable: true, //是否可拖拽
    maskable: true, //是否有遮罩
    isCancel:false, //是否有取消
    success:function(){}
}

*/


//根据默认配置传入一个用户自定义的配置项
let messagebox=new MessageBoxs({
    width: "50%",
    title: "动态的自己传入的标题",
    content: "动态的自己传入的内容"
})
  • 第二步:如何封装这个名为MessageBoxs的类并实现动态的数据呈现(利用到了存取器属性)
这里是'./test.js'的页面代码(默认要暴露出名为`MessageBoxs`的类)


export default class MessageBoxs{
    constructor(opts){
        //默认配置--在用户不传入配置的时候合并使用
        let defaultOpts={
            width: "30%",
            height: "250px",
            title: "测试标题",
            content: "测试内容",
            dragable: true, //是否可拖拽
            maskable: true, //是否有遮罩
            isCancel:false, //是否有取消
            success:function(){}
        }

        //合并配置
        this.opts=Object.assign(defaultOpts,opts)
        //调用下方的creatDom方法
        this.creatDom()
    }

    creatDom(){
        let MessageBoxEles=document.createElement('message-boxeles');


        //例如当给MessageBoxEles添加宽度会触发上方正文中提到的
        //MessageBoxeles中的set width()方法
        //set width()方法会根据传入的新值去呈现出动态的数据
        MessageBoxEles.width=this.opts.width;
        MessageBoxEles.title = this.opts.title;
        MessageBoxEles.content = this.opts.content;

        //往页面添加该元素
        document.body.appendChild(MessageBoxEles);
        //挂载一个实例化对象的属性供下方open等地方使用
        //这里是<message-boxele></message-boxele>
        this.MessageBoxEles=MessageBoxEles;
        console.log(this)
    }
}

效果图:以set width()方法为例,这次的 MessageBoxEles.width=this.opts.width;触发了上文中自定义命名组件MessageBoxeles中的set width(newValue){this._shadowRoot.querySelector(".k-dialog").style.width = newValue;}该方法,并成功了根据用户传入的配置渲染出了动态数据。

Web Components基础必备知识之EventTarget

事件的监听和触发(类似发布-订阅模式)

//HTMLElement 已经是继承了 EventTarget
//可以直接使用发布订阅自定义事件
//所以可以使用--自定义事件.addEventListener和自定义事件.dispatchEvent


class Person extends EventTarget{
    constructor(){
        super();
        this.name = "lth";
        // this.height = "178cm";
    }
    get height(){
        console.log("get");
        return "178cm"
    }
    set height(newValue){
        console.log("set",newValue);
    }
}

let zhangsan = new Person();
// console.log(zhangsan);
console.log(zhangsan.height);  //get
// zhangsan.height = "180cm";   //'set',180cm

function fn1(e){
    // console.log("发布了这个事件");
    console.log(e)
    console.log("发布了这个事件",e.detail)
}


//监听了'customEvent'这个自定义事件
zhangsan.addEventListener("customEvent",fn1);


document.onclick  = function(){
    zhangsan.dispatchEvent(new CustomEvent("customEvent",{detail:7}))
}

  • 结合上文的案例的实现基础上给我们的组件加上一些自定义事件
  • 功能一点击关闭按钮时弹框消失;功能二:点击取消按钮时弹框消失;功能三:点击确定按钮时弹框消失并出现输入框的文字
  • 基本思路:先给上文提到的导出类MessageBox中的<message-boxele>上添加自定义事件,然后再给上文提到的自定义自命名组件MessageBoxele中加入判断语句(判断为:当点击弹框的时候甄别是点击了那一块并在<message-boxele>上触发(发布)已经监听的自定义事件)。(记住添加和发布自定义事件都在<message-boxele>上)

先给上文提到的导出类MessageBox中的<message-boxele>上添加自定义事件

export default class MessageBox {
	 constructor(opts) {
     	//默认配置
     	let defalutOpts = {
            width: "30%",
            height: "250px",
            title: "测试标题",
            content: "测试内容",
            dragable: true, //是否可拖拽
            maskable: true, //是否有遮罩
            isCancel: false, //是否有取消
            success:function(){}
        }
       //合并配置 
       this.opts={...defalutOpts,...opts}
     }
     
     createDom(){
     	let MessageBoxEle = document.createElement("message-boxele");
        MessageBoxEle.style.display = "none";
     	
        
        document.body.appendChild(MessageBoxEle);
        this.MessageBoxEle = MessageBoxEle;
        
        //给<message-boxele>上添加三个自定义事件
        //给<message-boxele>上添加三个自定义事件
        MessageBoxEle.addEventListener("close",function(){
                console.log("close");
                MessageBoxEle.style.display = "none";  
        })
        MessageBoxEle.addEventListener("cancel",function(){
            console.log("cancel");
            MessageBoxEle.style.display = "none";
        })
        
        MessageBoxEle.addEventListener("primary",(e)=>{
            console.log(e);
            // console.log("primary确定",this);
            this.opts.success(e.detail);
            MessageBoxEle.style.display = "none";
        })
     }

}

然后再给上文提到的自定义自命名组件MessageBoxele中加入判断语句(判断为:当点击弹框的时候甄别是点击了那一块并在<message-boxele>上触发(发布)已经监听的自定义事件)

class MessageBoxele extends HTMLElement {
    constructor() {
        super();
        //console.log(this);
        this._shadowRoot = this.attachShadow({mode:"open"});
        this._shadowRoot.appendChild(template.content);
        
        // let that = this;
        this._shadowRoot.querySelector(".k-dialog").onclick = (e)=>{
            switch(e.target.className){
                case 'k-close':
                    console.log(this);
                    this.dispatchEvent(new CustomEvent("close"));
                    break;
                case 'k-cancel':
                    this.dispatchEvent(new CustomEvent("cancel"));
                    break;
                case 'k-primary':
                    let val = this._shadowRoot.querySelector(".input-inner").value;
                    console.log(val);
                    this.dispatchEvent(new CustomEvent("primary",{
                        detail:val
                    }));
                    break;
            }
        }

    }
}

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们