什麼是模組
模組(module)是什麼呢? 模組是為了軟體封裝,複用。當今開源運動盛行,我們可以很方便地使用別人編寫好的模組,而不用自己從頭開始編寫。在程式設計中,我們一直強調避免重複造輪子(Don't Repeat Yourself,DRY)。
想象一下,沒有模組的日子,第三庫基本都是匯出一個全域性變數供開發者使用。例如jQuery
的$
,lodash
的_
。這些庫已經儘量避免了全域性變數衝突,只使用幾個全域性變數。但是還是不能避免有衝突,jQuery
還提供了noConflict
。更遑論我們自己編寫的程式碼。
最初,Javascript 中是沒有模組的概念的。這可能與一開始 Javascript 的定位有關。Javascript 最初只是希望給網頁增加動態元素,定位是簡單易用的指令碼。 但是,隨著網頁端功能越來越豐富,程式越來越龐大,軟體變得越來越難以維護。特別是隨著 NodeJs 的興起,Javascript 語言進入服務端程式設計領域。在編寫大型複雜的程式,模組更是必須品。
模組只是一個抽象概念,要想在實際程式設計中使用還需要規範。如果沒有規範,我有這種寫法,你用那種寫法,豈不是亂了套。
目前,模組的規範主要有3中,CommonJS模組、AMD模組和ES6模組。本文著重講解 CommonJS 模組(以 Node 實現為代表)和ES6模組。
2.CommonJS模組
CommonJS 其實是一個通用的 Javascript 語言規範,並不僅僅是模組的規範。Node 中的模組遵循 CommonJS 標準。
基本用法
Node 中提供了一個require
方法用來載入模組。例如:
var fs = require('fs');
fs.readFile('file1.txt', 'utf8', function (err, data) {
if (err) {
console.error(err);
} else {
console.log(data);
}
});
複製程式碼
匯入模組之後就可以使用模組中定義的介面了,如上例中的readFile
。
模組類別
在 Node 中大體上有3種模組,普通模組、核心模組和第三方模組。
普通模組與核心模組的匯入方式稍微有些區別。普通模組是我們自己編寫的模組,核心模組是 Node 提供的模組。上面我們使用的fs
就是核心模組。匯入普通模組時,需要在require
的引數中指定相對路徑。例如:
var myModule = require('./myModule');
myModule.func1();
複製程式碼
模組myModule
的字尾.js
字尾可以省略。
Node 將核心模組編譯進了引擎。匯入核心模組只需要指定模組名,Node 引擎直接查詢核心模組字典。
第三方模組的匯入也是指定模組名,但是模組的查詢方式有所不同。
首先,在專案目錄下的node_modules
目錄中查詢。
如果沒有找到,接著去專案目錄的父目錄中查詢。
直到找到載入該模組,或者到根目錄還未找到返回失敗。
定義模組
在我們日常的程式設計中,經常需要將一些功能封裝在一個模組中,方便自己或他人使用。在 Node 中定義模組的語法很簡單。模組單獨在一個檔案中,檔案中可以使用exports
匯出介面或變數。例如:
function addTwoNumber(a, b) {
return a + b;
}
exports.addTwoNumber = addTwoNumber;
複製程式碼
假設該模組在檔案myMath.js
中。在同一目錄下,我們可以這樣來使用:
var myMath = require('./myMath');
console.log(myMath.addTwoNumber(10, 20)); // 30
複製程式碼
模組匯出詳解
函式具體是怎麼匯出的呢?除了exports
,我們經常看到的module.exports
,__dirname
,__filename
是從哪裡來的?
在執行require
函式的時候,我們可以理解 Node 額外做了一些處理。
- 首先,將模組所在檔案內容讀出來。然後將這些內容包裹在一個函式中:
function _doRequire(module, exports, __filename, __dirname) {
// 模組檔案內容
}
複製程式碼
- 接下來,Node 引擎構造一個空的模組物件,給這個物件一個空的
exports
屬性,然後推算出__filename
(當前匯入的這個模組的全路徑檔名)和__dirname
(模組檔案所在路徑):
var module = {};
module.exports = {}
// __filename = ...
// __dirname = ...
複製程式碼
- 然後,呼叫第一步構造的那個函式,傳入引數:
_doRequire(module, module.exports, __filename, __dirname);
複製程式碼
- 最後
require
返回的是module.exports
的值。
按照上面的過程,我們可以很清楚地理解模組的匯出過程。並且也能很快地判斷一些寫法是否有問題:
錯誤寫法:
function addTwoNumber(a, b) {
return a + b;
}
exports = {
addTwoNumber: addTwoNumber;
}
複製程式碼
這種寫法為什麼不對?exports
實際上初始時是module.exports
的一個引用。給exports
賦一個新值後,module.exports
並沒有改變,還是指向空物件。最後返回的物件是module.exports
,沒有addTwoNumber
介面。
正確寫法:
function addTwoNumber(a, b) {
return a + b;
}
// 正確寫法一
exports.addTwoNumber = addTwoNumber;
// 正確寫法二
module.exports.addTwoNumber = addTwoNumber;
// 正確寫法三
module.exports = {
addTwoNumber: addTwoNumber
};
複製程式碼
exports
和module.exports
開始指向的是同一個物件。寫法一通過exports
設定屬性,同樣對module.exports
也可見。寫法二通過module.exports
設定屬性也可以匯出。
寫法三直接設定module.exports
就更不用說了。
建議在程式開發中,堅持一種寫法。個人覺得寫法三顯示設定相對較容易理解。
**有一點需要注意:不是隻有物件可以匯出,函式、類等值也可以。**例如下面就匯出了一個函式:
function addTwoNumber(a, b) {
return a + b;
}
module.exports = addTwoNumber;
複製程式碼
3.ES模組
ES6 在標準層面為 Javascript 引入了一套簡單的模組系統。ES6 模組完全可以取代 CommonJS 和 AMD 規範。當前熱門的開源框架 React 和 Vue 都已經使用了 ES6 模組來開發。
基本使用
ES6 模組使用export
匯出介面,import from
匯入需要使用的介面:
// myMath.js
export var pi = 3.14;
export function addTwoNumber(a, b) {
return a + b;
}
// 或
var pi = 3.14;
function addTwoNumber(a, b) {
return a + b;
}
export { pi, addTwoNumber };
複製程式碼
// main.js
import { addTwoNumber } from './myMath';
console.log(addTwoNumber(10, 20));
複製程式碼
在myMath.js
中通過export
匯出一個變數pi
和一個函式addTwoNumber
。上例中演示了兩種匯出方式。一種是一個個匯出,對每一個需要匯出的介面都應用一次export
。第二種是在檔案中某處集中匯出。當然,也可以混合使用這兩種方式。推薦使用第二種匯出方式,因為能在一處比較清楚的看出模組匯出了哪些介面。
ES6 模組特性
ES6 模組有一些需要了解和注意的特性。
靜態載入
ES6 模組最重要的特性是“靜態載入”,匯入的介面是隻讀的,不能修改。NodeJS 中的模組,是動態載入的。
靜態載入就是“編譯”時就已經確定了模組匯出,可以做到高效率,並且便於做靜態程式碼分析。同時,靜態載入也限制了模組的載入在檔案中所有語句之前,並且匯入語法中不能含有動態的語法結構(例如變數、if語句等)。
例如:
// 可以呼叫,因為模組載入是“編譯”時進行的。
funcA();
import { funcA, funcB } from './myModule';
// 錯誤,匯入語法中含有變數
var foo = './myModule';
import { funcA, funcB } from './myModule';
// 錯誤,在if語句中
if (foo == "myModule") {
import { funcA, funcB } from './myModule';
} else {
import { funcA, funcB } from './hisModule';
}
// 錯誤,匯出的介面是隻讀的,不能修改
import { funcA, funcB } from './myModule';
funcA = function () {};
複製程式碼
匯出的介面與模組中定義的變數或函式必須是一一對應的。而且模組內相應的值修改了,外部也能感知到。看下面程式碼:
// 錯誤,匯出值1,模組中沒有對應
export 1;
// 錯誤,實際上也是匯出1,模組中沒有對應
var m = 1;
export m;
// 可以這樣來匯出,匯出的m與模組中的變數m對應
export var m = 1;
// 可以這樣匯出
var m = 1;
export {m};
複製程式碼
var foo = "bar";
setTimeout(2000, () => { foo = "baz"});
// 2s後foo變為"baz",外部能感知到
複製程式碼
別名
在匯出模組時,可以為介面指定一個別名。這樣,後續可以修改內部介面而保持匯出介面不變。例如:
// myModule.js
var funcA = function () {
}
var funcB = function () {
}
export {
funcA as func1,
funcB as func2,
funcB as myFunc,
}
複製程式碼
上面我們匯出以別名func1
匯出函式funcA
,以別名func2
和myFunc
匯出函式funcB
。func2
和myFunc
都是指向同一個函式funcB
的。下面看看使用這個模組:
// main.js
import { func1, func2, myFunc } from './myModule';
複製程式碼
同樣的,匯入模組時也可以指定別名:
// main.js
import { func1 as func } from './myModule';
複製程式碼
default匯出
上面介紹的模組匯入必須知道介面名字。有時候,使用者學習一個模組時希望能夠快速上手,不想去看文件(怎麼會有這個懶的人?)。ES6 提供了default匯出。例如:
// myModule.js
export default function () {
console.log('hi');
}
// default匯出方式可以看做是匯出了一個別名為default的介面
var f = function () {
console.log('hi');
}
export { f as default };
複製程式碼
在外部匯入的時候,需要省略花括號:
// main.js
import func from './myModule';
func();
複製程式碼
也可以兩種方式,同時使用:
// myModule.js
function foo() {
console.log('foo');
}
export default foo;
function bar() {
console.log('bar');
}
export { bar };
複製程式碼
// main.js
import foo, { bar } from './myModule';
複製程式碼
整體載入
ES6 還允許一種整體載入的方式匯入模組。通過使用import *
可以匯入模組中匯出的所有介面:
// myModule.js
export function funcA() {
console.log('funcA');
}
export function funcB() {
console.log('funcB');
}
複製程式碼
// main.js
import * as m from './myModule';
m.funcA();
m.funcB();
複製程式碼
整體載入所在的那個物件(m
),應該是可以靜態分析的,所以不允許執行時改變。所以,下面的寫法都是不允許的:
// main.js
import * as m from './myModule';
// 錯誤
m.name = 'darjun';
m.func = function () {};
複製程式碼
Node 中使用 ES6 模組
Node 由於已經有 CommonJS 的模組規範了,與 ES6 模組不相容。為了使用 ES6 模組,Node 要求 ES6 模組採用.mjs
字尾名,而且檔案中只能使用import
和export
,不能使用require
。而且該功能還在試驗階段,Node v8.5.0以上版本,指定--experimental-modules
引數才能使用:
// myModule.mjs
var counter = 1;
export function incCounter() {
console.log('counter:', counter);
counter++;
}
複製程式碼
// main.mjs
import { incCounter } from './myModule';
incCounter();
複製程式碼
使用下面命令列執行程式:
$ node --experimental-modules main.mjs
複製程式碼
4.總結
隨著 Javascript 在大型專案中佔用舉足輕重的位置,模組的使用稱為必然。Node 中使用 CommonJS 規範。ES6 中定義了簡單易用高效的模組規範。ES6 規範化是個必然的趨勢,所以在掌握當前 CommonJS 規範的前提下,學習 ES6 模組勢在必行。