前端搞搭建|沐童-如何设计实现 H5 页面搭建系统 - 数据模型
时间:2023-09-25 16:54:01 | 来源:网站运营
时间:2023-09-25 16:54:01 来源:网站运营
前端搞搭建|沐童-如何设计实现 H5 页面搭建系统 - 数据模型:
下期预告
6-20,是第十期 - 跨端跨栈
https://www.huodongxing.com/go/tl106-27,是第十一期 - 女生专场
https://www.huodongxing.com/go/tl11另外,每一期都提供了全年的联票,可以看所有场次的录播视频,包括下半年到年底的所有直播视频
前端早早聊大会目标成为用得上、听得懂、抄得走的技术大会,计划 2020 年办 >= 15 期,由前端早早聊与掘金联合举办,前端早早聊大会行程动态、录播视频/PPT/讲稿资料下载请关注 「前端早早聊」 公众号跟进。
你的支持,是早早聊办下去的唯一动力!
还想听哪方面的分享,直接加 Scott 微信: codingdreamer 提需求吧!
正文
本篇为第三届 - 前端页面搭建专场沐童讲师的分享:
1.我是谁?
Hello 大家好,很高兴今天有机会能在这里跟大家分享自己关于
页面可视化搭建的一些开发思路。
先简单自我介绍一下,我是
沐童,18 年毕业后加入京东,目前就职于
京东京喜业务部,也是 WecTeam 的核心成员,在团队里主要负责了内部使用的一个
H5 卖场可视化搭建系统 —— MPM 的建设工作。
2.我们的团队
京东京喜是京东致力于打造下沉电商的战略核心部门,包括京喜小程序(微信购物入口)、京东购物小程序及其他微信手 Q 渠道的业务,都是由我们团队负责维护。在去年,我们自发成立了对外的前端技术团队
WecTeam,希望通过技术分享和项目开源等方式,参与和推动前端技术发展,为行业带来更大价值。
WecTeam 公众号,欢迎关注二、分享主题
今天想要跟大家分享的主题是《
如何设计实现 H5 页面搭建中的数据模型设计》。
1.数据模型指的是什么?
- 简单地说,我们平时独立开发一个页面时,经常会创建一个 model 层 来作为数据请求出入口,承担、统管整个页面所有的数据请求,这就是页面数据模型的一个简单形态。
2.为什么要讨论数据模型?
- 不重视数据模型产生的问题:在 MPM(H5 卖场可视化搭建系统) 的早期,我们并没有怎么重视数据模型,相反,为了契合自搭建页面的楼层独立性,我们允许各个楼层自行发起请求、处理请求,把请求的逻辑完全交给各个组件独自完成。但是渐渐地,这种放养式的做法开始导致了维护上和页面性能优化上的种种问题。
- 因此。在 MPM 之后的多次系统迭代中,数据模型设计都是一个重头戏,也正是因为经验教训积累,才有了今天这样一个议题。
3.主题目录
- 首先,我们会对 MPM 做一个整体的介绍,确保大家对 MPM 有一定的了解。
- 其次,我们会举证一些实际例子,让大家深刻体会到数据层面临的一些痛点,明白数据模型设计为何非做不可。
- 再者,就是数据模型设计中的两个重要内容,页面模型设计和请求模型设计,这也是今天分享的重点。
- 最后,是对本次分享一个小小总结。
三、MPM 整体介绍
1.系统简介
- MPM 是京东内部运营使用的一个 H5 卖场可视化搭建系统。
- 从 2016 年诞生至今,已经上线服务 4 年,系统迭代超过 3 个大版本。
- 截止目前,MPM 累计使用人数 1400+,搭建页面数量也超过了 1.9 万张。除了平时一些日常活动(比如春上新)之外,历年来京东微 Q 业务的大促会场,有 80% 以上是由 MPM 搭建出来的。
2.能力概览
- MPM 已拥有特别庞大的物料仓库,其中包括 30 多个组件、500 多个模板,业务能力覆盖了商品、导购、营销等多个场景。
- MPM 支持页面的三端渲染能力,同时提供了强大的页面配置功能,包括页面楼层 BI 排序、自动化埋点、自动化测试、页面测速等。
- MPM 十分重视系统使用体验,不但配置了流畅的拖拽编辑器、实时预览和页面健康诊断能力,还对系统和页面做了全方面的监控和容灾降级方案。
3.效果展示
MPM编辑界面- 市面上大多数页面搭建系统以控件为最小粒度(比如按钮、输入框)进行搭建,但 MPM 的目标页面是卖场,相对更加复杂,使用控件搭建并不实际,因此我们采用了“组件-模板-属性”的三层配置结构,其中模板类似于组件的皮肤。
- 从图中可以看出,MPM 正常作业 4 步骤
- 从左侧组件列表拖一个组件添加到预览区
- 在右侧模板列表选择期望的模板
- 在属性配置区配置楼层属性
- 发布页面
配置发布页面最终效果图4.系统架构
- MPM 系统基于四大核心要素:组件、模板、属性和我们今天重点讲解的数据模型,打造了四个解析引擎,引擎能够对页面的配置数据 PageData 进行解析,生成实际页面。
- 最上层是 MPM 面向用户的应用层,包括了编辑后台、管理统计后台和三大渲染平台。
5.工作流程
- 在搭建页面时,运营通过拖放楼层、配置楼层数据、保存页面等多个步骤,生成了一份 PageData,而后将这份 PageData 发布到 CDN / Redis。
- 在用户打开页面时,各端的 MPM 引擎会先请求这份 PageData 并对其进行解析,而后根据配置数据请求接口,渲染楼层,最终展示完整页面。
四、数据层面临的痛点
了解完 MPM 的大致情况后,我们再把目光聚焦到 MPM 的数据模型。数据层面临的痛点究竟是什么?为什么 MPM 会对数据模型尤其重视?我们可以从以下几个例子感受到。
1.请求散乱无章
第一种场景:页面请求茫茫多,有时候想定位页面中某个请求来自哪个组件,可能得定位半天。
- MPM 负责搭建的是卖场,卖场往往是流量入口,承载了各线业务,因此接口场景特别复杂。如果我们简单地将请求完全交给组件自身,各自发起和处理,其结果就是页面请求散乱无章,维护困难不止,甚至可能互相影响。
2.多余的重复请求
第二种场景:某个页面中,有多个组件都配置了同一个预约 ID,导致页面发出了 N 个一模一样的预约态查询请求。
- 在自搭建场景,这种请求重复的问题再常见不过,假如我们没有对数据请求进行统一管理的话,那么很有可能这些无效的重复请求将严重拖垮你的页面性能。
3.接口压力大
第三种场景:商品接口支持批量请求,但由于页面的各个商品组件是独立请求的,导致多个商品请求并没有聚合,走批量调用。
- 一些常用的业务接口往往会支持批量调用,目的就是为了减轻服务调用压力。但是由于我们没有对请求进行统管,无法聚合,使得页面多次请求同个业务接口,给服务造成了不少压力。
4.数据模型多变
第四种场景:商品组件下的各个模板,除请求商品之外,有些模板会拉取新人价,有些模板会拉取补贴价。
- 这是页面搭建经常面临的问题 —— 数据模型多变。每个组件都对应了多个模板,每个模板又可能对应了不同的数据模型,那么如何进行数据模型的组合,这么多数据模型又该如何有效维护和管理,也是一个大问题。
5.三端同构诉求
第五种场景:以 Vue 为例,我们习惯在组件创建时,也就是
created
钩子函数中请求数据,这在客户端渲染时表现很完美,但在直出场景下却完全行不通。
- 归根结底,这其实是因为 Vue 虽然支持了 SSR,但对异步数据获取的同构支持却很不完善。为了适配 MPM 的三端同构,数据层设计必须考虑这个问题。
五、数据请求解决方案
基于以上种种问题,我们为自搭建卖场打造了一套高效通用的数据请求解决方案,它包括了以下三个解决目标:
- 统一管理 :将自搭建页面中所有数据请求进行统一管理,维护页面请求秩序,优化请求性能。
- 自由组合 :允许调用层(组件/模板)基于现有能力对数据模型进行自由组合,即插即用。
- 适配三端 :为三端同构提供统一的数据请求方案。
六、页面模型设计
- MPM 的页面模型,也就是我们前边提到的 PageData,是 MPM 页面的一层抽象描述。
- PageData 是一个普通的 JSON 对象,其中包含了页面的配置数据,经过解析引擎处理后,能够生成真实页面。
- PageData 主要包含两类配置:页面级配置和组件级配置。
- 页面级配置包括一些页面基础配置,它决定了一些页面级别的请求,比如楼层 BI 排序查询就是在这里发起的,此外还有页面对用户身份的要求配置,比如“是否需要查询新人”,所以用户身份查询会在这里发起。
- 组件级配置就是组件楼层的配置,决定了各个组件楼层的业务数据获取,这是 PageData 的重要组成部分。
组件楼层配置的结构和内容,它包括:
- 楼层的模板配置,即指定了什么模板进行渲染;
- 数据配置,包含了楼层请求接口所需的参数配置;
- 组件关系,描述了组件的父子级对应关系。
七、请求模型设计
1.数据源
我们认为,请求模型的复杂性在于请求组合的复杂性,请求可以串联、并联及串并联混合,请求还有
主次之分。
要应对请求模型复杂灵活的组合,首先我们需要对请求进行量化,也就是说,我们需要一个最小单元来描述请求,请求模型则基于这些基本单元进行自由组合,这个最小单元就是数据源。
数据源是请求模型的基本组成单位,描述了一类请求动作,它包括以下几个基本属性:
- 接口地址:数据源和接口是一一对应的
- 请求前置处理:发起请求前的参数组装处理
- 请求后置处理:请求响应后的一些通用的数据处理
- 入参校验:发起请求前引擎会先对入参进行合法校验,非法入参将不会发起请求
- 聚合分发策略:描述了如何对该接口的多个同类请求进行请求聚合和响应分发
- 监控统计配置:接口监控、统计相关的配置
我们用一个类来实现数据源,一旦想要请求这个接口,调用层只需要以配置参数为入参进行实例化,就可以得到一个请求对象,引擎可以理解请求对象,并发起一个真正的请求。
数据源有很多个,每个数据源都有自己的名称标识,在调用层,我们只需要通过数据中心提供的
fetch
方法,指定数据源标识并传入配置数据,就可以建立起和数据源的绑定关系,来选择调用某个数据源。
基于这样的设计,我们可以很方便地实现请求模型的自由组合。
首先我们要求数据源应该是纯粹且专一的,它应该只做一件简单的事,比如跟这个接口密切相关的一些通用处理逻辑,而像一些跟特定部分组件/模板业务逻辑相关的处理,则不应该出现在这里,这是自由组合的前提。
其次,我们允许由数据源以各种形式自由组合成更高级、更复杂的请求模型,或者叫高级数据源。对于调用层来说,既可以直接调用数据源,也可以调用封装好的高级数据模型。
数据源如何组合成高级请求模型呢?这里我们采用了最简单灵活的函数调用,而不再是以类的形式来组织。函数天然拥有的作用域机制,对实现灵活多变的请求模型十分有利。上图就是这样一个例子,我们在函数内串联调用了商品、优惠券两个数据源,简单地实现了一个“带券商品”的请求模型。
高级请求模型是个函数,同样也就拥有唯一的名称标识。所以向上,我们将调用方式对齐,因此对于调用层来说,究竟是直接调用数据源,还是调用高级请求模型,其实没什么区别。
另一方面,依靠数据源,我们也有效地实现了统一管理。上图是数据源请求的整体工作流程,其中有两个核心模块 —— 数据中心和请求中心:
- 数据中心:是页面请求的代理层,页面所有请求都将经过数据中心,请求聚合分发在这里进行;
- 请求中心:解析来自数据中心的请求对象,发起请求并返回响应,请求的去重在这里进行。
借助这两个核心模块,整个页面请求的流程大致是这样的:
- ①.页面或楼层向数据中心申请一次数据请求,申请内容携带了数据源标识
source
和配置数据; - ②.数据中心根据数据源标识
source
,选取相应数据源,并实例化一个请求对象,发给请求中心; - ③.请求中心解析请求对象,发起请求,并处理响应,返回处理结果到数据中心;
- ④.数据中心再透传给调用层,触发响应渲染。
2.请求优化策略
当对页面请求做了统一管理之后,我们就可以对请求做一些合理的优化了。
请求中心的内部机制2.1 如何避免页面发起重复请求呢?
首先,我们将请求分为了未发起、等待中、已完成三个生命阶段。当请求中心接收请求对象后,会先对请求对象做 MD5 判断:
- 如果是未发起,则直接发起请求,等待响应后会将响应结果写入缓存,并调用回调;
- 如果是等待中(即该请求对象来之前,已经有相同请求对象被处理过了,但还在等待响应),则不发起,仅推入回调等待队列;
- 如果是已完成(即该请求对象来之前,已经有相同请求对象被处理过了,且响应已返回),则直接使用缓存结果。
依靠这样一个简单的请求队列和请求缓存,我们有效避免了页面内发起重复请求。
2.2 如何实现页面内同类接口请求的有效聚合?
前边我们提到,数据源中的
batch
属性可以制定聚合分发策略,上图就是
batch
的内部结构。它包含了三个属性:
pack
:接收多个请求对象,返回合并后的请求对象;unpack
:接收聚合的响应数据和多个请求对象,返回拆包的映射结果;limit
:允许聚合的请求对象数量上限。
每当数据中心创建出一个请求对象的时候,并不会立刻发给请求中心处理,而是先推入一个缓冲队列。等到下一个 Tick 时,数据中心会将上个 Tick 收集到的这一批请求对象,经
pack
函数处理,聚合成一个请求对象,再发给请求中心。等到响应后,再经
unpack
函数拆包,根据拆包映射分发到对应的各个调用层。
当然,假设在当前 Tick 中,缓冲队列内的请求对象达到了规定的上限,那么聚合就会提前执行。
聚合分发的流程上图可以很明显看出,对于调用层来说,感知上依然是发出了 5 个请求,接收了 5 个响应结果,但对于请求中心来说,只接受并处理了 2 个请求对象,也就是只发出了 2 个请求。所以在这里,聚合分发层其实起到了一个隔离的作用,对于隔离层的两端来说,对方都是黑盒。因此,利用这样一套机制,我们可以很方便地让同类请求合多为一。
3.初态函数
为了实现 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 这类特殊场景设计的,有一定的针对性,但对于其他场景的页面搭建依然有它的借鉴意义。
1.页面搭建系统的开发心得
1.1 严谨设计
相比独立开发一个页面,搭建场景的开发可能随时随地都要求着严谨的设计。任何你认为的微不足道,如果不引起重视,最终都可能被放大,成为一个绕不开的绊脚石,阻碍你的系统进一步迭代优化。
1.2 重视规范
很多时候我们的设计,比如今天讨论的数据模型解决方案,并不是什么高深的技术,包括数据源的编写、三端同构流程,更多只是一套开发范式。搭建系统需要考虑的东西远比独立开发场景多得多,有了规范约束,才能更加自如地面对迭代和协同开发。
1.3 重视统一
独立和统一并不矛盾,并不是说搭建场景就是一切务求独立。相反,独立和统一是相辅相成的,虽然搭建的目的是自由组合,但在设计开发时却必须足够重视统一的思想。
2.关注我们
最后,感谢大家的观看,欢迎扫码关注我们的技术团队
WecTeam,给你带来更多技术分享。
近两年 Scott 观察到前端行业已经完全进入竞争的深水区,各大小公司的前端 TL 刚刚上任,初带团队,针对前端工程师这个群体,应该怎么管人理事,搭台拿结果,帮带有成长,就成立了这个前端技术主管学习交流群,在人的选用育留上互相学习成长,入群的门槛是你有实线或者虚线在带团队,请加 Scott 微信: codingdreamer 邀请入群:
看完若有启发,就请点赞评论转发三连吧