公司级组件库以及MIS系统的技术实践
wangjingbetty opened this issue · 2 comments
1. 公司级组件库 - 魔方整体设计
设计它的初衷其实在第一场分享中也已经提到了,我们可以再回顾一下:
痛点:
每一个系统 UI、交互规范、组件依赖底层技术都不一样,复用性低,依赖第三方开源但技术支持不到位,遇到问题没人服务。
(1)PC 类组件库搭建和编译细节
技术选型:
定位: PC 很清晰:就是内外 Mis 系统
组件需求:
简单举例:
- form 表单:
单个组件:下拉框、输入框、多选框、日历框等
组合组件:通过配置,根据类型,自动映射 - 列表:
单个组件:popup,sort,分页,搜索表单 等
组合:带搜索功能、分页功能、排序等功能列表
目前我们 PC 组件提供 2
套:
1、Angular
技术底层选型:
- 先和多个业务团队 FE 小伙伴沟通;
- 也考虑到部分团队的后端同学比较喜欢 angular,大部分 MIS 项目其实都是由后端同学直接就全搞的
基于这 2 点,我们第一期的组件库包含:
按钮、表格、下拉框、日历框、多选框、弹窗等等。
部分展示:
组件简介:
<didi-list
data="data"
resource-url="resourceUrl"
resource-index="resourceIndex"
pagination-options="paginationOptions"
grid-options="gridOptions"
></didi-list>
<script>
var app = angular.module('List', ['bn.list']);
app.controller('listController', ['$scope', function ($scope) {
var listInterface = 'http://xxx.json';
var options = {
resourceUrl: listInterface,
resourceIndex: 'id',
gridOptions: {
fields: [
{
title: '活动名称',
align: 'center',
field: 'name'
},
......
]
}
};
angular.extend($scope, options);
}]);
angular.bootstrap(document, ['List']);
</script>
部分配置项说明:
resource-url 配置列表的数据接口请求地址
resource-index 列表内置支持 RESTful 方式的增删改查,所以对应的就是主键、默认是id,可以不用配置
grid-options 配置列表项相关信息
pagination-options 配置分页相关信息
search-options 配置和列表绑定的搜索表单信息
param-obj 配置列表请求接口时在 url 中所带参数
event-hooks 事件钩子对象,onloadbefore钩子在服务端数据返回来之后可以对原始数据进 行加工格式化;ongetbefore钩子可以在请求发送之前进行字段校验等操作, 返回 false 时不会发送请求data,很多时候不需要自动发送 http请求来获取数据,而是直接设置
魔方 pc 组件项目源码目录结构如下:
mofang-pc-angular
|- app(生态模块)
|- dist (目标模块)
|- node-modules(依赖模块)
|- src (代码模块)
|- common (公共代码)
|- componets (组件)
|- vendor(angular 相关依赖)
|- webpack.config.js (测试环境配置)
|- webpack.min.js (上线环境配置)
|- gulpfile.js (打包环境配置)
|- package.json
打包方式
webpack
入口文件为 mofang.js,我们为 pc 组件库准备了两个配置文件,分别是测试环境和生产环境。我们通过 package.json 的 version 控制组件的版本迭代,由于我们想单独导出 css 文件,我们使用 ExtractTextPlugin 插件,我们采用读取配置文件的方式动态加载模块,最后通过 gulp 配置文件将文件压缩为zip包,以便上传 cdn。配置文件如下
var version = require('./package.json').version;
module.exports = {
entry: {
mofang-widget: './src/mofang.js'
},
output: {
publicPath: __dirname + '/dist/mofang-widget/' + version,
filename: '[name].min.js',
library: 'mofang',
libraryTarget: 'umd'
},
module: {
loaders: [
{
test: /\.css$/,
loader: ExtractTextPlugin.extract("style-loader", "css-loader!sass-loader!prepend-loader?data=" + sassData)
},
{
test: /\.js$/,
loader: 'callback'
}
…….
]
},
callbackLoader: {
dynamicRequireModule: function () {
var requireStr = '';
modules.forEach(function (moduleName) {
.....
});
return requireStr;
}
},
plugins: [
new ExtractTextPlugin("[name].css"),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
})
...
]
}
组件设计
1) 需求调研,了解现有的同类组件都实现哪些功能, 我们的业务都需要组件提供功能
2) 可拓展性,基础组件和业务组件分开
3) 使用和配置简便
4) 文档要全
didi-list 设计
需求分析:
1)搜索功能,我们依托配置类型,遍历生成对应的类型组件,包含select、 input、 radio、checkbox、日期等。
2)操作按钮(搜索、清空、刷新、导出、用户自定义按钮)
3)列表展示,操作列支持用户增删改查、排序,,是否全选,默认序列号等功能,意味着我们要提供 modal 组件,
4)分页功能
5)支持数据动态拉取和直接灌入
5)钩子函数,发送请求前、获取数据时等等
所需组件:
<didi-searchform>
<didi-input>
<didi-datetimepicker>
<didi-select>
....
<didi-grid>
<didi-pagination>
功能设计:
didi-list 组件是由其它底层组件共同协助,搜索控件将表单中内容与paramObj结合后,提供给列表组件进行数据的请求,返回的数据渲染自身展示外,同时传给分页组件,更新分页组件. 此外对于组件的http请求,我们扩展angular的$resource,对其进行封装,使其可以非常方便的同支持restful的服务单进行数据交互。
2、React
考虑到公司级组件库的初衷,也看到有部分业务还是喜欢 React,我们也快速封闭开发,去铺 React 版本。
组件需求:
react组件提供与pc端相同的功能
组件使用方式
<didi-list
paginationOptions={paginationOptions}
gridOptions={gridOptions}
data={data}
paramObj={paramObj}
resourceUrl={resourceUrl}
/>
目录结构
mofang-pc-react
|- dist ()
|- node_modules
|- src
|- components (组件)
|- utils (工具方法)
|- mofang.js (主入口)
|- package.json
|- webpack.config.js
|- webpack.min.js
组件开发
我们所有的组件是基于 ES6,所有的结构都是固定的,而且通过脚手架创建:
'use strict';
import React, { Component, PropTypes } from 'react';
import classnames from 'classnames';
class Button extends Component {
constructor (props) {
super(props);
this.state = {
disabled: props.disabled
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() { ... }
render() {
.....
return (
<button onClick={this.handleClick}
{...this.props}
disabled={this.state.disabled}
className={className}
>
{ this.props.children }
</button>
);
}
}
Button.propTypes = {
disabled: PropTypes.bool,
onClick: PropTypes.func,
...
};
module.exports = Button;
打包方式
react组件开发我们依然采用webpack的方式对文件进行打包,使用ES6进行编写,将react和react-dom从主文件中抽离,针对不同的加载环境进行不同的配置。
配置文件:
var path = require('path');
var version = require('./package.json').version;
var webpack = require('webpack');
module.exports = {
entry: {
mofang: './src/mofang.js'
},
output: {
publicPath: '/assets/',
path: __dirname + '/build',
filename: '[name].js',
library: '',
libraryTarget: 'umd'
},
externals: [
{
'react': {
root: 'React',
commonjs2: 'react',
commonjs: 'react',
amd: 'react'
}
}...
],
module: {
loaders: [
{test: /\.js$/, loader: 'babel'}
]
}
......
};
因为使用externals配置,打包后库文件可以在 AMD、CMD和全局环境下使用,但这几种环境中我们依赖的 react 和 react DOM 模块名不同,如:
AMD下 define(['react'], function (){})
全局使用时 window.React
CMD下 require('react')
配置external后 webpack编译结果:
注意:我们使用es2015-loose将ES6代码转译成ES5代码。在使用 ES6 解构 rest 属性时,需要安装 babel-plugin-transform-object-rest-spread 插件
例如下面代码的解析:
const { id, text, ...itemParams } = item
安装
npm install babel-plugin-transform-object-rest-spread
在 .babelrc 中配置
{
"presets": ["react", "es2015-loose"],
"plugins": ["transform-object-rest-spread"]
}
组件设计
我们组件是采用组件组合的开发方式,将组件做到最小颗粒化,每个组件实现单一的功能,使组件运用起来更加灵活。公共组件由每个功能单一的组件拼合而成。组件开发依赖state,通过componentwillreciveprops生命周期函数接受props更新state,来使view更新。
(2)H5 类组件库搭建和编译细节
技术栈
1) webpack
2) zepto + gmu + stylus + handlebar
目录结构:
mofang-webapp
|- app(生态模块)
|- dist (目标模块)
|- node-modules(依赖模块)
|- helpers (handlebar.helper)
|- src (代码模块)
|- common (公共代码)
|- componets (组件)
|- carchoose
|- carchoose.js
|- carchoose.html
|- carchoose.hanlebar
|- color.handlebar
|- type.handlebar
|- brand.handlebar
|- shortcut.handlebar.
|- city
|- dialog
…
|- vendor(angular 相关依赖)
|- webpack.config.js (测试环境配置)
|- webpack.min.js (生产环境配置)
|- webpack.module.js (生产环境配置)
|- gulpfile.js (打包环境配置)
最终目录结构为:
|- mofang-webapp()
|- 0.1.21 (版本号)
|- mofang.min.js
|- mofang.min.css
|- module(模块)
|-0.1.21
|- carchoose.min.js
|- city.min.js
|- dialog.min.js
组件使用方式:
我们以 carchoose 组件为例,来看一下它是如何被使用的:
var $carchoose = $('#carchoose');
$carchoose.carchoose({
onselect: function(obj){
...
}
});
如代码所示,我们的组件是绑定在元素上,组件内容全部通过配置参数
来控制。
组件设计
我们以carchoose为例,该组件主要是用来展示品牌车型的,其中包括车辆品牌、车辆型号、车辆颜色。分别通过点击事件依次从右侧划入屏幕。
组件如图:
1)功能拆分
将组件分为三块部分(品牌、型号、颜色),品牌中再分为热门、缩略、列表。
2)参数定义
brandsURL: '',
typesURL: '',
colorsURL: '',
hotcarbrands: [],
onselect:fuction(){},
onCloseSelector: function(){},
onrenderBefore: function(type){}
从功能上看,我们有三种主要数据需要渲染,数据通过请求方式获取,数据量太大一次性读取数据还是很耗时的,而且用户不会频繁操作该组件。但是不能因为用户不会频繁操作,而忽略这个问题。我们采取将用户获取数据进行缓存,当用户再次点击,我们只需用缓存数据进行渲染即可。热门品牌以及钩子函数是必须的。
3)技术实现
动画效果采用css3实现
列表展示采用bscroll组件
加载效果采用lodaing组件
(其中bscroll为滴滴内部研发类似iscroll,性能优于iscroll的组件)
(3)可视化类组件库搭建和编译细节
痛点:
我们没有一套滴滴定制化的可视化控件,每个设计师设计的图标各不相同, 而且代码没有可复用性,每次都要重新开发,浪费资源。加上目前流行的可视化组件配置项比较复杂,所以我们基于 canvas API 封装了一套基础图表组件。
技术选型:
定位:pc 端数据可视化图表
组件需求:
折线图 & 饼状图 & 柱状图 & 雷达图
两套皮肤
组件使用方式:
以折线图为例:
<canvas mofang-line chart-data="data"></canvas>
<script>
angular
.module( 'myModule', [ 'mofang.chart' ])
.controller( 'myController', function( $scope ) {
$scope.data = {
theme:'warm',
fill: true,
labels: ["11.01", "11.02", "11.03", "11.04"],
datasets: [{
label: "图例1",
data: [13, 24, 89, 65]
}, {
label: "图例2",
data: [25, 98, 87, 65],
}]
};
});
</script>
参数说明:
theme 皮肤,支持 2 套配色皮肤['warm','cold'],默认:warm
fill 是否填充颜色,支持 [true,false],默认:false
labels 配置横轴内容[数组格式]
datasets 配置线数据,包含两个字段:label是图例名称,data 是数据[数组格式]
$broadcast("update") 更新图表数据
现有可视化库:
- echarts 功能强大,Canvas实现,灵活的打包方式,可自由选择你需要的图表和组件
- highcharts 支持多设备 轻量 svg 实现
- chartjs 使用HTML5 Canvas元素的Javascript图形库,支持6种统计图形,不依赖其他库. 轻量 体积小
- D3 它被很多其他的表格插件所使用。它允许绑定任意数据到DOM,然后将数据驱动转换应用到Document中。你可以使用它用一个数组创建基本的HTML表格,或是利用它的流体过度和交互,用相似的数据创建惊人的SVG条形图。
组件设计:
我们的可视化组件是在 chartjs 的基础上,保留原有 chartjs 的基本框架结构,对内部的组件canvs画图方式以及组件间数据传递进行改造,以达到滴滴定制可视化组件的效果,为避免用户调用过于复杂,又鉴于MIS系统都是基于angularjs组件库开发的,就将可视化组件用angular封装,只对用户暴露简单的数据接口。
2. MIS 类项目配置化、服务化和GUI化
我们发现只解决了 UI 交互组件化、规范化,针对日益繁多的 MIS 项目,还是缺少点什么:
配置化:
现在很多人的做法:
把很多配置信息比如请求的 url 都放到一个 js 里面
// Action.js
var URLS = {
AJAXS: {
BUS_LIST: '*****'
},
LOGS: {
},
SCHEMAS: {
}
}
我们搭建了 MIS 配置平台,可以配置很多类似的东西:
各个 MIS 系统的用户权限,菜单配置
MIS 配置平台都是基于angular组件开发的系统,每个子系统配置不同的用户角色,每个角色针对应不同的权限。系统的左侧菜单也是通过配置平台,这样可以方便的控制每个角色对页面访问权限,同时我们还会记录每个用户的操作行为,方便回溯问题根源。配置平台中项目的环境有三套,一套针对内网环境,一套针对外网环境,一套测试环境。平台配置分别分别记录每个项目在相应机器中的端口号,以及域名,统一查询和维护。
由于项目组这边经常要协助其它组开发 MIS 系统相关的开发,我们创建了支持 angular、react及vue开发的脚手架,方便快速开发项目,只关注项目的逻辑功能,减少对环境的搭建。
mofang angular-demo
选择 PC 后:
然后我们会在当前目录下:
创建一定模板规则的目录,配置好依赖和构建脚本
(1)如何处理业务组件和通用组件
通用组件:底层组件,提供基础功能、可扩展性
业务组件:在通用组件基础上做拼接、定制
业务组件与通用组件是密切相关的,正如一句话“用的人多了,就会变成通用组件啦”, 业务组件是在通用组件的基础上做的拼接与定制。通用组件适用的业务场景比较广泛,业务组件业务场景比较单一。当好多业务场景下,都使用了相同组件时,我们就要考虑是否将业务组件提取成公共组件,方便大家使用,节约开发成本。
大多数技术人员在开发项目过程中都会遇到这样,产品经理提出的需求总是要在公共组件的基础上来点特殊的定制化业务逻辑,以使他的产品更加炫酷。如果满足这种需求,往往我们需要给公共组件加各种补丁,或者把组件拿过来自己再重新封装一下。遇到这种情况我们应该怎么办?
- 任何组件都不能达到“十全十美”,如果新增的需求满足通用性的抽取原则,我们可以将这部分业务功能融合到组件中,使组件更加完善。
- 如果新增的需求仅仅是锦上添花的效果且抽取组件的成本大于收益,将其视为业务组件。
(2)如何构建 DNode 服务化
前后端分离
在以往的工作中,在完成一个系统开发的时候,无论后端语言是php、java, 常见开发模式分两种情况:
第一种:前后端代码共同维护在一个项目中,前端开发完全依赖后端同学,我们需要后端同学给我们搭建一套测试环境,创建一个目录发布专属前端的代码,既麻烦又费事,还有可能可能出现代码冲突。
第二种:前后端同学各自维护项目,但是页面在后端系统中维护,前端只提供脚本文件。第二种方式其实稍好于第一种,只是这样都没有完全实现全后端代码的分离。比如说前端在业务开发的时候发现还需要引用另外的脚本文件,页面在后端项目中,他没有权限,他只能找后端同学帮他在页面加上相应脚本。
我们在开发MIS项目时使用DNode服务,所有的前端代码我们都由自己维护,我们只需要后端给我们提供 API 接口,前端自己启服务,搭建测试环境,真正的实现前后端分离,可以随心所欲的开发。
目前我们的做法:
1)为了防止恶意攻击,我们将所有与后端api的请求都做一层转发,并在请求之前对请求来源做验证,如果不是来自我们域名下的请求, 我们将其视为无效请求,并告知其请求无效,代码示例如下:
getProvinces: function(req,res){
res.header('Access-Control-Allow-Origin', '***.com');
res.header('Access-Control-Allow-Methods', 'GET');
res.header('Access-Control-Allow-Headers', 'Content-Type');
var util = sails.services.util;
var cookies = cookie.parse(req.headers.cookie);
if(req.headers.referer && req.headers.referer.indexOf(util.Referer')>=0) {
request.get(util , function (error, response, body){
res.json(JSON.parse(body));
});
} else {
res.json({
error: 10000,
data: [],
errormsg: '非法请求'
});
}
}
2)创建 DNode Auth 服务接入权限系统
var Q = require('q');
var request = require('request');
var defaults = {
url: 'xxx/xxx/xxx/xxx/index'
};
var sso = {
getUser: function (key) {
var deferred = Q.defer();
//...
request.post(obj, function(err,httpResponse,body) {
....
});
return deferred.promise;
}
};
微服务
我们内建了很多服务和 SDK,下面简单以 Mock Server 为例:
这里面的方案业界比较多,我们分 2 类:
1、JSON Editor + CDN 化
这种接口应用场景:
用来配置线上数据的,而且公网能访问,还是依托我们第一场分享中的 TMS
2、JSON Server + JSON schema + DSwagger Doc UI
这种接口应用场景:
用来构建按规则的假数据,不依赖 DB,一般都是 json 文件,然后加上类似 Swagger 的那种 UI 输出给相关协同开发
(3)如何构建 GUI 新开发模式
目前前端的状况:
编辑器差异化还好,但构建类工具和预编译类工具都各种各样
我们团队 IDE 为例:
- Sublime
- Webstorm
- Visual Studio Code
- Vim
- Atom
- Brackets
构建工具:
- grunt
- gulp
- scrat
- webpack
- rollup
而且我们发现编辑器越来越强大,很多插件化的东西都可以安装进编辑器里面,所以我们有一个目标:
搭建一个在线编辑器:
- 支持 git 相关操作
- 支持创建不同类型的项目(打通脚手架命令)
- 支持一键部署测试环境
- 支持对接内部发布平台
好处:
屏蔽各种本地安装带来的问题,专注于业务开发
继承了现有的工具:git、脚手架等
展示:
技术演变:
Ace:基于 web 的开源代码编辑器,star 数目 13000+
C9:内置命令行、各种语言工具的在线编辑器,目前已经发布到 3.* 版本了
C9 的功能非常强大,我们也自定义和开发了很多相关插件。
由于编辑器比较庞大,如果对编辑器感兴趣的,我们可以私下在联系。
致谢:
感谢领导的信任与栽培,感谢一路陪伴、一起奋斗的滴滴小伙伴,感谢infoq提供分享平台。
Mark
组件库是如何管理和维护?新增组件代码结构、质量标准、测试用例?