组件实现——对话框的前世今生

373 阅读5分钟

前言

对话框,也可以叫模态对话框(dialog/modal),应用场景非常多,比如提示信息、确认等操作、表单输入、详情展示等等。

对话框的用户体验优点:

  1. 集中注意力,通常对话框都有一层遮罩层,然后让内容展示在靠近用户平视的中心,位置大概在屏幕中间偏上;
  2. 保留上下文,比如我们想查看某个元素的详情,如果直接跳转页面,用户可能会忘记是从哪里来的、也可能会让当前页面的状态丢失,用户返回时的体验会较差(也要结合具体的场景和实现,不是一概而论);
  3. 执行流程更加清晰,防止用户误操作、减少额外的控制逻辑代码(也让我们少掉点头发哈哈);
  4. 有助于提高可访问性;

接下来介绍历来对话框的基本实现方法,以及基于主流框架实现的一些关键点。

早期(200x年)

Window

早期我们会用 window 原生提供的几个方法创建简单的对话框,如:

  • alert(message?: string): void

    展示一段文字,并等待用户确认,返回值为 undefined

  • confirm(message?: string): boolean

    展示一段文字,并等待用户确认/取消,返回值为 true/false

  • prompt(message?: string, defaultValue?: string): string | null

    展示一段文字,并等待用户填写一串字符串后确认/取消,返回值为输入的字符串/null(点击取消或字符串为空)

本段示例代码需要点击右上方【查看详情】按钮跳转到码上掘金页面才有效果

这几个方法的缺点除了功能比较“简陋”外,还会阻塞页面,整体评价只有 1 颗星⭐,比较少人用了。

还有一种特殊的对话框,就是在页面窗口关闭或刷新前弹出。使用场景通常是在表单页面防止误关或营销页面会用到,代码如下:

window.addEventListener('beforeunload', function(e) {
  // Gecko and Trident
  (e || window.event).returnValue = '确定离开此页吗?'; 
  // Gecko and WebKit
  return '确定离开此页吗?';
})

这个方法和前面几种类似,都不能定制化,但暂时好像没有替代方案,因此给⭐⭐⭐。

iframe

早期广告、第三方登录等小部件会通过 JS 控制 <iframe> 弹出达到效果,在当时有 3 个优点:① 隔离环境;② 解决跨域;③ 兼容性较好。但现在也只能给⭐⭐。

中期(201x年)

随着各大浏览器对 WEB 标准的支持越来越一致,且出现 CSS3 等新特性后,视觉效果更加丰富了,因此可以直接使用 JS + CSS 实现对话框。2016年之前通常会用 JQuery、AngularJS 等库实现。后面 React、Vue 也逐渐进入视线,后来也发展为主流之一。

还有一种比较少人用的方式:HTML5 原生标签 <dialog>,先上代码:

可以看到原生 <dialog> 调用 showModal() 后就会默认出现在屏幕中间,并且也有背景遮罩层。这种方式也支持嵌套,目前的兼容性也挺好,但实现一些功能交互/布局时需要额外处理或者模式上与以往不一致(如在遮罩层上点击关闭的功能),有一定的使用成本,导致开发人员会下意识的排斥,这可能是少人用的原因之一。但总的来说也是一种不错的实现方式,大家可以多尝试看还有哪些优缺点。

实现

基础功能

对话框基本实现流程:

  1. <body> 添加子元素作为对话框根节点

    如果直接在某个层级下添加对话框,可能会出现定位或遮挡问题

  2. 根节点需要填充整个页面作为遮罩层(用子元素也可以)

  3. 通过移除元素或操作样式显示/隐藏对话框

目前主流框架都是以模块化的方式去实现具体的组件,实现过程中有哪些需要注意的呢?

Vue2

Vue2 中,组件的真实 DOM 元素一般都是在其组件根元素下面。如果对话框也使用这种方式,就可能会出现上面提到的问题(定位或遮挡)。那 Vue2 官方有提供什么方法能够让组件中的某一部分出现在 <body> 或其它地方吗?答案是:没有。

我们需要直接操作 DOM,在生命周期钩子 mounted 和 beforeDestroy 中进行处理,上代码:

可以复制代码在本地运行,结合 Vue devtools 调试体验。

Vue3/React

将组件的部分真实 DOM 渲染在指定位置的场景还蛮多的(Dialog、Tooltip、Dropdown menu 等),因此框架也开发了对应原生解决方案:

z-index 管理

复杂页面通常不止有一个弹出层,如果不设置 z-index 可能会引起堆叠问题。因此我们需要全局统一管理 z-index。Element UI 和 Ant Design 采取了两种不同的方案。

Element UI

在每次展示时都自增 z-index。

  1. 维护一个记录最后一个弹出组件的 z-index
  2. 提供自增 z-index 并返回一个可用值的方法 nextZIndex
  3. 每当新增弹出组件或旧的需要重新展示时调用 nextZIndex 并将其作为对应 DOM 元素的 z-index

Ant Design

预置了各个类型组件的 z-index 基础值,并根据在 <body> 的先后顺序确保后面出现的总是渲染在最上层。

结语

以上就是对话框的进化史以及在实现中的要点。当然,完整的对话框还有很多需要处理的地方,比如:

  • 弹出时防止背景页面滚动
  • 让对话框可以用键盘操作(聚焦、快捷键)
  • 适配不同屏幕
  • 可访问性(ARIA)
  • 过渡动画
  • 封装方法调用

感谢阅读,有疑问或者建议欢迎评论或者私信,一起交流学习。