1. 程式人生 > >滴滴:WebApp實踐經驗總結

滴滴: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外掛、寫過框架、寫過元件、寫過服務,由於一直在做不同的東西,每一年我都有所收穫。

興趣導向,有的時候我感覺寫程式碼和玩遊戲是一樣爽的事情,我也很喜歡看優秀的開源作品,看看他們的程式碼設計、技術細節,會吸收一些不錯的東西到自己平時的工作中。

前端這幾年發展很快,新技術層出不窮,有的時候,我們要跳出自己的舒適圈,接納一些新事物,新技術,去讓自己不斷學習,而不是滿足於自己已掌握的那些技術。這裡我不是去倡導濫用新技術,而是要保持一顆學習的心態,一顆包容的心態。