Webpack 前端工程化入門
課程介紹
如果你覺得自己團隊的前端開發流程還不夠規範,想了解在公司級別的大型專案中是如何做工程化的;如果你一直沒弄清各種模組標準有什麼區別,以及是如何使用在專案中的;如果你想深入瞭解 Webpack,並且想用它的各種特性來提升構建效率——那麼你有理由瞭解一下這門課。
本課程兼顧理論和實戰。會從最基本的模組化開始介紹,以及如何正確地使用包管理器。會帶你從零搭建起一個 Webpack 開發環境,並且講解如何進行構建方面的優化。另外我會結合在去哪兒網的經歷,講講在企業環境下大型專案是如何做工程化的,還有我們所踩過的坑。各種樣例工程原始碼會給出 GitHub 地址供大家參考。
希望開發者能通過這門課來了解基於 Webpack 的前端工程化,並通過它來提升自己和團隊的開發效率,構建高質量的應用。
作者簡介
居玉浩,去哪兒前端構建工具 YKit 的核心開發者,YMFE 顏值擔當,多年 Webpack 前端工程化實踐經驗。
課程內容
導讀:當我們談論前端工程化時,我們談論什麼
在日常前端開發工作中,你是否困惑於 npm、Yarn、Browserify、Gulp、Grunt、Webpack 等工具而不清楚它們有什麼區別?是否覺得手頭的專案環境過於複雜,各種配置已經超出了可控的範圍,以至於都不敢更改它?你是否覺得工程模組太多構建太慢,點擊發布按鈕要等幾分鐘才能看到效果?
如果答案是肯定的,那麼你可能遇到了有關前端工程化的問題。在這篇文章以及接下來的課程中,我會去解釋前端工程化,並且嘗試去解決上面的那些問題。
前端工程化是什麼?
這是個很大的概念,但是在我們的日常開發中又很常見。當我們對一個工程進行設計並把它拆分成各個元件和模組時,我們是在做工程化;當我們用 Webpack 構建專案,配置好各個環境的打包配置時,我們是在做工程化;當我們為專案添加了 ESLint,並在每次提交之前自動檢查程式碼質量時,我們是在做工程化。
如果要用一句話來概括,在我的理解中前端工程化是把前端開發工作帶入到更加系統和規範體系的一系列過程。這個過程會包括原始碼的預編譯、模組處理、程式碼壓縮等構建方面的工作。工程化會盡可能保證開發者的開發體驗更加友好,保證原始碼的質量以及依賴的完整性。工程化也會盡可能高效地將構建完成後的程式碼送達給客戶端,來追求更加良好的使用者體驗。所有這些都屬於工程化。
為什麼要了解前端工程化?
如果一個開發者想做現代 JavaScirpt 應用,那麼他就有理由瞭解前端工程化,因為通過工程化可以提高程式碼質量,降低開發成本。
會有人質疑說現在的前端開發是不是過於複雜了?一個沒有用過 npm 和 Webpack 的開發者想寫一個 Hello World 可能都需要學習和配置半天。在以前沒有所謂“工程化”的時候,還不是照樣寫程式碼和釋出程式碼來實現使用者需求。
其實我們可以打個比方,把傳送給客戶端的頁面理解成呈獻給使用者的一道菜。前端開發者是廚師,而工程化可以使開發者遵循正確的做菜流程——洗菜、切菜、炒菜,而不是將生的東西直接放到使用者面前。
在 Web 技術剛開始的時候,還沒有前端工程化這樣一個東西。人們只是簡單地把 HTML、CSS 和 JavaScript 直接混在一起丟到使用者。而就如人類對於食物的追求在不斷進步一樣,雖然在最初級的階段需求只是能填飽肚子,但慢慢地人們開始追求食物的質量。對於前端來說也是一樣,使用者的需求從最開始簡單的頁面在向複雜的應用發展。前端需要做的事情更多,同時也要追求更友好的使用者體驗。
對於廚師來說,要想做出高質量的食物需要順手的工具以及正確的烹飪方法。對於開發者來說,要想輸出高質量的程式碼也同樣需要工程化來輔助。作為一個前端工程師,光會寫程式碼是不夠的,最多隻是提供了好的原材料。要想將程式碼轉化為高質量的產出提供給使用者,必須要了解工程化。
另外,工程化也是為開發者服務的。通過預編譯語言、模組熱載入等技術可以提升開發效率,而利用自動化測試、lint 工具等可以保證程式碼的功能和質量。工程化可以有效降低開發成本,誰不想省下埋頭 debug 的時間去做更有趣的事呢。
從工程化出現之前說起
前端工程化不是憑空出現的,一定是為了解決當時的一些問題而出現的,讓我們先簡單回溯一下前端開發的歷史。
曾經的 Web 開發不同於現在,頁面功能比較簡單。開發者想新增一段邏輯,最直接的做法就是在 HTML 中插入一個 script 標籤,然後直接在裡面書寫程式碼。聽上去似乎十分原始,但在當時確實很多都是這樣做的,而且在業務需求簡單的時候這也確實是最直接了當的實現方式。但是這種原始的做法也必然有它的缺點,主要有以下兩個方面:
- 全域性作用域的汙染。由於在每個 script 標籤下頂層作用域即全域性作用域,直接進行變數和函式宣告會造成全域性名稱空間汙染。假如一個頁面有多個 script 標籤,它們之間很有可能發生命名衝突。
- 程式碼重用性差。在一個多頁面應用的場景下,經常會有一些邏輯是這些頁面之間共有的,此時我們不得不將這些程式碼複製貼上到各個頁面中。而當此處邏輯改動的時候我們也需要去更新所有頁面的程式碼,造成很多額外的成本。
後來逐漸有一些針對這些問題的解決辦法。首先,可以將 HTML 中內聯的 JavaScript 提取出來成為單獨的 JavaScript 檔案。比如說一些頁面公有的邏輯可以放在類似 common.js 中來被各個頁面引用,這可以解決各個頁面之間重用的問題。至於全域性作用域汙染的問題,則可以使用立即執行函式表示式將它包起來( IIFE ),只把介面暴露到全域性上。
(function() { // 通過立即執行函式表示式將作用域隔離 var foo = 'bar';})();
看上去問題已經得到了解決,然而隨著頁面邏輯的複雜度增加開發者又面臨了新的問題:
- 頁面 JavaScript 檔案的引用順序。由於 HTML 頁面引用和處理 JavaScript 檔案只能是順序的(不考慮 async 等),因此頁面的 JavaScript 之間依賴關係也必須是順序的。而我們知道一個大型工程內部的模組依賴關係通常是樹狀的(比如 index.js 依賴 a、b、c 三個模組,而 a、b、c 又有各自的依賴),簡單的順序依賴關係無法滿足需求。例如在 jQuery 最流行的時期,jQuery 本身以及其相關的外掛之間有著各種各樣的依賴關係,有些庫可能自身包含 jQuery,不同的外掛可能需要不同的 jQuery 版本,這些問題都不是簡單的順序依賴關係可以解決的。
- 頁面引用的 JavaScript 檔案的長度與數量如何權衡。隨著頁面邏輯的增加,工程中的 JavaScript 檔案越來越長,也越來越難以維護。一個頁面的單個 JavaScript 檔案可能有數千行甚至上萬行。而如果按照功能來把頁面邏輯切割成一個個小的 JavaScript 檔案,則最終會走到另一個極端——頁面請求過多。我們知道每個 HTTP 請求都是需要連線時間的,對於小模組而言每一個都要單獨建立連線總歸得不償失,必然會導致頁面渲染速度的下降。
上面所說的這些問題都屬於前端開發的“原始時期”,那時候還沒有工程化這種說法。然而逐漸暴露出來的問題已經讓人們覺得不能再簡單粗暴地採用如此原始的開發方式,模組化是第一步。
走向正軌的第一步——模組化
一個設計良好的系統應該是模組化的。一個最簡單的原因,在一個模組化的系統中,當外界的需求亦或環境變化的時候,開發者可以更快地將問題定位到相應的模組,而不必面對糾纏在一起的邏輯不知如何下手。模組化可以使系統具備更強的可維護性。
被封裝良好的模組應該具備特定且單一的功能,對外界只提供介面,而將具體實現封裝在內部。Webpack 中有一個核心的理念——”一切皆模組”,即 HTML、JavaScript、CSS、圖片等等都是模組,在後面的文章中會展開講。
雖然模組化很重要,但是 JavaScript 誕生的時候並不具備模組這一特性,這主要是因為早期 Web 中的指令碼大都比較簡單,在設計之初只是為了實現 Web 上一些簡單的功能。一直到 CommonJS 以及 AMD 的出現,為前端定義了模組的標準。也有了實現這些模組化的庫,比如 RequireJS 以及 Browserify。可以讓開發者將自己工程中的程式碼按模組進行劃分,模組之間也不再僅僅是簡單的順序依賴關係。
另外在將程式碼提供給客戶端之前,開發者可以通過 Browserify、Webpack 這些工具將工程程式碼進行打包,把所有依賴模組打包為單一的 JavaScript 檔案。這樣一來,對於開發者而言開發體驗更加友好,因為開發中每次需要關注的僅僅是單個模組,而不是堆放在一起的上千行 JavaScript 檔案;而對於客戶端來說則只用接受單一的打包產物,解決了檔案數量過多導致 HTTP 請求耗時長的問題。解決模組之間的依賴,並根據依賴樹進行打包,是工程化解決的最基本的問題之一。
提升前端開發效率——預編譯語言
上面說的只是 JavaScript 的模組化,那麼我們很自然地就想到 CSS 的模組化。然而因 CSS 本身 @import
的效能問題,一般都是要通過 SASS、LESS 等預編譯語言去實現其模組化。
例如在 SASS 中,通過 @import
語句我們可以匯入其它模組。SASS 的編譯器會處理模組之間的依賴,並最終將程式碼打包在一起生成 CSS。
在實際開發中,我們很多時候都會使用預編譯語言來進行編碼工作,然後經過 Webpack 等工具的構建將其編譯為實際頁面中的程式碼。使用預編譯語言的主要目的是為了實現 HTML、CSS、JavaScript 不具備的特性。比如說上面提到的最常見的 SASS,它是 CSS 的預編譯語言,通過它開發者可以使用模組、定義變數、編寫巢狀規則等等來提高開發效率。
除了 CSS 的預編譯語言,HTML 對應的有 HAML,JavaScript 對應的有 Coffee 等等。總體而言這些預編譯語言的目的就是使開發體驗更友好,開發者可以更高效地編寫和維護程式碼。
當然現在預編譯已經不僅僅是這些,我們還可以使用 Babel 預編譯 JavaScript 來實現新的 ES 特性,以及使用 TypeScript 去做型別檢查等,在預編譯這裡還可以有更多的想象力。
現代 JavaScript 應用必不可少的部分——包管理器
和 Java、C++ 這些語言不同,JavaScript 沒有強大的標準庫。許多常用的功能,比如日期處理、URL 處理、非同步流程控制等往往都需要手工去編寫,而採用外部已有的開源框架庫或許是節約成本的最好辦法。
Bower 作為包管理器最早進入人們的視野,大部門前端框架庫也都提供了通過 Bower 安裝的方式。通過它你可以獲取專案需要的依賴,並且通過打包工具和業務程式碼打包到一起。
雖然現在當我們說包管理器可能首先想到的是 npm,但它最開始其實主要是為 Node.js 服務的,而人們逐漸意識到它也可以用於前端並且真正擔當起前端包管理器的大任也就是近兩年的時間。在現階段來說 npm 已經超越 Bower 成為開發者首選的包管理器,而 Yarn 作為 Facebook 出品的新生代也不過是管理 node_modules 的另一個工具罷了,與 npm 並沒有什麼本質上的區別。在後續的相關文章中會更詳細地介紹包管理器常見的問題和處理辦法。
讓機器做更多的事——構建流程管理
隨著工程化的發展,交給構建過程的任務也越來越多,並且在不同環境下需要對任務進行區分。比如對於一個前端工程來說,除了需要預編譯各種型別的檔案、資源打包之外,本地環境下還要生成 source-map、配置模組熱載入等等便於除錯程式碼;而到了生產環境下則要對資源進行壓縮,生成版本號等等。
因此對於開發者來說需要將這些任務統一起來進行管理,也就有了 Gulp 和 Grunt 等構建流程管理工具。這類工具的出現使得構建變得更加傻瓜化,通過專案中的一些配置,開發者可以使用簡單的一行命令啟動本地開發環境或者構建和釋出整個工程。
相比於 Gulp 和 Grunt,Webpack 出現的更晚。它和前兩者的核心定位其實不太一樣,Webpack 本身只是作為一個模組打包工具的姿態出現的,但是利用一些相關工具和外掛我們也可以完成整個工程的構建。Webpack 的“一切皆模組”以及“按需載入”兩大特性使得它更好地服務於工程化。在後面的文章中會有很多關於 Webpack 的部分,包括 Webpack 的打包原理及優化、從零搭建起一個開發環境等,會詳細講解 Webpack 的使用以及其最新的特性。
擺脫冗長的等待——構建流程優化
現在當我們討論工程化,效率和優化是出現得越來越頻繁的詞。當把工程化的各種功能都實現在我們的工程中之後,卻發現完整地構建一遍需要好幾分鐘甚至更長。工程本身的越發龐大以及越來越多的構建任務使得耗時越來越久,此時是不是又懷念起了在 HTML 裡寫內聯指令碼的日子。
除了構建速度的問題,推給使用者端的資源體積過大也是問題。需要針對專案的特點採用按需載入、非同步載入、長效快取等等策略。在後面的文章中會單獨有一篇來講構建方面的優化。
關於本課程
在這個系列文章中,我會將工程化相關的原理和實踐結合起來,穿插進行。首先是模組化的相關內容,後面則會介紹 Webpack、包管理器、構建優化等等。在實踐部分會有實際的工程演示,包括原始碼和配置都會給出 Github 地址,方便大家執行。
在這個系列文章的最後會介紹在去哪兒網前端開發流程中,大型專案是如何做工程化相關工作的,我們曾經踩過的坑以及解決方案。最後歡迎大家在讀者圈多多跟我交流。