為什么要使用模塊化?
當(dāng)我們一個(gè)項(xiàng)目越做越大的時(shí)候,維護(hù)起來肯定沒那么方便妻率,且多人協(xié)作的去進(jìn)行開發(fā)蔚晨,當(dāng)中肯定會遇到很多的問題,例如:
- 方法的覆蓋: 很有可能你定義的一些函數(shù)會覆蓋公共類中同名的函數(shù)咖杂,因?yàn)槟憧赡芨揪筒恢拦差愔杏心男┖瘮?shù)庆寺,也不知道是如何命名的。
- 這些公共的組件: 但是你又不知道這些組件又會依賴哪些模塊诉字,同時(shí)在維護(hù)這些公共方法的時(shí)候懦尝,會新增一些依賴或者刪除一些依賴,那么每個(gè)引入這些公共方法的地方都需要去對應(yīng)的新增或者刪除壤圃。等等陵霉,還會存在很多的問題。
我們使用模塊化就是為了讓各個(gè)模塊之間相對獨(dú)立伍绳,可能每個(gè)文件就是一個(gè)功能塊踊挠,能滿足于某項(xiàng)特定的功能,這樣我們在引用某項(xiàng)功能的時(shí)候就會很方便冲杀。
CommonJS
Node 應(yīng)用由模塊組成效床,采用 CommonJS 模塊規(guī)范。
每個(gè)文件就是一個(gè)模塊权谁,有自己的作用域剩檀。在一個(gè)文件里面定義的變量、函數(shù)旺芽、類沪猴,都是私有的,對其他文件不可見,CommonJS規(guī)范加載模塊是同步的采章,也就是說运嗜,加載完成才可以執(zhí)行后面的操作,Node.js主要用于服務(wù)器編程共缕,模塊一般都是存在本地硬盤中洗出,加載比較快,所以Node.js采用CommonJS規(guī)范图谷。且CommonJS模塊輸出的是值的緩存, 運(yùn)行時(shí)加載翩活。
CommonJS規(guī)范規(guī)定阱洪,每個(gè)模塊內(nèi)部,module變量代表當(dāng)前模塊菠镇。這個(gè)變量是一個(gè)對象冗荸,它的exports屬性(即module.exports)是對外的接口。加載某個(gè)模塊利耍,其實(shí)是加載該模塊的module.exports屬性蚌本。
// tools.ts
const add = (a: number, b: number) =>{
return a + b
}
const reduce = (a: number, b: number) => {
return a - b
}
const multy = (a: number, b: number) => {
return a * b
}
exports.add = add
exports.reduce = reduce
export default multy
// 等價(jià)于
module.exports = {
add,
reduce
}
// app.ts
const tools = require('./tools.ts')
tools.add(2, 3) // 5
tools.reduce(3, 2) // 1
Node內(nèi)部提供一個(gè)Module構(gòu)建函數(shù)。所有模塊都是Module的實(shí)例隘梨。
// Module 構(gòu)造函數(shù)
function Module(id, parent) {
this.id = id //模塊的識別符程癌,通常是帶有絕對路徑的模塊文件名
this.exports = {} //表示模塊對外輸出的值
this.parent = parent //返回一個(gè)對象,表示調(diào)用該模塊的模塊
...
}
// tools.ts
const add = (a: number, b: number) =>{
return a + b
}
exports.add = add
console.log(module)
// 輸出
Module {
id: '.',
exports: { add: [Function: add] },
parent: null,
filename: 'C:\\Users\\viruser.v-desktop\\Desktop\\zz\\aa.js', //模塊的文件名轴猎,帶有絕對路徑
loaded: false, //返回一個(gè)布爾值嵌莉,表示模塊是否已經(jīng)完成加載
children: [], //返回一個(gè)數(shù)組,表示該模塊要用到的其他模塊
paths: [
'C:\\Users\\viruser.v-desktop\\Desktop\\zz\\node_modules',
'C:\\Users\\viruser.v-desktop\\Desktop\\node_modules',
'C:\\Users\\viruser.v-desktop\\node_modules',
'C:\\Users\\node_modules',
'C:\\node_modules'
]
}
如果在命令行下調(diào)用某個(gè)模塊捻脖,比如node tools.js锐峭,那么module.parent就是null。如果是在腳本之中調(diào)用可婶,比如require('./tools.js')沿癞,那么module.parent就是調(diào)用它的模塊。利用這一點(diǎn)矛渴,可以判斷當(dāng)前模塊是否為入口腳本椎扬。
if (!module.parent) {
// ran with `node something.js`
app.listen(8088, function() {
console.log('app listening on port 8088')
})
} else {
// used with `require('/.something.js')`
module.exports = app
}
為了方便,Node為每個(gè)模塊提供一個(gè)exports變量曙旭,指向module.exports盗舰。這等同在每個(gè)模塊頭部晶府,有一行這樣的命令桂躏。造成的結(jié)果是,在對外輸出模塊接口時(shí)川陆,可以向exports對象添加方法剂习。注意,不能直接將exports變量指向一個(gè)值较沪,因?yàn)檫@樣等于切斷了exports與module.exports的聯(lián)系鳞绕。
const exports = module.exports
AMD
大多數(shù)的同學(xué)都應(yīng)該了解RequireJS,而且RequireJS是基于AMD規(guī)范的尸曼。AMD是"Asynchronous Module Definition"的縮寫们何,意思就是"異步模塊定義"。它采用異步方式加載模塊控轿,模塊的加載不影響它后面語句的運(yùn)行冤竹。所有依賴這個(gè)模塊的語句拂封,都定義在一個(gè)回調(diào)函數(shù)中,等到加載完成之后鹦蠕,這個(gè)回調(diào)函數(shù)才會運(yùn)行冒签。且同樣是運(yùn)行時(shí)加載的,用require.config()指定引用路徑等钟病,用define()定義模塊萧恕,用require()加載模塊, 但是不同于CommonJS,它要求兩個(gè)參數(shù):
定義模塊
// define([module], callback)
define(['myLib'], () =>{
function foo(){
console.log('mylib')
}
return {
foo : foo
}
})
require.config({
baseUrl: "js/lib",
paths: {
"jquery": "jquery.min", //實(shí)際路徑為js/lib/jquery.min.js
"underscore": "underscore.min",
}
});
使用模塊
// require([module], callback)
require(['myLib'], mod => {
mod.foo()
})
// myLib
為什么要使用AMD規(guī)范呢?
因?yàn)锳MD是專門為瀏覽器中js環(huán)境設(shè)計(jì)的規(guī)范肠阱。它吸取了CommonJS的一些優(yōu)點(diǎn)票唆,但是沒有全部都照搬過來。也是非常容易上手屹徘。
CMD
CMD在很多地方和AMD有相似之處惰说,在這里我只說兩者的不同點(diǎn)。首先缘回,CMD規(guī)范和CommonJS規(guī)范是兼容的吆视,相比AMD,它簡單很多酥宴。遵循CMD規(guī)范的模塊啦吧,可以在Node.js中運(yùn)行。SeaJS是推薦是用CMD的寫法拙寡,那么就使用SeaJS來編寫一個(gè)簡單的例子:
// AMD寫法 AMD的依賴需要前置書寫
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) {
// 等于在最前面聲明并初始化了要用到的所有模塊
a.doSomething()
if (false) {
// 即便沒用到某個(gè)模塊 b授滓,但 b 還是提前執(zhí)行了
b.doSomething()
}
})
// CMD寫法 CMD的依賴就近書寫即可,不需要提前聲明
define(function(require, exports, module) {
var a = require('./a') //在需要時(shí)申明 同步
a.doSomething()
if (false) {
var b = require('./b')
b.doSomething()
}
require.async('a', math =>{ //異步
a.add(1, 2);
})
})
/** sea.js **/
// 定義模塊 math.js
define(function(require, exports, module) {
var $ = require('jquery.js')
var add = function(a,b){
return a+b
}
exports.add = add
})
// 加載模塊
seajs.use(['math.js'], function(math){
var sum = math.add(1+2)
})
CMD規(guī)范我們可以發(fā)現(xiàn)其API職責(zé)專一肆糕,例如同步加載和異步加載的API都分為require和require.async般堆,而AMD的API比較多功能。
UMD
UMD是AMD和CommonJS的糅合
AMD模塊以瀏覽器第一的原則發(fā)展诚啃,異步加載模塊淮摔。
CommonJS模塊以服務(wù)器第一原則發(fā)展,選擇同步加載始赎,它的模塊無需包裝(unwrapped modules)和橙。
這迫使人們又想出另一個(gè)更通用的模式UMD (Universal Module Definition)。希望解決跨平臺的解決方案造垛。
UMD先判斷是否支持Node.js的模塊(exports)是否存在魔招,存在則使用Node.js模塊模式。
在判斷是否支持AMD(define是否存在)五辽,存在則使用AMD方式加載模塊办斑。
(function (window, factory) {
if (typeof exports === 'object') {
module.exports = factory()
} else if (typeof define === 'function' && define.amd) {
define(factory)
} else {
window.eventUtil = factory()
}
})(this, function () {
//module ...
})
ESModule
歷史上,JavaScript一直沒有模塊(module)體系杆逗,無法將一個(gè)大程序拆分成互相依賴的小文件乡翅,再用簡單的方法拼裝起來吁讨。其他語言都有這項(xiàng)功能,比如Ruby的 require 峦朗、Python的 import 建丧,甚至就連CSS都有 @import ,但是JavaScript任何這方面的支持都沒有波势,這對開發(fā)大型的翎朱、復(fù)雜的項(xiàng)目形成了巨大障礙。
ES6模塊的設(shè)計(jì)思想尺铣,是盡量的靜態(tài)化拴曲,使得編譯時(shí)就能確定模塊的依賴關(guān)系,以及輸入和輸出的變量凛忿。CommonJS和AMD模塊澈灼,都只能在運(yùn)行時(shí)確定這些東西。比如店溢,CommonJS模塊就是對象叁熔,輸入時(shí)必須查找對象屬性。
模塊功能主要由兩個(gè)命令構(gòu)成: export 和 import床牧。export 命令用于規(guī)定模塊的對外接口,import 命令用于輸入其他模塊提供的功能
// a.js
var num1 = 1
var num2 = 2
export { num1, num2 }
// b.js
import { num1, num2 } from './a.js'
function add(num1, num2) {
return num1 + num2
}
console.log(add(num1, num2))
如果想為輸入的變量重新取一個(gè)名字荣回,import命令要使用 as 關(guān)鍵字,將輸入的變量重命名戈咳。
import { num1 as snum } from './a'
mport 命令具有提升效果心软,會提升到整個(gè)模塊的頭部
add();
import { add} from './tools'
如果在一個(gè)模塊之中,先輸入后輸出同一個(gè)模塊著蛙, import 語句可以與 export 語句寫在一起删铃。
export { es6 as default } from './a'
// 等同于
import { es6 } from './a'
export default es6
還可以使用整體加載,即用星號( * )指定一個(gè)對象踏堡,所有輸出值都加載在這個(gè)對象上面, 但不包括default
import * as tools from './a'
import multy from './a
tools.add(2, 1) //3
tools.reduce(2, 1) //1
multy(2,1) //2
ES6模塊加載的實(shí)質(zhì)
ES6模塊加載的機(jī)制猎唁,與CommonJS模塊完全不同。CommonJS模塊輸出的是一個(gè)值的拷貝暂吉,而ES6模塊輸出的是值的引用胖秒。
CommonJS模塊輸出的是被輸出值的拷貝,也就是說慕的,一旦輸出一個(gè)值,模塊內(nèi)部的變化就影響不到這個(gè)值
// lib.js
var counter = 3
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
}
// main.js
var mod = require('./lib')
console.log(mod.counter) // 3
mod.incCounter()
console.log(mod.counter) // 3
lib.js 模塊加載以后挤渔,它的內(nèi)部變化就影響不到輸出的 mod.counter 了肮街。這是因?yàn)?mod.counter 是一個(gè)原始類型的值,會被緩存判导。除非寫成一個(gè)函數(shù)嫉父,才能得到內(nèi)部變動(dòng)后的值
// lib.js
var counter = 3
function incCounter() {
counter++
}
module.exports = {
get counter() {
return counter
},
incCounter: incCounter,
}
// main.js
var mod = require('./lib')
console.log(mod.counter) // 3
mod.incCounter()
console.log(mod.counter) // 4
ES6模塊的運(yùn)行機(jī)制與CommonJS不一樣沛硅,它遇到模塊加載命令 import 時(shí),不會去執(zhí)行模塊绕辖,而是只生成一個(gè)動(dòng)態(tài)的只讀引用摇肌。等到真的需要用到時(shí),再到模塊里面去取值仪际,換句話說围小,ES6的輸入有點(diǎn)像Unix系統(tǒng)的”符號連接“,原始值變了树碱,import 輸入的值也會跟著變肯适。因此,ES6模塊是動(dòng)態(tài)引用成榜,并且不會緩存值框舔,模塊里面的變量綁定其所在的模塊。