天天看點

面對前端六年曆史代碼,如何接入并應用ES6解放開發效率

很榮幸有機會和大家分享自己在前端工作中的一些經驗。更高興能邀請我的同僚顔海鏡同我一起做這件事情。其實經驗說不上,隻是希望能更多的和大家一起交流、學習。

為什麼要講“面對前端六年曆史代碼,如何接入并應用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解放開發效率

是以,不管是因為大勢所趨還是從自身收益出發,我們決定了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官網上可以找到關于子產品化插件的内容:

面對前端六年曆史代碼,如何接入并應用ES6解放開發效率

其中有一個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解放開發效率
面對前端六年曆史代碼,如何接入并應用ES6解放開發效率

這樣就出現頁面中多處“收藏”元件之間通信問題,點選頁面頂部收藏按鈕成功收藏之後,頁面底部的收藏按鈕狀态也需要變化,進行同步。

其實實作這個功能很簡單,但是曆史代碼實作方式較為落後,耦合嚴重。良好的設計和肆意而為的實作差别是巨大的。

在利用es6設計之後,我們的所有元件(包括收藏元件)都會繼承uibase:

而uibase本身會産生一個全局唯一的uuid,這樣使得所有元件都有一個唯一的id辨別。同時,uibase又繼承“eventemitter”這個pub/sub模式元件:

是以,所有的元件也同樣擁有了pub/sub模式,既事件釋出訂閱功能。這就相對完美的解決了元件之間的通信問題。達到了“高内聚、低耦合”的效果。

具體來說,我們的任何元件,當然包括收藏按鈕在發起收藏行為時:

同時,其他的收藏元件:

具體的實作結構如圖:

面對前端六年曆史代碼,如何接入并應用ES6解放開發效率

這樣的元件行為在一些先進的mvvm、mvc等架構中可以良好的實作。比如優秀的react架構中,我們可以對元件的state設計并切換。但是,我們的技術棧還停留在傳統的操作dom中,jquery類似類庫可以滿足我們的業務需求。我認為,所有抛離業務場景和需求的的談新架構,也是一種“耍流氓”。

不可否認,es6的接入并不是百利而無一害的,我們要正确客觀地看待它。伴随着開發效率提升的同時,它還帶來了以下困擾:

額外的編譯流程。

編譯代碼排錯追錯成本。

babel版本更新是個負擔。

api轉換還需shim.

潛在的bug.

很多特性面向node.js,浏覽器端并不實用。

學習成本。

然而,未來已來,接下來我們又該做什麼呢?

更大範圍的重構。

緊盯es6實作和es next發展。

同時,需要指出的是es6的先進性,還展現在和react架構的配合上。去年,我們團隊也接入了react開發棧,es6+react讓我們更加面向未來。

最後,呼應一下本文開篇,談一下想法和總結。每一名前端開發者可能都會感覺到處在前端發展的曆史時刻。面對未來,我們也許正在感受着葡萄牙詩人安德拉德的詩句:

我同樣不知道什麼是海, 赤腳站在沙灘上, 急切地等待着黎明的到來。

es6注定載入開發史冊,最後也許也難逃被替代的命運,完成承前啟後的使命。同樣是葡萄牙人的詩句:

大陸,在這裡是盡頭;大海,在這裡才開頭(陸止于此,海始于斯)。

在技術的海洋裡,這一站,既是一種結束,更是一個開始。

繼續閱讀