1.JS Module Systems
概述
隨着JS使用越來越普及,導致namespace
以及depedencies
變得難以維護。因此,爲了解決這類問題就開發處理不同的模塊系統。
JS Modules的必要性
如果有其他平臺開發的經驗,那麼可能對封裝
和依賴
比較容易理解。
在項目中引入一個新的代碼塊的話,那就要求新加入的代碼塊不會影響到原有項目的正常運行。
例如,在
C
中,通過添加前綴的方式,進行區分
#ifndef MYLIB_INIT_H
#define MYLIB_INIT_H
enum mylib_init_code {
mylib_init_code_success,
mylib_init_code_error
};
enum mylib_init_code mylib_init(void);
// (...)
#endif //MYLIB_INIT_H
封裝,是一種有效的解決衝突的手段。
傳統的JS客戶端開發中,依賴關係都是隱含的。換句話說,開發者需要手動控制代碼的引入順序(確保正確的依賴關係)。
例如,在
Backbone.js
中,必須手動控制模塊的加載順序。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Backbone.js Todos</title>
<link rel="stylesheet" href="todos.css"/>
</head>
<body>
<script src="../../test/vendor/json2.js"></script>
<script src="../../test/vendor/jquery.js"></script>
<script src="../../test/vendor/underscore.js"></script>
<script src="../../backbone.js"></script>
<script src="../backbone.localStorage.js"></script>
<script src="todos.js"></script>
</body>
<!-- (...) -->
</html>
當前,隨着JS開發越來越複雜化,依賴管理也變得難以維護。那麼,我們應當如何保證正確的加載順序呢?
JS Module Systems, 就是爲開發者們解決這類問題而開發的。
舊模塊處理方式
目前的模塊系統都是最新的概念,在這之前,定義模塊的方式如下:
let myModule = (function(){
let _uid = '123456'
function _fn() {
console.log('private fn',_uid)
}
function publicFn(uid) {
_uid = uid
}
function getUid () {
_fn()
}
return {
setUid: publicFn,
getUid: getUid
}
})()
myModule.setUid('345')
參考資料:JS設計模式
上面的例子中,返回了一個“Dictionary”,暴露了兩個公有方法。而沒有暴露出的變量和方法就成爲了私有屬性,外部禁止訪問。
JS變量作用域都是在Function
定義的{}(塊級作用域)內。
即:在函數體內聲明的任何變量,都無法脫離該函數聲明的作用域。
利用上面這個特性,可以揭示模塊模式是利用了函數來封裝私有屬性和方法。
這種特性在模塊依賴方面卻沒起多大作用,但是,功能完善的模塊系統,將會解決這一問題。
優點
- 簡潔並能在任何地方被引入(無需任何庫、不依賴任何語言)
- 在單個文件中定義多個module
缺點
- 無法使用編程方式導入模塊(除非使用eval)
- 手動處理模塊依賴
- 不能異步加載模塊
- 可能導致環形依賴
- 靜態編譯分析較難
2. CommonJs
概述
CommonJs 項目最初旨在定義一系列規範去幫助開發服務端應用的開發者。CommonJs 團隊嘗試的其中一個領域就是modules
,對應的API都是同步的。Node.js在最初嘗試使用CommonJs規範,後來決定不再遵循。但是,當講到modules時,Node.js深受影響。
const Square = require('./square.js');
const mySquare = new Square(2);
console.log(`The area of mySquare is ${mySquare.area()}`);
Node.js模塊系統抽象爲一種庫的形式。它關聯了Node.js模塊和CommonJs的差異。
在Node和CommonJs模塊中,兩者都有基本的兩個元素進行關聯:require
&exports
。
require
可以在當前模塊中,導入其他模塊代碼的符號。
- 傳遞給requrie中的參數,是模塊的id
- 在
node_modules
中,它是模塊的名稱(如果不存在,則爲它的路徑)
exports
是一個特殊的對象:任何元素(屬性或方法)存放入該對象中,將會暴露爲一個公共元素,並且保留字段名稱。
require vs exports
Node.js和CommonJs一個獨特的區別在於module.exports
對象。
在Node中,只能通過module.exports
纔會導出預構造對象。
// This won't work.
exports = (params) => {
return {
attr: () => {params + 1}
}
}
// This works as expected.
module.exports = (params) => {
return {
attr: () => {params + 1}
}
}
- 在Node.js中,
module.exports
是導出的真正對象,而exports
只是默認綁定到module.exports
上的變量。 - 在CommonJs中,並沒有定義
module.exports
對象。
優點
- 通俗易懂:開發者可以不用看文檔就可以理解概念
- 集成的依賴管理:模塊間的引用都是按需順序加載
require
可以在任何地方使用: module可以被編程化加載- 支持循環依賴
缺點
- 同步API不適用某些場景(client-side)
- 每個模塊對應單獨一個文件
- 瀏覽器需要加載庫或轉換
- 模塊沒有構造函數(儘管Node支持)
- 靜態編譯較難
3.Asynchronous Module Definition(AMD)
AMD 源於對CommonJs不滿的一批開發者。兩者最大的區別在於,AMD支持異步加載(asynchronous module loading)。
// 通過依賴數組或者工廠函數調用define
define(['dep1','dep2'], function(dep1,dep2){
// 通過返回值來定義module value
return function() {}
})
// Or
define(function(require) {
let dep1 = require('dep1')
dep2 = require('dep2')
return function () {}
})
在JS中,可以使用閉包來實現異步加載模塊:模塊請求加載完成時,再調用該函數。
模塊定義和模塊導入都有同一個函數進行表示:當定義一個模塊時,它的依賴關係就是明確的。
AMD加載模塊時,可以在運行時獲得給定項目的依賴關係圖。因此,可以在加載模塊的同時,將彼此不依賴的庫進行加載。
這對於瀏覽器來說十分重要,因爲啓動時間對於用戶良好的體驗有着至關重要。
優點
- 異步加載(快速啓動)
- 支持循環引用
- 支持
require
和exports
- 完全繼承依賴管理
- 模塊可以被拆分多個文件
- 支持構造
- 支持插件
缺點
- 語法稍微複雜
- 除非已編譯,否則需要加載程序庫
- 靜態編譯困難
在客戶端開發中,有兩種主流的模塊加載方式:
Webpack
和Browserify
。
Browserify
希望開發一套解析類似Node.js定義的模塊(許多Node包與它一起開箱即用),並且,將原有代碼和來自這些模塊中的代碼捆綁在一個包含所有依賴項的文件中。
Webpack
was developed to handle creating complex pipelines of source transformations before publishing. This includes bundling together CommonJS modules.
4.ES2015 Modules
幸運的是,ES Team決定討論發佈一套關於JS的模塊系統。
最終結果就是我們熟知的ES6(ES2015),它的語法很好的兼容了同步和異步的模塊操作。
// lib.js
export const sqrt = Math.sqrt
export function square (x) {
return x * x * x
}
///////////////
import { sqrt , square} from 'lib'
console.log('sqrt',sqrt(3)) // 9
console.log('square',square(2)) // 4
import
該指令是將模塊引入到namespace。和require
&define
指令定義相反,它無法動態引入。
export
該指令使暴露出的元素公有化。
草案中規範,ES2015並不支持動態加載模塊。
In practice, ES2015 implementations are not required to do anything after parsing these directives. Module loaders such as System.js are still required. A draft specification for browser module loading is available.
優點
- 同時支持同步和異步加載
- 語法簡單
- 支持靜態分析工具
- 支持動態分析工具
- 集成到JS中
- 循環依賴
缺點
- 不支持 run everywhere
遺憾的是,沒有一個主要的JavaScript運行時支持ES2015模塊。這意味着Firefox,Chrome或Node.js不支持。
幸運的是,許多轉發器都支持模塊,也可以使用polyfill。目前,Babel的ES2015預設可以處理模塊。
6. module.export vs exports
module.exports.method = function () {...}
//vs
export.method = function () {...}
6.1 module.exports
簡單示例
// calculator.js
module.exports.add = (a,b) => a+b
// use-calculator.js
const calc = require('./calculator.js')
calc.add(2,3) // 5
- 視爲從require()調用,並返回模塊對象的引用
- 是由Node.js創建
- 它只是引用一個JS空對象
- 默認是一個空對象,並且可以添加任意值
6.1.1 用法
- 爲模塊添加公共方法
- 繼承對象
如何理解繼承?
// 導出一個class實例
// calc.js
module.exports = class Calculator {
add(a,b){return a+b}
}
// 繼承該實例,並導出新的class
// calc-advance.js
const Calc = require('./calc.js')
class Advancec extends Calc {
sub(a,b) { return a-b}
}
module.exports = new Advancec()
用法
const calc = require('./calc-advance')
calc.add(2,3) // 5
calc.sub(3,5) // -2
6.1.2 module對象
module對象是指當前的模塊。
它的每一個模塊都是本地的和私有的。(只允許從module中訪問)
// calc.js
module.exports = (a,b) => a + b
console.log(module)
6.2 exports
exports
只是一箇中間變量,可以幫助模塊開發者寫更少的代碼- 推薦使用它的屬性,是安全的。(eg:exports.add = function …)
exports
不會通過require()
返回任何值
下面有一些正確和錯誤的範例:
// good
module.exports = {
add(a,b) { return a+b }
}
// good
module.exports.add = (a,b) => a+b
// valid
exports = module.exports
// bad
exports = {
add (a,b) { return a+b }
}
通常, 我們是將module.exports
替換爲一個對象或者自定義函數。(good example)
其中,exports = module.exports
是方便我們瞭解模塊中的屬性和方法。
結論
exports
變量可能只有部分方法導出,這一點對於Node.js新手來說有一些困惑,即使是Node官方文檔也有這種聲明。
拓展:
如何理解下面的代碼:
module.exports = exports = nano = (a,v) a*v
我們可以設想,在文件的起始處默認進行下面的聲明:
// hidden define
const module = new Module()
const exports = module.exports
// ....
exports 可以理解爲指向module.exports
的對象,而只有module.exports
纔會返回值。