2 万张京东 H5 页面,一梭子数据模型搞定

3,633 阅读20分钟

前端早早聊大会,前端成长的新起点,与掘金联合举办。 加微信 codingdreamer 进大会专属内推群,赢在新的起跑线。


第十四届|前端成长晋升专场,8-29 即将直播,9 位讲师(蚂蚁金服/税友等),点我上车👉 (报名地址):


正文如下

本篇为第三届 - 前端页面搭建专场沐童讲师的分享:

一、介绍

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 来表示异步之外,我们允许初态函数提供第二个参数 —— callbackcallback 是一个回调函数,用于通知引擎执行渲染,所以我们可以通过在初态函数中多次调用 callback 函数来实现初态数据的分阶段渲染。

对于这类写法的初态函数,直出端只会响应其中的第一个 callback ,也就是说,当第一个 callback 被执行时,直出端就默认你已经准备好了用于直出渲染的数据,余下的 callback 将直接忽略。等到了客户端之后,初态函数会再被执行一遍,以补充剩余的 callback 调用。这样一来,我们只需要把主接口数据放在第一个 callback 调用,次接口数据由第二个 callback 调用,就能实现主次接口数据的分端请求渲染了。 

八、总结

  • 以上就是 MPM 为自搭建 H5 卖场打造的整个数据模型解决方案。
  • 虽说方案是基于 MPM 这类特殊场景设计的,有一定的针对性,但对于其他场景的页面搭建依然有它的借鉴意义。

1.页面搭建系统的开发心得

1.1 严谨设计

相比独立开发一个页面,搭建场景的开发可能随时随地都要求着严谨的设计。任何你认为的微不足道,如果不引起重视,最终都可能被放大,成为一个绕不开的绊脚石,阻碍你的系统进一步迭代优化。

1.2 重视规范

很多时候我们的设计,比如今天讨论的数据模型解决方案,并不是什么高深的技术,包括数据源的编写、三端同构流程,更多只是一套开发范式。搭建系统需要考虑的东西远比独立开发场景多得多,有了规范约束,才能更加自如地面对迭代和协同开发。

1.3 重视统一

独立和统一并不矛盾,并不是说搭建场景就是一切务求独立。相反,独立和统一是相辅相成的,虽然搭建的目的是自由组合,但在设计开发时却必须足够重视统一的思想。

2.关注我们


最后,感谢大家的观看,欢迎扫码关注我们的技术团队 WecTeam,给你带来更多技术分享。