当组织团队达到一定的开发规模时,页面可视化搭建是一个减少冗复开发、释放生产力的最有效方案。由于专人专责,在平时的实际工作中,我们接触的大多都是一些比较固定的业务,慢慢地,你很容易发现,我们一直在不停地做很多重复的东西。在这种情况下,我们会去思考组件化开发,试着把通用的东西抽离复用,但这依然远远不够。每一次需求下达,我们依然要花上至少两三天的时间去构建开发,但这些内容可能大多都是已经做过、或者大同小异的。因此,我们需要一个更加灵活、更加彻底的解决方案,最理想情况是实现零开发响应需求。
页面可视化搭建,就是这样的一种解决方案。你大可以发现,无论行业,一旦你的组织规模够大,开发资源跟日益增长的需求量不匹配时,总会诞生这样性质的一个系统。利用页面可视化搭建系统,需求方可以在不经过开发流程的情况下,通过简单的编辑操作,在极短时间内迅速搭建出一个复杂的页面,并发布上线。这样一来,不仅成倍地提高了需求的响应效率,更是有效解放了开发侧的生产力,让我们可以不再把时间精力耗费在冗复开发中,而得以聚焦到其他亟待关注的场景。
MPM(Mart Page Maker)是京东自研的一个卖场可视化搭建系统,自 2016 年以来,MPM 历经三个大版本迭代,如今已经发育成为一个组件模板丰富、配置功能强大、受众群体广泛的运营系统。
上线服务四年来,MPM 积累了丰富的组件和模板,除去已下架的外,MPM 现有 30+ 个组件、500+ 个模板,业务能力覆盖商品、导购、营销等多个场景。
对于许多手工开发的页面,实现直出仍然是一个困难重重的事情,而从运营手里搭建出来的 MPM 页面默认就支持首屏直出。我们打造了一个高可用的 Node 直出层,来负责获取页面配置数据、聚合请求接口,并最终渲染出页面首屏内容,从而突破了复杂卖场页面的首屏体验瓶颈。
除了首屏直出支持外,MPM 还具备其他一些强大的功能,如:
MPM 是京东深圳团队的核心运营系统,支撑着历年来所有营销活动,包括 618、双 11 等各种大促,以及日常的节日营销活动,如春上新、家电盛典等。在 2019 年的双 11 及双 12 大促活动中,京东深圳业务 90% 以上的大促会场都是由 MPM 搭建,包括主会场、所有一级会场和大部分的二级会场。
MPM 编辑界面 - 楼层配置
MPM 编辑界面 - 页面配置
MPM 生成页面
系统要素是构成系统的基本组成元素,是设计实现一个系统之前最需要考虑的核心点。推导系统要素,首先要对系统的设计背景、解决场景具备深入的认知和理解。作为一个卖场可视化搭建系统,MPM 面临的场景被约束在了卖场上,也就是说,我们要搭建的不再是一切页面,而只是卖场页面,这是 MPM 一切设计的根基。因此,我们需要对卖场有一个充分的了解。
在电商行业中,卖场是一个重要的售卖频道入口,通常情况下,卖场汇集了众多不同品类的商品进行统一售卖,能够有效地营造 “逛” 的氛围,进而提高订单转化。通过分析,我们归纳出卖场具备这样三个明显的特征。
卖场的楼层大多呈瀑布流自上而下铺列分布,楼层与楼层之间相互独立,关联较少。相比之下,像商品详情这类的页面,所有板块的内容都与同一个商品有关,其楼层之间的关联也相对较多。
卖场的职能主要还是吸引购买,所以卖场基本上多是一些商品物料、图文素材的展示,少有像玩法活动一样复杂的交互逻辑。
也正是因为卖场强大的引流能力,各个业务线都希望在卖场上能够占据到属于自己的资源位,因此在这种情况下,卖场自然要承载起各种各样的业务场景,其涉及到的业务接口也就变得十分地多。
那么基于以上分析的卖场特征,我们如何推导出 MPM 的系统要素呢?
首先我们知道,对于任何一个页面可视化搭建系统,属性都是必不可少的。以大家比较熟悉的 H5 制作工具 iH5[1] 为例,其配置方式大抵就是「拖一个按钮,配置按钮文字」这样的操作,这其中,配置按钮的文字就是属性,这也不难看出,属性是一个页面可视化搭建系统的最小配置单元。
其次,配置结构一定是分层的,属性之上,需要粒度更粗的配置形态。对于这种形态,iH5 以控件(图片、文字、按钮)来实现,如上边例子的按钮,所以 iH5 的配置结构其实是 控件
- 属性
。然而 MPM 并不适合使用这套配置结构,这是因为虽然配置的粒度越细,配置可以更加灵活,但配置成本也相应变大。卖场是个内容丰富的页面,以控件来搭建页面,那么搭建一个卖场势必就要花费很大的时间和精力。并且,卖场楼层拥有很多复杂的数据展示逻辑,比如字段 A 有值就展示 A,否则兜底展示字段 B。如果以控件为维度去构建页面,那么这样的逻辑实现就会落到运营手上,但运营不想要也不应该关心这些。我们希望当运营想要页面拥有某个楼层的时候,直接增加并简单配置就能呈现出来。
因此,MPM 使用了粒度更粗的两种配置形态 —— 组件/模板。组件是业务场景的第一载体,而模板则类似于组件的皮肤,为其提供强大的 UI 展示、表达能力。组件/模板是一个楼层,这样的粒度极大地降低了运营的配置成本,而 组件
- 模板
- 属性
三层配置结构也有效保障了卖场搭建的灵活性。
再者,前边提到,卖场场景所承载的业务接口特别多,如果我们简单地把接口请求的逻辑交给组件来做,一来组件各自发起请求,请求无法得到有效管理,二来接口逻辑和组件逻辑耦合,无法组合和复用。因此我们需要一个东西来接管所有组件原应承担的数据交互逻辑,统一管理所有接口请求,这就是数据源。
组件、模板、属性、数据源,是 MPM 卖场可视化搭建系统的四大系统要素。
组件是业务场景的第一载体,每一类业务场景在 MPM 中都对应了一个组件,因此按照业务属性划分,组件现有包括商品组件、秒杀组件、优惠券组件等。
在卖场中,我们用独立的 MPM 组件实例来构建每个楼层,这是基于卖场 “楼层相对独立” 的特征来设计的。这样处理的好处是:在不考虑卖场特征的时候我们面对的是一个一般的页面,页面结构是明显的树形结构,树形是极难进行操作处理的,而当我们考虑卖场楼层无关联的特征时,卖场的页面结构就从一个节点树形结构直接被简化为一个楼层序列结构,说白了就是楼层的数组列表,这极大地简化了 MPM 搭建页面的实现。
每个组件代表了一个业务场景,所以作为三层配置结构最顶级的组件,它的职责主要是实现业务场景的通用逻辑,比如:导航组件负责实现导航定位、优惠券组件负责实现查券和领券。
基于 Vue,我们很容易联想到利用 Vue 组件来实现一个 MPM 组件:
/**
* 秒杀组件
*/
import Vue from 'vue';
import utilMixins from './utils';
/**
* 注册 Vue 组件
*/
export default function register () {
Vue.component('seckill', {
props: ['params'],
mixins: [utilMixins],
data () {
return {
// ...
};
},
created () {
// ...
},
methods: {
// ...
}
})
}
在 MPM 中,每个 MPM 组件都被注册为一个对应的 Vue 全局组件,组件中实现通用逻辑。每个 Vue 组件都有一个固定的 props 属性 params
,存放的是用户对于这个楼层的配置数据。由于是全局组件,组装页面时我们就可以直接遍历配置,逐个渲染楼层并挂载展示。
并且值得留意的是,我们在 Vue 组件中并不指定 template 属性,这是因为我们设计要素时把配置分成了组件和模板两层,可想而知,MPM 模板其实就是 Vue 组件的 template,我们将它抽离出来,在其他步骤中再动态注入。
模板是组件的 UI 层,MPM 要求组件具备灵活的 UI 表现能力,因此我们将组件的 UI 层单独拆分出来,动态配置。组件之下有多个模板,所以组件-模板是 1-N 的关系。但模板又绝不是纯粹的 UI 层,在实际需求中,模板总是会包含一些或简单、或复杂的私有逻辑,比如商品组件的一些模板可能要求携带预约或领券动作,这就要求我们的模板具备承担这些私有逻辑的能力。
对于 MPM 模板,我们以一个固定格式的 HTML 来描述:
<!-- 模板的CSS代码 -->
<style>
.rank_2212_215 {
background: #fff;
}
</style>
<!-- 模板的HTML代码,基于Vue编写 -->
<template>
<div>
<p>Welcome to develop a template of MPM! </p>
</div>
</template>
<!-- 私有属性 -->
<script class="extends">
const com_extend = [
{ "name": "标题", "nick": "title", "type": "text" }
]
</script>
<!-- 私有逻辑 -->
<script class="methods">
const com_js = {
priceFormat () {
// ...
}
};
</script>
<!-- 生命周期 -->
<script class="hooks">
const com_vueHook = {
mounted () {
// ...
}
}
</script>
这个 HTML 并不是规范的结构,而是以一个我们自定义的格式呈现,MPM 提供了一个专门的解析器来解析这样的结构。它具备 style
、template
、script.extends
、script.methods
、script.hooks
几个最基础的组成部分:
style
:模板的 CSS 代码,MPM 解析提取后,会将 CSS 代码直接注入到全局生效;template
:模板的 template 代码,MPM 解析提取后,通过 Vue.compile 编译成 render function 注入到组件中;script.extends
:模板的私有属性,MPM 解析提取后,会将私有属性的配置挂载到组件数据 data.extend
上;script.methods
:模板的私有方法,是一个补充组件 methods 的工具方法宏,MPM 解析提取后,会将私有属性的配置挂载到组件数据 data.fnObj
上;script.hooks
:模板的生命周期函数,对应 Vue 的组件生命周期,MPM 解析提取后,将会在该组件的生命周期内相应进行调用。这种形态其实跟 Vue 单文件组件的结构很类似,而我们之所以选用 HTML 来实现 MPM 模板,是因为当时 Vue 单文件还没有出现,用 HTML 能为我们提供现成的编辑器高亮和语法提示支持。因此实际上,我们大可以也自行定义一种 .mpm
文件来存放 MPM 模板,并提供相应的编辑器插件和一个编译流程来解析这样的文件,当然这是后话了。
属性是 MPM 配置的最小单元,灵活组合的配置属性是实现卖场多样化的原动力。由于配置场景多样,MPM 需要提供多种类型的配置属性,包括日期选择、文本填写、图片上传、颜色选取等。
另一方面,为了和分层结构契合,MPM 属性还需要分为公有属性和私有属性,公有属性是组件级别的属性,比如商品组组件的商品组 id;私有属性是模板级别的属性,主要是一些模板私有逻辑依赖的属性。
此外,对于一些关键配置,如链接、素材 id、奖池标识等,MPM 属性还需要对其进行合法性校验。
基于这些诉求,我们以一个固定结构的对象来描述配置属性:
[
{ "name": "日期", "nick": "date", "type": "date" },
{ "name": "标题", "nick": "title", "type": "text" },
{ "name": "图片", "nick": "image", "type": "img" },
{ "name": "颜色", "nick": "color", "type": "color" },
{ "name": "单选", "nick": "radio", "type": "radio", "data": [
{ "name": "选项一", "value": 1 },
{ "name": "选项二", "value": 2 }
], "value": "1" },
{ "name": "多选", "nick": "option", "type": "option", "data": [
{ "name": "选项一", "value": 1 },
{ "name": "选项二", "value": 2 }
], "value": ["1"]},
{ "name": "范围", "nick": "range", "type": "range", "min": 230, "max": 280 }
]
上边代码被 MPM 解析后呈现的属性配置如上图。每个 object 对应了一个配置,object 的 type
属性用于指定配置的类型,我们提供了多达 10+ 类的配置类型,以满足不同的配置场景。最后经用户配置,我们大概会保存为这样的数据格式:
{
"date": "2020-01-01 00:00:00",
"title": "我是配置的标题",
"image": "//a.com/image.png",
"color": "#FFFFFF",
"radio": 1,
"option": [1, 2],
"range": 250
}
此外,属性可以利用 type
、regex
字段对用户的配置进行简单的正则校验。
一些特殊的配置类型默认具备一定的校验能力,应用了这类类型的属性,配置外观与 text 无异,但能实时地对配置数据应用预设的校验规则,如 type=url
用于校验 url 链接 ,type=id
用于校验纯数字且不超过 30 位的 id,type=char
用户校验英文、数字、下划线组合的标识,等。
[
{
"name": "类目id",
"nick": "cateid",
"type": "id"
}
]
如果现有正则校验规则不满足,你还可以通过 regex
字段来自定义你的校验规则,同时,为了更好地复用已有正则规则,我们允许以 $ + type
的格式来指定引用系统自带的正则规则,如下方代码利用 $id
引用了 id 的校验规则,来实现「多个 id 以英文逗号分隔」的校验需求,十分简便易读。
[
{
"name": "类目id",
"nick": "cateid",
"type": "id",
"regex": "^$id(,$id)*$",
"tips": "格式有误,请检查符号和空格!",
"ps": "多个id用英文逗号分隔"
}
]
前边提到,卖场承载了许多业务场景,涉及的接口繁多,如果任由各组件各自请求数据、处理数据,那么数据请求将变得难维护、不可控。因此,我们需要为 MPM 设计一个数据中心,由它来统一管理和维护所有接口请求。
数据中心包括了若干个数据源,每个数据源对应着一个接口,或者更准确来说,每个数据源对应着一类请求动作,包括接口地址、入参处理、响应处理等。此外,MPM 的请求是各楼层独立发出的,假如没有一个合适的机制来保证,那么就很可能导致同一个 MPM 页面发出很多个的朝向相同接口的请求,而如果接口本身其实支持批量请求,那么这就是极大的网络资源浪费。因此,MPM 还需要为数据源提供合并请求、分发响应的能力。
针对这块的设计,我们提供了一个数据源中心和若干个数据源。
数据源是一个类,它根据不同的用户配置创建不同的请求对象,一个请求对象代表了一个请求动作,将至少包括接口地址、请求参数、响应处理:
export default class GroupBuying {
constructor (option) {
// 参数处理
this.params = {
activeid: option.groupid
}
}
// 请求地址
url = '//wqcoss.jd.com/mcoss/pingou/show';
// 请求参数
params = {};
// 请求回调
callback (result) {
// ...
return result;
}
}
数据源中心被表达为一个 Vue 全局组件 ds
,它接受来自于 props 的一个入参字段 mpmsource
,这个字段指定了使用哪个数据源,也就是根据这个字段我们可以分别走不同接口的请求逻辑:
/**
* 数据源中心
*/
import Vue from 'vue';
import requester from './requeter';
import utilMixins from './utils';
import * as dataSourceMap from './data-source-map';
export default function register () {
Vue.component('ds', {
props: ['params'],
mixins: [utilMixins],
data () {
return {
// ...
result: null
};
},
async created () {
const { mpmsource } = this.params;
// 获取对应的数据源类
const DataSource = dataSourceMap[mpmsource];
// 实例化一个请求对象
const req = new DataSource(this.params);
// 发起请求
const result = await requester.fetch(req);
// 挂载接口数据
this.data.result = result;
},
methods: {
// ...
}
})
}
创建一个 ds 实例主要完成这一系列动作:首先根据 mpmsource 获取对应的数据源 class,传入配置数据,我们可以实例化得到一个请求对象,MPM 自制的请求器 requester 能够理解请求对象,发起请求并处理数据,最后挂载 data。
而我们只需要在 MPM 模板中这样使用:
<template>
<ds :params="{ mpmsource: 'groupbuying', ... }" inline-template>
<p>拉取到的拼购商品数量为:{{result.list.length}}</p>
</ds>
</template>
Vue 内联模板允许动态指定组件的 template,在这里经由 ds 组件请求数据,我们就可以在 ds 组件的内联模板中直接使用获取到的数据了。
此外,为了支持接口合并和响应分发,我们为数据源提供了自定义接口合并及分发策略的能力:
export default class GroupBuying {
// ...
batch = {
// 限制20个
limit: 20,
// 合并请求
merge (reqlist) {
return {
activeid: reqlist.map(req => req.data.activeid).join(',')
}
},
// 分发响应结果
unpack (result, reqlist) {
const ret = {};
reqlist.forEach(req => {
const key = md5(JSON.stringify(req));
ret[key] = result[req.data.activeid];
});
return ret;
}
}
}
batch
描述了该数据源的请求合并和分发策略,当数据源具有 batch 属性时,请求并不会被立刻发起,而是进入了等待队列。batch.limit
规定了合并的请求数量上限,当请求等待队列达到了这个上限,亦或是达到了默认的最大等待时间时,请求就会经由 batch.merge
函数打包,构建出新的、合并后的请求参数,然后发出请求。
等请求响应之后,响应数据会首先进入 batch.unpack
函数进行拆包分发。拆包结果是一个映射对象,键是请求对象的 md5 值,值是与该请求对象对应的数据,MPM 的请求器 requester 会自动对这个映射对象进行分拣,将数据分发到各个请求对象,再进入响应处理函数进行处理。
基于卖场构建场景,我们提炼并重点设计了 MPM 卖场可视化搭建系统的四大系统要素,这也是 MPM 其他流程设计的基础。估计大家看完之后可能存在不少疑惑:MPM 编辑流程如何设计?保存发布如何进行?同构直出是怎么实现的?...,依然觉得对 MPM 没有一个完整的认知。这是当然的,MPM 是个庞大且复杂的系统,我们没办法一次性让大家完全理解它。所以在后续我们还将整理出更多关于 MPM 的有意思的设计,分享给大家,希望多多关注。
参考资料
[1]
iH5: https://www.ih5.cn/