🤨面试官:如何从0️⃣实现Dialog弹窗❓︎

810 阅读4分钟

手写diolog

面试官:"假设现在需要你从零实现一个Dialog弹窗组件,你会怎么设计?需要考虑哪些关键点?"

很多三方库已经封装了弹窗,给你自己从零封装一个弹窗组件,你会怎么做?

接下来来看看如何自行封装一个属于自己的弹窗组件。

利用浏览器天生支持的弹窗功能dialog: 一种普通弹窗、一种模态弹窗。

模态弹窗:1、半透明;2、毛玻璃;3、tab键切换(切换不到背后页面的文本框。);4、固定滚动条。

三点
居中、
控制tab、
放到顶层。

// 包裹一个dialog元素

<dialog id="myDialog">
    <div class="dialog-content">
        <div class="dialog-header">
            <h2 class="dialog-title">对话框标题</h2>
            <button class="close-button" onclick="closeDialog()"></button>
        </div>
        <div class="dialog-body">
            <p>这是对话框的内容区域。</p>
            <p>你可以在这里放置任何 HTML 内容。</p>
            <input type="text" placeholder="对话框中的输入框">
        </div>
    </div>
</dialog>

默认是关的。

点击按钮弹出弹窗:

// 给dialog取个id,获取的时候用id获取
const dialog = document.getElementById('myDialog');

// 普通弹窗 弹出来 事件是show
function openNormalDialog() {
    dialog.show();
}

// 模态弹窗 弹出来 事件是showModal
function openModalDialog() {
    dialog.showModal();
}

// 无论普通还是模态,都是close关闭弹窗
function closeDialog() {
    dialog.close();
}

再看一下上面的dialog普通和模态的效果对比:

1.gif

模态对话框背后是个半透明的背景:

image.png

这个透明的玻璃框有什么作用呢:

1、 背后按钮按不了了;

2、 tab键切换不用我们考虑了,原生帮我们做好了;

3、 如果底面那一层有input,点不了,用tab也切不了;(这点的话如果我们自己去做,是很难去做的,用原生,这些玩意都不用我们去考虑,浏览器原生天生就支持)。

弹窗永远是顶层,如果自己去定的话z-index的话,会有设得越来越大的可能性,有可能页面的弹窗太多了,自己设置了什么到最后也记不清了,导致会有一些失误导致达不到我们的效果。

原生这种dialog就很放心,保证是一定是最顶层,原因如下:

image.png

dialog后面这个一个top layer

top layer在整个页面之外,所以它不是通过index去控制的,它本身就在整个页面之外,无论哪个元素,它的index再搞,都不会把它覆盖。

dialog的样默认样式:

image.png

/* 对话框样式 */
dialog {
    padding: 20px;
    border: none;
    border-radius: 8px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

设置成这样:

image.png

设置蒙层的样式:

蒙层是一个伪元素,

image.png

dialog了,里面有一个伪元素backdrop,就是蒙层:

/* 模态框背景样式 */
dialog::backdrop {
  background-color: rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(1px); // 毛玻璃效果
}

image.png

封装类组件化

效果如下:

1.gif

// 基于原生 <dialog> 元素实现的弹窗组件类

类结构

export default class MyDialog {
  constructor(options) {
    // 初始化配置
    this.options = {
      id: "my-dialog",          // 默认ID
      title: "对话框标题",       // 默认标题
      content: "<p>这是对话框的内容区域。</p>", // 默认内容
      buttons: [                // 默认按钮配置
        { text: "取消", action: "cancel" },
        { text: "确定", action: "confirm" },
      ],
      ...options,               // 合并用户自定义配置
    };

    this.init();  // 初始化弹窗
  }

方法详解

init

init() {
  this.createDialog();  // 创建DOM结构
  this.bindEvents();    // 绑定事件
}

createDialog() —— 创建弹窗的dom结构:

reateDialog() {
  // 创建dialog元素
  const dialog = document.createElement("dialog");
  dialog.id = this.options.id;
  dialog.className = "dialog";

  // 生成按钮HTML
  const buttonsHTML = this.options.buttons
    .map(
      (btn) =>
        `<button class="dialog__btn dialog__btn--${btn.action}">${btn.text}</button>`
    )
    .join("");

  // 设置内部HTML结构
  dialog.innerHTML = `
    <div class="dialog__container">
      <div class="dialog__header">
        <h2 class="dialog__title">${this.options.title}</h2>
        <button class="dialog__close" aria-label="关闭弹窗">x</button>
      </div>
      <div class="dialog__body">${this.options.content}</div>
      <div >${buttonsHTML}</div>  <!-- 注意:这里缺少了footer的class -->
    </div>
  `;

  // 添加到body
  document.body.appendChild(dialog);
  this.dialog = dialog;  // 保存dialog引用
}

bindEvents() —— 绑定事件处理函数:

bindEvents() {
  // 关闭按钮事件
  this.dialog
    .querySelector(".dialog__close")
    .addEventListener("click", () => this.close());
  
  // 为每个按钮绑定事件
  this.options.buttons.forEach((btn) => {
    const btnEl = this.dialog.querySelector(`.dialog__btn--${btn.action}`);
    if (btnEl) {
      btnEl.addEventListener("click", () => {
        // 如果有回调函数则执行
        if (typeof btn.callback === "function") {
          btn.callback();
        }
        this.close();  // 关闭弹窗
      });
    }
  });
}

弹窗控制方法:

// 打开弹窗
open(modal = false) {
  if (modal) {
    this.dialog.showModal(); // 模态方式打开 
  } else {
    this.dialog.show(); // 非模态方式打开
  }
}

// 关闭弹窗
close() {
  this.dialog.close();
}

弹窗实例化

<button id="openDialog">打开对话框</button>
<script type="module">
    import MyDialog from './index.js'; // es6 模块化语法 导入自定义的MyDialog类
    // 使用示例
    const myDialog = new MyDialog({
      title: '自定义标题',
      content: '<p>自定义内容</p><input type="text" placeholder="测试输入">',
      buttons: [
        { text: '取消', action: 'cancel' },
        {
          text: '确定', action: 'confirm', callback: () => console.log('确认')
        }
      ]
    });

    document.getElementById('openDialog').addEventListener('click', () => {
      myDialog.open();
    });
</script>

总结

面试官:"假设现在需要你从零实现一个Dialog弹窗组件,你会怎么设计?需要考虑哪些关键点?"

:"我会优先考虑使用HTML5原生<dialog>元素来实现,
原因有三点
首先它能自动处理焦点锁定避免用户通过Tab键切换到背景内容
其次它自带模态遮罩层,不需要额外实现;
最后它有浏览器原生的层级管理,不会出现z-index冲突问题。"

面试官:"有意思,那具体怎么封装呢?如何保证组件的可复用性?"

:"我会设计一个类组件,通过配置化方式支持自定义标题、内容和按钮。
关键实现包括:
1)动态创建dialog DOM结构;
2)事件绑定机制;
3)两种打开模式(模态/非模态)"

原生

的四大优势:
自动焦点管理 - 解决Tab键穿透问题
原生遮罩层 - ::backdrop伪元素轻松实现
顶级层叠 - 永远在最上层
浏览器级性能优化 - 比JS实现的更流畅

方案优点缺点
第三方库功能丰富、兼容性好体积大、定制困难
原生dialog手动实现零依赖、性能好、功能完整,完全可控兼容性要求IE11+,实现成本高、易有缺陷