1 简介
乐高,是美团点评一个快速搭建后台系统页面的平台。名称来源于大家熟悉的丹麦知名玩具品牌,他们的玩具都是通过组合易拆卸、装配的零件,形成最终的作品。经过长期的发展,乐高品牌渐渐有了“快乐、想象、创意的未来”的寓意。
随着外卖业务的高速发展,大量的业务开发需求接踵而来。像人手紧缺、重复开发、沟通效率低下等问题,暴露得愈发明显。于是,我们有了这么一个想法:能否基于现有大量业务系统的结构固定、需求紧急、交互样式要求不高等特点,搭建一个平台,它把已经成型的组件像乐高玩具的零件一样,使用拖拽的方式组装成最终的页面,同时能够让各个业务快速的接入。
在美团点评一次黑客马拉松中,我们将这一想法付诸实践。在参赛的四十多支团队中,获得了第二名。这给了我们坚持下去的信心,也明确了后续努力的方向。经过一段时间的迭代,目前乐高形成了较完善的开发和生产流程:
2 用户使用
乐高平台的应用可分为三大部分:面向用户的组装工厂、面向开发者的开发视图以及面向后端服务化的暴露接口。
页面组装如图一所示,主要包含五部分:
(图一)
一个最终完成的页面中的脚本,按照来源分为了三块。
(图二)
乐高系统中提供了数据源的概念。用于模拟、校验模板页面中的数据。有时,后端需要乐高输出FreeMarker或者EJS模板,而非HTML页面。为了保障页面在乐高中的正常开发和预览,我们需要Mock一部分同步(可理解为后台写入ModelView的)数据。
将JSON格式的数据写入数据源,即可实现Mock数据。此外,在后端调用乐高接口时,数据源还会校验传入数据的合法性。
(图三)
乐高是一个平台。开发者可以在乐高中按照自己的喜好、想法、思路开发自己的模块。
(图四)
乐高目前提供了Java和Node.js两种SDK的接入方式。
在工程中引入JAR包。可通过调用SDK接口,从乐高系统获取页面或者模板。
获取的类型有两种:渲染完成的HTML代码和模板代码(目前支持FreeMarker模板)。
/**
* 无数据页面获取
* @param pageId 页面id或者vurl,取决于pageIdType传入值
* @param pageIdType,枚举类型,LegoService.PageIdType.ID,LegoService.PageIdType.NAME 前者传入页面id时使用,后者传入页面vurl时使用
* @return 枚举类型,OK(200,"成功"), FAILED(500,"失败");
*
*/public static final LegoStatus getPageWithoutData(HttpServletResponse servletResponse, String pageId, PageIdType pageIdType)/**
* 带有页面数据的页面获取
* @param model传入的数据
*
*/public static final LegoStatus getPage(HttpServletResponse servletResponse, String pageId, PageIdType pageIdType, ModelMap model)
安装完乐高依赖的模块后,可参照下述示例调用:
'use strict';var lego = require('lego');/**
* 请求页面id获取页面,
* @param data,页面渲染用数据,json格式
* @param rootId 只获取部分页面时使用,默认为空
* @param callback, 回调使用callback(err, body), 正常传入err为空,body为页面html内容。错误时err为错误信息。
*
*/lego.renderById(vid, data, rootId, callback)/**
* 请求页面短连接获取页面
*
*/lego.renderByUrl(vurl, data, rootId, callback)
3 原理
在AMD、CMD、CommonJS等模块化标准开始流行后,模块化的思维方式,给社区的前端开发者们造成了比较深刻的影响。
前端开发有了比较强烈的意识,对原本混沌的JavaScript代码按照标准模式进行组织和分类。据此来构建出的Web App,不仅提高了系统的可维护性,并且便于和其他开发者进行沟通,从而形成一个巨大的开发链条。借助其他开发者提供的一批模块,使我们能够专注于业务逻辑,同时降低代码冗余、开发成本和潜在风险。
基于模块化开发的大体思路。我们在对一堆看似杂乱无章的DOM结构进行归纳、总结、提炼,使之成为一个个独立的组件。数个组件的协同工作,呈现出一个完整的页面。乐高系统设计理念,正是源于如下所示的一个等式:
这里的 节点描述、数据、功能逻辑 就是下文中要提出的模块部分。
更为直观的表述,如下图所示的一个基本结构,可以组成一个简单的页面。组件粒度的划分,有比较充裕的灵活性,可以由组件设计者自己定制。
组件是组成任何一个视图的最基础元素,是整个平台的基石。组件之间的耦合度、通信、可扩展性、易用性是否足够强大,很大一部分程度上决定了整个系统的质量。
乐高的组件是对其背后庞大的样式/交互资源库的抽取(默认为袋鼠UI系列,也可以是其他的前端资源库),倚重于对组件HTML结构,交互调用的描述。每个组件负责拼装自己的HTML结构,和使用组件库中预存的交互。因此,推荐的组件的开发方式中,不包含对CSS的描述,但是也允许通过Hack的方式增加每个组件的CSS。
每个独立组件提供了8个需要实现的接口:
/**
* 组件基本展示及功能规则的描述,目前对外暴露了8个接口(name, pyname, desc, leaf, uilib, model, script, render)
*/'use strict';/**
* 组件名称,用来标识该组件在系统中的引用名称。
* 取值可以为汉字,大小写英文字母,数字和下划线的组合
* [建议取名为英文,每个组件name唯一,主要供程序和RD使用]
*/exports.name = 'Sample';/**
* 组件别名,只能为汉字或者字母
* [建议取名中文,每个组件pyname唯一,主要供PM等对hmtl及组件专有英文名称不太熟悉的人使用]
*/exports.pyname = '中文名称';/**
* 组件描述
*/exports.desc = '';/**
* 该组件可以添加的叶子节点
* 1.如果可包含子节点,请在数组中添加组件id,如: exports.leaf = [12,23,34]
* 2.如果不可包含任何子节点,请将leaf置为null,即: exports.leaf = null
* 3.如果可包含任何子节点,请将leaf置为空数组,即: exports.leaf = []
*/exports.leaf = [];/**
* 当前组件需要适配的组件库
*/exports.uilib = 'kui';/**
* 该接口用来描述组件配置的相关属性,其子组件可以在编程/渲染接口中读取到父组件的配置信息
*
* type:数据类型,目前含盖的数据类型:
* text: 文本输入框类型
* textEx: {
* name: '测试属性1',
* type: 'text',
* def : '默认值',
* desc: '属性描述'
* }
* select: 下拉选择框类型
* selectEx: {
* name: '测试属性1', //最长不超9个字,否则内容尽量放到注释里
* type: 'select',
* options:{
* value1: '这是值1',
* value2: '这是值2',
* },
* def : 'defValue',
* desc: '属性描述'
* }
* textarea: 多行输入框类型(配置同text)
* radio: 单选选择框类型(配置同select)
* checkbox: 复选类型(配置同select,最终值为 value1,value2 逗号分隔)
*/exports.model = {
}/**
* 组件脚本。会插入到页面html中执行,组件内部逻辑或与外部交互可放到该函数中执行
* @param mvId 组件用到的mvId,组件唯一标识
* @param evtMgr 页面全局事件中心,可以通过 bind(evt, handler) unbind(evt, handler) 和 trigger(evt, data, context)三个方法控制事件流的绑定和触发
* @param modelData 组件属性数据,默认传参encode字符串,首先需要decodeURI,然后换成json对象
*/exports.script = function (mvid, evtMgr, modelData) {
modelData = JSON.parse(decodeURI(modelData));
}/**
* 该组件在用户界面的展示
* @param {Object} node node中包括{_children:[], _parent:'', _innerHtml:'', _modelData:{}, _mid:'', _mvid:'', _mname:''},还有其他字段暂不建议使用
*/exports.render = function (node) { var _modelData = node._modelData; var _children = node._children; var content = '';
_children.map(function (child) {
content += child._innerHtml;
}); return ``;
}
乐高的组件作为一个界面系统的元素部分,在设计之初需要考虑到以下的五个方面。通过这几部分,逐渐的形成了一套比较完善的模块化方案:
业务组件之间的数据传递,是一个比较常见的的场景。
我们给每个组件实现的编程(Script)接口中传递了三个参数mvid、evtMgr、modalData。其中,evtMgr就是乐高系统中的事件中心。通过绑定或者触发相应的事件,在实现模块间通信的同时,较好的解耦了模块。示例代码:
// 事件的触发
evtMgr.trigger('tata', {a: 1}); // 事件的绑定
evtMgr.bind('tata', (params) => { // TODO Here.
console.log(params) // {a: 1}
})
部分业务组件会有自己的数据。数据从来源划分,可以分为系统数据、配置数据。系统数据又可以被划分为同步数据和异步数据:
exports.render = function (node) { var _modelData = node._modelData; var _children = node._children; var content = '';
_children.map(function (child) {
content += child._innerHtml;
}); return `${data_from_datasource}`; // 数据源数据字段读取
}
在“3.2.1 形态”章节中所示的8个编程接口,对模块的开发者开放。
为了在修改、发布组件时,不对线上页面造成影响,也为了满足多人协同开发的需求,我们实现了简单的版本控制功能。
每个组件预留了开发者版本。开发完毕后,需要单独的发布该组件。在视图中进行组装时,可以选择所需要的版本,如果新版本有问题,能够及时做到单独模块的线上回滚。
乐高中比较核心的功能。是实现了一个页面的解析引擎。输入为在工厂中形成的页面描述的数据结构,逐步添加外部资源(数据源、界面资源库、模块)进行组合,进而生成最终的HTML或者模板。
外卖的系统,大多使用freemarker.jar作为页面渲染引擎。因此,乐高中也包含了一个freemarker.jar的模板引擎。整体工作流程如下图所示:
来自于视图组装工厂产出的页面信息的数据结构,经过解析引擎调用了其他资源(数据源、框架、模块描述)渲染后。形成了最终的HTML或者模板,返回给后台的服务中转给最终的用户。
乐高使用了Node.js Express框架搭建,作为前端服务化的方式存在。
同时这个服务也提供了内部用户(页面组装者)访问的界面。整体工作流程如下所示:
上文介绍了组装工厂。我们知道在页面组建完毕之后,系统中最终形成的是一个扁平的页面数据结构,用于对页面的描述。
这个数据结构中,除了对每个节点的描述之外,使用了_children、_parent等来描述该节点在当前视图的树状结构中所处的位置和层级。具体的结构如下所示:
{
1_0: {
_children: [101_1], // children in current page
_mvid: '1_0', // unique id in current page
_parent: '#', // parent in current page
_mid: '1', // id
_version: '1.0.0' // version
_mname: 'Page', // name
_xxx: '' // other properties
},
101_1: {
_children: [5_2],
_mvid: '101_1',
_parent: '1_0,
_mid: '101',
_version: '1.0.0'
_mname: 'Body',
_xxx: ''
},
5_2: {
_xxx: ''
}
}
随着页面的创建和发布,页面的存储结构,会被持久化到乐高数据库的字段中,以便后续的调用修改。
4 结语
至截稿前,乐高拥有了55个框架组件和77个业务组件,共132个。覆盖了外卖事业部的6个项目,包含了108个可访问的线上页面(视图)。数目还在不断的迅速增加中。
举个例子,一个比较典型的场景,业务系统中常用的列表查询页面和编辑表单页面,之前开发页面需要半天时间,而乐高半小时内即可组装一个页面。开发效率上的提升效果比较明显。
在外卖的前端开发实践中,乐高系统已经成为大量业务系统开发的轴心一环。在对袋鼠UI资源库中的组件进行提取,形成最终的用户可见页面过程中,不仅推动了外卖整体视觉和交互规范的逐渐完善,也使其在前端技术中得到落实——袋鼠UI资源库。
视觉规范、袋鼠UI和乐高形成了一个完整和不断循环的开发生态。
当前市面上存在着比较多的前端组件化框架,大多门槛较高。乐高提供更低廉、简洁的使用方式组装大量重复存在而交互样式较为单一的业务系统,实现了自己的模块管理机制。
作为一种新开发模式的尝试,乐高在不断地完善。使用乐高平台,在前端自动化埋点、性能优化等等方向,会有很多有趣的创新和突破。我们将会持续给大家分享。