本文为 8 月 18 日,『前端之巅』群『滴滴公共 FE 团队技术开放月』第二期分享活动总结整理而成,转载请在文章开头处注明来自『前端之巅』公众号。查看前两期分享,请关注『前端之巅』公众号并发送“滴滴”。
王静,现就职于滴滴出行公共 FE 团队,高级前端开发工程师,负责滴滴 MIS 项目开发管理,熟悉 Angular、数据可视化、Vue 等组件开发,现热衷学习专研 NodeJS。
一、公司级组件库——魔方的整体设计
1. 设计初衷
设计公司级组件库的初衷其实在第一场分享中也已经提到了,主要是为了解决这些痛点:每一个系统 UI、交互规范、组件依赖底层技术都不一样,复用性低,依赖第三方开源但技术支持不到位,遇到问题没人服务。
2. 技术选型
定位: PC 端的定位很清晰,就是内外 MIS 系统。
组件需求简单举例如下:
- form 表单
单个组件:下拉框、输入框、多选框、日历框等。
组合组件:通过参数配置,根据组件类型,进行自动映射。 - 列表
单个组件:popup、sort、分页、搜索表单等。
组合:带搜索功能、分页功能、排序等功能列表
3. PC 类组件库搭建和编译细节之 Angular
目前滴滴的 PC 组件提供两套,首先介绍 Angular 的这套组件库。
(1)技术选型
在技术底层选型过程中,我们和多个业务团队的前端开发人员进行了沟通,考虑到部分团队的后端开发人员比较喜欢 Angular,而且大部分 MIS 项目其实都是由后端开发人员来做的。所以,我们提供了 Angular 的这套组件库,包含:按钮、表格、下拉框、日历框、多选框、弹窗,等等。
(2)部分组件展示
(点击放大图像)
(点击放大图像)
(点击放大图像)
(3)组件简介
相关代码如下:
<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>
(4)部分配置项说明
- resource-url:配置列表的数据接口请求地址
- resource-index:列表内置支持 restful 方式的增删改查,所以对应的就是主键、默认是 id,可以不用配置
- grid-options:配置列表项相关信息
- pagination-options:配置分页相关信息
- search-options:配置和列表绑定的搜索表单信息
- param-obj:配置列表请求接口时在 url 中所带参数
- event-hooks:事件钩子对象,onloadbefore 钩子在服务端数据返回来之后可以对原始数据进行加工格式化;ongetbefore 钩子可以在请求发送之前进行字段校验等操作,返回 false 时不会发送请求 data,很多时候不需要自动发送 http 请求来获取数据,而是直接设置。
(5)目录结构
魔方 pc 组件项目源码目录结构如下:
mofang-pc-angular |- app(生态模块) |- dist (目标模块) |- node-modules(依赖模块) |- src (代码模块) |- common (公共代码) |- componets (组件) |- vendor(angular 相关依赖) |- webpack.config.js (测试环境配置) |- webpack.min.js (上线环境配置) |- gulpfile.js (打包环境配置) |- package.json
(6)打包方式
打包方式为 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 } }) ... ] }
(7)组件设计
组件设计需要从以下几点出发:
- 需求调研,了解现有的同类组件都实现哪些功能,我们的业务都需要组件提供功能
- 可拓展性,基础组件和业务组件分开
- 使用和配置简便
- 文档要全
(8)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 的服务单进行数据交互。
4. PC 类组件库搭建和编译细节之 React
考虑到公司级组件库的初衷,也看到有部分业务线的开发人员喜欢 React,所以我们也快速封闭开发,去铺 React 版本。
(1)需求
React 组件提供与 PC 端相同的功能。
(2)使用方式
组件的使用,相关代码如下:
<didi-list paginationOptions={paginationOptions} gridOptions={gridOptions} data={data} paramObj={paramObj} resourceUrl={resourceUrl} />
(3)目录结构
mofang-pc-react |- dist () |- node_modules |- src |- components (组件) |- utils (工具方法) |- mofang.js (主入口) |- package.json |- webpack.config.js |- webpack.min.js
(4)组件开发
所有的组件是基于 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;
(5)打包方式
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"] }
(6)组件设计
采用组件组合的开发方式,将组件做到最小颗粒化,每个组件实现单一的功能,使组件运用起来更加灵活。公共组件由每个功能单一的组件拼合而成。组件开发依赖 state,通过 componentwillreciveprops 生命周期函数接受 props 更新 state,来使 View 更新。
5. H5 类组件库搭建和编译细节
(1)技术选型
定位: 移动端 H5 页面。
(2)组件需求
组件需求如以下几张图片所示:
(点击放大图像)
(点击放大图像)
(点击放大图像)
(点击放大图像)
(3)技术栈
1) webpack
2) zepto + gmu + stylus + handlebar
(4)目录结构
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 (打包环境配置)
(5)组件使用方式
以 carchoose 组件为例,来看一下它是如何被使用的:
var $carchoose = $('#carchoose'); $carchoose.carchoose({ onselect: function(obj){ ... } });
如代码所示,我们的组件是绑定在元素上,组件内容全部通过配置参数
来控制。
(6)组件设计
以 carchoose 为例。该组件主要是用来展示品牌车型的,其中包括车辆品牌、车辆型号、车辆颜色。分别通过点击事件依次从右侧划入屏幕。
组件如以下图片所示:
1)功能拆分
将组件分为三块部分(品牌、型号、颜色),品牌中再分为热门、缩略、列表。
2)参数定义
brandsURL: '', typesURL: '', colorsURL: '', hotcarbrands: [], onselect:fuction(){}, onCloseSelector: function(){}, onrenderBefore: function(type){}
从功能上看,我们有三种主要数据需要渲染,数据通过请求方式获取,数据量太大一次性读取数据还是很耗时的,而且用户不会频繁操作该组件。但是不能因为用户不会频繁操作,而忽略这个问题。我们采取将用户获取数据进行缓存,当用户再次点击,我们只需用缓存数据进行渲染即可。热门品牌以及钩子函数是必须的。
3)技术实现
动画效果采用 css3 实现
列表展示采用 bscroll 组件
加载效果采用 lodaing 组件
(其中 bscroll 为滴滴内部研发类似 iscroll, 性能优于 iscroll 的组件)
6. 可视化类组件库搭建和编译细节
(1)痛点
我们没有一套滴滴定制化的可视化控件,每个设计师设计的图标各不相同, 而且代码没有可复用性,每次都要重新开发,浪费资源。加上目前流行的可视化组件配置项比较复杂,所以我们基于 canvas API 封装了一套基础图表组件。
(2)技术选型
定位:pc 端数据可视化图表
组件需求:
折线图 & 饼状图 & 柱状图 & 雷达图
两套皮肤
(3)组件使用方式
以折线图为例:
<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>
(4)参数说明:
- theme:皮肤,支持 2 套配色皮肤 [‘warm’,‘cold’],默认:warm
- fill:是否填充颜色,支持 [true,false],默认:false
- labels:配置横轴内容 [数组格式]
- datasets:配置线数据,包含两个字段:label 是图例名称,data 是数据 [数组格式]
- $broadcast(“update”):更新图表数据
(5)组件设计
我们的可视化组件是在 chartjs 的基础上,保留原有 chartjs 的基本框架结构,对内部的组件 canvs 画图方式以及组件间数据传递进行改造,以达到滴滴定制可视化组件的效果,为避免用户调用过于复杂,又鉴于 MIS 系统都是基于 Angularjs 组件库开发的,就将可视化组件用 Angular 封装,只对用户暴露简单的数据接口。
二、MIS 类项目配置化、服务化和 GUI 化
1. MIS 平台配置化
我们发现只解决了 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 后:
然后我们会在当前目录下创建一定模板规则的目录,配置好依赖和构建脚本。
2. 如何处理业务组件和通用组件
- 通用组件:底层组件,提供基础功能、可扩展性。
- 业务组件:在通用组件基础上做拼接、定制。
业务组件与通用组件是密切相关的,正如一句话“用的人多了,就会变成通用组件啦”, 业务组件是在通用组件的基础上做的拼接与定制。通用组件适用的业务场景比较广泛,业务组件业务场景比较单一。当很多业务场景下,都使用了相同组件时,我们就要考虑是否将业务组件提取成公共组件,方便大家使用,节约开发成本。
大多数技术人员在开发项目过程中都会遇到这样,产品经理提出的需求总是要在公共组件的基础上来点特殊的定制化业务逻辑,以使他的产品更加炫酷。如果满足这种需求,往往我们需要给公共组件加各种补丁,或者把组件拿过来自己再重新封装一下。遇到这种情况我们应该怎么办呢?可以从以下两点考虑。
- 任何组件都不能达到“十全十美”,如果新增的需求满足通用性的抽取原则,我们可以将这部分业务功能融合到组件中,使组件更加完善。
- 如果新增的需求仅仅是锦上添花的效果且抽取组件的成本大于收益,将其视为业务组件。
3. 如何构建 DNode 服务化
(1)前后端分离
在以往的工作中,在完成一个系统开发的时候,无论后端语言是 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: '非法请求' }); } } {1}
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; } };
(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 为例:
- Sublime
- Webstorm
- Visual Studio Code
- Vim
- Atom
- Brackets
2)构建工具
- grunt
- gulp
- scrat
- webpack
- rollup
而且我们发现编辑器越来越强大,很多插件化的东西都可以安装进编辑器里面,所以我们制定了一个目标:搭建一个在线编辑器,包括以下功能。
- 支持 git 相关操作
- 支持创建不同类型的项目(打通脚手架命令)
- 支持一键部署测试环境
- 支持对接内部发布平台
主要的好处是:屏蔽各种本地安装带来的问题,专注于业务开发;继承了现有的工具,例如 git、脚手架等。
3)展示
(点击放大图像)
(点击放大图像)
4)技术演变:
Ace:基于 Web 的开源代码编辑器,star 数目 13000+。
C9:内置命令行、各种语言工具的在线编辑器,目前已经发布到 3.X 版本了。
C9 的功能非常强大,我们也自定义和开发了很多相关插件。
三、个人总结
我入职滴滴将近一年的时间,完成了多个通用组件的封装,参与了多个项目并且独立带队完成一个项目,参与 Vue 相关书籍的编写,以及通用组件库的搭建。
通过这些工作,我的变化也非常明显,技术上变得更加多元,从单纯的前端 JS 编写者转变成为用 Nodejs 构建自己的开发环境的开发者,向着全栈工程师迈进。同时,我也更多地去关注开源技术,分享自己的所得,让工作变的更佳充实有趣。
沟通方面,在与业务线的同事频繁的交流中,我学会了换位思考,学会了站在产品的角度思考问题,而不是仅仅关注开发的工作量。工作中也结交了一些特别好的朋友,大家一起娱乐一起分担,总之,滴滴之行,不虚此行。
感谢领导的信任与栽培,感谢一路陪伴、一起奋斗的滴滴小伙伴,感谢 InfoQ 这个平台。
感谢韩婷对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论