微信小程序团队在外界高昂的呼声中,终于从小程序基础库版本 1.6.3 开始,支持简洁的组件化编程。具体使用可参考小程序开发官方文档中自定义组件介绍章节。
自从小程序退出自定义组件的能力,cluo在项目中把之前通过小程序template实现的组件逐步进行改造。自定义组件带来的好处是毋庸置疑的,比如下面这些优点:
- 统一管理组件的dom结构、样式
- 自封闭的代码安全性
- 多层嵌套增强复用性的自由度
- 团队多人协作时耦合度的大幅下降
- ...
但是自定义组件的优劣也对于开发者本身具有较高的要求,比如组件的抽象程度(颗粒度)、组件使用时的自由度、组件使用时的便捷度、组件的扩展性、组件说明文档等等。对于一个大型的项目团队,在多人合作过程中,个人认为文档是第一重要的,没有一个详尽的设计、使用文档,对于自定义组件的维护者和使用者而言,是巨大的灾难。
而在引入自定义组件之后,初始遇到一系列问题,这里会将其中一个跟本文标题息息相关的问题着重与各位分享。先介绍一下问题的背景:
产品上线后有大量的用户行为统计需求,初始时小程序的PV、UV统计基本满足产品诉求,但随着产品迭代,对于用户实际关注的页面元素等点击行为更为关心,于是一波又一波的统计需求被提上日程。为了对用户行为(以点击为例)无差别的统计上报,组内设计了一个方案。方案如下步骤:
- 在用户交互(点击)的模块上添加相应标识
查看详情
- 在顶层DOM上添加相应的事件处理器
- 通过获取事件对象e的target的daid数据属性值,来标识用户的行为并上报到服务器
const logReportEvent = function(e) {
// 获取 用户行为的id
let _elemId = e.target.dataset.daid
// 上报
// ...
}
整个方案有2个关键的点
小程序的事件冒泡机制
操作区域的标示(daid统一覆盖)
在最外层的dom添加对tap事件的监听,凡是用户在我们关注的区域内发生点击,都可以通过e.target这个朴素的思路来获取,由于对关注区域内的daid的统一覆盖,所有上报的行为就是产品关注的部分,以此达到目标。
当项目中大量使用自定义组件后,按照原先的思路,在组件内部定义了产品关心的交互区域,但在测试中发现并没有如愿上报定义的事件id。e.target实际指向的是组件的根元素。
所以,前面铺垫如此之长,终于要引出我们要说的主题了。旧的用户行为统计方案为何在应用自定义组件后失效了呢?
各位客官,先看看C罗项目中的代码运行截图,如下所示:
【#shadow-root】有木有很熟悉?可以看出,小程序下(至少开发者工具中)自定义组件的实现使用的是shadow dom的方式。
有些朋友对这个东西可能比较陌生,谈到影子dom就不得不提Web Components,Web components致力于在web环境下复用自定义元素,而开发者不必担心代码出现冲突,它包含4个主要技术范畴,为避免自己翻译水平有限,直接将相关概念的说明复制如下:
- Custom elements: A set of JavaScript APIs that allow you to define custom elements and their behaviour, which can then be used as desired in your user interface.
- Shadow DOM: A set of JavaScript APIs for attaching an encapsulated "shadow" DOM tree to an element — which is rendered separately from the main document DOM — and controlling associated functionality. In this way you can keep an element's features private, so they can be scripted and styled without the fear of collision with other parts of the document.
- HTML templates: The
and
elements enable you to write markup templates that are not displayed in the rendered page. These can then be reused multiple times as the basis of a custom element's structure.
- HTML Imports: Once you've defined a custom component, the easiest way to reuse it is to keep its definition details in a separate file and then use an import mechanism to import it into pages where you want to actually use it. HTML Imports is one such mechanism, although there is controversy over it — Mozilla fundamentally disagrees with this approach and is intending to implement something more suitable in the future.
接下来,在具体探讨小程序自定义组件与旧的行为上报不兼容的问题之前,cluo会通过“桌子”的demo跟大家逐步了解web components的使用,并将在这个过程中和大家一起来看如果使用Web Components如何实现类小程序自定义组件。
首先,我们创建一张最朴素的“desk”,并把“desk”放到网页上,让大家都能看到,代码如下:
Document
cluo's desk
上面的例子中,定义一个模板、注册自定组件并在元素构造函数中将其append到shadow root上,最终页面上只需要一个自定义元素的标签即可便捷地使用此组件,是不是非常简单?但是,现实生活中,桌子往往会放上一束花,对于广大追求真理和艺术的文艺青年,花不能少。
Web Components
cluo's desk
cluo's flower
好的,现在花有了,但是,花要放到桌子上,怎么办?这种关系反馈到DOM上应该是桌子包含花束,
先试着直接使用自定组件的嵌套方式,代码如下
从dom查看发现元素确实是包含的,但是页面上是不展示的,只展示“cluo's desk”,看来这个方案行不通,但是前面提过,Web Components是指支持组件嵌套的,仔细查看文档,我们可以使用slot来进行组件的嵌套。
再通过dom查看,如下图所示
此时,在页面上可以看到“cluo's desk”和“cluo's flower”均正常展示。
桌子上只放一束花太单调,再放一支笔吧,嗯,文学青年一定要用钢笔。这时我们可以使用具名slot,并在嵌套时指定本内容会嵌到哪个插槽里面,代码如下:
cluo's desk
cluo's flower
cluo's pen
相应的dom结构截图与页面显示截图
由此,利用Web Components的custom elements、shadow dom、template&slot可以搭建高扩展性的组件集。可见小程序的自定义组件方案跟Web Components是一致的。而前文提到自定义组件内部的元素点击事件并未在e.target里面携带具体点击元素的信息,问题的关键在于Web Components是如何处理内部事件。
Shadow DOM中,子dom上发生的事件对于组件外部而言是透明的,所有内部的事件会被集中重定向到Shadow Root。相当于shadow root收集了子元素的可冒泡的事件,“消化”后自己再作为中间人把事件继续往外抛。以下面的代码为例,咱们来具体看看这种机制带来的影响
Web Components
cluo's desk
下图是打印的实际从e.target获取的dom对象
到此,总算是真相大白,基于web-components的组件嵌套时,由于shadow root对子元素事件的转发,导致实际被点击的元素是无法被识别的,只能知道是组件区域内被用户点击了。小程序如果只是在最外层进行用户行为事件的监听,则无法具体确认到用户实际点击的元素(假设自定义组件结构相对复杂)。
通过寻找这个项目中遇到的问题,对Web Components的基本概念、使用学习、体验一遍,下一期会分享针对这种情况,如何去继续兼容原先的自定义上报方案,敬请期待。