阅读 176

MPM 卖场可视化搭建系统 — 数据模型设计

看本文之前,不妨先看看:
1)MPM 卖场可视化搭建系统 — 要素设计
2) MPM 卖场可视化搭建系统 — 架构流程设计

前言

这是 MPM 分享系列的第三篇。在上一篇 MPM 卖场可视化搭建系统 — 架构流程设计 中聊到数据请求的时候,我们其实没怎么细讲,那是因为在 MPM 的卖场搭建场景下,页面的数据请求经过了我们精心设计之后,足以用单独的一章来了解。面对 MPM 搭建场景下的请求繁杂、组件组合、三端同构等种种问题和诉求时,如何打造一个高效通用的数据请求解决方案,这个问题正好前阵子有机会在第三届前端早早聊大会跟大家分享和探讨,现将演讲 PPT 整理成稿,以下就是大会的分享内容。

自我介绍

image.png

Hello 大家好,很高兴今天有机会能在这里跟大家分享自己关于页面可视化搭建的一些开发思路。先简单自我介绍一下,我是沐童,目前就职于京东京喜前端团队,在团队里主要负责了内部使用的一个 h5 卖场可视化搭建系统 —— MPM 的建设工作。

议题介绍

今天想要跟大家分享的主题是如何设计实现 H5 页面搭建中的数据模型设计。数据模型指的是什么?为什么要讨论这个东西呢?简单地说,我们平时独立开发一个页面时,也经常会创建一个 model 层来作为数据请求出入口,承担、统管整个页面所有的数据请求,这就是页面的数据层模型。在 MPM 的早期,我们其实并没有怎么重视数据模型,相反,为了契合自搭建页面的楼层独立性,我们允许各个楼层自行发起请求、处理请求,把请求的逻辑完全交给各个组件独自完成。但是渐渐地,这种放养式的做法开始导致了维护上和页面性能优化上的种种问题。因此。在 MPM 之后的多次系统迭代中,数据模型设计都是一个重头戏,也正是因为经验教训积累,才有了今天这样一个议题。

image.png

今天的分享路径大致分为以下几步。首先我们会对 MPM 做一个整体的介绍,确保大家对 MPM 有一定的了解。其次我们会举证一些实际例子,让大家深刻体会到数据层面临的一些痛点,明白数据模型设计为何非做不可。再者就是数据模型设计中的两个重要内容,页面模型设计和请求模型设计,这也是今天分享的重点。最后是对本次分享一个小小总结。

MPM 整体介绍

系统简介

image.png

MPM 是京东内部运营使用的一个 H5 卖场可视化搭建系统,从 2016 年诞生至今,已经上线服务 4 年,系统迭代超过 3 个大版本。截止目前,MPM 累计使用人数 1400+,搭建页面数量也超过了 1.9 万张。除了平时一些日常活动(比如春上新)之外,历年来京东微 Q 业务的大促会场,有 80% 以上是由 MPM 搭建出来的。

能力概览

image.png

这是 MPM 的能力大图。经过了这些年的沉淀,MPM 已经拥有一个特别庞大的物料仓库,其中包括 30 多个组件、500 多个模板,业务能力覆盖了商品、导购、营销等多个场景。另一方面,MPM 还支持页面的三端渲染能力,同时也提供了强大的页面配置功能,包括页面楼层 BI 排序、自动化埋点、自动化测试、页面测速等。此外,MPM 也十分重视系统使用体验,不但配置了流畅的拖拽编辑器、实时预览和页面健康诊断能力,还对系统和页面做了全方面的监控和容灾降级方案。

效果展示

image.png

这是 MPM 的编辑界面。市面上大多数页面搭建系统以控件为最小粒度(比如按钮、输入框)进行搭建,但 MPM 的目标页面是卖场,相对更加复杂,使用控件搭建并不实际,因此我们采用了“组件-模板-属性”的三层配置结构,其中模板类似于组件的皮肤。从图中可以看出,正常的作业步骤就是从左侧组件列表拖一个组件添加到预览区,然后在右侧模板列表选择期望的模板。

image.png

而后在属性配置区配置楼层属性,最后发布页面。

image.png

最终,我们就可以得到这样的一个页面展示效果。

系统架构

image.png

这是 MPM 的系统架构。MPM 系统基于四大核心要素:组件、模板、属性和我们今天重点讲解的数据模型,打造了四个解析引擎,引擎能够对页面的配置数据 PageData 进行解析,生成实际页面。最上层是 MPM 面向用户的应用层,包括了编辑后台、管理统计后台和三大渲染平台。

工作流程

image.png

这是 MPM 的工作流程。在搭建页面时,运营通过拖放楼层、配置楼层数据、保存页面等多个步骤,生成了一份 PageData,而后将这份 PageData 发布到 CDN / Redis。在用户打开页面时,各端的 MPM 引擎会先请求这份 PageData 并对其进行解析,而后根据配置数据请求接口,渲染楼层,最终展示完整页面。

数据层面临的痛点

了解完 MPM 的大致情况后,我们再把目光聚焦到 MPM 的数据模型。数据层面临的痛点究竟是什么?为什么 MPM 会对数据模型尤其重视?我们可以从以下几个例子感受到。

请求散乱无章

image.png

第一种场景:页面请求茫茫多,有时候想定位页面中某个请求来自哪个组件,可能得定位半天。

MPM 负责搭建的是卖场,卖场往往是流量入口,承载了各线业务,因此接口场景特别复杂。如果我们简单地将请求完全交给组件自身,各自发起和处理,其结果就是页面请求散乱无章,维护困难不止,甚至可能互相影响。

多余的重复请求

image.png

第二种场景:某个页面中,有多个组件都配置了同一个预约 ID,导致页面发出了 N 个一模一样的预约态查询请求。

在自搭建场景,这种请求重复的问题再常见不过,假如我们没有对数据请求进行统一管理的话,那么很有可能这些无效的重复请求将严重拖垮你的页面性能。

接口压力大

image.png

第三种场景:商品接口支持批量请求,但由于页面的各个商品组件是独立请求的,导致多个商品请求并没有聚合,走批量调用。

一些常用的业务接口往往会支持批量调用,目的就是为了减轻服务调用压力。但是由于我们没有对请求进行统管,无法聚合,使得页面多次请求同个业务接口,给服务造成了不少压力。

数据模型多变

image.png

第四种场景:商品组件下的各个模板,除请求商品之外,有些模板会拉取新人价,有些模板会拉取补贴价。

这是页面搭建经常面临的问题 —— 数据模型多变。每个组件都对应了多个模板,每个模板又可能对应了不同的数据模型,那么如何进行数据模型的组合,这么多数据模型又该如何有效维护和管理,也是一个大问题。

三端同构诉求

image.png

第五种场景:以 Vue 为例,我们习惯在组件创建时,也就是 created 钩子函数中请求数据,这在客户端渲染时表现很完美,但在直出场景下却完全行不通。

归根结底,这其实是因为 Vue 虽然支持了 SSR,但对异步数据获取的同构支持却很不完善。为了适配 MPM 的三端同构,数据层设计必须考虑这个问题。

数据请求解决方案

image.png

基于以上种种问题,我们为自搭建卖场打造了一套高效通用的数据请求解决方案,它包括了以下三个解决目标:

  • 统一管理 :将自搭建页面中所有数据请求进行统一管理,维护页面请求秩序,优化请求性能。

  • 自由组合 :允许调用层(组件/模板)基于现有能力对数据模型进行自由组合,即插即用。

  • 适配三端 :为三端同构提供统一的数据请求方案。

页面模型设计

image.png

MPM 的页面模型,也就是我们前边提到的 PageData,是 MPM 页面的一层抽象描述。它是一个普通的 JSON 对象,其中包含了页面的配置数据,经过解析引擎处理后,能够生成真实页面。PageData 主要包含两类配置:页面级配置和组件级配置。

页面级配置包括一些页面基础配置,这里决定了一些页面级别的请求,比如楼层 BI 排序查询就是在这里发起的,此外还有页面对用户身份的要求配置,比如“是否需要查询新人”,所以用户身份查询会在这里发起。

组件级配置其实就是组件楼层的配置,决定了各个组件楼层的业务数据获取,所以这是 PageData 的重要组成部分。

image.png

这就是组件楼层配置的结构和内容,它包括:楼层的模板配置,即指定了什么模板进行渲染;数据配置,包含了楼层请求接口所需的参数配置;组件关系,描述了组件的父子级对应关系。

请求模型设计

数据源

我们认为,请求模型的复杂性在于请求组合的复杂性,请求可以串联、并联,可以串并联混合,请求还有主次之分。要应对请求模型复杂灵活的组合,首先我们需要对请求进行量化,也就是说,我们需要一个最小单元来描述请求,请求模型则基于这些基本单元进行自由组合,这个最小单元就是数据源。

image.png

数据源是请求模型的基本组成单位,描述了一类请求动作,它包括以下几个基本属性:

  • 接口地址:数据源和接口是一一对应的

  • 请求前置处理:发起请求前的参数组装处理

  • 请求后置处理:请求响应后的一些通用的数据处理

  • 入参校验:发起请求前引擎会先对入参进行合法校验,非法入参将不会发起请求

  • 聚合分发策略:描述了如何对该接口的多个同类请求进行请求聚合和响应分发

  • 监控统计配置:接口监控、统计相关的配置

image.png

我们用一个类来实现数据源,一旦想要请求这个接口,调用层只需要以配置参数为入参进行实例化,就可以得到一个请求对象,引擎可以理解请求对象,并发起一个真正的请求。

数据源有很多个,每个数据源都有自己的名称标识,在调用层,我们只需要通过数据中心提供的 fetch 方法,指定数据源标识并传入配置数据,就可以建立起和数据源的绑定关系,来选择调用某个数据源。

image.png

基于这样的设计,我们可以很方便地实现请求模型的自由组合。

首先我们要求数据源应该是纯粹且专一的,它应该只做一件简单的事,比如跟这个接口密切相关的一些通用处理逻辑,而像一些跟特定部分组件/模板业务逻辑相关的处理,则不应该出现在这里,这是自由组合的前提。

其次,我们允许由数据源以各种形式自由组合成更高级、更复杂的请求模型,或者叫高级数据源。对于调用层来说,既可以直接调用数据源,也可以调用封装好的高级数据模型。

image.png

数据源如何组合成高级请求模型呢?这里我们采用了最简单灵活的函数调用,而不再是以类的形式来组织。函数天然拥有的闭包机制,对实现灵活多变的请求模型十分有利。上图就是这样一个例子,我们在函数内串联调用了商品、优惠券两个数据源,简单地实现了一个“带券商品”的请求模型。

高级请求模型是个函数,同样也就拥有唯一的名称标识。所以向上,我们将调用方式对齐,因此对于调用层来说,究竟是直接调用数据源,还是调用高级请求模型,其实没什么区别。

image.png

另一方面,依靠数据源,我们也有效地实现了统一管理。上图是数据源请求的整体工作流程,其中有两个核心模块 —— 数据中心和请求中心:

  • 数据中心:是页面请求的代理层,页面所有请求都将经过数据中心,请求聚合分发在这里进行;

  • 请求中心:解析来自数据中心的请求对象,发起请求并返回响应,请求的去重在这里进行。

借助这两个核心模块,整个页面请求的流程大致是这样的:

  1. 页面或楼层向数据中心申请一次数据请求,申请内容携带了数据源标识 source 和配置数据;

  2. 数据中心根据数据源标识 source,选取相应数据源,并实例化一个请求对象,发给请求中心;

  3. 请求中心解析请求对象,发起请求,并处理响应,返回处理结果到数据中心;

  4. 数据中心再透传给调用层,触发响应渲染。

请求优化策略

当对页面请求做了统一管理之后,我们就可以对请求做一些合理的优化了。

image.png

首先,如何避免页面发起重复请求呢?

上图呈现的是请求中心的内部机制。首先,我们将请求分为了未发起、等待中、已完成三个生命阶段。当请求中心接收请求对象后,会先对请求对象做 MD5 判断:

  • 如果是未发起,则直接发起请求,等待响应后会将响应结果写入缓存,并调用回调;

  • 如果是等待中(即该请求对象来之前,已经有相同请求对象被处理过了,但还在等待响应),则不发起,仅推入回调等待队列;

  • 如果是已完成(即该请求对象来之前,已经有相同请求对象被处理过了,且响应已返回),则直接使用缓存结果。

依靠这样一个简单的请求队列和请求缓存,我们有效避免了页面内发起重复请求。

image.png

其次,如何实现页面内同类接口请求的有效聚合?

前边我们提到,数据源中的 batch 属性可以制定聚合分发策略,上图就是 batch 的内部结构。它包含了三个属性:

  • pack :接收多个请求对象,返回合并后的请求对象;

  • unpack :接收聚合的响应数据和多个请求对象,返回拆包的映射结果;

  • limit :允许聚合的请求对象数量上限。

每当数据中心创建出一个请求对象的时候,并不会立刻发给请求中心处理,而是先推入一个缓冲队列。等到下一个 Tick 时,数据中心会将上个 Tick 收集到的这一批请求对象,经 pack 函数处理,聚合成一个请求对象,再发给请求中心。等到响应后,再经 unpack 函数拆包,根据拆包映射分发到对应的各个调用层。

当然,假设在当前 Tick 中,缓冲队列内的请求对象达到了规定的上限,那么聚合就会提前执行。

image.png

上图呈现的就是一个聚合分发的流程,可以很明显看出,对于调用层来说,感知上依然是发出了 5 个请求,接收了 5 个响应结果,但对于请求中心来说,只接受并处理了 2 个请求对象,也就是只发出了 2 个请求。利用这样一套机制,我们可以很方便地让同类请求合多为一。

初态函数

为了实现 MPM 的三端同构,我们设计了初态函数。

image.png

可能很多人有疑问:前后端渲染到底有什么区别?假如我把客户端渲染那一套,照搬到直出端,为什么不行?那么这里就跟大家稍微解释下。

image.png

在客户端渲染中,我们经常喜欢在 created 钩子函数中编写数据请求,同时以骨架屏或局部 loading 作为占位,等到数据就位后再渲染出有效内容。这是客户端渲染的惯用手段,这也就说明了一个问题:客户端允许存在多趟渲染,大可以边渲染边请求,渲染和请求之间没有严格的先后顺序。

image.png

但是在直出端,渲染完成的下一步是向客户端作页面流式输出,有且只有一趟渲染。所以,直出渲染前,用于渲染的数据必须全部到位,也就是说,请求必须在渲染之前完成。

image.png

如果你把客户端渲染直接搬到直出端,很遗憾,你可能就只能直出一份骨架屏。

image.png

因此,我们可以得出以下几个结论:

  1. 三端同构的问题在这里被简化成前后端同构的问题,而前后端同构的关键就是初态渲染,所谓初态,就是页面的初始化阶段。

  2. Vue 现有生命周期没有任何一个能够满足直出端的异步数据获取,要实现直出,数据模型就必须补充适配直出渲染的生命周期。

  3. 支持直出还不够,我们要实现三端同构,还需要规范解析流程,让三端解析流程保持高度统一。

image.png

基于这些,我们参考现有优秀的前后端同构框架 Next,设计了初态函数。Next 中也有初态函数,只不过 Next 的初态函数只能存在于页面级别,组件中是不允许有初态函数的。而 MPM 是组件搭建场景,我们不可能在页面级别去获取各个组件楼层的数据,因此我们对初态函数做了一些改造。

我们让每个 MPM 组件楼层都拥有一个初态函数,它是位于组件生命周期最开始的一个异步函数。初态函数以组件配置数据为入参,异步返回用于组件初始化渲染需要的初态数据。

三端引擎在创建组件实例之前,会先收集各组件的初态函数,执行,并将函数的异步返回结果作为组件初始化渲染的数据。

image.png

基于初态函数,我们对客户端,也就是静态 H5 和小程序端的渲染流程做了一些调整。我们不再允许客户端随意在 created 钩子函数中编写初态数据请求,而是要通过初态函数来实现,为的就是和直出端的页面解析渲染流程保持严格统一,便于同构。

image.png

而对于直出端,其解析流程大体和客户端相同,唯一区别是,直出流式渲染后,页面到达客户端需要进行楼层组件的激活,让直出楼层接受 Vue 的状态管理。

image.png

有时候我们可能遇到这种场景需求:有一个组件,串联请求了主、次两个接口,次接口内容没那么重要,为了提高直出效率,能不能只直出主接口,次接口等到了客户端再请求呢?

image.png

为了实现这类主次接口的分端请求,我们又进一步对初态函数做了一些改造。我们让初态函数支持了这种写法,除了返回一个 Promise 来表示异步之外,我们允许初态函数提供第二个参数 —— callback 。 callback 是一个回调函数,用于通知引擎执行渲染,所以我们可以通过在初态函数中多次调用 callback 函数来实现初态数据的分阶段渲染。

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

总结

image.png

以上就是 MPM 为自搭建 H5 卖场打造的整个数据模型解决方案。虽说方案是基于 MPM 这类特殊场景设计的,有一定的针对性,但对于其他场景的页面搭建依然有它的借鉴意义。在这里也想跟大家分享一些自己关于页面搭建系统的开发心得:

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

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

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

image.png

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