當我們談論前端工程化時,我們在談論什么?
來源:原創(chuàng) 時間:2017-11-13 瀏覽:0 次
在日常前端開發(fā)工作中,你是否困惑于 npm、Yarn、Browserify、Gulp、Grunt、Webpack 等工具而不清楚他們有什么區(qū)別?是否覺得手頭的項目環(huán)境過于復雜,各種配置已經(jīng)超出了可控的范圍,以至于都不敢更改它?你是否覺得工程模塊太多構建太慢,點擊發(fā)布按鈕要等幾分鐘才能看到效果?
如果答案是肯定的,那么你可能遇到了有關前端工程化的問題。在這篇文章以及接下來的課程中,我會去解釋前端工程化,并且嘗試去解決上面的那些問題。

前端工程化是什么?
這是個很大的概念,但是在我們的日常開發(fā)中又很常見。當我們對一個工程進行設計并把它拆分成各個組件和模塊時,我們是在做工程化;當我們用 Webpack 構建項目,配置好各個環(huán)境的打包配置時,我們是在做工程化;當我們?yōu)轫椖刻砑恿?ESLint,并在每次提交之前自動檢查代碼質(zhì)量時,我們是在做工程化。
如果要用一句話來概括,在我的理解中前端工程化是把前端開發(fā)工作帶入到更加系統(tǒng)和規(guī)范體系的一系列過程。這個過程會包括源代碼的預編譯、模塊處理、代碼壓縮等構建方面的工作。工程化會盡可能保證開發(fā)者的開發(fā)體驗更加友好,保證源代碼的質(zhì)量以及依賴的完整性。工程化也會盡可能高效地將構建完成后的代碼送達給客戶端,來追求更加良好的用戶體驗。所有這些都屬于工程化。
為什么要了解前端工程化?
如果一個開發(fā)者想做現(xiàn)代 JavaScirpt 應用,那么他就有理由了解前端工程化,因為通過工程化可以提高代碼質(zhì)量,降低開發(fā)成本。
會有人質(zhì)疑說現(xiàn)在的前端開發(fā)是不是過于復雜了?一個沒有用過 npm 和 Webpack 的開發(fā)者想寫一個 Hello World 可能都需要學習和配置半天。在以前沒有所謂“工程化”的時候,還不是照樣寫代碼和發(fā)布代碼來實現(xiàn)用戶需求。
其實我們可以打個比方,把發(fā)送給客戶端的頁面理解成呈獻給用戶的一道菜。前端開發(fā)者是廚師,而工程化可以使開發(fā)者遵循正確的做菜流程——洗菜、切菜、炒菜,而不是將生的東西直接放到用戶面前。
在 Web 技術剛開始的時候,還沒有前端工程化這樣一個東西。人們只是簡單地把 HTML、CSS 和 JavaScript 直接混在一起丟到用戶。而就如人類對于食物的追求在不斷進步一樣,雖然在最初級的階段需求只是能填飽肚子,但慢慢地人們開始追求食物的質(zhì)量。對于前端來說也是一樣,用戶的需求從最開始簡單的頁面在向復雜的應用發(fā)展。前端需要做的事情更多,同時也要追求更友好的用戶體驗。
對于廚師來說,要想做出高質(zhì)量的食物需要順手的工具以及正確的烹飪方法。對于開發(fā)者來說,要想輸出高質(zhì)量的代碼也同樣需要工程化來輔助。作為一個前端工程師,光會寫代碼是不夠的,最多只是提供了好的原材料。要想將代碼轉(zhuǎn)化為高質(zhì)量的產(chǎn)出提供給用戶,必須要了解工程化。

另外,工程化也是為開發(fā)者服務的。通過預編譯語言、模塊熱加載等技術可以提升開發(fā)效率,而利用自動化測試、lint 工具等可以保證代碼的功能和質(zhì)量。工程化可以有效降低開發(fā)成本,誰不想省下埋頭 debug 的時間去做更有趣的事呢。
從工程化出現(xiàn)之前說起
前端工程化不是憑空出現(xiàn)的,一定是為了解決當時的一些問題而出現(xiàn)的,讓我們先簡單回溯一下前端開發(fā)的歷史。
曾經(jīng)的 Web 開發(fā)不同于現(xiàn)在,頁面功能比較簡單。開發(fā)者想添加一段邏輯,最直接的做法就是在 HTML 中插入一個 script 標簽,然后直接在里面書寫代碼。聽上去似乎十分的原始,但在當時確實很多都是這樣做的,而且在業(yè)務需求簡單的時候這也確實是最直接了當?shù)膶崿F(xiàn)方式。但是這種原始的做法也必然有它的缺點,主要有以下兩個方面:
全局作用域的污染。由于在每個 script 標簽下頂層作用域即全局作用域,直接進行變量和函數(shù)聲明會造成全局命名空間污染。假如一個頁面有多個 script 標簽,它們之間很有可能發(fā)生命名沖突。
代碼重用性差。在一個多頁面應用的場景下,經(jīng)常會有一些邏輯是這些頁面之間共有的,此時我們不得不將這些代碼復制粘貼到各個頁面中。而當此處邏輯改動的時候我們也需要去更新所有頁面的代碼,造成很多額外的成本。
后來逐漸有一些針對這些問題的解決辦法。首先,可以將 HTML 中內(nèi)聯(lián)的 JavaScript 提取出來成為單獨的 JavaScript 文件。比如說一些頁面公有的邏輯可以放在類似 common.js 中來被各個頁面引用,這可以解決各個頁面之間重用的問題。至于全局作用域污染的問題,則可以使用立即執(zhí)行函數(shù)表達式將它包起來( IIFE ),只把接口暴露到全局上。
(function() {
// 通過立即執(zhí)行函數(shù)表達式將作用域隔離
var foo = 'bar';})();
看上去問題已經(jīng)得到了解決,然而隨著頁面邏輯的復雜度增加開發(fā)者又面臨了新的問題:
頁面 JavaScript 文件的引用順序。由于 HTML 頁面引用和處理 JavaScript 文件只能是順序的(不考慮 async 等),因此頁面的 JavaScript 之間依賴關系也必須是順序的。而我們知道一個大型工程內(nèi)部的模塊依賴關系通常是樹狀的(比如 index.js 依賴 a、b、c 三個模塊,而 a、b、c 又有各自的依賴),簡單的順序依賴關系無法滿足需求。例如在 jQuery 最流行的時期,jQuery 本身以及其相關的插件之間有著各種各樣的依賴關系,有些庫可能自身包含 jQuery,不同的插件可能需要不同的 jQuery 版本,這些問題都不是簡單的順序依賴關系可以解決的。
頁面引用的 JavaScript 文件的長度與數(shù)量如何權衡。隨著頁面邏輯的增加,工程中的 JavaScript 文件越來越長,也越來越難以維護。一個頁面的單個 JavaScript 文件可能有數(shù)千行甚至上萬行。而如果按照功能來把頁面邏輯切割成一個個小的 JavaScript 文件,則最終會走到另一個極端——頁面請求過多。我們知道每個 HTTP 請求都是需要連接時間的,對于小模塊而言每一個都要單獨建立連接總歸得不償失,必然會導致頁面渲染速度的下降。
上面所說的這些問題都屬于前端開發(fā)的“原始時期”,那時候還沒有工程化這種說法。然而逐漸暴露出來的問題已經(jīng)讓人們覺得不能再簡單粗暴地采用如此原始的開發(fā)方式,模塊化是第一步。
走向正軌的第一步——模塊化
一個設計良好的系統(tǒng)應該是模塊化的。一個最簡單的原因,在一個模塊化的系統(tǒng)中,當外界的需求亦或環(huán)境變化的時候,開發(fā)者可以更快地將問題定位到相應的模塊,而不必面對糾纏在一起的邏輯不知如何下手。模塊化可以使系統(tǒng)具備更強的可維護性。
被封裝良好的模塊應該具備特定且單一的功能,對外界只提供接口,而將具體實現(xiàn)封裝在內(nèi)部。Webpack 中有一個核心的理念——”一切皆模塊”,即 HTML、JavaScript、CSS、圖片等等都是模塊,在后面的文章中會展開講。
雖然模塊化很重要,但是 JavaScript 誕生的時候并不具備模塊這一特性,這主要是因為早期 Web 中的腳本大都比較簡單,在設計之初只是為了實現(xiàn) Web 上一些簡單的功能。一直到 CommonJS 以及 AMD 的出現(xiàn),為前端定義了模塊的標準。也有了實現(xiàn)這些模塊化的庫,比如 RequireJS 以及 Browserify??梢宰岄_發(fā)者將自己工程中的代碼按模塊進行劃分,模塊之間也不再僅僅是簡單的順序依賴關系。
另外在將代碼提供給客戶端之前,開發(fā)者可以通過 Browserify、Webpack 這些工具將工程代碼進行打包,把所有依賴模塊打包為單一的 JavaScript 文件。這樣一來,對于開發(fā)者而言開發(fā)體驗更加友好,因為開發(fā)中每次需要關注的僅僅是單個模塊,而不是堆放在一起的上千行 JavaScript 文件;而對于客戶端來說則只用接受單一的打包產(chǎn)物,解決了文件數(shù)量過多導致 HTTP 請求耗時長的問題。解決模塊之間的依賴,并根據(jù)依賴樹進行打包,是工程化解決的最基本的問題之一。
提升前端開發(fā)效率——預編譯語言
上面說的只是 JavaScript 的模塊化,那么我們很自然地就想到 CSS 的模塊化。然而因 CSS 本身 @import 的性能問題,一般都是要通過 SASS、LESS 等預編譯語言去實現(xiàn)其模塊化。
例如在 SASS 中,通過 @import 語句我們可以導入其它模塊。SASS 的編譯器會處理模塊之間的依賴,并最終將代碼打包在一起生成 CSS。
在實際開發(fā)中,我們很多時候都會使用預編譯語言來進行編碼工作,然后經(jīng)過 Webpack 等工具的構建將其編譯為實際頁面中的代碼。使用預編譯語言的主要目的是為了實現(xiàn) HTML、CSS、JavaScript 不具備的特性。比如說上面提到的最常見的 SASS,它是 CSS 的預編譯語言,通過它開發(fā)者可以使用模塊、定義變量、編寫嵌套規(guī)則等等來提高開發(fā)效率。
除了 CSS 的預編譯語言,HTML 對應的有 HAML,JavaScript 對應的有 Coffee 等等??傮w而言這些預編譯語言的目的就是使開發(fā)體驗更友好,開發(fā)者可以更高效地編寫和維護代碼。
當然現(xiàn)在預編譯已經(jīng)不僅僅是這些,我們還可以使用 Babel 預編譯 JavaScript 來實現(xiàn)新的 ES 特性,以及使用 TypeScript 去做類型檢查等,在預編譯這里還可以有更多的想象力。
現(xiàn)代 JavaScript 應用必不可少的部分——包管理器
和 Java、C++ 這些語言不同,JavaScript 沒有強大的標準庫。許多常用的功能,比如日期處理、URL 處理、異步流程控制等往往都需要手工去編寫,而采用外部已有的開源框架庫或許是節(jié)約成本的最好辦法。
Bower 作為包管理器最早進入人們的視野,大部門前端框架庫也都提供了通過 Bower 安裝的方式。通過它你可以獲取項目需要的依賴,并且通過打包工具和業(yè)務代碼打包到一起。
雖然現(xiàn)在當我們說包管理器可能首先想到的是 npm,但它最開始其實主要是為 Node.js 服務的,而人們逐漸意識到它也可以用于前端并且真正擔當起前端包管理器的大任也就是近兩年的時間。在現(xiàn)階段來說 npm 已經(jīng)超越 Bower 成為開發(fā)者首選的包管理器,而 Yarn 作為 Facebook 出品的新生代也不過是管理 node_modules 的另一個工具罷了,與 npm 并沒有什么本質(zhì)上的區(qū)別。在后續(xù)的相關文章中會更詳細地介紹包管理器常見的問題和處理辦法。
讓機器做更多的事——構建流程管理
隨著工程化的發(fā)展,交給構建過程的任務也越來越多,并且在不同環(huán)境下需要對任務進行區(qū)分。比如對于一個前端工程來說,除了需要預編譯各種類型的文件、資源打包之外,本地環(huán)境下還要生成 source-map、配置模塊熱加載等等便于調(diào)試代碼;而到了生產(chǎn)環(huán)境下則要對資源進行壓縮,生成版本號等等。
因此對于開發(fā)者來說需要將這些任務統(tǒng)一起來進行管理,也就有了 Gulp 和 Grunt 等構建流程管理工具。這類工具的出現(xiàn)使得構建變得更加傻瓜化,通過項目中的一些配置,開發(fā)者可以使用簡單的一行命令啟動本地開發(fā)環(huán)境或者構建和發(fā)布整個工程。
相比于 Gulp 和 Grunt,Webpack 出現(xiàn)的更晚。它和前兩者的核心定位其實不太一樣,Webpack 本身只是作為一個模塊打包工具的姿態(tài)出現(xiàn)的,但是利用一些相關工具和插件我們也可以完成整個工程的構建。Webpack 的“一切皆模塊”以及“按需加載”兩大特性使得它更好地服務于工程化。在后面的文章中會有很多關于 Webpack 的部分,包括 Webpack 的打包原理及優(yōu)化、從零搭建起一個開發(fā)環(huán)境等,會詳細講解 Webpack 的使用以及其最新的特性。
擺脫冗長的等待——構建流程優(yōu)化
現(xiàn)在當我們討論工程化,效率和優(yōu)化是出現(xiàn)得越來越頻繁的詞。當把工程化的各種功能都實現(xiàn)在我們的工程中之后,卻發(fā)現(xiàn)完整地構建一遍需要好幾分鐘甚至更長。工程本身的越發(fā)龐大以及越來越多的構建任務使得耗時越來越久,此時是不是又懷念起了在 HTML 里寫內(nèi)聯(lián)腳本的日子。
除了構建速度的問題,推給用戶端的資源體積過大也是問題。需要針對項目的特點采用按需加載、異步加載、長效緩存等等策略。在后面的文章中會單獨有一篇來講構建方面的優(yōu)化。
關于本課程
在這個系列文章中,我會將工程化相關的原理和實踐結(jié)合起來,穿插進行。首先是模塊化的相關內(nèi)容,后面則會介紹 Webpack、包管理器、構建優(yōu)化等等。在實踐部分會有實際的工程演示,包括源代碼和配置都會給出 Github 地址,方便大家運行。
在這個系列文章的最后會介紹在去哪兒網(wǎng)前端開發(fā)流程中,大型項目是如何做工程化相關工作的,我們曾經(jīng)踩過的坑以及解決方案。最后歡迎大家在讀者圈多多跟我交流。

