如何用纯原生 JS 封装一个组件(五):实现 modal 组件

4,662 阅读3分钟

「这是我参与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 的自定义元素 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,效果如下图所示 image.png

小结

  • 到这里 web components 的使用通过几篇文章已经介绍的差不多了
  • 个人认为,三大件,自定义元素、HTML 模板、影子 dom,在封装一些交互较为简单的组件场景时比较适用
  • 在大型企业项目中,不推荐全部使用 web components 来封装组件,更推荐使用现在流行的成熟框架来构建,因为成熟框架做了很多兼容性的封装,避免我们重复造轮子

最后

  • 今天的分享就到这里了,欢迎大家在评论区里面进行讨论 👏。
  • 如果觉得文章写的不错的话,希望大家不要吝惜点赞,大家的鼓励是我分享的最大动力 🥰