1. 前言
現(xiàn)在的前端開(kāi)發(fā), 通常是一個(gè)單頁(yè)面應(yīng)用,每一個(gè)視圖通過(guò)異步的方式加載旱幼,這導(dǎo)致頁(yè)面初始化和使用過(guò)程中會(huì)加載越來(lái)越多的 JS 代碼搂捧,如何在開(kāi)發(fā)環(huán)境組織好這些碎片化的代碼和資源浑此,并且保證他們?cè)跒g覽器端快速蚯妇、優(yōu)雅的加載和更新,就需要一個(gè)模塊化系統(tǒng)涛舍。
1.1 最簡(jiǎn)單的模塊
其實(shí)我們?cè)押瘮?shù)作為模塊,但會(huì)污染全局變量富雅,并且模塊成員之間沒(méi)什么關(guān)系掸驱。這個(gè)時(shí)候我們可以運(yùn)用面向?qū)ο笏枷耄褂昧⒓磮?zhí)行函數(shù)實(shí)現(xiàn)閉包没佑,可以避免變量污染亭敢,同時(shí)同一模塊內(nèi)的成員也有了關(guān)系,在模塊外部無(wú)法修改我們沒(méi)有暴露出來(lái)的變量图筹、函數(shù),這就是簡(jiǎn)單的模塊让腹。但是這樣處理起來(lái)麻煩远剩,并且遠(yuǎn)遠(yuǎn)不夠。
1.2 期望的模塊系統(tǒng)
模塊的加載和傳輸骇窍,我們首先能想到兩種極端的方式瓜晤,一種是每個(gè)模塊文件都單獨(dú)請(qǐng)求,另一種是把所有模塊打包成一個(gè)文件然后只請(qǐng)求一次腹纳。顯而易見(jiàn)痢掠,每個(gè)模塊都發(fā)起單獨(dú)的請(qǐng)求造成了請(qǐng)求次數(shù)過(guò)多驱犹,導(dǎo)致應(yīng)用啟動(dòng)速度慢;一次請(qǐng)求加載所有模塊導(dǎo)致流量浪費(fèi)足画、初始化過(guò)程慢雄驹。這兩種方式都不是好的解決方案,它們過(guò)于簡(jiǎn)單粗暴淹辞。
分塊傳輸医舆,按需進(jìn)行懶加載,在實(shí)際用到某些模塊的時(shí)候再增量更新象缀,才是較為合理的模塊加載方案蔬将。要實(shí)現(xiàn)模塊的按需加載,就需要一個(gè)對(duì)整個(gè)代碼庫(kù)中的模塊進(jìn)行靜態(tài)分析央星、編譯打包的過(guò)程霞怀。
在上面的分析過(guò)程中,我們提到的模塊僅僅是指 JS 模塊文件莉给。然而毙石,在前端開(kāi)發(fā)過(guò)程中還涉及到樣式、圖片禁谦、字體胁黑、HTML 模板等等眾多的資源。如果他們都可以視作模塊州泊,并且都可以通過(guò)require
的方式來(lái)加載丧蘸,將帶來(lái)優(yōu)雅的開(kāi)發(fā)體驗(yàn),那么如何做到讓 require
能加載各種資源呢遥皂?在編譯的時(shí)候力喷,要對(duì)整個(gè)代碼進(jìn)行靜態(tài)分析,分析出各個(gè)模塊的類型和它們依賴關(guān)系演训,然后將不同類型的模塊提交給適配的加載器來(lái)處理弟孟。Webpack 就是在這樣的需求中應(yīng)運(yùn)而生。
2. 模塊系統(tǒng)
2.1 script
- 全局作用域下容易造成變量沖突
- 文件只能按照
<script>
的書(shū)寫(xiě)順序進(jìn)行加載 - 開(kāi)發(fā)人員必須主觀解決模塊和代碼庫(kù)的依賴關(guān)系
- 在大型項(xiàng)目中各種資源難以管理样悟,長(zhǎng)期積累的問(wèn)題導(dǎo)致代碼庫(kù)混亂不堪
2.2 CommonJS
服務(wù)器端的 Node.js 遵循 CommonJS 規(guī)范拂募,該規(guī)范的核心思想是允許模塊通過(guò) require
方法來(lái)同步加載所要依賴的其他模塊,然后通過(guò) exports
或 module.exports
來(lái)導(dǎo)出需要暴露的接口窟她。
require('module');
require('../file.js');
exports.doStuff = function() {};
module.exports = someValue;
或
// moduleA.js
module.exports = function(value) {
return value * 2;
};
// moduleB.js
var multiplyBy2 = require('./moduleA');
var result = multiplyBy2(4);
優(yōu)點(diǎn):
- 服務(wù)器端模塊便于重用
- NPM 中已經(jīng)有將近 20 萬(wàn)個(gè)可以使用模塊包
- 簡(jiǎn)單并容易使用
缺點(diǎn):
- 同步的模塊加載方式不適合在瀏覽器環(huán)境中陈症,同步意味著阻塞加載,瀏覽器資源是異步加載的
- 不能非阻塞的并行加載多個(gè)模塊
2.3 AMD
define(id?, dependencies?, factory)
震糖,它要在聲明模塊的時(shí)候指定所有的依賴 dependencies
录肯,并且還要當(dāng)做形參傳到 factory
中,對(duì)于依賴的模塊提前執(zhí)行吊说,依賴前置论咏。
define('module', ['dep1', 'dep2'], function(d1, d2) {
return someExportedValue;
});
require(['module', '../file'], function(module, file) {
/* ... */
});
一些用例:
定義一個(gè)名為 myModule
的模塊优炬,它依賴 jQuery
模塊:
define('myModule', ['jquery'], function($) {
// $ 是 jquery 模塊的輸出
$('body').text('hello world');
});
// 使用
define(['myModule'], function(myModule) {});
注意:在 webpack 中,模塊名只有局部作用域厅贪,在 Require.js 中模塊名是全局作用域蠢护,可以在全局引用。
定義一個(gè)沒(méi)有 id
值的匿名模塊卦溢,通常作為應(yīng)用的啟動(dòng)函數(shù):
define(['jquery'], function($) {
$('body').text('hello world');
});
依賴多個(gè)模塊的定義:
define(['jquery', './math.js'], function($, math) {
// $ 和 math 一次傳入 factory
$('body').text('hello world');
});
模塊輸出:
define(['jquery'], function($) {
var HelloWorldize = function(selector) {
$(selector).text('hello world');
};
// HelloWorldize 是該模塊輸出的對(duì)外接口
return HelloWorldize;
});
在模塊定義內(nèi)部引用依賴:
define(function(require) {
var $ = require('jquery');
$('body').text('hello world');
});
優(yōu)點(diǎn):
- 適合在瀏覽器環(huán)境中異步加載模塊
- 可以并行加載多個(gè)模塊
缺點(diǎn):
- 提高了開(kāi)發(fā)成本糊余,代碼的閱讀和書(shū)寫(xiě)比較困難,模塊定義方式的語(yǔ)義不順暢
- 不符合通用的模塊化思維方式单寂,是一種妥協(xié)的實(shí)現(xiàn)
2.4 CMD
define(function(require, exports, module) {
var $ = require('jquery');
var Spinning = require('./spinning');
exports.doSomething = ...
module.exports = ...
})
優(yōu)點(diǎn):
- 依賴就近贬芥,延遲執(zhí)行
- 可以很容易在 Node.js 中運(yùn)行
缺點(diǎn):
- 依賴 SPM 打包,模塊的加載邏輯偏重
2.5 UMD (暫未接觸)
2.6 ES6 模塊
ES6 模塊的設(shè)計(jì)思想宣决,是盡量的靜態(tài)化蘸劈,使得編譯時(shí)就能確定模塊的依賴關(guān)系,以及輸入和輸出的變量尊沸。CommonJS 和 AMD 模塊威沫,都只能在運(yùn)行時(shí)確定這些東西。
import "jquery";
export function doStuff() {}
module "localModule" {}
優(yōu)點(diǎn):
- 容易進(jìn)行靜態(tài)分析
- 面向未來(lái)的 EcmaScript 標(biāo)準(zhǔn)
缺點(diǎn):
- 原生瀏覽器端還沒(méi)有實(shí)現(xiàn)該標(biāo)準(zhǔn)
- 全新的命令字洼专,新版的 Node.js 才支持
實(shí)現(xiàn):
3. 模塊系統(tǒng)/規(guī)范對(duì)比
3.1 AMD 與 CMD
從前有兩個(gè)規(guī)范棒掠,一個(gè)是 AMD,一個(gè)是 CMD屁商。RequireJS 是 AMD 規(guī)范的實(shí)現(xiàn)烟很,SeaJS 是 CMD 規(guī)范的實(shí)現(xiàn)。一個(gè)主張?zhí)崆凹虞d依賴蜡镶,一個(gè)主張延遲加載依賴雾袱。后來(lái)出現(xiàn)了 CommomJS 規(guī)范,CommomJS 是服務(wù)端規(guī)范官还,node 就是采用這個(gè)規(guī)范芹橡,他是同步加載,畢竟服務(wù)端不用考慮異步望伦。
3.2 AMD 與 CommonJs
AMD 的應(yīng)用場(chǎng)景則是瀏覽器林说,異步加載的模塊機(jī)制。require.js 的寫(xiě)法大致如下:
define(['firstModule'], function(module) {
//your code...
return anotherModule;
});
CommonJs 是應(yīng)用在 NodeJs屯伞,是一種同步的模塊機(jī)制述么。它的寫(xiě)法大致如下:
var firstModule = require('firstModule');
//your code...
module.export = anotherModule;
其實(shí)我們單比較寫(xiě)法,就知道 CommonJs 是更為優(yōu)秀的愕掏。它是一種同步的寫(xiě)法,友好而且代碼也不會(huì)繁瑣臃腫顶伞。但更重要的原因是饵撑,隨著 npm 成為主流的 JS 組件發(fā)布平臺(tái)剑梳,越來(lái)越多的前端項(xiàng)目也依賴于 npm 上的項(xiàng)目,或者自身就會(huì)發(fā)布到 npm 平臺(tái)滑潘。所以我們對(duì)如何可以使用 npm 包中的模塊是我們的一大需求垢乙。
3.3 browserify 與 webpack
browserify 工具支持我們直接使用 require()的同步語(yǔ)法去加載 npm 模塊,使用不多這里就不做介紹语卤。
Webpack 其實(shí)就是一個(gè)打包工具追逮,他的思想就是一切皆模塊,css 是模塊粹舵,js 是模塊钮孵,圖片是模塊。并且提供了一些列模塊加載(各種-loader)來(lái)編譯模塊眼滤。官方推薦使用 commonJS 規(guī)范巴席,但是也支持 CMD 和 AMD。無(wú)論是
node
應(yīng)用模塊诅需,還是webpack
配置 漾唉,均是采用CommonJS
模塊化規(guī)范。webpack 支持哪些功能特性:
- 支持 CommonJs 和 AMD 模塊堰塌,意思也就是我們基本可以無(wú)痛遷移舊項(xiàng)目赵刑。
- 支持模塊加載器和插件機(jī)制,可對(duì)模塊靈活定制场刑。特別是我最愛(ài)的 babel-loader般此,有效支持 ES6。
- 可以通過(guò)配置摇邦,打包成多個(gè)文件恤煞。有效利用瀏覽器的緩存功能提升性能。
- 將樣式文件和圖片等靜態(tài)資源也可視為模塊進(jìn)行打包施籍。配合 loader 加載器居扒,可以支持 sass,less 等 CSS 預(yù)處理器丑慎。
- 內(nèi)置有 source map喜喂,即使打包在一起依舊方便調(diào)試。
- 看完上面這些竿裂,可以想象它就是一個(gè)前端工具玉吁,可以讓我們進(jìn)行各種模塊加載,預(yù)處理后腻异,再打包进副。之前我們對(duì)這些的處理是放在 grunt 或 gulp 等前端自動(dòng)化工具中。有了 webpack悔常,我們無(wú)需借助自動(dòng)化工具對(duì)模塊進(jìn)行各種處理影斑,讓我們工具的任務(wù)分的更加清晰给赞。
4. 相關(guān)知識(shí)
4.1 ES6 模塊
4.1.1 對(duì)象的導(dǎo)出
1. export default{
add(){}
}
2. export fucntion add(){} 相當(dāng)于 將add方法當(dāng)做一個(gè)屬性掛在到exports對(duì)象
4.1.2 對(duì)象的導(dǎo)入
如果導(dǎo)出的是:export default{ add(){}}
那么可以通過(guò) import obj from './calc.js'
如果導(dǎo)出的是:
export fucntion add(){}
export fucntion substrict(){}
export const PI=3.14
那么可以通過(guò)按需加載 import {add,substrict,PI} from './calc.js'
4.2 Node 模塊
4.2.1 傳統(tǒng)非模塊化開(kāi)發(fā)有如下的缺點(diǎn)
- 命名沖突
- 文件依賴
4.2.2 前端標(biāo)準(zhǔn)的模塊化規(guī)范
- AMD - requirejs
- CMD - seajs
4.2.3 服務(wù)器端的模塊化規(guī)范
CommonJS - Node.js
4.2.4 Node 模塊化相關(guān)的規(guī)則
- 如何定義模塊:一個(gè) js 文件就是一個(gè)模塊,模塊內(nèi)部的成員都是相互獨(dú)立
- 模塊成員的導(dǎo)出和引入:
exports 與 module 的關(guān)系:module.exports = exports = {};
模塊成員的導(dǎo)出最終以 module.exports 為準(zhǔn)
如果要導(dǎo)出單個(gè)的成員或者比較少的成員矫户,一般我們使用 exports 導(dǎo)出片迅;如果要導(dǎo)出的成員比較多,一般我們使用 module.exports 的方式;這兩種方式不能同時(shí)使用
var sum = function(a, b) {
return parseInt(a) + parseInt(b);
};
// 方法1
// 導(dǎo)出模塊成員
exports.sum = sum;
//引入模塊
var module = require('./xx.js');
var ret = module.sum(12, 13);
// 方法2
// 導(dǎo)出模塊成員
module.exports = sum;
//引入模塊
var module = require('./xx.js');
module();
// // 方法1
// exports.sum = sum;
// exports.subtract = subtract;
//
// var m = require('./05.js');
// var ret = m.sum(1,2);
// var ret1 = m.subtract(1,2);
// console.log(ret,ret1);
//
// // 方法2
// module.exports = {
// sum : sum,
// subtract : subtract,
// multiply : multiply,
// divide : divide
// }
//
// var m = require('./05.js');
// console.log(m);
4.3 webpack
4.3.1 模塊打包器
根據(jù)模塊的依賴關(guān)系進(jìn)行靜態(tài)分析皆辽,然后將這些模塊按照指定的規(guī)則生成對(duì)應(yīng)的靜態(tài)資源柑蛇。如何在一個(gè)大規(guī)模的代碼庫(kù)中,維護(hù)各種模塊資源的分割和存放驱闷,維護(hù)它們之間的依賴關(guān)系耻台,并且無(wú)縫的將它們整合到一起生成適合瀏覽器端請(qǐng)求加載的靜態(tài)資源。市面上已經(jīng)存在的模塊管理和打包工具并不適合大型的項(xiàng)目遗嗽,尤其單頁(yè)面 Web 應(yīng)用程序粘我。最緊迫的原因是如何在一個(gè)大規(guī)模的代碼庫(kù)中,維護(hù)各種模塊資源的分割和存放痹换,維護(hù)它們之間的依賴關(guān)系征字,并且無(wú)縫的將它們整合到一起生成適合瀏覽器端請(qǐng)求加載的靜態(tài)資源。
這些已有的模塊化工具并不能很好的完成如下的目標(biāo):
- 將依賴樹(shù)拆分成按需加載的塊
- 初始化加載的耗時(shí)盡量少
- 各種靜態(tài)資源都可以視作模塊
- 將第三方庫(kù)整合成模塊的能力
- 可以自定義打包邏輯的能力
- 適合大項(xiàng)目娇豫,無(wú)論是單頁(yè)還是多頁(yè)的 Web 應(yīng)用
4.3.2 Webpack 的特點(diǎn)
Webapck 和其他模塊化工具有什么區(qū)別呢匙姜?
- 代碼拆分
Webpack 有兩種組織模塊依賴的方式,同步和異步冯痢。異步依賴作為分割點(diǎn)氮昧,形成一個(gè)新的塊。在優(yōu)化了依賴樹(shù)后浦楣,每一個(gè)異步區(qū)塊都作為一個(gè)文件被打包袖肥。 - Loader
Webpack 本身只能處理原生的 JavaScript 模塊,但是 loader 轉(zhuǎn)換器可以將各種類型的資源轉(zhuǎn)換成 JavaScript 模塊振劳。這樣椎组,任何資源都可以成為 Webpack 可以處理的模塊。 - 智能解析
Webpack 有一個(gè)智能解析器历恐,幾乎可以處理任何第三方庫(kù)寸癌,無(wú)論它們的模塊形式是 CommonJS、 AMD 還是普通的 JS 文件弱贼。甚至在加載依賴的時(shí)候蒸苇,允許使用動(dòng)態(tài)表達(dá)式require("./templates/" + name + ".jade")
。 - 插件系統(tǒng)
Webpack 還有一個(gè)功能豐富的插件系統(tǒng)吮旅。大多數(shù)內(nèi)容功能都是基于這個(gè)插件系統(tǒng)運(yùn)行的溪烤,還可以開(kāi)發(fā)和使用開(kāi)源的 Webpack 插件,來(lái)滿足各式各樣的需求。 - 快速運(yùn)行
Webpack 使用異步 I/O 和多級(jí)緩存提高運(yùn)行效率氛什,這使得 Webpack 能夠以令人難以置信的速度快速增量編譯莺葫。
4.3.3 webpack 是什么?
CommonJS 和 AMD 是用于 JavaScript 模塊管理的兩大規(guī)范枪眉,前者定義的是模塊的同步加載,主要用于 NodeJS再层;而后者則是異步加載贸铜,通過(guò) requirejs 等工具適用于前端。隨著 npm 成為主流的 JavaScript 組件發(fā)布平臺(tái)聂受,越來(lái)越多的前端項(xiàng)目也依賴于 npm 上的項(xiàng)目蒿秦,或者 自身就會(huì)發(fā)布到 npm 平臺(tái)。因此蛋济,讓前端項(xiàng)目更方便的使用 npm 上的資源成為一大需求棍鳖。
web 開(kāi)發(fā)中常用到的靜態(tài)資源主要有 JavaScript、CSS碗旅、圖片渡处、Jade 等文件,webpack 中將靜態(tài)資源文件稱之為模塊祟辟。 webpack 是一個(gè) module bundler(模塊打包工具)医瘫,其可以兼容多種 js 書(shū)寫(xiě)規(guī)范,且可以處理模塊間的依賴關(guān)系旧困,具有更強(qiáng)大的 js 模塊化的功能醇份。Webpack 對(duì)它們進(jìn)行統(tǒng) 一的管理以及打包發(fā)布
4.3.4 為什么使用 webpack?
1. 對(duì) CommonJS 吼具、 AMD 僚纷、ES6 的語(yǔ)法做了兼容
2. 對(duì) js、css拗盒、圖片等資源文件都支持打包
3. 串聯(lián)式模塊加載器以及插件機(jī)制怖竭,讓其具有更好的靈活性和擴(kuò)展性,例如提供對(duì) CoffeeScript锣咒、ES6 的支持
4. 有獨(dú)立的配置文件 webpack.config.js
5. 可以將代碼切割成不同的 chunk侵状,實(shí)現(xiàn)按需加載,降低了初始化時(shí)間
6. 支持 SourceUrls 和 SourceMaps毅整,易于調(diào)試
7. 具有強(qiáng)大的 Plugin 接口趣兄,大多是內(nèi)部插件,使用起來(lái)比較靈活
8.webpack 使用異步 IO 并具有多級(jí)緩存悼嫉。這使得 webpack 很快且在增量編譯上更加快