一厢钧、什么是模塊化
在js出現(xiàn)的時(shí)候专酗,js一般只是用來(lái)實(shí)現(xiàn)一些簡(jiǎn)單的交互睹逃,后來(lái)js開(kāi)始得到重視,用來(lái)實(shí)現(xiàn)越來(lái)越復(fù)雜的功能祷肯,而為了維護(hù)的方便唯卖,我們也把不同功能的js抽取出來(lái)當(dāng)做一個(gè)js文件,但是當(dāng)項(xiàng)目變的復(fù)雜的時(shí)候躬柬,一個(gè)html頁(yè)面可能需要加載好多個(gè)js文件拜轨,而這個(gè)時(shí)候就會(huì)出現(xiàn)各種命名沖突,如果js也可以像java一樣允青,把不同功能的文件放在不同的package中橄碾,需要引用某個(gè)函數(shù)或功能的時(shí)候,import下相關(guān)的包颠锉,這樣可以很好的解決命名沖突等各種問(wèn)題法牲,但是js中沒(méi)有模塊的概念,又怎么實(shí)現(xiàn)模塊化呢
模塊化開(kāi)發(fā)是一種管理方式琼掠,是一種生產(chǎn)方式拒垃,一種解決問(wèn)題的方案,一個(gè)模塊就是實(shí)現(xiàn)特定功能的文件瓷蛙,有了模塊悼瓮,我們就可以更方便地使用別人的代碼,想要什么功能艰猬,就加載什么模塊横堡,但是模塊開(kāi)發(fā)需要遵循一定的規(guī)范,否則就都亂套了冠桃,因此命贴,才有了后來(lái)大家熟悉的AMD規(guī)范,CMD規(guī)范
接下來(lái)食听,我們就一起學(xué)習(xí)下AMD胸蛛,CMD和es6中的模塊化吧
二、AMD
AMD 即Asynchronous Module Definition樱报,中文名是“異步模塊定義”的意思葬项,它采用異步方式加載模塊,模塊的加載不影響它后面語(yǔ)句的運(yùn)行肃弟,所有依賴這個(gè)模塊的語(yǔ)句玷室,都定義在一個(gè)回調(diào)函數(shù)中,等到加載完成之后笤受,這個(gè)回調(diào)函數(shù)才會(huì)運(yùn)行
一般來(lái)說(shuō)穷缤,AMD是 RequireJS 在推廣過(guò)程中對(duì)模塊定義的規(guī)范化的產(chǎn)出,因?yàn)槠綍r(shí)在開(kāi)發(fā)中比較常用的是require.js進(jìn)行模塊的定義和加載箩兽,一般是使用define來(lái)定義模塊津肛,使用require來(lái)加載模塊
1、定義模塊
AMD規(guī)范只定義了一個(gè)函數(shù)define汗贫,它是全局變量身坐,我們可以用它來(lái)定義一個(gè)模塊
define(id?, dependencies?, factory);
其中,id是定義中模塊的名字落包,這個(gè)參數(shù)是可選的部蛇,如果沒(méi)有提供該參數(shù),模塊的名字應(yīng)該默認(rèn)為模塊加載器請(qǐng)求的指定腳本的名字咐蝇,如果提供了該參數(shù)涯鲁,模塊名必須是“頂級(jí)”的和絕對(duì)的
dependencies是定義的模塊中所依賴模塊的數(shù)組,依賴模塊必須根據(jù)模塊的工廠方法優(yōu)先級(jí)執(zhí)行有序,并且執(zhí)行的結(jié)果應(yīng)該按照依賴數(shù)組中的位置順序以參數(shù)的形式傳入(定義中模塊的)工廠方法中
factory是模塊初始化要執(zhí)行的函數(shù)或?qū)ο竽ㄍ龋绻麨楹瘮?shù),它應(yīng)該只被執(zhí)行一次旭寿,如果是對(duì)象警绩,此對(duì)象應(yīng)該為模塊的輸出值
下面來(lái)看一個(gè)定義模塊的例子
define("alpha", ["require", "exports", "beta"], function (require, exports, beta) {
exports.verb = function() {
return beta.verb();
//Or:
return require("beta").verb();
}
});
上面的代碼定義了一個(gè)alpha的模塊,這個(gè)模塊依賴require盅称,exports肩祥,beta,因此需要先加載它們缩膝,再執(zhí)行后面的factory
2搭幻、加載模塊
require.js中采用require()語(yǔ)句加載模塊,在定義好了模塊后逞盆,我們可以使用require進(jìn)行模塊的加載
require([module], callback);
require要傳入兩個(gè)參數(shù)檀蹋,第一個(gè)參數(shù)[module],是一個(gè)數(shù)組云芦,里面的成員就是要加載的模塊俯逾,第二個(gè)參數(shù)callback,則是加載成功之后的回調(diào)函數(shù)
下面我們來(lái)看一個(gè)例子
require([increment'], function (increment) {
increment.add(1);
});
上面的代碼中舅逸,比如我們現(xiàn)在已經(jīng)定義了一個(gè)模塊桌肴,名字為increment,里面有一個(gè)add方法琉历,我們現(xiàn)在需要用到里面的方法坠七,只要像上面一樣將模塊加載進(jìn)來(lái)水醋,然后調(diào)用方法就可以了
3、requirejs使用例子
在使用require.js時(shí)彪置,可以通過(guò)define()定義模塊拄踪,這時(shí)候里面的模塊的方法和變量外部是無(wú)法訪問(wèn)到的,只有通過(guò)return拳魁,然后再加載這個(gè)模塊惶桐,才可以進(jìn)行訪問(wèn)
define('math',['jquery'], function ($) {//引入jQuery模塊
return {
add: function(x,y){
return x + y;
}
};
});
上面的代碼定義了一個(gè)math模塊,返回了一個(gè)add方法潘懊,要使用這個(gè)模塊的方法姚糊,我們需要向下面這樣進(jìn)行訪問(wèn)
require(['jquery','math'], function ($,math) {
console.log(math.add(10,100));//110
});
通過(guò)require,我們加載了math模塊授舟,這樣就可以使用math模塊里面的add方法了
三救恨、CMD
CMD 即Common Module Definition通用模塊定義,CMD規(guī)范是國(guó)內(nèi)發(fā)展出來(lái)的释树,同時(shí)忿薇,CMD是在SeaaJS推廣的過(guò)程中形成的,CMD和AMD要解決的都是同個(gè)問(wèn)題躏哩,在使用上也都很像署浩,只不過(guò)兩者在模塊定義方式和模塊加載時(shí)機(jī)上有所不同
1、定義模塊
在 CMD 規(guī)范中扫尺,一個(gè)模塊就是一個(gè)文件筋栋,通過(guò)define()進(jìn)行定義
define(factory);
define接受factory參數(shù),factory可以是一個(gè)函數(shù)正驻,也可以是一個(gè)對(duì)象或字符串
factory為對(duì)象弊攘、字符串時(shí),表示模塊的接口就是該對(duì)象姑曙、字符串襟交,比如可以如下定義一個(gè) JSON 數(shù)據(jù)模塊
define({ "foo": "bar" });
也可以通過(guò)字符串定義模板模塊
define('I am a template. My name is {{name}}.');
factory為函數(shù)時(shí),表示是模塊的構(gòu)造方法伤靠,執(zhí)行該構(gòu)造方法捣域,可以得到模塊向外提供的接口,factory方法在執(zhí)行時(shí)宴合,默認(rèn)會(huì)傳入三個(gè)參數(shù):require焕梅,exports和 module
define(function(require, exports, module) {
// 模塊代碼
});
其中,require用來(lái)加載其它模塊卦洽,而exports可以用來(lái)實(shí)現(xiàn)向外提供模塊接口
define(function(require, exports) {
// 對(duì)外提供 foo 屬性
exports.foo = 'bar';
// 對(duì)外提供 doSomething 方法
exports.doSomething = function() {};
});
module是一個(gè)對(duì)象贞言,上面存儲(chǔ)了與當(dāng)前模塊相關(guān)聯(lián)的一些屬性和方法,傳給factory構(gòu)造方法的exports參數(shù)是module.exports對(duì)象的一個(gè)引用阀蒂,只通過(guò)exports參數(shù)來(lái)提供接口该窗,有時(shí)無(wú)法滿足開(kāi)發(fā)者的所有需求弟蚀,比如當(dāng)模塊的接口是某個(gè)類的實(shí)例時(shí),需要通過(guò)module.exports來(lái)實(shí)現(xiàn)
define(function(require, exports, module) {
// exports 是 module.exports 的一個(gè)引用
console.log(module.exports === exports); // true
// 重新給 module.exports 賦值
module.exports = new SomeClass();
// exports 不再等于 module.exports
console.log(module.exports === exports); // false
});
說(shuō)了這么多酗失,相信大家可能有點(diǎn)亂义钉,來(lái)個(gè)簡(jiǎn)單的例子,我們看看使用AMD和CMD定義的模塊的寫(xiě)法
// CMD
define(function(require, exports, module) {
var a = require('./a')
a.doSomething()
// 此處略去 100 行
var b = require('./b') // 依賴可以就近書(shū)寫(xiě)
b.doSomething()
// ...
})
// AMD 默認(rèn)推薦的是
define(['./a', './b'], function(a, b) { // 依賴必須一開(kāi)始就寫(xiě)好
a.doSomething()
// 此處略去 100 行
b.doSomething()
...
})
在上面的代碼中级零,相信大家很容易可以看出區(qū)別吧断医,AMD和CMD都是通過(guò)define()定義模塊滞乙,AMD需要把依賴的模塊先寫(xiě)出來(lái)奏纪,可以通過(guò)return暴露接口,CMD在定義模塊需要傳入require斩启,exports和module這幾個(gè)參數(shù)序调,要加載某個(gè)模塊時(shí),使用require進(jìn)行加載兔簇,要暴露接口時(shí)发绢,可以通過(guò)exports,module.exports和return
2垄琐、加載模塊
在前面定義模塊時(shí)边酒,我們說(shuō)過(guò),當(dāng)factory為函數(shù)時(shí)狸窘,require會(huì)作為默認(rèn)參數(shù)傳遞進(jìn)去墩朦,而require可以實(shí)現(xiàn)模塊的加載
require是一個(gè)方法,接受模塊標(biāo)識(shí)作為唯一參數(shù)翻擒,用來(lái)獲取其他模塊提供的接口
define(function(require, exports) {
// 獲取模塊 a 的接口
var a = require('./a');
// 調(diào)用模塊 a 的方法
a.doSomething();
});
從上面定義模塊和加載模塊的方式上氓涣,我們也可以看出AMD和CMD主要有下面幾個(gè)不同:
(1)AMD是RequireJS在推廣過(guò)程中對(duì)模塊定義的規(guī)范化產(chǎn)出, CMD是SeaJS在推廣過(guò)程中對(duì)模塊定義的規(guī)范化產(chǎn)出
(2)對(duì)于依賴的模塊陋气,AMD是提前執(zhí)行劳吠,CMD是延遲執(zhí)行
(3)對(duì)于依賴的模塊,AMD推崇依賴前置巩趁,CMD推崇依賴就近
3痒玩、seajs使用例子
因?yàn)镃MD是SeaJS在推廣過(guò)程中對(duì)模塊定義的規(guī)范化產(chǎn)出,因此一般在實(shí)際開(kāi)發(fā)中议慰,我們都是通過(guò)SeaJS進(jìn)行模塊的定義和加載
下面是一個(gè)簡(jiǎn)單的例子
// 定義模塊 myModule.js
define(function(require, exports, module) {
var $ = require('jquery.js')
$('div').addClass('active');
exports.data = 1;
});
// 加載模塊
seajs.use(['myModule.js'], function(my){
var star= my.data;
console.log(star); //1
});
上面的代碼中定義了myModule.js模塊凰荚,因?yàn)樵撃K依賴于jquery.js,因此在需要使用該模塊時(shí)可以使用require進(jìn)行模塊的加載褒脯,然后通過(guò)exports暴露出接口便瑟,通過(guò)SeaJS的use方法我們可以加載該模塊,并且使用該模塊暴露出的接口
四番川、es6中的模塊化
在es6沒(méi)有出來(lái)之前到涂,社區(qū)制定了一些模塊加載方案脊框,最主要的有 CommonJS 和 AMD 兩種,前者用于服務(wù)器践啄,后者用于瀏覽器浇雹,ES6 在語(yǔ)言標(biāo)準(zhǔn)的層面上,實(shí)現(xiàn)了模塊功能屿讽,而且實(shí)現(xiàn)得相當(dāng)簡(jiǎn)單昭灵,完全可以取代 CommonJS 和 AMD 規(guī)范,成為瀏覽器和服務(wù)器通用的模塊解決方案
es6中的模塊化有一個(gè)比較大的特點(diǎn)伐谈,就是實(shí)現(xiàn)盡量的靜態(tài)化烂完,比如說(shuō)在CommonJS中我們要加載fs中的幾個(gè)方法,需要這樣寫(xiě)
// CommonJS模塊
let { stat, exists, readFile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
上面的代碼其實(shí)是加載了fs中的所有方法诵棵,生成一個(gè)對(duì)象抠蚣,再?gòu)倪@個(gè)對(duì)象上讀取方法,這種加載其實(shí)叫做運(yùn)行時(shí)加載履澳,也就是只有運(yùn)行時(shí)才能得到這個(gè)對(duì)象嘶窄,不能實(shí)現(xiàn)在編譯時(shí)實(shí)現(xiàn)靜態(tài)優(yōu)化
ES6 模塊不是對(duì)象,而是通過(guò)export命令顯式指定輸出的代碼距贷,再通過(guò)import命令輸入
// ES6模塊
import { stat, exists, readFile } from 'fs';
上面代碼的實(shí)質(zhì)是從fs模塊加載 3 個(gè)方法柄冲,其他方法不加載,這種加載稱為“編譯時(shí)加載”或者靜態(tài)加載忠蝗,即 ES6 可以在編譯時(shí)就完成模塊加載现横,效率要比 CommonJS 模塊的加載方式高,當(dāng)然什湘,這也導(dǎo)致了沒(méi)法引用 ES6 模塊本身长赞,因?yàn)樗皇菍?duì)象
1、export
模塊功能主要由兩個(gè)命令構(gòu)成:export和import闽撤,export命令用于規(guī)定模塊的對(duì)外接口得哆,import命令用于輸入其他模塊提供的功能
一般來(lái)說(shuō),一個(gè)模塊就是一個(gè)獨(dú)立的文件哟旗,該文件內(nèi)部的所有變量贩据,外部無(wú)法獲取,如果你希望外部能夠讀取模塊內(nèi)部的某個(gè)變量闸餐,就必須使用export關(guān)鍵字輸出該變量
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
如果要輸出函數(shù)饱亮,可以像下面這樣定義
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
上面的代碼中,我們使用了as對(duì)函數(shù)的對(duì)外接口進(jìn)行了重命名
2舍沙、import
使用export命令定義了模塊的對(duì)外接口以后近上,其他 JS 文件就可以通過(guò)import命令加載這個(gè)模塊
// main.js
import {firstName, lastName, year} from './profile.js';
function setName(element) {
element.textContent = firstName + ' ' + lastName;
}
import命令接受一對(duì)大括號(hào),里面指定要從其他模塊導(dǎo)入的變量名拂铡。大括號(hào)里面的變量名壹无,必須與被導(dǎo)入模塊(profile.js)對(duì)外接口的名稱相同
我們也可以對(duì)加載的模塊進(jìn)行重命名
import { lastName as surname } from './profile.js';
除了指定加載某個(gè)輸出值葱绒,還可以使用整體加載,即用星號(hào)(*
)指定一個(gè)對(duì)象斗锭,所有輸出值都加載在這個(gè)對(duì)象上面
下面是一個(gè)circle.js文件地淀,它輸出兩個(gè)方法area和circumference
// circle.js
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}
整體加載的寫(xiě)法如下
import * as circle from './circle';
console.log('圓面積:' + circle.area(4));
console.log('圓周長(zhǎng):' + circle.circumference(14));
這里有一個(gè)地方需要注意,模塊整體加載所在的那個(gè)對(duì)象(上例是circle
)岖是,應(yīng)該是可以靜態(tài)分析的帮毁,所以不允許運(yùn)行時(shí)改變,下面的寫(xiě)法都是不允許的
import * as circle from './circle';
// 下面兩行都是不允許的
circle.foo = 'hello';
circle.area = function () {};