爲什麼要使用模塊化?
當我們一個項目越做越大的時候,維護起來肯定沒那麼方便,且多人協作的去進行開發,當中肯定會遇到很多的問題,例如:
- 方法的覆蓋: 很有可能你定義的一些函數會覆蓋公共類中同名的函數,因爲你可能根本就不知道公共類中有哪些函數,也不知道是如何命名的。
- 這些公共的組件: 但是你又不知道這些組件又會依賴哪些模塊,同時在維護這些公共方法的時候,會新增一些依賴或者刪除一些依賴,那麼每個引入這些公共方法的地方都需要去對應的新增或者刪除。等等,還會存在很多的問題。
我們使用模塊化就是爲了讓各個模塊之間相對獨立,可能每個文件就是一個功能塊,能滿足於某項特定的功能,這樣我們在引用某項功能的時候就會很方便。
CommonJS
Node 應用由模塊組成,採用 CommonJS 模塊規範。
每個文件就是一個模塊,有自己的作用域。在一個文件裏面定義的變量、函數、類,都是私有的,對其他文件不可見,CommonJS規範加載模塊是同步的,也就是說,加載完成纔可以執行後面的操作,Node.js主要用於服務器編程,模塊一般都是存在本地硬盤中,加載比較快,所以Node.js採用CommonJS規範。且CommonJS模塊輸出的是值的緩存, 運行時加載。
CommonJS規範規定,每個模塊內部,module變量代表當前模塊。這個變量是一個對象,它的exports屬性(即module.exports)是對外的接口。加載某個模塊,其實是加載該模塊的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
// 等價於
module.exports = {
add,
reduce
}
// app.ts
const tools = require('./tools.ts')
tools.add(2, 3) // 5
tools.reduce(3, 2) // 1
Node內部提供一個Module構建函數。所有模塊都是Module的實例。
// Module 構造函數
function Module(id, parent) {
this.id = id //模塊的識別符,通常是帶有絕對路徑的模塊文件名
this.exports = {} //表示模塊對外輸出的值
this.parent = parent //返回一個對象,表示調用該模塊的模塊
...
}
// 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, //返回一個布爾值,表示模塊是否已經完成加載
children: [], //返回一個數組,表示該模塊要用到的其他模塊
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'
]
}
如果在命令行下調用某個模塊,比如node tools.js,那麼module.parent就是null。如果是在腳本之中調用,比如require(’./tools.js’),那麼module.parent就是調用它的模塊。利用這一點,可以判斷當前模塊是否爲入口腳本。
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爲每個模塊提供一個exports變量,指向module.exports。這等同在每個模塊頭部,有一行這樣的命令。造成的結果是,在對外輸出模塊接口時,可以向exports對象添加方法。注意,不能直接將exports變量指向一個值,因爲這樣等於切斷了exports與module.exports的聯繫。
const exports = module.exports
AMD
大多數的同學都應該瞭解RequireJS,而且RequireJS是基於AMD規範的。AMD是"Asynchronous Module Definition"的縮寫,意思就是"異步模塊定義"。它採用異步方式加載模塊,模塊的加載不影響它後面語句的運行。所有依賴這個模塊的語句,都定義在一個回調函數中,等到加載完成之後,這個回調函數纔會運行。且同樣是運行時加載的,用require.config()指定引用路徑等,用define()定義模塊,用require()加載模塊, 但是不同於CommonJS,它要求兩個參數:
定義模塊
// define([module], callback)
define(['myLib'], () =>{
function foo(){
console.log('mylib')
}
return {
foo : foo
}
})
require.config({
baseUrl: "js/lib",
paths: {
"jquery": "jquery.min", //實際路徑爲js/lib/jquery.min.js
"underscore": "underscore.min",
}
});
使用模塊
// require([module], callback)
require(['myLib'], mod => {
mod.foo()
})
// myLib
爲什麼要使用AMD規範呢?
因爲AMD是專門爲瀏覽器中js環境設計的規範。它吸取了CommonJS的一些優點,但是沒有全部都照搬過來。也是非常容易上手。
CMD
CMD在很多地方和AMD有相似之處,在這裏我只說兩者的不同點。首先,CMD規範和CommonJS規範是兼容的,相比AMD,它簡單很多。遵循CMD規範的模塊,可以在Node.js中運行。SeaJS是推薦是用CMD的寫法,那麼就使用SeaJS來編寫一個簡單的例子:
// AMD寫法 AMD的依賴需要前置書寫
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) {
// 等於在最前面聲明並初始化了要用到的所有模塊
a.doSomething()
if (false) {
// 即便沒用到某個模塊 b,但 b 還是提前執行了
b.doSomething()
}
})
// CMD寫法 CMD的依賴就近書寫即可,不需要提前聲明
define(function(require, exports, module) {
var a = require('./a') //在需要時申明 同步
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規範我們可以發現其API職責專一,例如同步加載和異步加載的API都分爲require和require.async,而AMD的API比較多功能。
UMD
UMD是AMD和CommonJS的糅合
AMD模塊以瀏覽器第一的原則發展,異步加載模塊。
CommonJS模塊以服務器第一原則發展,選擇同步加載,它的模塊無需包裝(unwrapped modules)。
這迫使人們又想出另一個更通用的模式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)體系,無法將一個大程序拆分成互相依賴的小文件,再用簡單的方法拼裝起來。其他語言都有這項功能,比如Ruby的 require 、Python的 import ,甚至就連CSS都有 @import ,但是JavaScript任何這方面的支持都沒有,這對開發大型的、複雜的項目形成了巨大障礙。
ES6模塊的設計思想,是儘量的靜態化,使得編譯時就能確定模塊的依賴關係,以及輸入和輸出的變量。CommonJS和AMD模塊,都只能在運行時確定這些東西。比如,CommonJS模塊就是對象,輸入時必須查找對象屬性。
模塊功能主要由兩個命令構成: export 和 import。export 命令用於規定模塊的對外接口,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))
如果想爲輸入的變量重新取一個名字,import命令要使用 as 關鍵字,將輸入的變量重命名。
import { num1 as snum } from './a'
mport 命令具有提升效果,會提升到整個模塊的頭部
add();
import { add} from './tools'
如果在一個模塊之中,先輸入後輸出同一個模塊, import 語句可以與 export 語句寫在一起。
export { es6 as default } from './a'
// 等同於
import { es6 } from './a'
export default es6
還可以使用整體加載,即用星號( * )指定一個對象,所有輸出值都加載在這個對象上面, 但不包括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模塊加載的實質
ES6模塊加載的機制,與CommonJS模塊完全不同。CommonJS模塊輸出的是一個值的拷貝,而ES6模塊輸出的是值的引用。
CommonJS模塊輸出的是被輸出值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值
// 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 模塊加載以後,它的內部變化就影響不到輸出的 mod.counter 了。這是因爲 mod.counter 是一個原始類型的值,會被緩存。除非寫成一個函數,才能得到內部變動後的值
// 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模塊的運行機制與CommonJS不一樣,它遇到模塊加載命令 import 時,不會去執行模塊,而是隻生成一個動態的只讀引用。等到真的需要用到時,再到模塊裏面去取值,換句話說,ES6的輸入有點像Unix系統的”符號連接“,原始值變了,import 輸入的值也會跟着變。因此,ES6模塊是動態引用,並且不會緩存值,模塊裏面的變量綁定其所在的模塊。