很榮幸有機會和大家分享自己在前端工作中的一些經驗。更高興能邀請我的同僚顔海鏡同我一起做這件事情。其實經驗說不上,隻是希望能更多的和大家一起交流、學習。
為什麼要講“面對前端六年曆史代碼,如何接入并應用es6解放開發效率”這個主題呢?其實相信很多人認為es6已經不再新鮮。在前端疊代迅速的今天,會不會有些“老生常談”?我了解并不是這樣的,因為很多人其實對es6的了解主要集中在新特性、語言用法等層面上。這些内容是大部分學習者都能通過共享得到的。但是,對于es6往往缺少了在實際大型工程中的接入和應用。尤其維護開發pv億級以上的産品,并不是每個人都有機會的。
是以,相比于es6語言本身,我更希望能介紹一些工程上、設計上的想法,以及接入es6過程中不為人知,但又至關重要的的“細枝末節”上。同時,希望想了解bat公司技術項目流程從立項到落地的讀者、參與者能有所收獲。
全篇文章分為五個部分:
對前端發展的态度和看法。
大型網際網路公司對新技術的前期調研和評估。
我們面臨的曆史背景。
當我們說調研時到底在說什麼。
為什麼非要折騰es6。
正确解鎖es6開發姿勢。
使用babel進行es6編譯。
傳說中的“最佳實踐”。
一個設計執行個體。
es6帶來的困擾和展望。
篇幅并不短,其中還有一些es6“黑魔法”和babel編譯分析,以及相容性處理等内容。
借用查爾斯·狄更斯在《雙城記》中的不朽開篇來形容如今的前端開發,我覺得再合适不過了:
這是最好的時代,這是最壞的時代,這是智慧的時代,這是愚蠢的時代;這是信仰的時期,這是懷疑的時期;這是光明的季節,這是黑暗的季節;這是希望之春,這是失望之冬;人們面前有着各樣事物,人們面前一無所有;人們正在直登天堂;人們正在直下地獄。
沒錯,我們脫離了之前“刀耕火種”的“腳本玩具”時期。伴随着nodejs的強勢崛起,社群交流的如火如荼,子產品化開發的如虎添翼,html5的攻城掠地,徹底迎來了 前端“工業革命”的爆發。
同時,這也意味着大量的技術更疊。即便沒有“南朝四百八十寺,多少樓台煙雨中”那般誇張,也足以讓各階段前端開發者疲于奔命,應接不暇。舉個例子,想想我們也許剛熟悉了commonjs,又要去了解amd、cmd,稍不留神,就在2017年5月這個初夏:es6 module要開始在浏覽器端實作了!
好吧,也正好以此“es6發展的标志性事件”來為這次分享拉開序幕,我們今天就要談談:es6在大型項目中的接入和發展的方方面面。所謂“沉舟側畔千帆過,病樹前頭萬木春”,古詩中以“沉舟”、“病樹”比喻紛擾和困惑,但卻并不尤怨,反而表現的是一種對世事變遷和潮起潮落的豁達開朗。同樣我們認為es6代表了未來,對未來理應擁抱。
全文看下來,也許你會了解“所有的發展都是站在曆史的基礎上”,停止不前的“沉舟”也有指引千帆航向的意義。“合抱之木,生于毫末;九層之台,起于壘土”。技術更疊中,深厚的基礎是多麼重要。
這次分享,我們不會去把時間浪費在es6新特性的講解和文法細節層面上,這些内容畢竟都可以輕松且“免費”地找到。比如阮一峰老師的書中講解就很透徹了。
我們會把重點放在es6工程接入和開發維護上,背靠大流量的産品,這些不是所有人都能接觸到的。
先從背景說起,我們負責的項目是百度知識搜尋部某明星産品,該産品代碼曆史在6年以上。在很多大型網際網路公司裡,這種“曆史負擔”其實屢見不鮮。也就不奇怪為什麼知乎上會有人質疑:“qq空間的前端技術水準如何?”,“為什麼很牛的網際網路公司代碼卻不能看?”等等。
在我們這邊,曆史問題主要集中在以下幾點:
使用古老tangram類庫,開發體驗不友好。
建構工具以fis為主,但是版本不統一。
子產品設計不合理,内外耦合嚴重。
js,需要相容ie6+。
這些問題都會對es6接入,造成一些潛在障礙。這就需要對新技術進行更加合理的評估和調研。
也許會有一些讀者認為“這有什麼好調研評估的,不就是新的特性學習嗎?”,其實在大型工程中這樣的想法是片面的。
首先,對于新特性的熟悉,當然是最基本的。
此外,對于保證pv過億的大型線上産品,就要求對es6的方方面面面要足夠了解。會一些let,const,箭頭函數,子產品化等語言層面知識還是不夠的。
這就說明,在新技術前期調研工作當中,新特性、新文法的學習僅僅是很小的一方面。同樣重要的是執行環境保障、生産配置、線下開發流程、線上bug跟蹤等各環節内容。
比如,這個項目的前期調研就包括但不限于:
如何相容舊版本浏覽器(ie6+)?
編譯器/轉換器是否真能擺平一切,應用是否完全可靠?
編譯器/轉換器面臨版本更新怎麼辦?
編譯器/轉換器的接入對于現有的代碼是否有影響?
編譯器/轉換器的編譯結果對于現有的代碼是否有影響,能否完全相容?
引入es6後開發效率是否真的可以提升?
就算開發效率确實提升了,上線的代碼量是不是更大了?對于産品性能是否有影響?
所有可能産生的負面影響如何復原?誰來擔責?
es6現在處于什麼階段,是否會被廢除,就像第四版本一樣?
對于ecmascript語言标準的提案分為哪幾個階段?
等等一切可能影響産品穩定或存在潛在bug的問題……
這個問題其實就是“如何評估es6?”,“es6的接入能帶來哪些收益”。或者更直白一些:“你靠什麼來說服技術經理,配置設定給你時間、人力去搞es6?”畢竟大公司裡的資源申(争)請(奪),都要拿收益來說話。
這就需要以自己所負責的業務為背景,在充分調研的基礎上做出合理評估。
最終我們認為從以下幾個角度來看,es6的推廣勢在必行:
解放開發效率。
新特性的合理使用,優雅而簡潔。
減少第三方庫的依賴。
可維護性提升,代碼量減少。
面向未來。
向标準靠攏。
官方支援。
“遲早要學”。
其他方面。
提升技術先進性。
促進技術交流,提高技術氛圍。
“程式設計激情”。
整合部分曆史代碼的好機會。
面試中的加分項。
以上是出于我們自身産品開發的角度。同時,整個前端在es6發展環境和普及率上,我們參考了ponyfoo.com在2015年底做的一個知名調查:javascript developer survey results,該調查以5千多個前端開發者為背景,得出以下結論:
es6普及率?

es6是否是一個很重大的版本進步?
你都使用哪些es6新特性?
是以,不管是因為大勢所趨還是從自身收益出發,我們決定了es6接入作為該年度最大的技術項目之一。
目前各大浏覽器和開發環境對es6的支援情況參差不齊,我們的産品對浏覽器相容性要求又比較高。是以,當然不能荒謬地“裸寫”es6代碼,釋出上線。是以,在實際項目開發中,需要降級為es5文法以相容各平台。
幸好有幾款工具可以将es6文法轉換成es5,讓我們在使用es6新特性編寫代碼的同時,不需要考慮具體的相容性情況。比較知名的兩款編譯器為:
babel
traceur
我們選擇了babel 5.x版本,主要是因為以下幾個原因:
babel對es6的支援程度比其它同類更高或相當。
babel擁有完善的文檔和較好體驗的線上編譯環境。
babel使用廣泛,使用者基礎好。
關于第一點原因的主要資料支援可以在bebel官網,我們可以看到不同版本babel對es6跟進和支援的情況;
另外,關于線上編譯平台,可以通路官網:進行體驗,這對于研究babel編譯結果十分友善。
關于babel的接入和使用方法,社群上的資料很多,這裡不再進行科普浪費時間了。以下,從幾個關鍵性的工程問題進行延伸。
配合建構工具
首先,因為我們使用的是百度自己的fis來做前端建構工具,是以隻需要在fis的配置檔案中加入依賴,并安裝插件就可直接使用。這一切,就像社群上使用更多的webpack一樣。
babel-polyfill
同樣需要說明的是,babel預設隻轉換新的javascript文法(syntax),而不轉換新的api。
比如:babel可以編譯let, const等特性,但是諸如iterator、generator、reflect、promise等全局對象,或者數組執行個體的find這些新的方法并不會得到編譯。如果想讓這個方法運作,必須使用babel-polyfill,同時要保證這個polyfill在你的所有其他腳本之前就要加載執行。同時,因為編譯産出為es5代碼,是以又要處在es5墊片es5-shim,es5-sham之後。
實際情況中,我們放棄了使用babel-polyfill,這是出于減少js引用的考慮。我們頁面已經加載很多js了,并且babel-polyfill由于其特殊性(搶先執行),難以和其他業務腳本打包。再者,我們認為es6新增的這些方法的必要性并不絕對。就像上圖統計的那樣,es6新特性被廣泛使用的大多是let, const, 解構,箭頭函數等,這些使用預設babel編譯就已經可以達到要求了。
當然,promise這個使用廣泛的特性我們專門引入了單獨的polyfill來處理。這樣的定制化完全可以滿足需求。
babel-runtime
babel-runtime是為了減少重複代碼而生的。babel編譯生成的代碼,可能會用到一些_extend(),classcallcheck()之類的工具函數(後文在分析編譯結果部分會有介紹)。預設情況下,這些工具函數的代碼會被引入在編譯後的檔案中。如果存在多個檔案,那每個檔案都有可能含有一份重複工具函數的代碼。
這種備援一定是我們不能忍的。
babel-runtime插件能夠将這些工具函數的代碼轉換成require語句,指向為對babel-runtime的引用,如 :
這樣,classcallcheck這個工具函數的代碼就不需要在每個檔案中都存在了。當然,最終你需要利用webpack之類的打包工具,将runtime代碼打包到目标檔案中。
但是要注意,這是babel 6版本才引入的,對我們來說,這就面臨一個關于“babel版本更新部署”的問題。
關于這個插件的更多介紹,同樣可以在官方網站中找到。
babel的部署和更新
在真正部署babel的前前後後,我和我的同僚針對每一個es6特性的編譯穩定性都進行了嚴密的測試。測試包括了驗證黑盒輸出情況和不同浏覽器的支援情況,以確定上線後的萬無一失。
另一方面,我們在使用的babel版本就如上所說,為5.x,當然babel在社群的蓬勃發展和自身定位的調整,使得自身版本更新換代也非常頻繁。同時,随着越來越多的庫更新至babel6,将我們的項目更新至babel6似乎也有必要。這樣的更新工作想想也确實頭疼,尤其是要保證線上代碼的穩定運作。
截止目前為止,我們還未對babel進行更新,因為這個需求還并不迫在眉睫。但是,着眼于未來還是很有必要的。我們及時關注了babel 6.x版本帶來的新變化。這方面對于大家的建議其實隻有一個,就是緊盯官網+快速調研。點選這裡會把大家連結到babel官方部落格,裡面同步了每一次更新的細枝末節,内容非常詳盡。
同時,你可能會問,那我們就保持初始版本不去更新,豈不是一勞永逸了嗎?
當然不是這樣,我認為,每一個版本的疊代和演進自然有其原因。如果一直固守成規,不管是在代碼組織上和工程化上都會吃虧。除了剛才提到的babel-runtime插件,新版本的babel(5.x-6.x)收益還展現在:
性能提升:據說compile速度提升20%。
可配置的插件:更強的靈活性,以及更簡單的插件api.
更簡潔的配置。
選擇編譯和其他
在進行es6編譯的同時,對于大量的曆史代碼檔案,我們不會進行es6的翻新重寫。這些曆史代碼是以就不需要使用babel進行編譯。為此,我們使用了檔案字尾名來進行區分,并在建構工具的配置檔案中進行正則比對,達到選擇性編譯的效果。最終的規範是,es6代碼統一以.es為字尾名。
最後,babel社群的蓬勃發展,導緻“你以為的babel”其實已經不再是那個babel了;同時,babel知識的廣泛性遠遠超乎了很多人的想象,比如babel編譯的loose模式、normal模式;比如babel依賴的引擎babylon;比如babylon fork的acorn;比如babel将源碼轉換ast的了解等等。很多東西其實我研究的還隻是皮毛,但是不到浏覽器廣泛支援es6的一天,不到擺脫相容性需求的一天,恐怕我們是脫離不開babel了。
在es6大量的新特性中,我們推薦并有廣泛應用的包括但不限于:
預設參數
模版表達式
多行字元串
解構指派
改進的對象表達式
箭頭函數 =>
promise
塊級作用域的let和const
類
子產品化
當然還有很多優秀的新特性,但是在應用中頻率相對較少,不再一一列出。
我認為,一切所謂的最佳實踐都要依賴基礎。在抛出幾個“奇技異巧“之前,我想從一個簡單的例子說起。
const例子:
舉一個簡單的例子(出自阮一峰es6一書),可能大家都了解const聲明一個隻讀的常量。一旦聲明,常量的值就不能改變。
為此,我們可以延伸出:const聲明的變量不得改變值,這意味着,const一旦聲明變量,就必須立即初始化,不能留到以後指派。
同時,我們還要注意:const的作用域與let指令相同:隻在聲明所在的塊級作用域内有效。
因而,它也不存在常量提升的概念。
但是,還需要了解的是:
const實際上保證的,并不是變量的值不得改動,而是變量指向的那個記憶體位址不得改動。
對于簡單類型的資料(數值、字元串、布爾值),值就儲存在變量指向的那個記憶體位址,是以等同于常量。
但對于複合類型的資料(主要是對象和數組),變量指向的記憶體位址,儲存的隻是一個指針,const隻能保證這個指針是固定的,至于它指向的資料結構是不是可變的,就完全不能控制了。
上面代碼中,常量foo儲存的是一個位址,這個位址指向一個對象。不可變的隻是這個位址,即不能把foo指向另一個位址,但對象本身是可變的,是以依然可以為其添加新屬性。
是以,僅僅就是一個聲明常量的const,裡邊牽扯出的基礎内容卻很多。這就需要在掌握es6基本用法的同時,需要有更強大的基礎概念才能進一步提升了解。
這裡,給大家留一個思考題目: 如何真的講一個對象當機?
es6黑魔法:
其實我想大家對es6特性越來越熟悉,以及社群的大力宣傳,一些es6黑魔法已經“非常平常”了。
比如,擴充運算符結合解構指派,除了“你想象的那種用法”外,它還可以優雅完成:
合并數組
複制數組
把僞數組轉為數組
交換兩個變量值
等等。。。
babel到底編譯成了什麼?
這是一個很關鍵的問題。也是正确使用es6的高難度姿勢。
因為我們所有的es6代碼都依賴babel編譯,是以如果你不去了解它的編譯産出,那麼最後上線的代碼都是“心裡沒底”的。
舉例來說,我剛才提到的const,在經過babel編譯後其實一律換成var;
可能你緊接着會問:“那如何保證不變性呢?”,原因就在于如果你在源碼中第二次修改const常量的值,babel編譯會直接報錯。
這是一個比較輕量甚至取巧的例子。接下來, 我們再來看看class+extends的編譯情況。
javascript實作oop其實一直以來都是熱門話題,這些争議性的内容我們不去讨論。先來看看babel的實作過程。
會被編譯為:
我們看到,還是用了構造函數來完成。同時,上文提到過的_classcallcheck也出現了,他作為工具函數,保障class調用的正确性:
關于繼承:
編譯結果:
上面_inherits方法的本質其實就是讓student子類繼承person父類原型鍊上的方法。它實作原理可以歸結為一句話:
是以說到底,還是“構造函數+原型原型鍊”内容,并且輔助object.create等es5功能實作。
我建議大家對于編譯源碼嘗試去進行了解,對于自己的基礎也是一種提高。
了解了這些,對于es6的接入是很有幫助的。試想一下,我們在es6環境下聲明的類,如何在曆史代碼中(es5環境下)實作繼承呢?
通過對babel編譯結果的研究,我也實作了一個工具函數,用來完成這兩種開發環境下類的銜接和過渡。具體代碼實作難度不大,可以簡要參考:
如果您有興趣,可以看我的系列文章:揭秘babel的魔法之class魔法處理。
打通兩種開發環境的任督二脈
這裡還是聊聊上面展示inherits工具方法,其實這屬于“打通es5和es6環境”。同樣還是在es6環境下定義的person class,es6環境代碼:
在es5環境中就可以直接進行引用并繼承person類,es5環境代碼:
這當然是極其有必要的。想象一下6年代碼曆史,es5環境的代碼量是多麼的龐大,這樣我們在維護過程中,便可直接獲利于es6的特性。
同樣,對于子產品化上,我們也存在兩種環境共生的問題:之前的代碼我們遵循了commonjs規範,并通過打包工具(fis部分功能),來保證浏覽器端的支援。接入es6之後,自然也就有了es6子產品化的寫法。
那麼js檔案内如何相容這兩種子產品化寫法的表達方式呢?
也很簡單,同樣依賴于babel的實作。我們在babel官網上可以找到關于子產品化插件的内容:
其中有一個es2015_modules_commonjs,就是将es6 modules編譯轉換成commonjs形式的。我們當然選用這種編譯方式。
對es next支援
截止目前,es7也已經取得了重大進展。很多最新一代的es特性已經被廣大開發者熟知并應用。那麼在我們的環境中,對于es next的支援也自然要跟進。這又回到了babel的話題,我們當然還是離不開這個神器。
同時,你首先要知道,es7不同階段文法提案包括:
stage 0:strawman: just an idea, possible babel plugin.
stage 1:proposal: this is worth working on.
stage 2:draft: initial spec.
stage 3:candidate: complete spec and initial browser implementations.
stage 4:finished: will be added to the next yearly release.
對應的,官方提供以下的規則集來對不同階段的特性進行編譯,你可以根據需要安裝:
$ npm install —save-dev babel-preset-stage-0
$ npm install —save-dev babel-preset-stage-1
$ npm install —save-dev babel-preset-stage-2
$ npm install —save-dev babel-preset-stage-3
需要注意的是,這些工作應當在初期調研設計時,就要有規劃和方案。而不是,今天頭腦一熱,想應用async/await es7新特性,再去花費時間進行了解。因為,在公司内成熟的開發體系中,嚴謹的排期需求與個人私下的學習了解完全是兩碼事。
這些年踩過的相容性的坑
我們代碼能夠相容到ie6+,接入es6之後,對于相容性的保證是個挑戰。在實際情況中,我們也踩過這方面的坑。
babel對于es6的編譯是在es5之上的,那麼想要相容ie6,自然編譯産出的es5代碼是無法滿足要求的。為此,解決方式隻有提供es5的polyfill,并保證在所有其他腳步加載之前執行。
我們采用了最出名的es5-shim+es5-sham來進行es5代碼的“降級”。期間各種ie版本相容性的測試,那可謂是“一把鼻涕一把淚”。
同時,這裡所指的相容性也不僅僅是浏覽器相容性。也要考慮到引用社群上第三方元件庫、類庫的問題,如果這些源代碼是基于es6的,那就要慎重考慮是否符合我們使用的babel版本,我們是否保證并相容了babel插件進行編譯等相關性問題。這當然也是不小的工作量。
這個執行個體充分反映了es6 class帶來的便捷之處。
我們産品當中,一個頁面可能存在多處“收藏”元件:點選按鈕,對頁面進行收藏,成功收藏之後,按鈕的狀态會變為“已收藏”,再點選不會有響應。
這樣就出現頁面中多處“收藏”元件之間通信問題,點選頁面頂部收藏按鈕成功收藏之後,頁面底部的收藏按鈕狀态也需要變化,進行同步。
其實實作這個功能很簡單,但是曆史代碼實作方式較為落後,耦合嚴重。良好的設計和肆意而為的實作差别是巨大的。
在利用es6設計之後,我們的所有元件(包括收藏元件)都會繼承uibase:
而uibase本身會産生一個全局唯一的uuid,這樣使得所有元件都有一個唯一的id辨別。同時,uibase又繼承“eventemitter”這個pub/sub模式元件:
是以,所有的元件也同樣擁有了pub/sub模式,既事件釋出訂閱功能。這就相對完美的解決了元件之間的通信問題。達到了“高内聚、低耦合”的效果。
具體來說,我們的任何元件,當然包括收藏按鈕在發起收藏行為時:
同時,其他的收藏元件:
具體的實作結構如圖:
這樣的元件行為在一些先進的mvvm、mvc等架構中可以良好的實作。比如優秀的react架構中,我們可以對元件的state設計并切換。但是,我們的技術棧還停留在傳統的操作dom中,jquery類似類庫可以滿足我們的業務需求。我認為,所有抛離業務場景和需求的的談新架構,也是一種“耍流氓”。
不可否認,es6的接入并不是百利而無一害的,我們要正确客觀地看待它。伴随着開發效率提升的同時,它還帶來了以下困擾:
額外的編譯流程。
編譯代碼排錯追錯成本。
babel版本更新是個負擔。
api轉換還需shim.
潛在的bug.
很多特性面向node.js,浏覽器端并不實用。
學習成本。
然而,未來已來,接下來我們又該做什麼呢?
更大範圍的重構。
緊盯es6實作和es next發展。
同時,需要指出的是es6的先進性,還展現在和react架構的配合上。去年,我們團隊也接入了react開發棧,es6+react讓我們更加面向未來。
最後,呼應一下本文開篇,談一下想法和總結。每一名前端開發者可能都會感覺到處在前端發展的曆史時刻。面對未來,我們也許正在感受着葡萄牙詩人安德拉德的詩句:
我同樣不知道什麼是海, 赤腳站在沙灘上, 急切地等待着黎明的到來。
es6注定載入開發史冊,最後也許也難逃被替代的命運,完成承前啟後的使命。同樣是葡萄牙人的詩句:
大陸,在這裡是盡頭;大海,在這裡才開頭(陸止于此,海始于斯)。
在技術的海洋裡,這一站,既是一種結束,更是一個開始。