【第1910期】如何设计实现 h5 页面搭建-数据模型
前言
京东京喜相信大家有见过吧。比如【第1902期】京喜小程序的高性能打造之路今日早读文章由京东@沐童授权分享。
@沐童,京东京喜业务部前端开发工程师,WecTeam 团队核心成员,主要负责 MPM 可视化页面搭建系统的建设工作。
正文从这开始~~
介绍
今天想要跟大家分享的主题是如何设计实现 H5 页面搭建中的数据模型设计。数据模型指的是什么?为什么要讨论这个东西呢?简单地说,我们平时独立开发一个页面时,也经常会创建一个 model 层来作为数据请求出入口,承担、统管整个页面所有的数据请求,这就是页面的数据层模型。在 MPM 的早期,我们其实并没有怎么重视数据模型,相反,为了契合自搭建页面的楼层独立性,我们允许各个楼层自行发起请求、处理请求,把请求的逻辑完全交给各个组件独自完成。但是渐渐地,这种放养式的做法开始导致了维护上和页面性能优化上的种种问题。因此。在 MPM 之后的多次系统迭代中,数据模型设计都是一个重头戏,也正是因为经验教训积累,才有了今天这样一个议题。
今天的分享路径大致分为以下几步。首先我们会对 MPM 做一个整体的介绍,确保大家对 MPM 有一定的了解。其次我们会举证一些实际例子,让大家深刻体会到数据层面临的一些痛点,明白数据模型设计为何非做不可。再者就是数据模型设计中的两个重要内容,页面模型设计和请求模型设计,这也是今天分享的重点。最后是对本次分享一个小小总结。
MPM 整体介绍
系统简介
MPM 是京东内部运营使用的一个 H5 卖场可视化搭建系统,从 2016 年诞生至今,已经上线服务 4 年,系统迭代超过 3 个大版本。截止目前,MPM 累计使用人数 1400+,搭建页面数量也超过了 1.9 万张。除了平时一些日常活动(比如春上新)之外,历年来京东微 Q 业务的大促会场,有 80% 以上是由 MPM 搭建出来的。
能力概览
这是 MPM 的能力大图。经过了这些年的沉淀,MPM 已经拥有一个特别庞大的物料仓库,其中包括 30 多个组件、500 多个模板,业务能力覆盖了商品、导购、营销等多个场景。另一方面,MPM 还支持页面的三端渲染能力,同时也提供了强大的页面配置功能,包括页面楼层 BI 排序、自动化埋点、自动化测试、页面测速等。此外,MPM 也十分重视系统使用体验,不但配置了流畅的拖拽编辑器、实时预览和页面健康诊断能力,还对系统和页面做了全方面的监控和容灾降级方案。
效果展示
这是 MPM 的编辑界面。市面上大多数页面搭建系统以控件为最小粒度(比如按钮、输入框)进行搭建,但 MPM 的目标页面是卖场,相对更加复杂,使用控件搭建并不实际,因此我们采用了“组件-模板-属性”的三层配置结构,其中模板类似于组件的皮肤。从图中可以看出,正常的作业步骤就是从左侧组件列表拖一个组件添加到预览区,然后在右侧模板列表选择期望的模板。
而后在属性配置区配置楼层属性,最后发布页面。
最终,我们就可以得到这样的一个页面展示效果。
系统架构
这是 MPM 的系统架构。MPM 系统基于四大核心要素:组件、模板、属性和我们今天重点讲解的数据模型,打造了四个解析引擎,引擎能够对页面的配置数据 PageData 进行解析,生成实际页面。最上层是 MPM 面向用户的应用层,包括了编辑后台、管理统计后台和三大渲染平台。
工作流程
这是 MPM 的工作流程。在搭建页面时,运营通过拖放楼层、配置楼层数据、保存页面等多个步骤,生成了一份 PageData,而后将这份 PageData 发布到 CDN / Redis。在用户打开页面时,各端的 MPM 引擎会先请求这份 PageData 并对其进行解析,而后根据配置数据请求接口,渲染楼层,最终展示完整页面。
数据层面临的痛点
了解完 MPM 的大致情况后,我们再把目光聚焦到 MPM 的数据模型。数据层面临的痛点究竟是什么?为什么 MPM 会对数据模型尤其重视?我们可以从以下几个例子感受到。
请求散乱无章
第一种场景:页面请求茫茫多,有时候想定位页面中某个请求来自哪个组件,可能得定位半天。
MPM 负责搭建的是卖场,卖场往往是流量入口,承载了各线业务,因此接口场景特别复杂。如果我们简单地将请求完全交给组件自身,各自发起和处理,其结果就是页面请求散乱无章,维护困难不止,甚至可能互相影响。
多余的重复请求
第二种场景:某个页面中,有多个组件都配置了同一个预约 ID,导致页面发出了 N 个一模一样的预约态查询请求。
在自搭建场景,这种请求重复的问题再常见不过,假如我们没有对数据请求进行统一管理的话,那么很有可能这些无效的重复请求将严重拖垮你的页面性能。
接口压力大
第三种场景:商品接口支持批量请求,但由于页面的各个商品组件是独立请求的,导致多个商品请求并没有聚合,走批量调用。
一些常用的业务接口往往会支持批量调用,目的就是为了减轻服务调用压力。但是由于我们没有对请求进行统管,无法聚合,使得页面多次请求同个业务接口,给服务造成了不少压力。
数据模型多变
第四种场景:商品组件下的各个模板,除请求商品之外,有些模板会拉取新人价,有些模板会拉取补贴价。
这是页面搭建经常面临的问题 —— 数据模型多变。每个组件都对应了多个模板,每个模板又可能对应了不同的数据模型,那么如何进行数据模型的组合,这么多数据模型又该如何有效维护和管理,也是一个大问题。
三端同构诉求
第五种场景:以 Vue 为例,我们习惯在组件创建时,也就是 created
钩子函数中请求数据,这在客户端渲染时表现很完美,但在直出场景下却完全行不通。
归根结底,这其实是因为 Vue 虽然支持了 SSR,但对异步数据获取的同构支持却很不完善。为了适配 MPM 的三端同构,数据层设计必须考虑这个问题。
数据请求解决方案
基于以上种种问题,我们为自搭建卖场打造了一套高效通用的数据请求解决方案,它包括了以下三个解决目标:
统一管理 :将自搭建页面中所有数据请求进行统一管理,维护页面请求秩序,优化请求性能。
自由组合 :允许调用层(组件/模板)基于现有能力对数据模型进行自由组合,即插即用。
适配三端 :为三端同构提供统一的数据请求方案。
页面模型设计
MPM 的页面模型,也就是我们前边提到的 PageData,是 MPM 页面的一层抽象描述。它是一个普通的 JSON 对象,其中包含了页面的配置数据,经过解析引擎处理后,能够生成真实页面。PageData 主要包含两类配置:页面级配置和组件级配置。
页面级配置包括一些页面基础配置,这里决定了一些页面级别的请求,比如楼层 BI 排序查询就是在这里发起的,此外还有页面对用户身份的要求配置,比如“是否需要查询新人”,所以用户身份查询会在这里发起。
组件级配置其实就是组件楼层的配置,决定了各个组件楼层的业务数据获取,所以这是 PageData 的重要组成部分。
这就是组件楼层配置的结构和内容,它包括:楼层的模板配置,即指定了什么模板进行渲染;数据配置,包含了楼层请求接口所需的参数配置;组件关系,描述了组件的父子级对应关系。
请求模型设计
数据源
我们认为,请求模型的复杂性在于请求组合的复杂性,请求可以串联、并联,可以串并联混合,请求还有主次之分。要应对请求模型复杂灵活的组合,首先我们需要对请求进行量化,也就是说,我们需要一个最小单元来描述请求,请求模型则基于这些基本单元进行自由组合,这个最小单元就是数据源。
数据源是请求模型的基本组成单位,描述了一类请求动作,它包括以下几个基本属性:
接口地址:数据源和接口是一一对应的
请求前置处理:发起请求前的参数组装处理
请求后置处理:请求响应后的一些通用的数据处理
入参校验:发起请求前引擎会先对入参进行合法校验,非法入参将不会发起请求
聚合分发策略:描述了如何对该接口的多个同类请求进行请求聚合和响应分发
监控统计配置:接口监控、统计相关的配置
我们用一个类来实现数据源,一旦想要请求这个接口,调用层只需要以配置参数为入参进行实例化,就可以得到一个请求对象,引擎可以理解请求对象,并发起一个真正的请求。
数据源有很多个,每个数据源都有自己的名称标识,在调用层,我们只需要通过数据中心提供的 fetch
方法,指定数据源标识并传入配置数据,就可以建立起和数据源的绑定关系,来选择调用某个数据源。
基于这样的设计,我们可以很方便地实现请求模型的自由组合。
首先我们要求数据源应该是纯粹且专一的,它应该只做一件简单的事,比如跟这个接口密切相关的一些通用处理逻辑,而像一些跟特定部分组件/模板业务逻辑相关的处理,则不应该出现在这里,这是自由组合的前提。
其次,我们允许由数据源以各种形式自由组合成更高级、更复杂的请求模型,或者叫高级数据源。对于调用层来说,既可以直接调用数据源,也可以调用封装好的高级数据模型。
数据源如何组合成高级请求模型呢?这里我们采用了最简单灵活的函数调用,而不再是以类的形式来组织。函数天然拥有的闭包机制,对实现灵活多变的请求模型十分有利。上图就是这样一个例子,我们在函数内串联调用了商品、优惠券两个数据源,简单地实现了一个“带券商品”的请求模型。
高级请求模型是个函数,同样也就拥有唯一的名称标识。所以向上,我们将调用方式对齐,因此对于调用层来说,究竟是直接调用数据源,还是调用高级请求模型,其实没什么区别。
另一方面,依靠数据源,我们也有效地实现了统一管理。上图是数据源请求的整体工作流程,其中有两个核心模块 —— 数据中心和请求中心:
数据中心:是页面请求的代理层,页面所有请求都将经过数据中心,请求聚合分发在这里进行;
请求中心:解析来自数据中心的请求对象,发起请求并返回响应,请求的去重在这里进行。
借助这两个核心模块,整个页面请求的流程大致是这样的:
页面或楼层向数据中心申请一次数据请求,申请内容携带了数据源标识 source
和配置数据;
数据中心根据数据源标识 source
,选取相应数据源,并实例化一个请求对象,发给请求中心;
请求中心解析请求对象,发起请求,并处理响应,返回处理结果到数据中心;
数据中心再透传给调用层,触发响应渲染。
请求优化策略
当对页面请求做了统一管理之后,我们就可以对请求做一些合理的优化了。
首先,如何避免页面发起重复请求呢?
上图呈现的是请求中心的内部机制。首先,我们将请求分为了未发起、等待中、已完成三个生命阶段。当请求中心接收请求对象后,会先对请求对象做 MD5 判断:
如果是未发起,则直接发起请求,等待响应后会将响应结果写入缓存,并调用回调;
如果是等待中(即该请求对象来之前,已经有相同请求对象被处理过了,但还在等待响应),则不发起,仅推入回调等待队列;
如果是已完成(即该请求对象来之前,已经有相同请求对象被处理过了,且响应已返回),则直接使用缓存结果。
依靠这样一个简单的请求队列和请求缓存,我们有效避免了页面内发起重复请求。
其次,如何实现页面内同类接口请求的有效聚合?
前边我们提到,数据源中的 batch
属性可以制定聚合分发策略,上图就是 batch
的内部结构。它包含了三个属性:
pack
:接收多个请求对象,返回合并后的请求对象;
unpack
:接收聚合的响应数据和多个请求对象,返回拆包的映射结果;
limit
:允许聚合的请求对象数量上限。
每当数据中心创建出一个请求对象的时候,并不会立刻发给请求中心处理,而是先推入一个缓冲队列。等到下一个 Tick 时,数据中心会将上个 Tick 收集到的这一批请求对象,经 pack
函数处理,聚合成一个请求对象,再发给请求中心。等到响应后,再经 unpack
函数拆包,根据拆包映射分发到对应的各个调用层。
当然,假设在当前 Tick 中,缓冲队列内的请求对象达到了规定的上限,那么聚合就会提前执行。
上图呈现的就是一个聚合分发的流程,可以很明显看出,对于调用层来说,感知上依然是发出了 5 个请求,接收了 5 个响应结果,但对于请求中心来说,只接受并处理了 2 个请求对象,也就是只发出了 2 个请求。利用这样一套机制,我们可以很方便地让同类请求合多为一。
初态函数
为了实现 MPM 的三端同构,我们设计了初态函数。
可能很多人有疑问:前后端渲染到底有什么区别?假如我把客户端渲染那一套,照搬到直出端,为什么不行?那么这里就跟大家稍微解释下。
在客户端渲染中,我们经常喜欢在 created
钩子函数中编写数据请求,同时以骨架屏或局部 loading 作为占位,等到数据就位后再渲染出有效内容。这是客户端渲染的惯用手段,这也就说明了一个问题:客户端允许存在多趟渲染,大可以边渲染边请求,渲染和请求之间没有严格的先后顺序。
但是在直出端,渲染完成的下一步是向客户端作页面流式输出,有且只有一趟渲染。所以,直出渲染前,用于渲染的数据必须全部到位,也就是说,请求必须在渲染之前完成。
如果你把客户端渲染直接搬到直出端,很遗憾,你可能就只能直出一份骨架屏。
因此,我们可以得出以下几个结论:
三端同构的问题在这里被简化成前后端同构的问题,而前后端同构的关键就是初态渲染,所谓初态,就是页面的初始化阶段。
Vue 现有生命周期没有任何一个能够满足直出端的异步数据获取,要实现直出,数据模型就必须补充适配直出渲染的生命周期。
支持直出还不够,我们要实现三端同构,还需要规范解析流程,让三端解析流程保持高度统一。
基于这些,我们参考现有优秀的前后端同构框架 Next,设计了初态函数。Next 中也有初态函数,只不过 Next 的初态函数只能存在于页面级别,组件中是不允许有初态函数的。而 MPM 是组件搭建场景,我们不可能在页面级别去获取各个组件楼层的数据,因此我们对初态函数做了一些改造。
我们让每个 MPM 组件楼层都拥有一个初态函数,它是位于组件生命周期最开始的一个异步函数。初态函数以组件配置数据为入参,异步返回用于组件初始化渲染需要的初态数据。
三端引擎在创建组件实例之前,会先收集各组件的初态函数,执行,并将函数的异步返回结果作为组件初始化渲染的数据。
基于初态函数,我们对客户端,也就是静态 H5 和小程序端的渲染流程做了一些调整。我们不再允许客户端随意在 created
钩子函数中编写初态数据请求,而是要通过初态函数来实现,为的就是和直出端的页面解析渲染流程保持严格统一,便于同构。
而对于直出端,其解析流程大体和客户端相同,唯一区别是,直出流式渲染后,页面到达客户端需要进行楼层组件的激活,让直出楼层接受 Vue 的状态管理。
有时候我们可能遇到这种场景需求:有一个组件,串联请求了主、次两个接口,次接口内容没那么重要,为了提高直出效率,能不能只直出主接口,次接口等到了客户端再请求呢?
为了实现这类主次接口的分端请求,我们又进一步对初态函数做了一些改造。我们让初态函数支持了这种写法,除了返回一个 Promise 来表示异步之外,我们允许初态函数提供第二个参数 —— callback
。callback
是一个回调函数,用于通知引擎执行渲染,所以我们可以通过在初态函数中多次调用 callback
函数来实现初态数据的分阶段渲染。
对于这类写法的初态函数,直出端只会响应其中的第一个 callback
,也就是说,当第一个 callback
被执行时,直出端就默认你已经准备好了用于直出渲染的数据,余下的 callback
将直接忽略。等到了客户端之后,初态函数会再被执行一遍,以补充剩余的 callback
调用。这样一来,我们只需要把主接口数据放在第一个 callback
调用,次接口数据由第二个 callback
调用,就能实现主次接口数据的分端请求渲染了。
总结
以上就是 MPM 为自搭建 H5 卖场打造的整个数据模型解决方案。虽说方案是基于 MPM 这类特殊场景设计的,有一定的针对性,但对于其他场景的页面搭建依然有它的借鉴意义。在这里也想跟大家分享一些自己关于页面搭建系统的开发心得:
严谨设计。相比独立开发一个页面,搭建场景的开发可能随时随地都要求着严谨的设计。任何你认为的微不足道,如果不引起重视,最终都可能被放大,成为一个绕不开的绊脚石,阻碍你的系统进一步迭代优化。
重视规范。很多时候我们的设计,比如今天讨论的数据模型解决方案,并不是什么高深的技术,包括数据源的编写、三端同构流程,更多只是一套开发范式。搭建系统需要考虑的东西远比独立开发场景多得多,有了规范约束,才能更加自如地面对迭代和协同开发。
重视统一。独立和统一并不矛盾,并不是说搭建场景就是一切务求独立。相反,独立和统一是相辅相成的,虽然搭建的目的是自由组合,但在设计开发时却必须足够重视统一的思想。
关于本文 作者:@沐童 原文:https://mp.weixin.qq.com/s/Fo1BzPmxFA3EkWtvSDlhRg
为你推荐