语法设计

基础介绍那里有提到 LuLu UI 组件往往支持多种调用方式,自定义元素、自定义属性、对象构造和 DOM 扩展方法同时支持,那为什么这么设计,以及背后是如何实现的呢?

这里就会给大家详细解答。

设计多种调用语法的原因

Edge 主题开发的核心是 Web Components,从技术角度上将,使用自定义元素组件,例如:

<ui-drop></ui-drop>

以及使用内置自定义元素,例如:

<dialog is="ui-dialog"></ui-drop>

就可以了,正如目前很多 Web Components UI 组件库做的那样。

然而,实践下来,上面的策略并不能满足所有的使用场景。

自定义属性语法

例如在 Vue 框架中,类似 <ui-drop> 这样的自定义元素会报错,因为 Vue 会认为是一个 Vue 组件,而 <ui-drop> 是原生语言开发,并没有和任何第三方框架关联,因此 Vue 无法识别,导致出错。

所以 Edge 主题给所有的自定义组件扩展的“自定义属性”的能力,使得标准 HTML 元素也具有组件能力,例如:

<button is-drop></button>

任意的标准 HTML 元素,只要设置 is-drop 属性,就可以拥有类似 <ui-drop> 自定义元素的交互能力。

由于标准 HTML 元素在 Vue 中都是合法的,因此,Edge 主题就有了在 Vue 中无障碍使用的能力。

除了上面的原因,自定义属性语法还可以保持标准 HTML 元素良好的语义以及使用方便快捷等优点。

new 构造语法

为何各个组件依然保留了 new 构造语法呢?

是这样的,在 Edge 主题中,new 构造语法的底层逻辑变了,不再是创建新的对象实例,而是创建新的自定义元素,这个其实很好解释。

以 Dialog 弹框举例,虽然在文档中预置 <dialog is="ui-dialog"> 这么一段 HTML,然后设置 <dialog> 元素的 open 属性就可以让弹框显示,但是实际开发不可能都这么处理。

原因很简单,需要的提示框太多了,且不确定,例如需要交互弹框、需要成功提示框、还需要错误提示框,这么多弹框不可能一个不拉都提前在页面上显示,既浪费人力,也损耗性能,关键会有遗漏。

正确的做法是当业务逻辑需要调用弹框的时候,再即时创建。

但是这就带来一个问题,每次都创建弹框元素并载入页面是很啰嗦的:

let dialog = document.createElement('dialog');
dialog.setAttribute('is', 'ui-dialog');
dialog.addEventListener('DOMContentLoaded', function () {
    this.alert('Hello, LuLu UI!');
});
document.body.appendChild(dialog);

此时就需要 new 构造语法出马。

new 构造语法本质上就是创建自定义元素,并与页面发生连接,隐藏了其中的种种细节,让开发者更专注于本身的业务可否,例如,上面的代码使用 new 构造语法书写就这么一句:

new Dialog().alert('Hello, LuLu UI!');

new Dialog() 返回的就是 <dialog> 元素,alert() 就是 <dialog> 元素上的扩展方法。

其他的自定义元素也遵循一致的设计的策略。

DOM 扩展语法

几乎所有的自定义元素组件都在 DOM 元素上设置了扩展方法,例如:

element.tips();
element.errorTip();
element.tab();
// ... 等

DOM 扩展语法没有什么特别之处,可以看成是 new 构造语法以外的另一种调用形式,底层全部都是 new 构造语法。

语法汇总

下表是所有组件调用方法的汇总。

组件名 自定义元素 内置自定义元素 自定义属性 new 构造语法 DOM 扩展方法
Select下拉框 - - - -
Range范围选择 - - - -
Color颜色选择 - - - -
Datetime日期选择 - - - -
Datalist数据列表 - - - -
Dialog弹框 - (?) - -
Tip提示 -
ErrorTip出错提示 - - -
LightTip轻提示 - - -
Pagination分页 -
Drop下拉 -
Tab切换 -
Validate表单验证 - -
Table列表 - - - -
Form表单 - - - -

从上表可以看出,所有的内置自定义元素否是仅只有一种用法,而支持自定义元素的 UI 组件一定支持 new 构造语法,且绝大多数自定义元素同时支持 4 种调用方法。

多种调用方法并存的原理

如果让多种用法集中在一起,而又不相互冲突,还是需要一点技术手段的,这里简单介绍下多语法共存实现的核心。

1. 以自定义元素为核心

无论哪种调用方法,其核心都是自定义元素。

比方说下面这段 HTML:

<button is-drop></button>

并没有出现任何自定义元素,实际上,背后的交互实现全部都是依托于 <ui-drop> 这个自定义元素实现的。

也就是内存中(部分组件在文档流中)存在一个 <ui-drop> DOM 对象,当点击 <button> 按钮元素,所有的重定位、事件处理,显隐设置其实都是围绕 <ui-drop> 这个对象展开的。

下面问题来了,这里的 <ui-drop> 是哪里创建的呢?

2. 自定义元素本质上就是个对象

自定义元素本质上就是个对象,只是继承于特定的 HTML 元素类型而已,例如:

class Drop extends HTMLElement {
}
customElements.define('ui-drop', Drop);

所以,我们可以使用 new Drop() 创建一个 <ui-drop> 元素对象。

好,知道了 <ui-drop> 的创建,那这个 <ui-drop> 元素是如何和 <button> 元素建立连接的呢?

3. 通过for 属性转移 trigger

Edge 主题中,在几乎所有的自定义元素上扩展了一个 for 属性,表示事件的触发源要发生转移。

例如:

<ui-drop for="someId"></ui-drop>

此时,id 为 'someId' 的元素点击的时候,就相当于点击这个 <ui-drop> 元素,从而实现和原始自定义元素一致的交互效果。

因此,要想让 <ui-drop> 元素和 <button> 元素建立连接,只需要设置 <ui-drop> 元素的 for 属性值是 <button> 元素的 id(没有就创建一个随机 id)。

3. 实时 DOM 观察

is-drop 属性属于自定义属性,不同于 is 属性,只要元素存在,浏览器自动会扩展对应的组件行为。

因此,需要手动进行绑定与观察,这是借助 MutationObserver 对象实现的。实时观察页面所有的 DOM 节点变化,当出现包含 is-drop 属性的元素的时候,再底层执行 new Drop() 方法,实现对应的下拉效果。

4. 总结

一图胜千言,各个调用方法的依赖关系如下图所示:

多语法实现依赖示意

代码层面实现的逻辑关系图如下所示:

多语法实现原理示意

这就是为什么使用非自定义元素语法时候,下拉浮层的显隐控制需要使用类似的语法:

button['ui-drop'].hide();

因为,显隐设置都是围绕 <ui-drop> 元素展开的(button['ui-drop'] 的返回值就是 <ui-drop> 元素)。

本页贡献者:

zhangxinxu