[关闭]
@hanting003 2016-08-19T22:02:29.000000Z 字数 10304 阅读 1052

滴滴:公司级组件库以及MIS系统的技术实践分享

本文为8月18日,『前端之巅』群『滴滴公共FE团队技术开放月』第二期分享活动总结整理而成,转载请在文章开头处注明来自『前端之巅』公众号。查看前两期分享,请关注『前端之巅』公众号并发送“滴滴”。

王静,现就职于滴滴出行公共FE团队,高级前端开发工程师,负责滴滴MIS项目开发管理,熟悉Angular、数据可视化、Vue等组件开发,现热衷学习专研NodeJS。

一、公司级组件库——魔方的整体设计

1. 设计初衷

设计公司级组件库的初衷其实在第一场分享中也已经提到了,主要是为了解决这些痛点:每一个系统 UI、交互规范、组件依赖底层技术都不一样,复用性低,依赖第三方开源但技术支持不到位,遇到问题没人服务。

2. 技术选型

定位: PC端的定位很清晰,就是内外 MIS 系统。

组件需求简单举例如下:

3. PC类组件库搭建和编译细节之Angular

目前滴滴的 PC 组件提供两套,首先介绍Angular的这套组件库。

(1)技术选型

在技术底层选型过程中,我们和多个业务团队的前端开发人员进行了沟通,考虑到部分团队的后端开发人员比较喜欢 Angular,而且大部分 MIS 项目其实都是由后端开发人员来做的。所以,我们提供了Angular的这套组件库,包含:按钮、表格、下拉框、日历框、多选框、弹窗,等等。

(2)部分组件展示

(3)组件简介

相关代码如下:

  1. <didi-list
  2. data="data"
  3. resource-url="resourceUrl"
  4. resource-index="resourceIndex"
  5. pagination-options="paginationOptions"
  6. grid-options="gridOptions"
  7. ></didi-list>
  8. <script>
  9. var app = angular.module('List', ['bn.list']);
  10. app.controller('listController', ['$scope', function ($scope) {
  11. var listInterface = 'http://xxx.json';
  12. var options = {
  13. resourceUrl: listInterface,
  14. resourceIndex: 'id',
  15. gridOptions: {
  16. fields: [
  17. {
  18. title: '活动名称',
  19. align: 'center',
  20. field: 'name'
  21. },
  22. ......
  23. ]
  24. }
  25. };
  26. angular.extend($scope, options);
  27. }]);
  28. angular.bootstrap(document, ['List']);
  29. </script>

(4)部分配置项说明

(5)目录结构

魔方 pc 组件项目源码目录结构如下:

  1. mofang-pc-angular
  2. |- app(生态模块)
  3. |- dist (目标模块)
  4. |- node-modules(依赖模块)
  5. |- src (代码模块)
  6. |- common (公共代码)
  7. |- componets (组件)
  8. |- vendor(angular 相关依赖)
  9. |- webpack.config.js (测试环境配置)
  10. |- webpack.min.js (上线环境配置)
  11. |- gulpfile.js (打包环境配置)
  12. |- package.json

(6)打包方式

打包方式为webpack,入口文件为 mofang.js,我们为 PC 组件库准备了两个配置文件,分别是测试环境和生产环境。通过 package.json 的 version 控制组件的版本迭代。由于想单独导出 CSS 文件,所以使用 ExtractTextPlugin插件,采用读取配置文件的方式动态加载模块,最后通过 gulp 配置文件将文件压缩为zip包,以便上传 cdn。配置文件如下:

  1. var version = require('./package.json').version;
  2. module.exports = {
  3. entry: {
  4. mofang-widget: './src/mofang.js'
  5. },
  6. output: {
  7. publicPath: __dirname + '/dist/mofang-widget/' + version,
  8. filename: '[name].min.js',
  9. library: 'mofang',
  10. libraryTarget: 'umd'
  11. },
  12. module: {
  13. loaders: [
  14. {
  15. test: /\.css$/,
  16. loader: ExtractTextPlugin.extract("style-loader", "css-loader!sass-loader!prepend-loader?data=" + sassData)
  17. },
  18. {
  19. test: /\.js$/,
  20. loader: 'callback'
  21. }
  22. …….
  23. ]
  24. },
  25. callbackLoader: {
  26. dynamicRequireModule: function () {
  27. var requireStr = '';
  28. modules.forEach(function (moduleName) {
  29. .....
  30. });
  31. return requireStr;
  32. }
  33. },
  34. plugins: [
  35. new ExtractTextPlugin("[name].css"),
  36. new webpack.optimize.UglifyJsPlugin({
  37. compress: {
  38. warnings: false
  39. }
  40. })
  41. ...
  42. ]
  43. }

(7)组件设计

组件设计需要从以下几点出发:

(8)didi-list 设计

需求分析:

1)搜索功能,依托配置类型,遍历生成对应的类型组件,包含select、 input、 radio、checkbox、日期等。
2)操作按钮(搜索、清空、刷新、导出、用户自定义按钮)。
3)列表展示,操作列支持用户增删改查、排序,是否全选,默认序列号等功能,意味着要提供 modal 组件。
4)分页功能。
5)支持数据动态拉取和直接灌入。
5)钩子函数,发送请求前、获取数据时等。

所需组件:

  1. <didi-searchform>
  2. <didi-input>
  3. <didi-datetimepicker>
  4. <didi-select>
  5. ....
  6. <didi-grid>
  7. <didi-pagination>

功能设计:

didi-list 组件是由其它底层组件共同协助,搜索控件将表单中内容与paramObj结合后,提供给列表组件进行数据的请求,返回的数据渲染自身展示外,同时传给分页组件,更新分页组件.

此外,对于组件的http请求,我们扩展angular的$resource,对其进行封装,使其可以非常方便地同支持restful的服务单进行数据交互。

4. PC类组件库搭建和编译细节之React

考虑到公司级组件库的初衷,也看到有部分业务线的开发人员喜欢 React,所以我们也快速封闭开发,去铺 React 版本。

(1)需求

React组件提供与PC端相同的功能。

(2)使用方式

组件的使用,相关代码如下:

  1. <didi-list
  2. paginationOptions={paginationOptions}
  3. gridOptions={gridOptions}
  4. data={data}
  5. paramObj={paramObj}
  6. resourceUrl={resourceUrl}
  7. />

(3)目录结构

  1. mofang-pc-react
  2. |- dist ()
  3. |- node_modules
  4. |- src
  5. |- components (组件)
  6. |- utils (工具方法)
  7. |- mofang.js (主入口)
  8. |- package.json
  9. |- webpack.config.js
  10. |- webpack.min.js

(4)组件开发

所有的组件是基于ES6的,所有的结构都是固定的,而且通过脚手架创建,相关代码如下:

  1. 'use strict';
  2. import React, { Component, PropTypes } from 'react';
  3. import classnames from 'classnames';
  4. class Button extends Component {
  5. constructor (props) {
  6. super(props);
  7. this.state = {
  8. disabled: props.disabled
  9. };
  10. this.handleClick = this.handleClick.bind(this);
  11. }
  12. handleClick() { ... }
  13. render() {
  14. .....
  15. return (
  16. <button onClick={this.handleClick}
  17. {...this.props}
  18. disabled={this.state.disabled}
  19. className={className}
  20. >
  21. { this.props.children }
  22. </button>
  23. );
  24. }
  25. }
  26. Button.propTypes = {
  27. disabled: PropTypes.bool,
  28. onClick: PropTypes.func,
  29. ...
  30. };
  31. module.exports = Button;

(5)打包方式

React组件开发我们依然采用webpack的方式对文件进行打包,使用ES6进行编写,将React和React-DOM从主文件中抽离,针对不同的加载环境进行不同的配置。

配置文件:

  1. var path = require('path');
  2. var version = require('./package.json').version;
  3. var webpack = require('webpack');
  4. module.exports = {
  5. entry: {
  6. mofang: './src/mofang.js'
  7. },
  8. output: {
  9. publicPath: '/assets/',
  10. path: __dirname + '/build',
  11. filename: '[name].js',
  12. library: '',
  13. libraryTarget: 'umd'
  14. },
  15. externals: [
  16. {
  17. 'react': {
  18. root: 'React',
  19. commonjs2: 'react',
  20. commonjs: 'react',
  21. amd: 'react'
  22. }
  23. }...
  24. ],
  25. module: {
  26. loaders: [
  27. {test: /\.js$/, loader: 'babel'}
  28. ]
  29. }
  30. ......
  31. };

因为使用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 插件。

例如下面代码的解析:

  1. const { id, text, ...itemParams } = item

安装:

  1. npm install babel-plugin-transform-object-rest-spread

在 .babelrc 中配置:

  1. {
  2. "presets": ["react", "es2015-loose"],
  3. "plugins": ["transform-object-rest-spread"]
  4. }

(6)组件设计

采用组件组合的开发方式,将组件做到最小颗粒化,每个组件实现单一的功能,使组件运用起来更加灵活。公共组件由每个功能单一的组件拼合而成。组件开发依赖state,通过componentwillreciveprops生命周期函数接受props更新state,来使View更新。

5. H5 类组件库搭建和编译细节

(1)技术选型

定位: 移动端H5页面。

(2)组件需求

组件需求如以下几张图片所示:

此处输入图片的描述

此处输入图片的描述

此处输入图片的描述

此处输入图片的描述

(3)技术栈

1) webpack
2) zepto + gmu + stylus + handlebar

(4)目录结构

  1. mofang-webapp
  2. |- app(生态模块)
  3. |- dist (目标模块)
  4. |- node-modules(依赖模块)
  5. |- helpers (handlebar.helper)
  6. |- src (代码模块)
  7. |- common (公共代码)
  8. |- componets (组件)
  9. |- carchoose
  10. |- carchoose.js
  11. |- carchoose.html
  12. |- carchoose.hanlebar
  13. |- color.handlebar
  14. |- type.handlebar
  15. |- brand.handlebar
  16. |- shortcut.handlebar.
  17. |- city
  18. |- dialog
  19. |- vendor(angular 相关依赖)
  20. |- webpack.config.js (测试环境配置)
  21. |- webpack.min.js (生产环境配置)
  22. |- webpack.module.js (生产环境配置)
  23. |- gulpfile.js (打包环境配置)

(5)组件使用方式

以 carchoose 组件为例,来看一下它是如何被使用的:

  1. var $carchoose = $('#carchoose');
  2. $carchoose.carchoose({
  3. onselect: function(obj){
  4. ...
  5. }
  6. });

如代码所示,我们的组件是绑定在元素上,组件内容全部通过配置参数来控制。

(6)组件设计

以carchoose为例。该组件主要是用来展示品牌车型的,其中包括车辆品牌、车辆型号、车辆颜色。分别通过点击事件依次从右侧划入屏幕。
组件如以下图片所示:
此处输入图片的描述

此处输入图片的描述

1)功能拆分
将组件分为三块部分(品牌、型号、颜色),品牌中再分为热门、缩略、列表。

2)参数定义

  1. brandsURL: '',
  2. typesURL: '',
  3. colorsURL: '',
  4. hotcarbrands: [],
  5. onselect:fuction(){},
  6. onCloseSelector: function(){},
  7. onrenderBefore: function(type){}

从功能上看,我们有三种主要数据需要渲染,数据通过请求方式获取,数据量太大一次性读取数据还是很耗时的,而且用户不会频繁操作该组件。但是不能因为用户不会频繁操作,而忽略这个问题。我们采取将用户获取数据进行缓存,当用户再次点击,我们只需用缓存数据进行渲染即可。热门品牌以及钩子函数是必须的。

3)技术实现
动画效果采用css3实现
列表展示采用bscroll组件
加载效果采用lodaing组件
(其中bscroll为滴滴内部研发类似iscroll,性能优于iscroll的组件)

6. 可视化类组件库搭建和编译细节

(1)痛点

我们没有一套滴滴定制化的可视化控件,每个设计师设计的图标各不相同, 而且代码没有可复用性,每次都要重新开发,浪费资源。加上目前流行的可视化组件配置项比较复杂,所以我们基于 canvas API 封装了一套基础图表组件。

(2)技术选型

定位:pc 端数据可视化图表
组件需求:
折线图 & 饼状图 & 柱状图 & 雷达图
两套皮肤

此处输入图片的描述

此处输入图片的描述

此处输入图片的描述

(3)组件使用方式

以折线图为例:

  1. <canvas mofang-line chart-data="data"></canvas>
  2. <script>
  3. angular
  4. .module( 'myModule', [ 'mofang.chart' ])
  5. .controller( 'myController', function( $scope ) {
  6. $scope.data = {
  7. theme:'warm',
  8. fill: true,
  9. labels: ["11.01", "11.02", "11.03", "11.04"],
  10. datasets: [{
  11. label: "图例1",
  12. data: [13, 24, 89, 65]
  13. }, {
  14. label: "图例2",
  15. data: [25, 98, 87, 65],
  16. }]
  17. };
  18. });
  19. </script>

(4)参数说明:

(5)组件设计

我们的可视化组件是在 chartjs 的基础上,保留原有 chartjs 的基本框架结构,对内部的组件canvs画图方式以及组件间数据传递进行改造,以达到滴滴定制可视化组件的效果,为避免用户调用过于复杂,又鉴于MIS系统都是基于Angularjs组件库开发的,就将可视化组件用Angular封装,只对用户暴露简单的数据接口。

二、MIS 类项目配置化、服务化和GUI化

1. MIS平台配置化

我们发现只解决了 UI 交互组件化、规范化,针对日益繁多的MIS项目,还是缺少点什么。很多人会把很多配置信息比如请求的 url 都放到一个 JS 里面,例如:

  1. // Action.js
  2. var URLS = {
  3. AJAXS: {
  4. BUS_LIST: '*****'
  5. },
  6. LOGS: {
  7. },
  8. SCHEMAS: {
  9. }
  10. }

我们搭建了 MIS 配置平台,可以配置很多类似的东西:各个 MIS 系统的用户权限,菜单配置。

MIS 配置平台都是基于Angular组件开发的系统,每个子系统配置不同的用户角色,每个角色针对应不同的权限。系统的左侧菜单也是通过配置平台,这样可以方便的控制每个角色对页面访问权限,同时还会记录每个用户的操作行为,方便回溯问题根源。

配置平台中项目的环境有三套,一套针对内网环境,一套针对外网环境,一套测试环境。平台配置分别分别记录每个项目在相应机器中的端口号,以及域名,统一查询和维护。

由于项目组经常要协助其他组开发 MIS 系统相关的开发,所以我们创建了支持Angular、React及Vue开发的脚手架,方便快速开发项目,只关注项目的逻辑功能,减少对环境的搭建。

  1. mofang angular-demo

此处输入图片的描述

选择 PC 后:

此处输入图片的描述

然后我们会在当前目录下创建一定模板规则的目录,配置好依赖和构建脚本。

2. 如何处理业务组件和通用组件

业务组件与通用组件是密切相关的,正如一句话“用的人多了,就会变成通用组件啦”, 业务组件是在通用组件的基础上做的拼接与定制。通用组件适用的业务场景比较广泛,业务组件业务场景比较单一。当很多业务场景下,都使用了相同组件时,我们就要考虑是否将业务组件提取成公共组件,方便大家使用,节约开发成本。

大多数技术人员在开发项目过程中都会遇到这样,产品经理提出的需求总是要在公共组件的基础上来点特殊的定制化业务逻辑,以使他的产品更加炫酷。如果满足这种需求,往往我们需要给公共组件加各种补丁,或者把组件拿过来自己再重新封装一下。遇到这种情况我们应该怎么办呢?可以从以下两点考虑。

3. 如何构建 DNode 服务化

(1)前后端分离

在以往的工作中,在完成一个系统开发的时候,无论后端语言是php还是Java,常见开发模式分两种情况:

在开发MIS项目时使用DNode服务,所有的前端代码我们都由自己维护,我们只需要后端给我们提供 API 接口,前端自己启服务,搭建测试环境,真正的实现前后端分离,可以“随心所欲”地开发。

目前我们的做法:

1)为了防止恶意攻击,我们将所有与后端API的请求都做一层转发,并在请求之前对请求来源做验证,如果不是来自我们域名下的请求,将其视为无效请求,并告知其请求无效,代码示例如下:

  1. getProvinces: function(req,res){
  2. res.header('Access-Control-Allow-Origin', '***.com');
  3. res.header('Access-Control-Allow-Methods', 'GET');
  4. res.header('Access-Control-Allow-Headers', 'Content-Type');
  5. var util = sails.services.util;
  6. var cookies = cookie.parse(req.headers.cookie);
  7. if(req.headers.referer && req.headers.referer.indexOf(util.Referer')>=0) {
  8. request.get(util , function (error, response, body){
  9. res.json(JSON.parse(body));
  10. });
  11. } else {
  12. res.json({
  13. error: 10000,
  14. data: [],
  15. errormsg: '非法请求'
  16. });
  17. }
  18. }

2)创建 DNode Auth 服务接入权限系统。

  1. var Q = require('q');
  2. var request = require('request');
  3. var defaults = {
  4. url: 'xxx/xxx/xxx/xxx/index'
  5. };
  6. var sso = {
  7. getUser: function (key) {
  8. var deferred = Q.defer();
  9. //...
  10. request.post(obj, function(err,httpResponse,body) {
  11. ....
  12. });
  13. return deferred.promise;
  14. }
  15. };

(2)微服务

我们内建了很多服务和 SDK,下面简单以 Mock Server 为例介绍。

这里面的方案业界比较多,主要分为以下两类:

1)JSON Editor + CDN 化。

这种接口应用场景:

用来配置线上数据的,而且公网能访问,还是依托我们第一场分享中的 TMS

2)JSON Server + JSON schema + DSwagger Doc UI。

这种接口应用场景:

用来构建按规则的假数据,不依赖 DB,一般都是 json 文件,然后加上类似 Swagger 的那种 UI 输出给相关协同开发

(3)如何构建 GUI 新开发模式

1)目前前端的状况

编辑器差异化还好,但构建类工具和预编译类工具都各种各样,以我们团队 IDE 为例:

2)构建工具

而且我们发现编辑器越来越强大,很多插件化的东西都可以安装进编辑器里面,所以我们制定了一个目标:搭建一个在线编辑器,包括以下功能。

主要的好处是:屏蔽各种本地安装带来的问题,专注于业务开发;继承了现有的工具,例如git、脚手架等。

3)展示

此处输入图片的描述

此处输入图片的描述

4)技术演变:

Ace:基于 Web 的开源代码编辑器,star数目13000+。

C9:内置命令行、各种语言工具的在线编辑器,目前已经发布到 3.X 版本了。

C9 的功能非常强大,我们也自定义和开发了很多相关插件。

三、个人总结

我入职滴滴将近一年的时间,完成了多个通用组件的封装,参与了多个项目并且独立带队完成一个项目,参与Vue相关书籍的编写,以及通用组件库的搭建。

通过这些工作,我的变化也非常明显,技术上变得更加多元,从单纯的前端JS编写者转变成为用Nodejs构建自己的开发环境的开发者,向着全栈工程师迈进。同时,我也更多地去关注开源技术,分享自己的所得,让工作变的更佳充实有趣。

沟通方面,在与业务线的同事频繁的交流中,我学会了换位思考,学会了站在产品的角度思考问题,而不是仅仅关注开发的工作量。工作中也结交了一些特别好的朋友,大家一起娱乐一起分担,总之,滴滴之行,不虚此行。

感谢领导的信任与栽培,感谢一路陪伴、一起奋斗的滴滴小伙伴,感谢InfoQ这个平台。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注