滴滴:WebApp實踐經驗總結
本文為滴滴公共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 裡提供 init、onEnter、onExit、orderRecover 等鉤子函式,業務線的程式碼通過呼叫 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)元件化開發。一個元件的 JS、CSS、模板放在一個目錄,方便維護。
(3)多個專案按專案名稱 + 版本部署,採用非覆蓋式釋出。
(4)允許第三方引用專案公共模組。
(5)要支援 CSS 前處理器,前端模板。
(6)與公司的 jenkis 釋出平臺配合,上線方便。
(7)前後端分離,前端可以獨立自主上線。
解決方案
(1)使用 scrat 做前端工程化工具,完美支援上述需求分析中的前五條需求。
(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 文件,建立專門的釘釘服務交流群。
技術方案
技術棧
(1)原生 JS。
(2)webpack 打包。
5. 通用客戶端JSBridge的封裝
需求分析
(1)內嵌在滴滴 App 端裡的頁面,需要通過 JSBrigde 的方式獲取端的一些能力。
(2)遮蔽 iOS 端和 Android 端的一些底層通訊差異。
(3)提供簡單、友好的介面。
解決方案
(1)對 iOS 和 Anroid 的互動介面進一層封裝,所有需要與端通訊的介面封裝成 DDBridge.funcName(options,callback) 的方式。
(2)對端的一些具象介面,比如分享微信、分享微博等做更高階封裝,提供share介面,通過引數指定分享到不同的渠道。
(3)提供完善 Wiki 文件,建立專門的釘釘服務交流群。
技術棧
(1)ES6 + eslint。
(2)webpack 打包。
部分程式碼示例:
export function initGlobalAPI(DDBridge) {
each(config.api, (conf, name) => {
DDBridge[name] = makeBridgeFn(conf);
DDBridge[name].support = conf.support;
});
initSupport(DDBridge);
initVersion(DDBridge);
initShare(DDBridge);
initPay(DDBridge);
};
export function makeBridgeFn(conf) {
return function (param = '', callback = noop) {
if (arguments.length === 1 && isFn(param)) {
callback = param;
param = '';
}
let fn = conf.fn;
if (supportPrompt) {
promptSend(fn, param, callback);
} else {
bridgeReady((bridge) => {
bridge.callHandler(fn, param, (data) => {
if (isStr(data)) {
data = JSON.parse(data);
}
callback(data);
});
});
}
};
};
6.在公共部門做通用服務的一些感悟
入職滴滴一年,造了不少公司級別的“輪子”,不少輪子已經在業務線跑起來了,執行狀況還算可以。我也總結了做通用服務要注意的幾點。
(1)一定要好用,用起來要簡單。
這是我一直貫徹的理念,如果通用服務不好用,那一定會受到質疑和吐槽。同樣我們用開源的框架,也一定會選簡單好用的,當年 jQuery,prototype,tangram 等 JS 庫百家爭鳴的時候,jQuery笑到了最後,為什麼呢?很簡單:jQuery好用啊,一個 $(xxx) 搞定一切。相比 tangram 那種 Baidu.T.createDom() 的方式,高下立判。
我們在設計通用 JS 庫的時候,一定要站在更高的角度去對需求做抽象。比如在設計統一登入 SDK,首先要想的不是複雜的互動邏輯、如何去實現、有哪些技術難點,而是去想,別人怎麼用這個庫,怎麼用起來爽。登入的需求就是使用者觸發一個登入動作,登入完成能拿到使用者一些資訊,所以我就設計一個login(callback)介面,那麼使用方只需要簡單呼叫這個方法,就可以完成登入需求,而不用去關心登入各種複雜的細節。
(2)該做封裝的地方要封裝,對外暴露的介面越少越好。
封裝很重要,舉個通俗的例子,有一天我去洗手,發現水龍頭的開關把手沒了,把原始的開關暴露給我了,雖然也能用,但是體驗就會很不好。水龍頭的開關把手就是對原始開關的封裝。我在做 JSBridge 庫 的時候,也是一樣的道理,如果讓使用者直接呼叫 iOS 和 Andrid 提供的原生 bridge 介面的,也能 work,但是非常難用,需要判斷 iOS 和 Android 介面的差異,還需要考慮 bridge ready 事件後才能執行方法等,這些都是我原本不需要關心的細節。所以我們的庫就是幫助使用者封裝掉這些“髒活”,對外提供簡單的 DDBridge.funcName(options,callback) 介面,優化使用體驗。
為什麼說對外暴露的介面越少越好,因為介面越多,則說明使用者的學習成本越高,比如如火如荼的 Vue.js,其1.x 版本很多介面的功能大同小異,所以在 2.0 版本的 Vue 就幹掉了很多介面,減少了使用者的學習成本。同樣的,我們在做 JSBridge 分享介面相關的時候,也通過一個share介面封裝了端提供的微信分享、支付寶分享、微博分享等介面。
(3)先思考再動手,設計合理的程式碼組織方式。
在寫程式碼之前,一定要先思考清楚,切忌上來就寫程式碼,那樣很容易寫成一波流程式碼。合理的程式碼組織方式,有利於程式碼的擴充套件和維護,最基本的就是模組化。這裡沒有銀彈,需要大量的實踐和總結,學會抽象的看問題,看一些設計模式相關多書籍,多看優秀的開源的程式碼,可以先從模仿開始。
由於我們寫的是通用服務,使用者也可能會提出各種需求,當遇到這個問題的時候,不能上來就寫程式碼去實現甚至hack,而是先思考這個需求是不是可以抽象成通用的需求,如果不能抽象,我們如何更優雅的實現,之前的設計是不是有問題。總之,要多想多思考,也可以和小夥伴討論,爭取做到是在設計程式碼而不是堆程式碼。
(4)追求體驗極致。
現在很多前端都在玩 Node,玩構建工具,玩 MVVM 框架,玩 ES6,好像感覺學會了這些就可以提高身價。其實,這些大部分都是工具,是服務我們平時工作的,不要忘了我們的本行還是前端,還是需要寫頁面的。其實前端有些元件和效果如果想要追求體驗極致的話,也不容易。如果能做到極致,身價也不會低。舉個例子,我在寫 mofang 移動端元件的時候,有個篩選器元件 picker,類似 iOS 原生 UIPickerView 的東東,我當時拿到需求的時候,也從 GitHub 上搜索過,沒有滿意的,體驗都很一般,於是我就對比 iOS 原生的 UIPickerView的體驗,思考它的實現、一點點細節的除錯,最終也擼出來體驗幾乎一致的移動端 H5 picker 元件。舉這個例子其實想說明,我們在做通用服務的時候,要多花心思,如果能做出一些極致體驗的東東,不僅對使用者來說他們很樂意使用,對自己也是一種鍛鍊。
(5)一定要寫 Wiki。
要寫 Wiki!要寫 Wiki!要寫 Wiki!重要的事情說 3 遍。由於我們做通用服務,免不了和使用者打交道,Wiki 就尤為重要了。我們需要把通用服務的介面,使用方式,常見問題等都寫清楚。好的文件可以很好的指導使用者如何使用我們的服務,這樣可以大大的減少溝通成本,節約我們自身和使用者的時間。
(6)要學會銷售。
有些人可能會覺得寫通用服務似乎比做業務的同學更高大上,其實不然,本質上我們都是在為公司打工,都是在輸出自己的價值,只是做事的重心不同。那麼做公共的同學的價值在哪裡?就是讓自己寫的通用服務被更多的人用,去提升他們的工作效率。所以,我們要學會銷售自己的服務,而不是寫完一個的服務擺出一副你愛用不用的態度。如果你寫出來的東西沒人用,就算它再牛逼,對公司的價值也是 0。另外,我們還要學會從業務中去沉澱服務,要去發現業務中的痛點,可以提升效率的地方,然後用技術的手段和工具去解決它。
(7)一顆服務的心。
做公共的同學一定要有顆服務的心。我們售賣的是自己的服務,那麼也一定要做好售後服務,除了 Wiki,各種溝通釘釘微信溝通群也要積極響應,耐心的去幫助使用者解決問題,其實很多時候,都是靠著使用者去幫我們去發現 Bug ,完善功能和優化體驗的。
7.談下我的個人成長
我入前端這行已經4年了,在學校的時候我是玩 .net 的,喜歡折騰。畢業後當然和大部分應屆生一樣,渴望進 BAT 這樣的大公司,不過 BAT 幾乎不招 .net 的崗位。由於我讀研的時候做過一些網站方向的開發,所以就投了百度的一個相近的職位——Web前端開發。這裡我要特別感謝我百度的mentor張袁煒,他是一個對技術要求很高的人,受他的影響,我也成為一個對技術有追求的人。四年來,我寫過頁面,寫過網頁遊戲、寫過Chrome外掛、寫過框架、寫過元件、寫過服務,由於一直在做不同的東西,每一年我都有所收穫。
興趣導向,有的時候我感覺寫程式碼和玩遊戲是一樣爽的事情,我也很喜歡看優秀的開源作品,看看他們的程式碼設計、技術細節,會吸收一些不錯的東西到自己平時的工作中。
前端這幾年發展很快,新技術層出不窮,有的時候,我們要跳出自己的舒適圈,接納一些新事物,新技術,去讓自己不斷學習,而不是滿足於自己已掌握的那些技術。這裡我不是去倡導濫用新技術,而是要保持一顆學習的心態,一顆包容的心態。