這些坑,我做前端構(gòu)建時(shí)都遇見了
來源:原創(chuàng) 時(shí)間:2017-11-28 瀏覽:0 次
很多做前端的朋友經(jīng)常問我:為什么我打包總是很慢?為什么我內(nèi)存占用這么大,該怎么優(yōu)化?去哪兒網(wǎng)是怎么做前端構(gòu)建的?
這里,我和大家分享一下在去哪兒網(wǎng)是如何進(jìn)行前端工程化相關(guān)工作的。

在企業(yè)環(huán)境下的大型工程構(gòu)建會(huì)遇到種種問題,我們也是通過不斷地摸索才逐漸總結(jié)出一套解決方案,現(xiàn)在把我們所遇到的一些坑和如何處理這些問題拿出來與大家交流。
一、構(gòu)建工具演化篇
作為公司內(nèi)部的構(gòu)建工具,最重要的一點(diǎn)是穩(wěn)定性,因?yàn)榧偃缢隽藛栴},不僅會(huì)導(dǎo)致項(xiàng)目發(fā)布失敗,還有可能會(huì)引起線上故障。
然而在保持穩(wěn)定的同時(shí),它也一定是在不斷發(fā)展的。無論是開發(fā)語言的變化,還是前端框架的迭代更新,都會(huì)帶來新的需求。
為了滿足這些需求,在設(shè)計(jì)時(shí)就一定要有一定的預(yù)見性,知道哪部分未來可能需要擴(kuò)展以適應(yīng)新的場(chǎng)景。
如何同時(shí)擁有穩(wěn)定性和擴(kuò)展性是公司級(jí)別的構(gòu)建工具面臨的最大挑戰(zhàn)。
階段一:完全定制化構(gòu)建
在 2014 年我最開始加入去哪兒網(wǎng)的時(shí)候,公司中有一個(gè)統(tǒng)一的開發(fā)和構(gòu)建工具——FEKit。它當(dāng)時(shí)主要解決幾個(gè)問題:
模塊依賴。類似于 Browserify,可以遵循 CommonJS 標(biāo)準(zhǔn)打包模塊。
包管理器。那時(shí)候 npm 還沒有完全流行于前端,F(xiàn)EKit 自己實(shí)現(xiàn)了一套類似于 npm 的包管理器,并搭建了模塊倉庫,開發(fā)者可以從倉庫上面安裝包也可以發(fā)布自己的包。
目錄約定。FEKit 對(duì)源碼目錄、目標(biāo)目錄以及打包生成資源的命名格式有自己的規(guī)定,它的好處是可以讓開發(fā)和構(gòu)建發(fā)布的流程相對(duì)統(tǒng)一,減少學(xué)習(xí)成本。
除上面之外,F(xiàn)EKit 還具備 SASS 編譯、代碼壓縮、版本號(hào)生成等特性,在當(dāng)時(shí)可以說是比較完備的開發(fā)構(gòu)建工具了。
同時(shí)也可以看出它其實(shí)是將一整套構(gòu)建流程封裝好,只要開發(fā)者熟悉了它的配置,在面對(duì)任何一個(gè) FEKit 的項(xiàng)目的時(shí)候都能較快上手。
但是隨著時(shí)間的推移也逐漸暴露出 FEKit 在擴(kuò)展性方面的不足,由于它內(nèi)置的編譯打包流程是規(guī)定死的,因此它很難去適應(yīng)新的需求。
比如,當(dāng) ES2015 出現(xiàn)后,有些同學(xué)提出想把 Babel 放在 FEKit 的構(gòu)建流程里面。
但是在公司的打包發(fā)布平臺(tái)上面 FEKit 只能存在一個(gè)版本,任何改動(dòng)都會(huì)影響到所有項(xiàng)目的發(fā)布,我們很難去冒這樣的風(fēng)險(xiǎn)去改動(dòng)所有業(yè)務(wù)的打包流程,這也阻礙了 FEKit 向前推進(jìn)的腳步。
階段二:放開手腳自定義構(gòu)建
為了實(shí)現(xiàn)各自的需求,不同的業(yè)務(wù)開始嘗試自己搭建一套開發(fā)和發(fā)布流程。Gulp、Grunt、Webpack 全都有,大家各玩各的。
看似都能實(shí)現(xiàn)新的需求了,但從這類孤立搭建的工程中也發(fā)現(xiàn)了幾個(gè)共同的問題:
重復(fù)造輪子。像是配置預(yù)編譯器、生成資源版本號(hào)、Mock 服務(wù)等這些類似的工作經(jīng)常被重頭到尾實(shí)現(xiàn)一遍。
而如果它們由統(tǒng)一的工具來實(shí)現(xiàn)則可以節(jié)省很多開發(fā)成本。
缺少構(gòu)建層面的優(yōu)化。不管是從打包出來的資源體積以及編譯速度上,不少工程都還有很大的提升空間。
我們?cè)囍鴮⑵渲幸粋€(gè)工程的公用庫用 CommonChunks 的方式提取出來,將整體資源體積減小了多一半。
造成這個(gè)問題的原因是很多時(shí)候開發(fā)同學(xué)只是想短平快地完成一些業(yè)務(wù),并不會(huì)過多關(guān)注流程上的優(yōu)化。
溝通和學(xué)習(xí)成本高。大家各玩一套的結(jié)果是跨團(tuán)隊(duì)和跨工程時(shí)會(huì)由于構(gòu)建工具和流程不一樣而帶來額外的成本。
當(dāng)面對(duì)一個(gè)缺少文檔的新工程時(shí),一個(gè)剛接觸的同學(xué)可能會(huì)完全不知所措,而工程之間構(gòu)建工具的不一致也會(huì)導(dǎo)致切換環(huán)境時(shí)要花費(fèi)更多的精力。
階段三:約束與自由的平衡
基于以上幾點(diǎn)問題,我們決定開發(fā)一個(gè)新的構(gòu)建工具,在設(shè)計(jì)時(shí)希望它能解決以下幾個(gè)問題:
提供統(tǒng)一的配置,如資源輸入輸出目錄、版本號(hào)規(guī)則、壓縮插件等,并且允許更改這些配置。
有默認(rèn)的構(gòu)建流程,也允許自定義工作流。比如開發(fā)者可以選擇使用 Babel、TypeScript 來編譯 JavaScript,也可以用 SASS、LESS 來做樣式的預(yù)編譯等等。
提供統(tǒng)一的構(gòu)建優(yōu)化手段和工具服務(wù),像是按需加載、多進(jìn)程打包編譯等這類工作最好內(nèi)置在工具中,業(yè)務(wù)不需要過多地關(guān)心如何優(yōu)化項(xiàng)目。
于是后來我們開發(fā)了 YKit。它最大的特點(diǎn)在于允許開發(fā)者在其基礎(chǔ)上自定義配置,并且可以將一系列構(gòu)建工作流封裝為模塊,然后就可以像搭積木一樣快速地通過這些模塊搭建起一個(gè)環(huán)境。
舉個(gè)例子,當(dāng)你想開發(fā)一個(gè) React 項(xiàng)目的時(shí)候,使用 ykit init react 一行命令就可以將一個(gè)內(nèi)置好各項(xiàng) React 相關(guān)配置的工程初始化好。而當(dāng)你需要使用其它配置的時(shí)候安裝和引入更多的 YKit 插件即可。
比如,希望在項(xiàng)目中使用 TypeScript,則執(zhí)行命令 npm install ykit-config-ts;
想要使用 Mock 數(shù)據(jù)中間件,則執(zhí)行 npm install ykit-config-mock,以此類推。
YKit 其實(shí)是基于 Webpack 的一層封裝,在面向開發(fā)者的時(shí)候它把復(fù)雜的 Webpack 配置封裝在各個(gè) npm 模塊內(nèi)部,對(duì)于開發(fā)者而言則只要關(guān)注功能即可。
同時(shí) YKit 也留了一個(gè)更改配置的接口 —— modifyWebpackConfig,把內(nèi)部的 Webpack 配置對(duì)象暴露出來,開發(fā)者可以基于自身的需求進(jìn)行進(jìn)一步深入定制。
從上面可以看出,YKit 是一個(gè)兼顧統(tǒng)一性與自由度的解決方案。在去哪兒那么多業(yè)務(wù)線,每個(gè)小團(tuán)隊(duì)都有自己習(xí)慣的開發(fā)方式。
YKit 希望做的事情是幫助每個(gè)團(tuán)隊(duì)將它們的構(gòu)建配置封裝起來,通過 npm 模塊的方式來管理,這樣當(dāng)每次新建一個(gè)項(xiàng)目的時(shí)候,直接拿來以前封裝好的配置即可,以最快的速度搭建好一整套開發(fā)和發(fā)布流程。
二、構(gòu)建速度優(yōu)化篇
1. npm 包版本號(hào)固定
相面在講包管理器的時(shí)候時(shí)候已經(jīng)介紹過,使用版本固定文件可以帶來兩個(gè)好處。
避免語義化版本號(hào)( semver )帶來的包版本變化風(fēng)險(xiǎn)。
加快 npm 解析包依賴的速度。
在去哪兒網(wǎng)發(fā)布前端項(xiàng)目時(shí)(其實(shí)也包括 Node.js 項(xiàng)目)會(huì)進(jìn)行版本固定文件是否存在的校驗(yàn),如果沒有則會(huì)直接阻斷發(fā)布。
2. npm 包緩存
在一般企業(yè)環(huán)境中進(jìn)行前端發(fā)布一般有兩種方式。
在開發(fā)者本地進(jìn)行工程構(gòu)建,將打包好的資源( JS、CSS 等)上傳 CDN。
通過特定的服務(wù)器將代碼倉庫克隆下來,進(jìn)行構(gòu)建和發(fā)布。
在去哪兒網(wǎng)我們使用第二種方式。但是有一個(gè)問題,由于是在服務(wù)器上進(jìn)行 npm 模塊安裝和打包,并且當(dāng)時(shí) npm 的緩存機(jī)制效率較低,通常 npm install 的耗時(shí)很長。
針對(duì)這種情況,我們?cè)O(shè)計(jì)了一個(gè) npm 包緩存工具—— ncs( npm-cache-share )。
ncs 不僅可以充分利用本機(jī)的 npm 緩存,還可以和多臺(tái)服務(wù)器間共享緩存。由于發(fā)布的服務(wù)器有多臺(tái),因此每一臺(tái)的安裝結(jié)果都可以緩存下來供其它的服務(wù)器使用。
通過這種緩存機(jī)制可以大大提高安裝 npm 模塊速度,在一個(gè)充分緩存的環(huán)境下,一個(gè)大型工程的安裝過程也只有短短幾秒。
3. 打包編譯優(yōu)化
談到打包和編譯這部分,就又回到我們說的 YKit 上面。在每一個(gè)封裝的配置模塊的里面 YKit 其實(shí)已經(jīng)做了優(yōu)化。
比如說,對(duì)于 React 項(xiàng)目來說,YKit 會(huì)使用 Happypack 來進(jìn)行多進(jìn)程編譯。
在生產(chǎn)環(huán)境下,會(huì)添加 process.env.NODE_ENV= production 的環(huán)境變量來使 React 去掉開發(fā)環(huán)境代碼。
在各類封裝好的構(gòu)建方案中 YKit 也會(huì)提供相應(yīng)的最佳實(shí)踐,這樣業(yè)務(wù)同學(xué)基本不用踩坑,各項(xiàng)優(yōu)化以及各種兼容性的問題基本都在方案內(nèi)部得到實(shí)現(xiàn)和解決,對(duì)于開發(fā)者而言是透明的。
三、代碼質(zhì)量篇
每個(gè)公司幾乎都有自己的一套代碼風(fēng)格或者是編碼約定。在不斷有新人進(jìn)公司的過程中,如何保證風(fēng)格的統(tǒng)一?代碼質(zhì)量檢測(cè)是一個(gè)很好的方法。
1. 代碼質(zhì)量控制的意義
試想一下,假如你剛進(jìn)一個(gè)公司沒多久,提交了自己的第一段代碼。然后你的 Leader 來找你,跟你說你這個(gè)代碼哪里哪里不規(guī)范(其實(shí)也許只是不符合公司的風(fēng)格),會(huì)不會(huì)覺得有點(diǎn)不太舒服?
如果換成是一個(gè)質(zhì)量檢測(cè)工具,在你提交的時(shí)候自動(dòng)攔截下來并指出問題。
你稍作修改之后重新提交并且完美通過,同時(shí) Leader 看到的也是符合他編碼要求的代碼,這樣是不是顯得友好地多?
很多代碼中容易疏忽的錯(cuò)誤都可以靠質(zhì)量控制工具檢測(cè)出來。
比如忘記刪掉了打印調(diào)試日志的代碼、哪個(gè)變量聲明了但是沒有用到、或者說在頁面打了個(gè) Alert 出來。
機(jī)器完全能幫我們查出來并且修改掉,為什么還要去人力測(cè)試和改正呢。
2. Sonar
Sonar 是一個(gè)質(zhì)量檢測(cè)平臺(tái),在去哪兒網(wǎng)我們用它來做持續(xù)集成,控制前后端代碼的質(zhì)量。
每當(dāng)我們提交了一段代碼上去以后,都會(huì)自動(dòng)通過 Sonar 進(jìn)行質(zhì)量檢測(cè),并把檢測(cè)結(jié)果發(fā)回來。
結(jié)果中的信息會(huì)進(jìn)行分級(jí),有一些問題是阻斷性質(zhì)的,意思是必須要進(jìn)行修改,否則無法進(jìn)行發(fā)布。
還有一些則只是警告,告訴開發(fā)者這種編碼風(fēng)格不太好,但并不會(huì)影響發(fā)布流程。
3. ESLint
對(duì)于前端開發(fā)者來說 ESLint 應(yīng)該再熟悉不過了。如果說 Sonar 是公司級(jí)別的質(zhì)量控制工具,那么 ESLint 則是各個(gè)前端團(tuán)隊(duì)自己使用的工具。
在前面我們提到的 YKit 中有一個(gè)專門為去哪兒網(wǎng)設(shè)計(jì)的配置插件,內(nèi)部包含了一些基本的規(guī)則。
但是對(duì)于更詳細(xì)的規(guī)則而言,由于不同的團(tuán)隊(duì)內(nèi)部約束并不一樣,這里留給了團(tuán)隊(duì)自己進(jìn)行配置。
四、發(fā)布平臺(tái)篇
之前在去哪兒網(wǎng)主要使用 Jenkins 作為發(fā)布平臺(tái),它基本能夠滿足所有業(yè)務(wù)的需求。
比如,用它來打包和發(fā)布前端、后端、客戶端應(yīng)用、通過 Sonar 進(jìn)行代碼質(zhì)量檢測(cè)、集成郵件通知等等。
而 Jenkins 也存在一些不足之處,比如界面比較過時(shí)、用戶體驗(yàn)差、在一些多應(yīng)用聯(lián)合發(fā)布的場(chǎng)景下速度較慢。
因此后來我們自己搭了一個(gè)內(nèi)部的發(fā)布平臺(tái) Portal,它有如下幾項(xiàng)優(yōu)勢(shì):
更現(xiàn)代的設(shè)計(jì)以及更友好的用戶體驗(yàn),特別是進(jìn)度和日志得到了更清晰地呈現(xiàn)。
錯(cuò)誤處理。對(duì)于不具備版本描述文件這類阻斷發(fā)布性質(zhì)的錯(cuò)誤可以提前進(jìn)行校驗(yàn),不必讓大家等了一會(huì)然后突然報(bào)錯(cuò)了。
性能提升。一次發(fā)布任務(wù)可能牽扯到前后端多個(gè)應(yīng)用的發(fā)布,以前只能集中在一臺(tái)服務(wù)器上順序發(fā)布,現(xiàn)在可以多臺(tái)服務(wù)器分散執(zhí)行,資源調(diào)配更均衡。
在目前發(fā)布平臺(tái)的使用上,Jenkins 和 Portal 都有,畢竟 Jenkins 用了這么久很多業(yè)務(wù)同學(xué)會(huì)比較習(xí)慣。
但就未來來說 Portal 這類新的平臺(tái)應(yīng)該會(huì)占據(jù)越來越大的比重,畢竟它更適合于快速實(shí)現(xiàn)一些公司內(nèi)部定制化的需求。