前提概要
了解和学习原生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.我们根据上文提到到内容
template和shoadow 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;
}
}
}
}