本文为滴滴公共 FE 团队在 webApp 方向的一些实践经验总结, 主要内容包括: WebApp 首页技术架构前端工程化在 WebApp 的实践通用地图 JS 库的设计和实践 统一登录 SDK 的设计通用客户端 JSBridge 的封装在公共部门做通用服务的一些感悟个人成长总结
1. WebApp 首页技术架构
需求分析
(1)滴滴多条业务线在一个 WebApp 页面里运行, 业务线之间互不影响
(2)业务线发单流程基本一致, 部分业务线支持自定义化
(3)业务线可以独立自主迭代上线, 不需要公共团队的参与
(4)新业务线可以快速接入首页
解决方案
(1)每个业务线提供自已的 biz.js, 首页加载的时候会异步请求这些 JS 文件
(2)公共提供全局的 dd.registerBiz(option) 方法, 供业务线 biz.js 调用, 同时在 option 里提供 initonEnteronExitorderRecover 等钩子函数, 业务线的代码通过调用 dd.registerBiz 方法完成接入
(3)把页面拆分成多个区块, 有一些公共区块如一级导航菜单和地址选择区块; 也有一些业务线区块如 ETA 区块发单区块自定义区块等公共会在业务线区块下根据 registerBiz 注册的业务线动态创建业务线独立的子区块, 业务线可以填充这些子区块的 DOM, 公共这边提供通用的样式创建业务线区块的时候完毕会调用 init 钩子函数, 业务线可以在这个函数里做一些初始化操作
(4)公共负责管理业务线的切换, 来控制每个业务线子区块的 show 和 hide, 这些细节业务线不用关心在切入的时候会调用 onEnter 钩子函数, 切出的时候会调用 onExit 钩子函数
(5)公共会提供业务线一些公共方法调用, 比如统一的 sendOrder 发单方法还会通过事件机制和业务线通讯, 比如当公共定位完成会调用 events.emit('location.suceess',posInfo) 派发事件, 业务线可以监听该事件拿到定位信息
(6)公共会提供一些封装好的通用组件, 供业务线调用
(7)业务线的 biz.js 地址是通过服务端渲染前端模板的时候通过变量传到模板里的, 这个 JS 地址业务线可以自主配置, 达到业务线自主上线的目的
(8)新业务线的接入只需要提供业务线 biz.js, 实现 dd.registerBiz 接口公共不用关心具体接入的业务线, 与业务线这边完全解耦公共这边还提供了一套完整的 Wiki, 方便业务线接入
技术栈
(1)scrat 完成模块化 + 构建
(2)zepto + gmu 实现组件化
(3)前端模板 handlebar
(4)combo 服务
部分代码示例
业务线接入的 biz.js 示例如下:
- dd.registerBiz({
- id: 123,
- _tpl: {
- // ...
- },
- init: function(ids) {
- // ...
- },
- onEnter: function() {
- // ...
- },
- onExit: function() {
- // ...
- }
- });
2. 前端工程化在 WebApp 的实践
需求分析
(1)支持模块化开发, 包括 JS 和 CSS
(2)组件化开发一个组件的 JSCSS 模板放在一个目录, 方便维护
(3)多个项目按项目名称 + 版本部署, 采用非覆盖式发布
(4)允许第三方引用项目公共模块
(5)要支持 CSS 预处理器, 前端模板
(6)与公司的 jenkis 发布平台配合, 上线方便
(7)前后端分离, 前端可以独立自主上线
解决方案
(1)使用做前端工程化工具, 完美支持上述需求分析中的前五条需求
(2)每个项目用一个 git 的 repo 维护, 然后有专门上线的 2 个 repo, 一个存储静态资源, 另一个存储页面模板每个项目有一个 shell 脚本, 脚本通过 scrat 编译当前项目, 把编译后的结果分别 push 到上线的 repo 然后上线的 2 个 repo 关联公司的 jenkis 平台发布上线
(3)每个项目迭代上线前修改版本号, 所有静态资源都会增量发布上线过程先全量上线静态资源, 线上模板仍然指向旧的资源, 不会有任何问题然后再上线模板, 先上到预发布环境让 qa 回归, 回归完后再全量上线模板, 完成整个上线流程
部分代码示例
一个 WebApp 项目的目录结构如下:
project | -component_modules(生态模块) | -components(工程模块) | -views(非模块资源) | -component.json(模块化资源描述文件) | -fis - conf.js(构建工具配置文件) | -package.json(项目描述文件) | -index.html | -
一个组件的目录结构如下:
components | -header | -header.js | -header.styl | -header.tpl | -logo.png
按项目名称 + 版本发布的 fis-conf.js 配置规则如下:
- var meta = require('./package.json');
- fis.config.set('name', meta.name);
- fis.config.set('version', meta.version);
- // 自定义发布规则
- var userRoadMap = [{
- reg: /^\/components\/(.*\.tpl)$/i,
- isHtmlLike: true,
- release: '/pages/c/${name}/${version}/$1'
- },
- {
- reg: /^\/pages\/(.*\.tpl)$/,
- useCache: false,
- isViews: true,
- isHtmlLike: true,
- release: '/pages/${name}/${version}/$1'
- },
- {
- reg: /^\/pages\/boot\.js$/,
- useOptimizer: false,
- },
- {
- reg: /^\/pages\/(.*\.(?:js))$/,
- useCache: false,
- isViews: true,
- url: '/${name}/${version}/$1',
- release: '/public/${name}/${version}/$1'
- },
- {
- reg: /^\/pages\/(.*\.(?:html))$/,
- useCache: false,
- useOptimizer: false,
- isViews: true,
- release: '$1'
- },
- {
- reg: /^\/pages\/(.*)$/,
- useSprite: true,
- isViews: true,
- url: '/${name}/${version}/$1',
- release: '/public/${name}/${version}/$1'
- }];
- var defaultRoadmap = fis.config.get('roadmap.path');
- fis.config.set('roadmap.path', userRoadMap.concat(defaultRoadmap));
编译后部署的目录结构如下:
| -public(生成的静态资源目录) | -c | -project | -1.0.0 | -header | -header.css | -header.css.js | -header.js | -home... | -project | -1.0.0 | -lib | -index.html | -views(模板目录) | -
3. 通用地图 JS 库的设计和实践
需求分析
(1)支持多种地图多种地图场景的开发
(2)屏蔽底层地图库 (高德腾讯) 的接口差异
(3)实现小车平滑移动
解决方案
(1)底层对腾讯地图和高德地图分别封装(不会在源码中出现 if(qq){} 风格的代码), 依据 webpack 动态打包成 2 个 JS 文件, 上游根据需求异步加载 JS , 对外提供同一套编程接口
(2)抽象地图场景的概念, 可以通过接口注册一个场景类, 在场景中可以操作各种封装好的地图组件和方法, 编写业务逻辑, 实现需求
(3)小车的平滑移动通过封装地图 SDK 提供的底层 marker, 轮询小车坐标点, 实现小车平滑移动(CSS3), 并把 "移动 + 转向 + 移动..." 一系列操作抽象出动画队列的概念
技术栈
(1)原生 JS
(2)webpack 打包
行程分享实践
行程分享这个场景中, 有等待接驾行程中行程结束等状态, 有轨迹, 小车平滑移动等功能我们要做的就是利用通用地图 JS 库暴露的接口去编写行程分享的逻辑
贴一下部分代码, 看一下如何去使用封装好的地图 JS 库
我们可以先去写一个行程分享的场景: tripShare.js
- var Map = window.map;
- var _ = Map.utils._;
- var inherit = Map.utils.inherit;
- var api = Map.utils.api;
- var config = Map.utils.config;
- var EventEmitter = Map.utils.EventEmitter;
- var Car = Map.component.Car;
- var StartPoint = Map.component.StartPoint;
- var EndPoint = Map.component.EndPoint;
- var TrackControl = Map.control.TrackControl;
- var TrafficControl = Map.control.TrafficControl;
- var TrafficLayer = Map.layer.TrafficLayer;
- var Polyline = Map.Polyline;
- function TripShare(map, options) {
- TripShare._super.call(this);
- // ...
- }
- inherit(TripShare, EventEmitter);
- TripShare.prototype.begin = function() {
- // ...
- };
- // ...
然后我们这样去注册场景
- var Map = window.map;
- var fromlat = 31.17626;
- var fromlng = 121.425;
- var tolat = 31.20425;
- var tolng = 121.40398;
- Map.ready(function(mapInstance) {
- var map = mapInstance.createMap('container', {});
- var TripShare = require('./tripShare');
- var scene = map.scene.register(TripShare, {
- orderStatus: 1,
- url: 'xxxx',
- oid: 'aaaa',
- pathUrl: 'xxxx'fromlat: fromlat,
- fromlng: fromlng,
- tolat: tolat,
- tolng: tolng,
- usePath: true
- });
- scene.begin();
- scene.on('path.badCase',
- function(badCase) {
- // do anything
- });
- });
我们可以调用场景的方法, 又由于场景继承了 EventEmitter 事件中心, 它会通过 trigger 派发事件, 我们可以监听这些事件, 去做一些事情
4. 统一登录 SDK 的设计
需求分析
(1)滴滴有众多业务线, 每个业务线都有独立的域名, 需要打通各个 WebApp 域名的登录态
(2)方便新老业务线运营活动等页面接入登录
(3)提供简单友好的接口
解决方案
(1)与账号部门合作, 通过跨域方式访问 passport 域名下的接口跨域方案是通过 iframe passport 域名下的页面, 利用 postmessage 进行通信登录成功后会在 passport 域名下利用种下 ticket 后端提供判断是否登录的接口, 前端请求这个接口的时候会从 passport 域名下读取 ticket 并把它作为请求的参数传给后端, 这样一旦用户在 a 域名下登录成功, 那么在 b 域名下调用是否登录接口, 返回的就是登录成功的结果, 这样就打通了多个域名的登录态
(2)封装复杂的登录交互细节, 对外提供简单的交互接口
(3)提供完善 Wiki 文档, 建立专门的钉钉服务交流群
技术方案
来源: http://www.infoq.com/cn/articles/webapp-practical-experience-summary