在基础介绍那里有提到 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