【webcomponent】从0到1基于stencil实现多弹窗管理demo【一】

333 阅读4分钟

1. 背景:

项目内部首页会展示触发多弹窗逻辑,由于已上线弹窗数量较多,且业务不断堆积,不同的弹窗显示隐藏分治,会存在用户进来同时展示多个弹窗,存在弹窗互相叠盖的现象,用户关闭上层弹窗在依次点击下方的弹窗,用户交互体验差。并且随着需求不断迭代,不易维护,定制化弹窗展示顺序的需求不易开展管理。

May-22-2024 14-27-35.gif

所以需要定制一套弹窗管理方案:
体验上:优先级高的弹窗先展示,关闭后再展示优先级次之的弹窗,依次进行
功能上:有一套多弹窗管理方案,能够统一管理所有弹窗,包括优先级、业务定制化展示规则等

本文会首先从零到一,实现一个webcomponent的多弹窗的demo,后续文章会再整理输出弹窗管理相关的功能。本文最终输出的是基于stencil进行封装的webcomponent组件,可实现跨框架部署。

2. 项目初始化

官方文档:stenciljs.com/

npm init stencil

项目结构介绍说明:juejin.cn/post/736202…

3. 实现弹窗:

3.1 先熟悉stencil采用jsx的方式封装一个简单的弹窗组件
// /components/comp-dialog.tsx
import { Component, Prop, State, Method, Event, EventEmitter, h } from '@stencil/core';

@Component({
  tag: 'comp-dialog',
  styleUrl: 'comp-dialog.css',
  shadow: true,
})
export class CompDialog {
  @Prop() visible: boolean = true;

  @Prop() dialogTitle: string;

  @Event() handleClose: EventEmitter<any>;


  async hide(){
    this.handleClose.emit()
  }
  render() {
    return this.visible && 
      <div class="dialog-box">
        <div class="mark"></div>
        <div class="content-box">
          <p class="dialog-title">{this.dialogTitle}</p>
          <div class="dialog-cont">
            <slot>默认内容 默认内容默认内容默认内容默认内容默认内容默认内容</slot>
          </div>
          <div class="footer">
            <div class="dialog-btn" onClick={ () => this.hide() }>我知道了</div>
          </div>
        </div>
      </div>;
  }
}

// /components/comp-dialog.css
p{
  margin: 0;
}
.dialog-box{
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  font-size: 0px;
  overflow-y: initial;
  /* 这里防止当用户长按屏幕,出现的黑色背景色块,以及 iPhone 横平时字体的缩放问题 */
  -webkit-text-size-adjust: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.mark{
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.7);
}


.content-box{
  z-index: 10;
  min-width: 490px;
  padding: 24px;
  border-radius: 8px;
  background: #FFF;
  box-shadow: 0px 3px 0px 0px #E0252B inset, 0px 8px 18px 3px rgba(0, 14, 51, 0.06);
}

.content-box .dialog-title{
  color: #333;
  font-size: 16px;
  font-weight: 600;
  line-height: 24px;
}

.content-box .dialog-cont{
  margin: 16px 0 32px;
  color: #666;
  font-size: 14px;
  line-height: 22px; /* 157.143% */
}
.content-box .footer{
  display: flex;
  flex-direction: row-reverse;
}

.dialog-btn{
  padding: 6px 20px;
  border-radius: 4px;
  border: 1px solid rgba(130, 131, 149, 0.30);
  color: #333;
  font-size: 14px;
  cursor: pointer;
}
.dialog-btn+.dialog-btn{
  margin-right: 10px;
}

创建一个展示的页面引入组件,我这里是comp-page,并在www/index.html中引入这个页面组件,目录路径如下,非常easy

image.png

3.2 改造弹窗组件支持函数式调用

采用标签引入弹窗的方式对父组件的代码侵入比较大,函数式调用更方便便捷

  1. 改造弹窗模版文件:暴露弹窗显隐方法
// /components/comp-dialog.tsx
...
export class CompDialog {
  ...  
  @State() visible: boolean = false; // 替换之前的props

  @Method()
  async show() {
    this.visible = true;
  }

  @Method()
  async hide(){
    this.visible = false;
    this.handleClose.emit();
  }
  ...  
}
  1. 创建一个独立的服务文件返回弹窗实例,用于管理弹窗的显示和隐藏,并支持传递参数。
// /components/comp-dialog-service.ts
type ModalOptions = {
  content: string;
  dialogTitle?: string;
};

class DialogService {
  private dialogElement: HTMLCompDialogElement;

  constructor() {
    this.dialogElement = document.createElement('comp-dialog') as HTMLCompDialogElement;
    document.body.appendChild(this.dialogElement);
  }

  show(options: ModalOptions) {
    this.dialogElement.innerHTML = options.content;
    this.dialogElement.dialogTitle = options.dialogTitle || 'Default Title';
    return this.dialogElement.show();
  }

  hide() {
    this.dialogElement.hide();
  }
}

const dialogService = new DialogService();
export default dialogService;

  1. 使用示例
// /components/comp-page.tsx
import { Component, h } from '@stencil/core';
import dialogService from '../comp-dialog/comp-dialog-service';

@Component({
  tag: 'comp-page',
  styleUrl: 'comp-page.css',
  shadow: true,
})
export class CompPage {
  open() {
    dialogService.show({
      content: '<p>This is the modal content!</p>',
      dialogTitle: 'My Modal Title',
    });
  }

  render() {
    return <div>
      <button onClick={ () => this.open()}>打开</button>
    </div>;
  }
}
3.3 实现一个弹窗关闭下一个弹窗打开

promise链异步执行完成这样的功能,在弹窗打开初始化一个promise,在弹窗关闭的时候将promise的状态置成fulfilled,状态完成,打开下一弹窗

  1. 改造弹窗模版文件为异步
// /components/comp-dialog.tsx
export class CompDialog {
  ...
  @State() dialogResolve: any = null;

  @State() dialogReject: any = null;

  @Method()
  async show() {
    this.visible = true;
    return new Promise((resolve, reject)=>{
      this.dialogResolve = resolve
      this.dialogReject = reject
    })
  }
  
  @Method()
  async hide(){
    this.visible = false;
    this.dialogResolve('成功关闭')
  }
  
  ...
}

  1. 多弹窗示例
// /components/comp-page.tsx
export class CompPage {
  open() {
    dialogService.show({
      content: '<p>This is the modal content!</p>',
      dialogTitle: 'My Modal Title1',
    }).then((data) => {
      setTimeout(() => {
        dialogService.show({
          content: data.toString(),
          dialogTitle: 'My Modal Title2',
        });
      }, 1000);
    });
  }
  ...
}

以上完成基本弹窗顺序显隐demo,还可以通过责任链的方式进行管理达到一样的效果,可参考【juejin.cn/post/711825… ,本篇仅为demo实现多弹窗的效果