天天看點

YUI3在美團的實踐

美團網在2010年引爆了團購行業,并在2012年銷售額超過55億,實作了全面盈利。在業務規模不斷增長的背後,作為研發隊伍中和使用者最接近的前端團隊承擔着非常大的壓力,比如使用者量急劇上升帶來的産品多樣化,業務營運系統的界面互動日益複雜,代碼膨脹造成維護成本增加等等。面對這些挑戰,我們持續改進前端技術架構,在提升使用者體驗和工作效率的同時,成功支撐了美團業務的快速發展,這一切都得益于建構在YUI3架構之上穩定高效的前端代碼。在應用YUI3的過程中,我們團隊積累了一些經驗,這裡總結成篇,分享給大家。

為什麼選擇YUI3

使用什麼前端基礎架構是建立前端團隊最重要的技術決策之一。美團項目初期因為要加快開發進度,選擇了當時團隊最熟悉的YUI2(前架構時代傑出的類庫),保證美團能夠更快更早地上線,搶占市場先機。不久由于前端技術發展很快,YUI2的缺點逐漸凸顯,例如開發方式落後、影響工作效率等等,于是我們開始考慮基礎庫的遷移。

經過一段時間對主流前端庫、架構的反複考量,我們認為YUI3是最适合我們團隊使用的基礎架構。

首先,國内的開源架構及其社群剛開始起步,在代碼品質、架構設計和理念創新上還難以跟YUI3比肩,是以基本排除在外。其次,國外像YUI3這樣面向使用者産品、文檔豐富、擴充性良好的成熟架構屈指可數,例如ExtJS和Dojo則更适合業務複雜的傳統企業級開發。最後,使用jQuery這種類庫建構同YUI3一樣強大的架構對創業團隊來說并不可取,美團快速發展、競争多變的業務特點決定了我們必須把主要精力放在更高一層的業務開發上,而不是去重複發明一個蹩腳的YUI。

YUI3成為最終選擇有以下幾個直接的原因:

  • 非常優秀,是真正的架構,真正的重型武器,具有強勁的持續開發能力,可以應對業務的快速發展。不管是規模不斷增長的使用者産品,還是互動日趨複雜的業務系統(美團有超過100個業務系統作全電子化的營運支撐),YUI3都遊刃有餘。
  • 代碼整齊規範,容易維護,适合有潔癖的工程師,同時能夠顯著提高團隊協作時的開發效率。因為人手緊缺,後端工程師也需要參與前端開發,一緻的代碼風格使前後端配合輕松簡單。
  • 有出色的架構設計,是很好的架構範本,通過研究學習可以幫助工程師成長,培養良好的工程思維。人是美團最重要的産品。

随着團隊成長,我們最後引入了YUI3,在遷移過程中,遇到了很多技術上的和工程上的挑戰,但是我們一直在前進,一直在行進中開火。從結果來看,YUI3為我們團隊提供了先進生産力,為快速開發、快速部署、快速疊代提供了源源不斷的力量。

YUI3的優秀主要表現在子產品群組件架構的出色設計,下面我們着重介紹這兩方面的一些實踐經驗。

改變一切的子產品

前端開發日益複雜化,代碼組織成為一個顯著的問題。受到後端代碼普遍采用的子產品機制啟發,很多前端子產品機制應運而生。目前比較著名的有CommonJS和AMD。但早在2008年8月13日,YUI3 Preview Release 1中就已經給出了YUI團隊的解決方案,并在2009年9月29日YUI3正式版釋出時定型。

以下是使用YUI3進行子產品化開發的簡單例子

// 定義子產品YUI.add('greeting', function (Y) {
    Y.sayHello = function () {        console.log('Hello, world!');
    };
});// 調用子產品YUI().use('greeting', function (Y) {
    Y.sayHello(); // output 'Hello, world!'});      

子產品的引入,使得更細粒度的按功能進行代碼組織成為可能,也為友善的進行擴充和分層提供了基礎,自底向上的徹底改變了YUI3。一套完整的子產品機制,還包括解決關系依賴、自動加載的Loader和提高加載效率的Combo。

面對如此徹底的改變,我們需要解決很多挑戰:

  • 如何将原來的功能劃分為子產品?
  • 如何管理子產品元資訊?
  • 如何高效的擷取子產品?

劃分子產品

經過兩年來不斷的實踐和總結,我們歸納了如下幾條劃分子產品的原則:

  • 抽象與應用脫離。更通用的功能放在更低的層級,應用層完全面向實際問題,在解決的過程中調用抽象出來的方法。
  • 職責單一。保持每個子產品的足夠簡單和專一,友善維護和可持續開發。
  • 粒度得當。有了Combo,我們可以不必擔心粒度太小,檔案過多導緻的速度問題。但是,從可維護的角度來考慮,粒度應該适當而不宜過小,避免海底撈針的情形出現。
  • 海納百川。我們的子產品體系應該是開放的,不符合YUI規範的第三方子產品,可以借鑒整合進來,使我們的基礎架構更加完善,更加性感。

按照子產品的層次劃分,美團的JS架構可以分為四個層次:

  • 最底層交給強悍的YUI3,為我們提供跨浏覽器相容的API和良好的架構設計。
  • 第二層是我們二次開發的核心方法、元件(Component)和控件(Widget)。現已獨立為前端核心庫,為美團所有系統提供前端支援。核心庫的種子檔案中定義了全局變量M,除了對YUI3進行封裝的代碼以外,還包含了對語言層面的擴充,以及一些基礎工具類。核心庫有一個非常重要的組成部分,就是我們功能豐富的控件集合,比如常用的自動完成、排序表格、氣泡提示、對話框等基礎控件。除了這些,核心庫還包含了常用的基礎元件、插件(Plugin)、擴充(Extension)以及單元測試代碼。
  • 第三層包含各個系統的一些通用子產品。例如www-base子產品包含美團主站(www)的消息系統、使用者行為追蹤系統等通用功能。這一層更加接近應用。
  • 最上面一層,應用子產品。這些子產品的方法都是用來解決實際業務問題。例如www-deal用來處理美團主站所有deal相關功能的互動,finance-pay用來處理财務系統中付款相關的互動。一些零碎的應用方法我們放在對應系統的misc子產品中,避免子產品碎片化。

這套架構仍在不斷演變,以便更好的支撐業務需求。其中一個明顯的方向是,在第二層和第三層之間,出現一個為了更好整合所有内部業務系統前端通用資源的中間層。

管理子產品元資訊

子產品元資訊主要包括子產品名稱、路徑、依賴關系等内容。其中最為重要的是依賴關系,這決定了有哪些子產品需要加載。為了實作自動加載,需要将所有子產品的元資訊提供給YUI的Loader。

最初,為了更快的從YUI2遷移到YUI3,子產品元資訊放在PHP中進行維護。随着時間的推移,漸漸顯示出很多弊端。首先,在定義子產品的js檔案中已經包含子產品名稱、依賴關系等資訊,和PHP中内容重複。其次,這些元資訊最終直接輸出到html中,沒有有效利用緩存。

随後,我們使用NodeJS開發了一系列腳本,收集所有子產品元資訊,儲存為獨立js檔案,并實作了自動化。為了防止出錯,在Git Hooks和上線腳本中都加入了校驗過程。工程師需要做的,隻是修改子產品定義中的元資訊。

最近一段時間,我們的精力主要放在兩個方面:

  • 自動生成依賴。随着子產品粒度細化和子產品數量的增長,依賴關系日益複雜,依靠人工配置經常出現過多依賴或過少依賴等問題。我們準備開發一套自動掃描子產品引用API,并确定依賴關系的機制。
  • 自動打包依賴子產品。如果在代碼釋出時,就已根據頁面子產品調用計算好所有依賴子產品,并進行打包,可以避免引用全部子產品元資訊、Loader計算依賴等過程,提高網站性能。

Combo

Combo可以一次請求多個檔案,能夠有效解決多個子產品加載帶來的性能問題。Yahoo提供了Combo服務,但隻能提供YUI3子產品,而且速度在國内并不理想。為了提供更好的體驗,讓使用者通路速度更快,我們最終考慮搭建自己的Combo服務,并把Combo釋出到CDN上。

以下是一個Combo請求的例子:

http://c.meituan.net/combo/?f=mt-yui-core.v3.5.1.js;fecore/mt/js/base.js      

為了節約時間,我們最開始采用了開源的minify,經過一些修改和配置,就可以在生産和開發環境提供Combo服務。使用一段時間後,發現minify過于複雜,以至于添加一些定制功能相當困難。我們需要的隻是簡單的檔案合并功能,在明确需求和開發量後,着手開發自己的Combo程式。從最初的僅支援檔案合并,後來陸續添加了伺服器/浏覽器端緩存、檔案集别名、調試模式、CSS圖檔相對路徑轉URI、錯誤日志等特性,全部代碼僅有300多行。經過兩年時間以及每天幾千萬PV的考驗,服務一直非常穩定。

靈活健壯的元件架構

YUI3之是以成為純粹的架構,真正的原因在于提供了一套靈活、健壯的元件架構。借助這套架構,可以輕松的将業務場景進行解耦、分層,并持續的進行改進。通過不斷的實踐,我們越發認為這是YUI3的精髓所在。

從YUI3定義的開發範式和源代碼中可以看出,YUI團隊非常重視AOP(Aspect Oriented Programming)和OOP(Object Oriented Programming),這一點可以在接下來的介紹中有所體會。

EventTarget、Attribute和Base

在介紹元件架構之前,有必要首先了解下EventTarget。YUI3建立了一套類似DOM事件的自定義事件體系,支援冒泡傳播、預設行為等功能。EventTarget提供了操作自定義事件的接口,可以讓任意一個對象擁有定義、監聽、觸發、登出自定義事件的功能。YUI元件架構中的所有類,以及在此架構之上開發的所有元件,都繼承了EventTarget。

Attribute是元件架構中最底層的類,實作了資料和邏輯的完美解耦。為什麼說是完美呢?存儲在attribute(Attribute提供的資料存取接口)中的資料發生變化時,會觸發相應的事件,為相關的邏輯處理提供了便捷的接口。從下面這個簡單的例子可以感受到這一點:

// 在name屬性變化時,觸發nameChange事件this.on('nameChange', function (e) {    console.log(e.newVal);
});// 修改name屬性this.set('name', 'meituan'); // output 'meituan'      

實踐中發現,妥善處理屬性的分類非常重要。供執行個體進行操作的屬性适合作為attribute,例如表單驗證元件FormChecker的fields屬性,友善應用層進行表單項的增删改。在類方法内部使用的一些屬性可以作為私有屬性,例如計時器、監聽器句柄。供所有類的執行個體使用的一些常量适合作為類的靜态屬性,例如一些模闆、樣式類。

Base是元件架構的核心類。它模拟了C++、Java等語言的經典繼承方式和生命周期管理,借助Attribute來實作資料與邏輯的分離,并提供擴充、插件支援,進而獲得了良好的擴充性以及強大的可持續開發能力。YUI團隊通過多年來對業務實踐的抽象,最終演化而成一種開發範式,這,就是一切元件的基石——Base,實至名歸。

依照這種範式,我們開發了一系列元件,例如之前提到的FormChecker,以及延遲加載器LazyLoader、地圖的封裝Map等。最顯著的體會是,開發思路更為清晰,代碼結構更有條理,維護變得簡單輕松。

// 構造方法FormChecker.prototype.initializer = function () {    var form = this.get('form');    this._handle = form.on('submit', function (e) {        // check fields
    });
};// 析構方法FormChecker.prototype.destructor = function () {    this._handle.detach();
};// 建立執行個體時,自動執行構造方法var checker = new FormChecker({ form: Y.one('#buy-form') });// 銷毀執行個體時,自動執行析構方法checker.destroy();      

Extension和Plugin

Extension(擴充)是為了解決多重繼承,以一種類似組合的方式在類上添加功能的模式,它本身不能建立執行個體。這種設計非常像Ruby等語言中的Mixin。Plugin(插件)的作用是在對象上添加一些功能,這些功能也可以很友善的移除。

它們有什麼差別呢?簡單來說,Extension是在類上加一些功能,所有類的執行個體都擁有這些功能。Plugin隻是在某些類的執行個體中添加功能。舉兩個典型的例子:一些節點需要使用動畫效果,這個功能适合作為Plugin。氣泡提示控件需要支援多種對齊方式,所有執行個體都需要此功能,是以使用YUI3的WidgetPositionAlign擴充。

// 傳統的函數方式實作動畫Effect.fadeIn(nodeTip);// 插件方式實作動畫nodeTip.plug(NodeEffect);
nodeTip.effect.fadeIn();      

Extension和Plugin很好的解決了我們遇到的諸多功能重用問題。我們開發了提供全屏功能的WidgetFullScreen、自動對齊對話框的DialogAutoAlign等擴充,以及進行異步查詢的AsyncSearch、提供動畫效果的NodeEffect等插件。将這些偏重OOP的程式設計思想應用在前端開發中,比較深刻的體會是:有更多的概念清晰、定位明确的開發模式可以選擇。

Widget體系

Widget(控件)建立在Base之上,主要增加了UI層面的功能,例如

renderUI

bindUI

syncUI

等生命周期方法,

HTML_PARSER

等漸進增強功能,以及樣式類、HTML結構和DOM事件的統一管理。Widget提供了控件開發的通用範式。

由于前端資源相對緊張,我們傾向于大量使用控件,尤其在業務系統這樣更注重功能的場景。主要出于兩點考慮:

  • 減少不必要的重複勞動,提高産出。通過将互動、業務邏輯合理抽象,一次解決一類問題,One Shot One Kill。
  • 節約前端工程師資源。通過自動加載和初始化控件、封裝簡單易用的後端方法、制作Demo和使用手冊等措施,降低使用門檻,後端工程師隻需要知道參數的資料結構就可以輕松調用,提高了開發效率。

以下是一個自動加載控件的例子

// 頁面初始化時,會掃描所有帶有data-widget屬性的節點,自動加載對應控件,并根據data-params資料進行初始化下載下傳手機版a>      

目前,我們已經建構了一個包含近30個控件的Widget體系,為所有系統提供豐富、便捷、內建的解決方案。

行進中開火

在整個YUI3的實踐中,我們犯過很多錯誤,例如全局隻有一個YUI執行個體、Combo的CSS圖檔依賴等等,但這些并沒有成為放棄的理由。從今天回過頭來看,YUI3帶給我們團隊的,不隻是更高的開發效率、更好的可持續開發能力,還有它本身的設計思路、源碼書寫、輔助工具等諸多方面潛移默化的影響。這些回報的價值,比起較高的使用門檻、犯過的一些錯誤,要貴重百倍。

指導這一切的,是我們始終堅持的 “行進中開火”。在網際網路這個高速發展的行業裡,對于我們這種小規模的創業團隊,一天不前進,就意味落後。做事不應該準備太多,一定要先做起來,然後發現不足并不斷改進,甯可十年不将軍,不可一日不拱卒。每天都做得更好一點,日積月累,我們才會在激烈的競争中占據越來越大的優勢。

YUI3并非完美,存在着學習成本高、對社群不夠開放等問題。我們所做的更遠非完美,但經過不斷的嘗試和經驗的積累,已經漸漸摸索出一條明确的路線,并會堅持不懈的繼續走下去。

繼續閱讀