当组织团队达到必定的开发规模时,页面可视化搭建是一个减小冗复开发、释放生产力的最有效方案。因为专人专责,在平时的实际工做中,咱们接触的大多都是一些比较固定的业务,慢慢地,你很容易发现,咱们一直在不停地作不少重复的东西。在这种状况下,咱们会去思考组件化开发,试着把通用的东西抽离复用,但这依然远远不够。每一次需求下达,咱们依然要花上至少两三天的时间去构建开发,但这些内容可能大多都是已经作过、或者大同小异的。所以,咱们须要一个更加灵活、更加完全的解决方案,最理想状况是实现零开发响应需求。html
页面可视化搭建,就是这样的一种解决方案。你大能够发现,不管行业,一旦你的组织规模够大,开发资源跟日益增加的需求量不匹配时,总会诞生这样性质的一个系统。利用页面可视化搭建系统,需求方能够在不通过开发流程的状况下,经过简单的编辑操做,在极短期内迅速搭建出一个复杂的页面,并发布上线。这样一来,不只成倍地提升了需求的响应效率,更是有效解放了开发侧的生产力,让咱们能够再也不把时间精力耗费在冗复开发中,而得以聚焦到其余亟待关注的场景。vue
MPM(Mart Page Maker)是京东自研的一个卖场可视化搭建系统,自 2016 年以来,MPM 历经三个大版本迭代,现在已经发育成为一个组件模板丰富、配置功能强大、受众群体普遍的运营系统。json
上线服务四年来,MPM积累了丰富的组件和模板,除去已下架的外,MPM 现有 30+ 个组件、500+ 个模板,业务能力覆盖商品、导购、营销等多个场景。数组
对于许多手工开发的页面,实现直出仍然是一个困难重重的事情,而从运营手里搭建出来的 MPM 页面默认就支持首屏直出。咱们打造了一个高可用的 Node 直出层,来负责获取页面配置数据、聚合请求接口,并最终渲染出页面首屏内容,从而突破了复杂卖场页面的首屏体验瓶颈。微信
除了首屏直出支持外,MPM 还具有其余一些强大的功能,如:markdown
楼层 BI 排序:千人千面,根据不一样用户属性呈现不一样的楼层优先级排序;网络
自动化埋点:自动建立用于数据统计的 RD 标识并埋点到页面上,规避手工开发过程当中 RD 错埋、漏埋的问题;并发
页面健康诊断:对页面配置进行校验诊断,并给出诊断单,包括配置数据的合法性、有效性验证,以及页面中一些组件可能存在影响的检测,如多个导航组件是否存在吸顶冲突、部分组件要求强制登陆是否符合预期。async
历年来的屡次大促活动都少不了 MPM 的身影,好比 2019 年的 11.十一、12.12 大促活动,深圳京东业务 90% 以上的大促会场都是由 MPM 搭建,包括主会场、全部一级会场和大部分的二级会场。编辑器
系统要素是构成系统的基本组成元素,是设计实现一个系统以前最须要考虑的核心点。推导系统要素,首先要对系统的设计背景、解决场景具有深刻的认知和理解。做为一个卖场可视化搭建系统,MPM 面临的场景被约束在了卖场上,也就是说,咱们要搭建的再也不是一切页面,而只是卖场页面,这是 MPM 一切设计的根基。所以,咱们须要对卖场有一个充分的了解。
在电商行业中,卖场是一个重要的售卖频道入口,一般状况下,卖场聚集了众多不一样品类的商品进行统一售卖,可以有效地营造 “逛” 的氛围,进而提升订单转化。经过分析,咱们概括出卖场具有这样三个明显的特征。
卖场的楼层大多呈瀑布流自上而下铺列分布,楼层与楼层之间相互独立,关联较少。相比之下,像商品详情这类的页面,全部板块的内容都与同一个商品有关,其楼层之间的关联也相对较多。
卖场的职能主要仍是吸引购买,因此卖场基本上可能是一些商品物料、图文素材的展现,少有像玩法活动同样复杂的交互逻辑。
也正是由于卖场强大的引流能力,各个业务线都但愿在卖场上可以占据到属于本身的资源位,所以在这种状况下,卖场天然要承载起各类各样的业务场景,其涉及到的业务接口也就变得十分地多。
那么基于以上分析的卖场特征,咱们如何推导出 MPM 的系统要素呢?
首先咱们知道,对于任何一个页面可视化搭建系统,属性都是必不可少的。以你们比较熟悉的 H5 制做工具 iH5 为例,其配置方式大抵就是「拖一个按钮,配置按钮文字」这样的操做,这其中,配置按钮的文字就是属性,这也不难看出,属性是一个页面可视化搭建系统的最小配置单元。
其次,配置结构必定是分层的,属性之上,须要粒度更粗的配置形态。对于这种形态,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 的有意思的设计,分享给你们,但愿多多关注。
若是你以为这篇内容对你有价值,请点赞,并关注咱们的官网和咱们的微信公众号(WecTeam),每周都有优质文章推送: