「这是我参与2022首次更文挑战的第20天,活动详情查看:2022首次更文挑战」。
前情回顾
- 前面几篇文章,我们介绍了 web components 的三大件,并且分别都写了一些小 demo 进行演示
- 自定义元素,可以让开发者根据需要自定义一些元素标签
- HTML 模板,可以让开发者根据需要构建特定的 dom 结构,并复用到其他需要的地方
- 影子 dom,可以让开发者在任意一个常规 dom 内添加一棵与外界隔离的 dom 树,这个特性非常适合组件封装
- 接下来,我们将使用 web components 技术实现一个 modal 组件
实现 Modal Class
- 下面是 modal.js,里面封装了我们要实现的 modal 组件的构造函数的所有功能
let template = document.createElement("template");
template.innerHTML = `
<style>
.my-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%;
}
.my-wrapper {
position: fixed;
left: 0px;
top: 0px;
bottom: 0px;
right: 0px;
background: black;
opacity: 0.4;
z-index: 2000;
}
.my-header {
padding: 20px 20px 10px;
}
.my-header .my-title {
line-height: 24px;
font-size: 18px;
color: #303133;
float: left;
}
.my-body {
padding: 30px 20px;
color: #606266;
font-size: 14px;
}
.my-footer {
padding: 10px 20px 30px;
text-align: right;
}
.my-close {
color: #909399;
font-weight: 400;
float: right;
cursor: pointer;
}
.my-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;
}
.my-cancel:hover {
color: #409eff;
background: #ecf5ff;
border-color: #c6e2ff;
}
.my-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;
}
.my-primary:hover {
background: #66b1ff;
}
.my-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="my-wrapper"></div>
<div class="my-dialog">
<div class="my-header">
<span class="my-title">提示</span><span class="my-close">X</span>
</div>
<div class="my-body">
<span>这是一段文本</span>
<input class="input-inner" type="text" />
</div>
<div class="my-footer">
<span class="my-cancel">取消</span>
<span class="my-primary">确定</span>
</div>
</div> `;
String.prototype.isTrue = function () {
// console.log(this);
return /^true$/.test(this);
};
class Modal extends HTMLElement {
#shadowDom;
constructor() {
super();
this.#shadowDom = this.attachShadow({ mode: "open" });
this.#shadowDom.appendChild(template.content);
this.#close();
this.addEvent();
let hasDrag = this.getAttribute("hasDrag");
// 判断自定义的 modal 标签拥有值为 "true" 的属性 hasDrag
if (typeof hasDrag === "string" && hasDrag.isTrue()) {
this.dragFn();
}
}
set hasDrag(newValue) {
if (newValue === true) {
this.dragFn();
}
}
addEvent() {
let dialog = this.#shadowDom.querySelector(".my-dialog");
this.dialog = dialog;
dialog.onclick = (e) => {
switch (e.target.className) {
case "my-close":
console.log("--", this);
this.#close();
this.dispatchEvent(
new CustomEvent("cancel", { detail: "这是点击了叉叉" })
);
break;
case "my-cancel":
this.#close();
this.dispatchEvent(new CustomEvent("cancel"));
break;
case "my-primary":
this.#close();
this.dispatchEvent(new CustomEvent("success"));
break;
default:
break;
}
};
}
dragFn() {
this.dialog.onmousedown = (e) => {
let x = e.clientX - this.dialog.offsetLeft;
let y = e.clientY - this.dialog.offsetTop;
this.onmousemove = (e) => {
let xx = e.clientX;
let yy = e.clientY;
this.dialog.style.left = `${xx - x}px`;
this.dialog.style.top = `${yy - y}px`;
};
};
this.dialog.onmouseup = () => {
this.onmousemove = null;
};
}
#close() {
this.#shadowDom.querySelector(".my-wrapper").style.display = "none";
this.#shadowDom.querySelector(".my-dialog").style.display = "none";
}
open() {
this.#shadowDom.querySelector(".my-wrapper").style.display = "block";
this.#shadowDom.querySelector(".my-dialog").style.display = "block";
}
}
- 上面的代码中,首先创建一个 template 元素,然后在其内部填充我们的 modal 的 dom 结构,包括组件的样式 style 标签
- 同样的,创建一个 Modal 类继承至 HTMLElement
- 在 Modal 类内部设置一个私有变量
#shadowDom
从命名上可以看出来,它是用来存放 shadom dom 的根结点 shadom root - 然后我们将 template 的 content 通过
attachShadow
添加到 Modal 实例上,然后将其返回的 shadom root 赋值给 #shadowDom - modal 包括两部分 dialog 主体和 wrapper 遮罩层,在初始化是可以先调用一次 私有方法
#close
,将这两块隐藏起来 - 组件的一些交互操作,如关闭组件,可以使用了自定义事件技术,配合事件委托机制,将 dialog 上面的点击事件全部托管起来,然后辨别被点击的元素对象,以进行对应的操作,具体实现见上面
addEvent
方法 - 组件实例化时会接受一个
hasDrag
参数,用于控制 dialog 主体是否可以拖拽 - dialog 有绝对定位样式,它的拖拽就是通过改变其 left、top 属性值来实现的
- 在 Modal 类内部设置一个私有变量
- 下面的代码,即为注册一个构造函数为 Modal 的自定义元素 my-modal
customElements.define("my-modal", Modal);
实现 ModalConfig Class
- 下面的代码,即为 Modal 的控制逻辑
export default class ModalConfig {
constructor(opt) {
let defaultConfig = {
width: "30%",
height: "250px",
title: "测试标题",
content: "测试内容",
hasDrag: false, //是否可拖拽
hasCancel: false, //是否有取消
};
this.option = { ...defaultConfig, ...opt };
this.createModal();
}
createModal() {
let modal = document.createElement("my-modal");
document.body.append(modal);
modal.hasDrag = this.option.hasDrag; // 触发 set
this.modal = modal;
}
open() {
this.modal.open();
}
}
- ModalConfig 初始化时,会先将外部传入的配置与 Modal 内置配置进行融合
- 然后调用
createModal
方法,创建 my-modal 元素,并将其添加到 body 内部
开箱即用
- 下面是组件的使用示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<button class="btn">出现 modal</button>
</body>
<script type="module">
import ModalClass from "./modal.js";
let modal = new ModalClass({ hasDrag: false });
document.querySelector(".btn").onclick = () => {
modal.open();
};
</script>
</html>
- 只需要引入 ModalConfig 类,并实例化出一个 modal 实例
- 在打开 modal 的按钮上绑定点击事件 modal.open()
- 当该按钮被点击时,即可出现 modal,效果如下图所示
小结
- 到这里 web components 的使用通过几篇文章已经介绍的差不多了
- 个人认为,三大件,自定义元素、HTML 模板、影子 dom,在封装一些交互较为简单的组件场景时比较适用
- 在大型企业项目中,不推荐全部使用 web components 来封装组件,更推荐使用现在流行的成熟框架来构建,因为成熟框架做了很多兼容性的封装,避免我们重复造轮子
最后
- 今天的分享就到这里了,欢迎大家在评论区里面进行讨论 👏。
- 如果觉得文章写的不错的话,希望大家不要吝惜点赞,大家的鼓励是我分享的最大动力 🥰