請注意,這是一篇站在完全新手的角度上來寫的文章。可能你是一個后端人員想了解前端工具的使用和概念;也可能你是一個前端小菜(還在DIV+CSS的世界里掙扎着)。本文比較適合那些以前完全沒有接觸過WebPack,而又想使用的朋友。通過本文你能理解webPack工作原理及做用!(不會至於看了半天資料還沒有頭緒!)
前言:本人是一個從后端轉向前端的程序猿,在此之前對於前端的印象一直是:HTML + CSS + JS。完全沒有想過前端會發展的如此的迅速,各種名詞的出現:Node、NPM、Grunt、Gulp、Bower、Webpack、Browserify、Yeoman。瞬間讓感覺到不知道如何下手(好像根本學不完的樣子)!
先上一張別人的圖,目前的前端工具!

一、背景
如果你和我一樣,之前對於前端打包工具的發展一無所知,甚至於不知道這些工具出現的必要性。你可以瀏覽此部分的內容,如果你不想知道這些或者對這些並不感興趣,可以直接跳過此部分。
互聯網程序現狀
隨着移動互聯的來襲,當前越來越多的網站已經從單純的網頁模式,開始升級為webapp模式。它們運行在現代的瀏覽器中,使用HTML5、CSS3、ES6等技術開發,已經從單一的瀏覽功能轉變為一個基於瀏覽器的富客戶端。並且webapp通常是一個SPA(Single Page Application 單頁面應用)。每個頁面(View)通過異步的方式加載,有着良好的用戶體驗。但是這樣做的結果是導致程序初始化和使用的過程中需要更多、更復雜的JavaScript代碼來實現,這就對前端程序的開發帶來巨大的挑戰!
模塊化系統的演變
隨着程序的復雜性的增加,項目結構的龐大。把單一js文件按職責進行模塊化划分。
我們在寫頁面的時候會這樣寫:
<script src="base.js"></script>
<script src="utils.js"></script>
<script src="vipPush.js"></script>
這是最基礎的JavaScript加載方式,每個JS的所有方法和屬性都是暴露在window對象中的(就像把所有代碼都放在一個命名空間或者同一個包下),借助全局對象,我們就能使用這些屬性和方法。如果更為復雜的程序會使用命名空間的概念來組織這些模塊的接口,比如:YUI
這種開發方式帶來的弊端:
- 全局的作用域下容易造成變量的相互沖突(這是一個很常見的問題)
- 文件只能按照
<script>的書寫順序進行加載 - 開發者要解決各個模塊和代碼庫之間的依賴
- 如果按照此模式進行開發,長期下去整個項目(前端)代碼必定會混亂不堪
因為有了模塊的概念,讓我們的開發變得比較方便。讓我們可以很方便的使用別人的代碼,想要什么功能就加載什么模塊。這樣下去模塊的規范就變的更重要。目前:通用的JavaScript模塊主要有:
CommonJS: 同步加載解決方案
著名的node.js模塊系統就是參照CommonJS規范來實現的。其核心思想就是通過require來進行同步加載其它模塊,然后通過exports 或 module.exports來導出需要暴露的接口。
require("module");
require("./file.js");
exports.doStuff = function() {};
module.exports = someValue;
優點:
- 服務器端模塊便於重用
- 在NPM里有很多功能模塊
- 簡單易用
缺點:
- 同步加載的方式注定不能用於客戶端(clients),同步的加載意味着阻塞加載,瀏覽器的加載方式是異步的
- 不能非阻塞的並行加載多個模塊
代表:
- 服務端 node.js
- Browserify,瀏覽器端的 CommonJS 實現,可以使用 NPM 的模塊,但是編譯打包后的文件體積可能很大
AMD: 異步加載解決方案
AMD(asynchronous Module Definition)意思就是"異步模塊定義",其規范主要是一個接口define(id?, dependencies?, factory),它采用的是異步加載的方式加載模塊,模塊的加載不影響它后面請語句的運行。所有執行語句都是在模塊加載完成之后的回調函數中執行的。
define("module", ["dep1", "dep2"], function(d1, d2) {
return someExportedValue;
});
require(["module", "../file"], function(module, file) { /* ... */ });
優點:
- 適合在瀏覽器環境中進行加載模塊
- 可以並行多個模塊
缺點:
- 提高了並發的成功,代碼的閱讀和書寫比較困難
- 不符合通用模塊化的思維方式,是一種妥協的實現
實現:
CMD: 另一種異步加載解決方案
CMD(Common Module Definition)規范與AMD很相似,盡量保持簡單,並與CommonJs和Node.js的Module規范保持了很大的兼容性
define(function(require, exports, module) {
var $ = require('jquery');
var Spinning = require('./spinning');
exports.doSomething = ...
module.exports = ...
})
優點:
- 依賴就近,延遲執行
- 可以很容易在 Node.js 中運行
缺點:
- 依賴 SPM 打包,模塊的加載邏輯偏重
實現:
ES6 模塊
在ECMAScript2015(es6)中,增加了JavaScript語言層面上的模塊體系定義,其設計思想是:盡量的靜態化,使得編譯時就能確定模塊的依賴關系,以及輸入和輸出變量。
import "jquery";
export function doStuff() {}
module "localModule" {}
優點:
- 容易進行靜態分析
- 面向未來的 EcmaScript 標准
缺點:
- 原生瀏覽器端還沒有實現該標准
- 全新的命令字,新版的 Node.js才支持
實現:
把程序所有的文件進行模塊化之后,我們還要處理一個問題那就是傳輸問題。模塊的化分讓我們可以讓程序變得可以組件化進行開發,組件雖然被客戶端執行,但是依然要由服務器傳送給客戶端。
關於組件的傳送有兩個極端:
-
每個組件,一個HTTP請求
- 優點:僅僅傳送依賴項
- 缺點:請求多,負載高,更慢的啟動延遲
-
所有的組件,一個HTTP請求
- 優點: 更快,更低的延遲
- 傳送了沒有必要傳送的東西
讓我在這兩種情況之間做一個妥協:分塊傳輸,按需進行懶加載,在實際用某些模塊的時候進行增量的更新,才是比較合理的加載方案。
要實現這個功能,需要在編譯打包時進行靜態的分析、模塊進行分批次的打包。那么這個分批次誰來做呢?
答案就是:WebPack
參考資料:
