YUI3在美团的实践
美團(tuán)網(wǎng)在2010年引爆了團(tuán)購行業(yè),并在2012年銷售額超過55億,實(shí)現(xiàn)了全面盈利。在業(yè)務(wù)規(guī)模不斷增長的背后,作為研發(fā)隊(duì)伍中和用戶最接近的前端團(tuán)隊(duì)承擔(dān)著非常大的壓力,比如用戶量急劇上升帶來的產(chǎn)品多樣化,業(yè)務(wù)運(yùn)營系統(tǒng)的界面交互日益復(fù)雜,代碼膨脹造成維護(hù)成本增加等等。面對這些挑戰(zhàn),我們持續(xù)改進(jìn)前端技術(shù)架構(gòu),在提升用戶體驗(yàn)和工作效率的同時(shí),成功支撐了美團(tuán)業(yè)務(wù)的快速發(fā)展,這一切都得益于構(gòu)建在YUI3框架之上穩(wěn)定高效的前端代碼。在應(yīng)用YUI3的過程中,我們團(tuán)隊(duì)積累了一些經(jīng)驗(yàn),這里總結(jié)成篇,分享給大家。
為什么選擇YUI3
使用什么前端基礎(chǔ)框架是建立前端團(tuán)隊(duì)最重要的技術(shù)決策之一。美團(tuán)項(xiàng)目初期因?yàn)橐涌扉_發(fā)進(jìn)度,選擇了當(dāng)時(shí)團(tuán)隊(duì)最熟悉的YUI2(前框架時(shí)代杰出的類庫),保證美團(tuán)能夠更快更早地上線,搶占市場先機(jī)。不久由于前端技術(shù)發(fā)展很快,YUI2的缺點(diǎn)逐漸凸顯,例如開發(fā)方式落后、影響工作效率等等,于是我們開始考慮基礎(chǔ)庫的遷移。
經(jīng)過一段時(shí)間對主流前端庫、框架的反復(fù)考量,我們認(rèn)為YUI3是最適合我們團(tuán)隊(duì)使用的基礎(chǔ)框架。
首先,國內(nèi)的開源框架及其社區(qū)剛開始起步,在代碼質(zhì)量、架構(gòu)設(shè)計(jì)和理念創(chuàng)新上還難以跟YUI3比肩,所以基本排除在外。其次,國外像YUI3這樣面向用戶產(chǎn)品、文檔豐富、擴(kuò)展性良好的成熟框架屈指可數(shù),例如ExtJS和Dojo則更適合業(yè)務(wù)復(fù)雜的傳統(tǒng)企業(yè)級開發(fā)。最后,使用jQuery這種類庫構(gòu)建同YUI3一樣強(qiáng)大的框架對創(chuàng)業(yè)團(tuán)隊(duì)來說并不可取,美團(tuán)快速發(fā)展、競爭多變的業(yè)務(wù)特點(diǎn)決定了我們必須把主要精力放在更高一層的業(yè)務(wù)開發(fā)上,而不是去重復(fù)發(fā)明一個(gè)蹩腳的YUI。
YUI3成為最終選擇有以下幾個(gè)直接的原因:
- 非常優(yōu)秀,是真正的框架,真正的重型武器,具有強(qiáng)勁的持續(xù)開發(fā)能力,可以應(yīng)對業(yè)務(wù)的快速發(fā)展。不管是規(guī)模不斷增長的用戶產(chǎn)品,還是交互日趨復(fù)雜的業(yè)務(wù)系統(tǒng)(美團(tuán)有超過100個(gè)業(yè)務(wù)系統(tǒng)作全電子化的運(yùn)營支撐),YUI3都游刃有余。
- 代碼整齊規(guī)范,容易維護(hù),適合有潔癖的工程師,同時(shí)能夠顯著提高團(tuán)隊(duì)協(xié)作時(shí)的開發(fā)效率。因?yàn)槿耸志o缺,后端工程師也需要參與前端開發(fā),一致的代碼風(fēng)格使前后端配合輕松簡單。
- 有出色的架構(gòu)設(shè)計(jì),是很好的框架范本,通過研究學(xué)習(xí)可以幫助工程師成長,培養(yǎng)良好的工程思維。人是美團(tuán)最重要的產(chǎn)品。
隨著團(tuán)隊(duì)成長,我們最后引入了YUI3,在遷移過程中,遇到了很多技術(shù)上的和工程上的挑戰(zhàn),但是我們一直在前進(jìn),一直在行進(jìn)中開火。從結(jié)果來看,YUI3為我們團(tuán)隊(duì)提供了先進(jìn)生產(chǎn)力,為快速開發(fā)、快速部署、快速迭代提供了源源不斷的力量。
YUI3的優(yōu)秀主要表現(xiàn)在模塊和組件框架的出色設(shè)計(jì),下面我們著重介紹這兩方面的一些實(shí)踐經(jīng)驗(yàn)。
改變一切的模塊
前端開發(fā)日益復(fù)雜化,代碼組織成為一個(gè)顯著的問題。受到后端代碼普遍采用的模塊機(jī)制啟發(fā),很多前端模塊機(jī)制應(yīng)運(yùn)而生。目前比較著名的有CommonJS和AMD。但早在2008年8月13日,YUI3 Preview Release 1中就已經(jīng)給出了YUI團(tuán)隊(duì)的解決方案,并在2009年9月29日YUI3正式版發(fā)布時(shí)定型。
以下是使用YUI3進(jìn)行模塊化開發(fā)的簡單例子
// 定義模塊 YUI.add('greeting', function (Y) {Y.sayHello = function () {console.log('Hello, world!');}; });// 調(diào)用模塊 YUI().use('greeting', function (Y) {Y.sayHello(); // output 'Hello, world!' });模塊的引入,使得更細(xì)粒度的按功能進(jìn)行代碼組織成為可能,也為方便的進(jìn)行擴(kuò)展和分層提供了基礎(chǔ),自底向上的徹底改變了YUI3。一套完整的模塊機(jī)制,還包括解決關(guān)系依賴、自動(dòng)加載的Loader和提高加載效率的Combo。
面對如此徹底的改變,我們需要解決很多挑戰(zhàn):
- 如何將原來的功能劃分為模塊?
- 如何管理模塊元信息?
- 如何高效的獲取模塊?
劃分模塊
經(jīng)過兩年來不斷的實(shí)踐和總結(jié),我們歸納了如下幾條劃分模塊的原則:
- 抽象與應(yīng)用脫離。更通用的功能放在更低的層級,應(yīng)用層完全面向?qū)嶋H問題,在解決的過程中調(diào)用抽象出來的方法。
- 職責(zé)單一。保持每個(gè)模塊的足夠簡單和專一,方便維護(hù)和可持續(xù)開發(fā)。
- 粒度得當(dāng)。有了Combo,我們可以不必?fù)?dān)心粒度太小,文件過多導(dǎo)致的速度問題。但是,從可維護(hù)的角度來考慮,粒度應(yīng)該適當(dāng)而不宜過小,避免海底撈針的情形出現(xiàn)。
- 海納百川。我們的模塊體系應(yīng)該是開放的,不符合YUI規(guī)范的第三方模塊,可以借鑒整合進(jìn)來,使我們的基礎(chǔ)框架更加完善,更加性感。
按照模塊的層次劃分,美團(tuán)的JS框架可以分為四個(gè)層次:
- 最底層交給強(qiáng)悍的YUI3,為我們提供跨瀏覽器兼容的API和良好的框架設(shè)計(jì)。
- 第二層是我們二次開發(fā)的核心方法、組件(Component)和控件(Widget)。現(xiàn)已獨(dú)立為前端核心庫,為美團(tuán)所有系統(tǒng)提供前端支持。核心庫的種子文件中定義了全局變量M,除了對YUI3進(jìn)行封裝的代碼以外,還包含了對語言層面的擴(kuò)展,以及一些基礎(chǔ)工具類。核心庫有一個(gè)非常重要的組成部分,就是我們功能豐富的控件集合,比如常用的自動(dòng)完成、排序表格、氣泡提示、對話框等基礎(chǔ)控件。除了這些,核心庫還包含了常用的基礎(chǔ)組件、插件(Plugin)、擴(kuò)展(Extension)以及單元測試代碼。
- 第三層包含各個(gè)系統(tǒng)的一些通用模塊。例如www-base模塊包含美團(tuán)主站(www)的消息系統(tǒng)、用戶行為追蹤系統(tǒng)等通用功能。這一層更加接近應(yīng)用。
- 最上面一層,應(yīng)用模塊。這些模塊的方法都是用來解決實(shí)際業(yè)務(wù)問題。例如www-deal用來處理美團(tuán)主站所有deal相關(guān)功能的交互,finance-pay用來處理財(cái)務(wù)系統(tǒng)中付款相關(guān)的交互。一些零碎的應(yīng)用方法我們放在對應(yīng)系統(tǒng)的misc模塊中,避免模塊碎片化。
這套框架仍在不斷演變,以便更好的支撐業(yè)務(wù)需求。其中一個(gè)明顯的方向是,在第二層和第三層之間,出現(xiàn)一個(gè)為了更好整合所有內(nèi)部業(yè)務(wù)系統(tǒng)前端通用資源的中間層。
管理模塊元信息
模塊元信息主要包括模塊名稱、路徑、依賴關(guān)系等內(nèi)容。其中最為重要的是依賴關(guān)系,這決定了有哪些模塊需要加載。為了實(shí)現(xiàn)自動(dòng)加載,需要將所有模塊的元信息提供給YUI的Loader。
最初,為了更快的從YUI2遷移到Y(jié)UI3,模塊元信息放在PHP中進(jìn)行維護(hù)。隨著時(shí)間的推移,漸漸顯示出很多弊端。首先,在定義模塊的js文件中已經(jīng)包含模塊名稱、依賴關(guān)系等信息,和PHP中內(nèi)容重復(fù)。其次,這些元信息最終直接輸出到html中,沒有有效利用緩存。
隨后,我們使用NodeJS開發(fā)了一系列腳本,收集所有模塊元信息,保存為獨(dú)立js文件,并實(shí)現(xiàn)了自動(dòng)化。為了防止出錯(cuò),在Git Hooks和上線腳本中都加入了校驗(yàn)過程。工程師需要做的,只是修改模塊定義中的元信息。
最近一段時(shí)間,我們的精力主要放在兩個(gè)方面:
- 自動(dòng)生成依賴。隨著模塊粒度細(xì)化和模塊數(shù)量的增長,依賴關(guān)系日益復(fù)雜,依靠人工配置經(jīng)常出現(xiàn)過多依賴或過少依賴等問題。我們準(zhǔn)備開發(fā)一套自動(dòng)掃描模塊引用API,并確定依賴關(guān)系的機(jī)制。
- 自動(dòng)打包依賴模塊。如果在代碼發(fā)布時(shí),就已根據(jù)頁面模塊調(diào)用計(jì)算好所有依賴模塊,并進(jìn)行打包,可以避免引用全部模塊元信息、Loader計(jì)算依賴等過程,提高網(wǎng)站性能。
Combo
Combo可以一次請求多個(gè)文件,能夠有效解決多個(gè)模塊加載帶來的性能問題。Yahoo提供了Combo服務(wù),但只能提供YUI3模塊,而且速度在國內(nèi)并不理想。為了提供更好的體驗(yàn),讓用戶訪問速度更快,我們最終考慮搭建自己的Combo服務(wù),并把Combo發(fā)布到CDN上。
以下是一個(gè)Combo請求的例子:
http://c.meituan.net/combo/?f=mt-yui-core.v3.5.1.js;fecore/mt/js/base.js為了節(jié)約時(shí)間,我們最開始采用了開源的minify,經(jīng)過一些修改和配置,就可以在生產(chǎn)和開發(fā)環(huán)境提供Combo服務(wù)。使用一段時(shí)間后,發(fā)現(xiàn)minify過于復(fù)雜,以至于添加一些定制功能相當(dāng)困難。我們需要的只是簡單的文件合并功能,在明確需求和開發(fā)量后,著手開發(fā)自己的Combo程序。從最初的僅支持文件合并,后來陸續(xù)添加了服務(wù)器/瀏覽器端緩存、文件集別名、調(diào)試模式、CSS圖片相對路徑轉(zhuǎn)URI、錯(cuò)誤日志等特性,全部代碼僅有300多行。經(jīng)過兩年時(shí)間以及每天幾千萬PV的考驗(yàn),服務(wù)一直非常穩(wěn)定。
靈活健壯的組件框架
YUI3之所以成為純粹的框架,真正的原因在于提供了一套靈活、健壯的組件框架。借助這套框架,可以輕松的將業(yè)務(wù)場景進(jìn)行解耦、分層,并持續(xù)的進(jìn)行改進(jìn)。通過不斷的實(shí)踐,我們越發(fā)認(rèn)為這是YUI3的精髓所在。
從YUI3定義的開發(fā)范式和源代碼中可以看出,YUI團(tuán)隊(duì)非常重視AOP(Aspect Oriented Programming)和OOP(Object Oriented Programming),這一點(diǎn)可以在接下來的介紹中有所體會(huì)。
EventTarget、Attribute和Base
在介紹組件框架之前,有必要首先了解下EventTarget。YUI3創(chuàng)建了一套類似DOM事件的自定義事件體系,支持冒泡傳播、默認(rèn)行為等功能。EventTarget提供了操作自定義事件的接口,可以讓任意一個(gè)對象擁有定義、監(jiān)聽、觸發(fā)、注銷自定義事件的功能。YUI組件框架中的所有類,以及在此框架之上開發(fā)的所有組件,都繼承了EventTarget。
Attribute是組件框架中最底層的類,實(shí)現(xiàn)了數(shù)據(jù)和邏輯的完美解耦。為什么說是完美呢?存儲(chǔ)在attribute(Attribute提供的數(shù)據(jù)存取接口)中的數(shù)據(jù)發(fā)生變化時(shí),會(huì)觸發(fā)相應(yīng)的事件,為相關(guān)的邏輯處理提供了便捷的接口。從下面這個(gè)簡單的例子可以感受到這一點(diǎn):
// 在name屬性變化時(shí),觸發(fā)nameChange事件 this.on('nameChange', function (e) {console.log(e.newVal); });// 修改name屬性 this.set('name', 'meituan'); // output 'meituan'實(shí)踐中發(fā)現(xiàn),妥善處理屬性的分類非常重要。供實(shí)例進(jìn)行操作的屬性適合作為attribute,例如表單驗(yàn)證組件FormChecker的fields屬性,方便應(yīng)用層進(jìn)行表單項(xiàng)的增刪改。在類方法內(nèi)部使用的一些屬性可以作為私有屬性,例如計(jì)時(shí)器、監(jiān)聽器句柄。供所有類的實(shí)例使用的一些常量適合作為類的靜態(tài)屬性,例如一些模板、樣式類。
Base是組件框架的核心類。它模擬了C++、Java等語言的經(jīng)典繼承方式和生命周期管理,借助Attribute來實(shí)現(xiàn)數(shù)據(jù)與邏輯的分離,并提供擴(kuò)展、插件支持,從而獲得了良好的擴(kuò)展性以及強(qiáng)大的可持續(xù)開發(fā)能力。YUI團(tuán)隊(duì)通過多年來對業(yè)務(wù)實(shí)踐的抽象,最終演化而成一種開發(fā)范式,這,就是一切組件的基石——Base,實(shí)至名歸。
依照這種范式,我們開發(fā)了一系列組件,例如之前提到的FormChecker,以及延遲加載器LazyLoader、地圖的封裝Map等。最顯著的體會(huì)是,開發(fā)思路更為清晰,代碼結(jié)構(gòu)更有條理,維護(hù)變得簡單輕松。
// 構(gòu)造方法 FormChecker.prototype.initializer = function () {var form = this.get('form');this._handle = form.on('submit', function (e) {// check fields}); }; // 析構(gòu)方法 FormChecker.prototype.destructor = function () {this._handle.detach(); };// 創(chuàng)建實(shí)例時(shí),自動(dòng)執(zhí)行構(gòu)造方法 var checker = new FormChecker({ form: Y.one('#buy-form') }); // 銷毀實(shí)例時(shí),自動(dòng)執(zhí)行析構(gòu)方法 checker.destroy();Extension和Plugin
Extension(擴(kuò)展)是為了解決多重繼承,以一種類似組合的方式在類上添加功能的模式,它本身不能創(chuàng)建實(shí)例。這種設(shè)計(jì)非常像Ruby等語言中的Mixin。Plugin(插件)的作用是在對象上添加一些功能,這些功能也可以很方便的移除。
它們有什么區(qū)別呢?簡單來說,Extension是在類上加一些功能,所有類的實(shí)例都擁有這些功能。Plugin只是在某些類的實(shí)例中添加功能。舉兩個(gè)典型的例子:一些節(jié)點(diǎn)需要使用動(dòng)畫效果,這個(gè)功能適合作為Plugin。氣泡提示控件需要支持多種對齊方式,所有實(shí)例都需要此功能,因此使用YUI3的WidgetPositionAlign擴(kuò)展。
// 傳統(tǒng)的函數(shù)方式實(shí)現(xiàn)動(dòng)畫 Effect.fadeIn(nodeTip);// 插件方式實(shí)現(xiàn)動(dòng)畫 nodeTip.plug(NodeEffect); nodeTip.effect.fadeIn();Extension和Plugin很好的解決了我們遇到的諸多功能重用問題。我們開發(fā)了提供全屏功能的WidgetFullScreen、自動(dòng)對齊對話框的DialogAutoAlign等擴(kuò)展,以及進(jìn)行異步查詢的AsyncSearch、提供動(dòng)畫效果的NodeEffect等插件。將這些偏重OOP的編程思想應(yīng)用在前端開發(fā)中,比較深刻的體會(huì)是:有更多的概念清晰、定位明確的開發(fā)模式可以選擇。
Widget體系
Widget(控件)建立在Base之上,主要增加了UI層面的功能,例如renderUI、bindUI、syncUI等生命周期方法,HTML_PARSER等漸進(jìn)增強(qiáng)功能,以及樣式類、HTML結(jié)構(gòu)和DOM事件的統(tǒng)一管理。Widget提供了控件開發(fā)的通用范式。
由于前端資源相對緊張,我們傾向于大量使用控件,尤其在業(yè)務(wù)系統(tǒng)這樣更注重功能的場景。主要出于兩點(diǎn)考慮:
- 減少不必要的重復(fù)勞動(dòng),提高產(chǎn)出。通過將交互、業(yè)務(wù)邏輯合理抽象,一次解決一類問題,One Shot One Kill。
- 節(jié)約前端工程師資源。通過自動(dòng)加載和初始化控件、封裝簡單易用的后端方法、制作Demo和使用手冊等措施,降低使用門檻,后端工程師只需要知道參數(shù)的數(shù)據(jù)結(jié)構(gòu)就可以輕松調(diào)用,提高了開發(fā)效率。
以下是一個(gè)自動(dòng)加載控件的例子
// 頁面初始化時(shí),會(huì)掃描所有帶有data-widget屬性的節(jié)點(diǎn),自動(dòng)加載對應(yīng)控件,并根據(jù)data-params數(shù)據(jù)進(jìn)行初始化 <a href="…" data-widget="bubbleTip" data-params='{ "tip": "全新改版,支持隨時(shí)退款" }'>下載手機(jī)版</a>目前,我們已經(jīng)構(gòu)建了一個(gè)包含近30個(gè)控件的Widget體系,為所有系統(tǒng)提供豐富、便捷、集成的解決方案。
行進(jìn)中開火
在整個(gè)YUI3的實(shí)踐中,我們犯過很多錯(cuò)誤,例如全局只有一個(gè)YUI實(shí)例、Combo的CSS圖片依賴等等,但這些并沒有成為放棄的理由。從今天回過頭來看,YUI3帶給我們團(tuán)隊(duì)的,不只是更高的開發(fā)效率、更好的可持續(xù)開發(fā)能力,還有它本身的設(shè)計(jì)思路、源碼書寫、輔助工具等諸多方面潛移默化的影響。這些回報(bào)的價(jià)值,比起較高的使用門檻、犯過的一些錯(cuò)誤,要貴重百倍。
指導(dǎo)這一切的,是我們始終堅(jiān)持的 “行進(jìn)中開火”。在互聯(lián)網(wǎng)這個(gè)高速發(fā)展的行業(yè)里,對于我們這種小規(guī)模的創(chuàng)業(yè)團(tuán)隊(duì),一天不前進(jìn),就意味落后。做事不應(yīng)該準(zhǔn)備太多,一定要先做起來,然后發(fā)現(xiàn)不足并不斷改進(jìn),寧可十年不將軍,不可一日不拱卒。每天都做得更好一點(diǎn),日積月累,我們才會(huì)在激烈的競爭中占據(jù)越來越大的優(yōu)勢。
YUI3并非完美,存在著學(xué)習(xí)成本高、對社區(qū)不夠開放等問題。我們所做的更遠(yuǎn)非完美,但經(jīng)過不斷的嘗試和經(jīng)驗(yàn)的積累,已經(jīng)漸漸摸索出一條明確的路線,并會(huì)堅(jiān)持不懈的繼續(xù)走下去。
總結(jié)
以上是生活随笔為你收集整理的YUI3在美团的实践的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2019最新拼多多Java面试题:幻影读
- 下一篇: Spring Boot自动化配置的利弊及