前端模态框的最佳实践

一、引言

为什么选择这篇文章呢?
(1) 前端工程师的定位不仅仅在于讨论如何解决架构层面的问题,更应该重视交互体验;

(2) 前端工程师从未停止过对用户体验的追求,如何思考模态框在产品中的存在和使用,存在许多争议;

二、知识普及

1、定义

维基百科上对模态框的定义如下:模态框是一个定位于应用视窗顶层的元素。它创造了一种模式让自身保持在一个最外层的子视窗下显示,并让主视窗失效。用户必须通过在它上面做交互动作,才可以回到主视窗。

2、用途
  • 抓住用户的吸引力;
  • 需要用户输入内容;
  • 在上下文下显示额外的信息;
  • 不在上下文下显示额外的信息;

注意:不要在模态框显示错误、成功或警告的信息,如果要显示,则让他们在页面上显示;

3、组成
  • 退出的方式,可以是模态框上的一个按钮,键盘上的一个按键,也可以是模态框外的区域;
  • 描述性标题,标题是给用户标识在哪个位置进行操作的上下文信息;
  • 按钮的内容,它一定是用户可以理解,并促使用户进行下一步行动的内容,不会产生迷惑;
  • 大小与位置,模态框不要太大,也不要太小,位置建议在视窗中间偏上(太低在移动端显示不全);
  • 焦点的切换,模态框的出现会吸引用户的注意力,建议键盘焦点切换到框内;
  • 需用户发起,通过用户的动作,如点击按钮促使模态框出现,不然会惊吓到用户;
4、移动端

模态框由于太大,占用太多控件,在移动端很难适配。通常建议增加设备按键或内置滚动条来操作,用户可以滑动或放大缩小来操作模态框。

5、无障碍访问
  • 快捷键 - 考虑在打开、移动、管理焦点和关闭时增加对模态框的快捷键;
  • ARIA - 在前端代码层面加上 aria 的标识,如 Role = “dailog”,aria-hidden,aria-label;

三、思考与实践

1、定位思考

Modal 与 Toast、Notification、Message 以及 Popover 都会在某个时间点被触发并弹出一个浮层。但从定义上来看上述组件都不属于模态框,因为模态框始终会阻塞原来主视窗下的操作,只能在框内做后续操作。模态框的出现从界面上彻底打断了用户的心流。如果是一般的消息提醒,可以用信息条、小红点等交互形式,至少不会阻塞用户的操作。

那么模态框有什么优点吗?要知道模态框的体验要比页面跳转更加轻量,更让用户感觉舒适。例如,用户在电商网站看中一款产品,想登陆购买,此时弹出模态框的体验就远远好于跳转到登录页面,因为用户在模态框中登录后,就可以直接购买了。

也就是说,当我们设计好模态框出现的时机,流畅的弹出体验,必要的上下文信息,以及友好的退出反馈,足以提升体验。模态框的目的在于吸引用户的注意,而且一定要提供能够在本页面即可完成额外的流程操作或是信息告知,可以是一个重要的操作,也可以是一份重要的协议。

2、合理使用
  • 内容是否相关。模态框作为当前页面的一种衍生或补充,若其内容与当前页面无关联,可以使用其他操作代替模态框,例如页面跳转。
  • 避免过多操作。模态框应该给用户留下一种看完即走,潇洒自如的感觉,而非繁杂的交互留住或牵制用户。
  • 避免多模态框。出现多个模态框,会加深产品垂直深度,提高视觉复杂度,而且让用户感到烦躁。
  • 用户主动触发。不要突然打开或自动打开模态框,会惊吓到用户。
  • 大小尺寸适中。模态框大小要严格限制,避免内容过多出现滚动条的情况,除非需展示明细内容。
3、可访问性

对于不同终端的用户体验,能否够无障碍访问,是完善用户体验的关键。每一个模态框,都要通过键盘(通常是 ESC)键关闭。但这似乎成为我们对待产品的一种惯性思维,尤其是当我们能够敲着外置的键盘,用PC进行访问时。但下面是对可访问性的反思,也是一直被产品忽视的:

  • 用户可能没有鼠标,也可能没有键盘,甚至两者都没有,只能语音控制?用户该如何退出?
  • 针对支持触屏操作的 PC 电脑,而你的网页支持鼠标和键盘?
  • 在没有触摸板的地方,横线的滚动条是不是一个逆天的操作?
  • 在网页里,使用 Ctrl and +/- 和使用触摸板的放大缩放,表现的行为不一致?
  • 当用户看不清你网页上的内容,而且此时没有触摸板,如何保证网页上的内容正常显示?
4、代码实现

前端开发中对于模态框的代码实现,大多采用了有状态或无状态的使用方式。对于有状态的模态框,很多库都已支持 .show 直接调用,模态框内部会实现渲染逻辑。

而对于无状态模态框(Stateless Modal),模态框的显示与否则交由父级组件控制,我们只需将模态框代码预先写好,由外部控制是否显示。

然而在循环列表中的无状态模态框,如果不做控制,会引发首屏出发几十次的模态框初始化运算,从而引发性能危机。最佳实践是模态框显示时执行一次,由于同一时间只会出现一个,首屏最多初始化一次就可以了。

// 错误示例
const TableElement = data.map(item => {
    return (
        <Table>
            <Button>了解详情</Button>
            <Modal show={item.show}/>
        </Table>
    )
});

上面的代码初始化时执行了N个模态框的初始化操作,对于性能是极大的浪费。对于 table 操作列中所触发的模态框,所有行中的模态框,应该通过父级中的一个状态变量来控制显示与否:

class Table extends Component {
    static state = { activeItem:null };
    render() {
        const { activeItem } = this.state;
        return (
            <div>
                <Modal show={!!activeItem} data={activeItem} />
            </div>
        )
    }
}

以上这种方案减少了被触发的节点数,但是会带来新的问题,每次模态框被触发展示的时候,都会更新(componentDidUpdate)渲染而非增加,上面 Table 中的代码可以做以下优化:

{activeItem ?<Modal show={true} data={activeItem} /> : null }

四、总结

对于模态框的可访问性,属于典型的长尾需求,多数研发在做产品时只考虑90%的用户,而不清楚我们放弃的一部分用户的真实需求,这是产品到研发整体思考的缺失。

对于模态框或其他形式的产品,我们不可死守着[最佳实践],我们应该意识到不同的产品或不同的用户会带给我们不同的认知,我们要尽可能摆脱感性的结论,而是通过采集和研究用户需求的方法,通过数据结论理性去分析和改进用户体验。