之前 JS 主要用在瀏覽器,它是沒有模塊系統的。若是咱們作的項目有點大,那麼管理項目的依賴就很是困難,好比 A
依賴 B
和 C
,而 C
和 B
有依賴其餘的庫。這時人爲的用script
標籤的前後順序來讓項目依賴正常是很是困難的。這時就有了不少解決方案。javascript
CommonJS 是以在瀏覽器環境以外構建 JavaScript 生態系統爲目標而產生的項目,好比在服務器和桌面環境中。html
好比nodejs
就是用的 CommonJS 規範,可是它也沒徹底接受規範。java
CommonJS 中,每一個文件就是一個模塊,有本身的做用域。在一個文件裏面定義的變量、函數、類,都是私有的,對其餘文件不可見。node
每一個模塊(文件)內部都有一個 module 對象,它有如下屬性。react
id
模塊的識別符,一般是帶有絕對路徑的模塊文件名。exports
表示模塊對外輸出的值。parent
一個對象,表示調用該模塊的模塊。(沒有則是undefined
)filename
模塊的文件名,帶有絕對路徑。loaded
一個布爾值,表示模塊是否已經完成加載。children
一個數組,表示該模塊要用到的其餘模塊paths
一個數組是nodejs
引入庫文件的絕對路徑(node_modules)通常nodejs
主文件會用到parent
屬性。jquery
if (module.parent == null) { // 表示不是被當爲庫應用,而是直接運行
// 運行程序
}
複製代碼
exports
屬性通常用來對外暴露接口。webpack
module.exports = function () {} // 暴露一個函數
// --------------
module.exports = {} // 暴露一個對象
// ...
複製代碼
除了使用module.exports
對外暴露接口,還可使用exports
對外面暴露。git
exports.area = function (r) { // 暴露是一個對象,它有一個 area 方法
return Math.PI * r * r;
};
複製代碼
須要注意,不能將exports
直接指向一個值。es6
exports = function(x) {console.log(x)};
// 無效
// 由於 exports 至關於
var exports = module.exports
複製代碼
它使用require
函數導入模塊。github
// a.js
module.exports = { data: 100 }
// b.js
var a = require('./a') // 後面的 js 能夠省略
console.log(a) // { data: 100 }
// 也能夠用下 es6 寫法
let { data } = require('./a.js')
複製代碼
require
參數是一個路徑字符串,
/
表示加載的是一個位於絕對路徑的模塊文件。./
表示加載的是一個位於相對路徑(跟當前執行腳本的位置相比)的模塊文件。module.paths
屬性查找node_modules
文件夾中的第三方庫文件。若是沒有帶後綴 nodejs 會以.js、.json、.node
的順序查找模塊文件。
require
上也有一些屬性和方法。
resolve()
獲得require
命令加載的確切文件名。cache
nodejs 會將已經加載過的模塊緩存起來,方便下次加載,已經緩存的模塊就在這個對象中,可使用delete require.cache[moduleName]
刪除緩存。main
屬性用來判斷模塊是直接執行,仍是被調用執行。直接執行的時候(node module.js),require.main屬性指向模塊自己require.main === module // true
。extensions
函數數組,根據文件的後綴名,調用不一樣的執行函數nodejs 中模塊的代碼至關於寫在下面這個函數中。
(function (exports, require, module, __filename, __dirname) {
// 代碼
});
複製代碼
若是發生模塊的循環加載(A加載B,B又加載A),則B將加載A的不完整版本。
// a.js
exports.x = 'a1';
console.log('a.js ', require('./b.js').x);
exports.x = 'a2';
// b.js
exports.x = 'b1';
console.log('b.js ', require('./a.js').x);
exports.x = 'b2';
// main.js
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);
/* b.js a1 a.js b2 main.js a2 main.js b2 */
複製代碼
首先是 Asynchronous Module Definition 規範,它在適合在瀏覽器環境中異步加載模塊,而且能夠併發的加載。
它主要只有一個接口define(id?, dependencies[]?, factory)
函數。它只要有三個參數,前兩個可選,它用來定義一個模塊。
id
模塊名,不推薦使用,通常被工具自動生成。
dependencies
是一個字符串數組,當前這個模塊要依賴的模塊。
factory
是函數或者對象,若是是對象,那麼這個對象就是向外暴露的值,若是是一個函數那麼這個函數的返回值就是對外暴露的值。若是dependencies
參數爲空,那麼這個函數的參數默認是require
, exports
和 module
。
define({
color: "black",
size: "unisize"
});
// 等同於
define(function () {
return {
color: "black",
size: "unisize"
}
});
// ---------------------
// my/shirt.js 文件
define(["./cart", "./inventory"], function(cart, inventory) {
// cart 和 inventory 於 shirt 同一個文件
// 返回一個對象定義 my/shirt 模塊
return {
color: "blue",
size: "large",
addToCart: function() {
inventory.decrement(this);
cart.add(this);
}
}
}
);
// -------------
define(function(require, exports, module) {
var a = require('a'),
b = require('b');
// 依賴的 a 和 b 模塊
// 這其實就是 CommonJS 規範的寫法
return function () {};
}
);
複製代碼
requirejs 是對 AMD 的具體實現。
有了它 HTML 只須要引入它一個文件。
<!DOCTYPE html>
<html>
<head>
<script data-main="app" src="lib/require.js"></script>
<!-- 建議放在 head 中 -->
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
複製代碼
script
上的data-main
是一個特殊屬性,經過它能夠找到 requirejs 的配置文件。
服務器上的目錄
www
app 項目代碼
main.js
lib 庫
require.js
jquery.js
app.js 配置文件
index.html 上面 html 文件
複製代碼
app.js 配置文件
requirejs.config({
baseUrl: 'lib', // 默認狀況下加載 www/lib 下的模塊 id
paths: {
app: '../app' // 可是一旦模塊以 app 開頭那麼,就加載 app 文件夾中的文件
}
});
// 開始加載項目主文件
requirejs(['app/main']);
複製代碼
多頁面時
page1.html(page2.html 和 page1.html 相似。)
<!DOCTYPE html>
<html>
<head>
<title>Page 1</title>
<script src="js/lib/require.js"></script>
<script> // 加載 js 下的 common 配置文件。 requirejs(['./js/common'], function (common) { // 配置文件加載好調用 // 由於配置文件中設置了路徑因此能夠直接用 app/main1 無需加 js requirejs(['app/main1']); }); </script>
</head>
<body>
<a href="page2.html">Go to Page 2</a>
</body>
</html>
複製代碼
配置文件
requirejs.config({
baseUrl: 'js/lib',
paths: {
app: '../app'
},
shim: {
// 爲不使用 define() 聲明依賴項並設置模塊值的舊的傳統「瀏覽器全局」腳本配置依賴項
// 導出和自定義初始化。
backbone: {
deps: ['jquery', 'underscore'],
exports: 'Backbone'
},
underscore: {
exports: '_'
}
}
});
複製代碼
若是兩個模塊發生循環依賴,a
依賴 b
,b
依賴 a
。
define(["require", "a"],
function(require, a) {
// 若是 a 也依賴 b ,這時參數 a 爲 undefined
// b 能夠在以後使用 require 函數獲取 a
// require 函數依賴是必須的
return function(title) {
return require("a").doSomething();
}
}
);
// 或
define(function(require, exports, module) {
// 在 b 返回以前不能使用 a 的屬性
// 這隻在 a 和 b 返回的都是對象時有用
var a = require("a");
exports.foo = function () {
return a.bar();
};
});
複製代碼
對於第三方庫通常會判斷當前的環境,決定使用 AMD 仍是 CommonJS,好比 underscore。由於 AMD 和 CommonJS 都很流行,因此咱們要一個兼容兩種風格的規範,因而通用模塊規範 UMD 就誕生了。
(function () {
var root = typeof self == 'object' && self.self === self && self ||
typeof global == 'object' && global.global === global && global ||
this || {};
// root 等於當前環境頂層對象
if (typeof exports != 'undefined' && !exports.nodeType) {
// 若是用的 CommonJS 則用CommonJS 導出
if (typeof module != 'undefined' && !module.nodeType && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
// 不然定義在頂層對象上
root._ = _;
}
if (typeof define == 'function' && define.amd) {
// 若是用的 AMD 則用 define 導出
define('underscore', [], function() {
return _;
});
}
}());
複製代碼
CMD(CMD 模塊定義規範) 和 AMD 很是相似。seajs 是對 CMD 實現。
新版的 requirejs 幾乎和 seajs 寫法一摸同樣。
define(function(require, exports, module) {
// 模塊代碼
// requirejs 也支持這種寫法,固然 seajs 也接受 define 三參數寫法,
});
複製代碼
CMD 和 AMD 的最大區別就是,AMD 是依賴提早執行(或許你沒用到這個依賴),CMD 是延遲執行。新版本的 requirejs 也改成了延遲執行。
ES6 是 JS 語言層面的模塊化支持,未來服務器和瀏覽器都會支持 ES6 模塊格式。它是一個文件就是一個模塊,它使用import
指令導入模塊,使用export
導出模塊。
ES6 模塊的設計思想是儘可能的靜態化,使得編譯時就能肯定模塊的依賴關係,以及輸入和輸出的變量。
import { stat, exists, readFile } from 'fs'; // es6
let { stat, exists, readFile } = require('fs'); // commonjs
複製代碼
上面代碼中 CommonJS 實質是總體加載fs模塊,而 es6 是從fs模塊加載 3 個方法,其餘方法不加載。
這種加載稱爲「編譯時加載」或者靜態加載,即 ES6 能夠在編譯時就完成模塊加載,效率要比 CommonJS 模塊的加載方式高。固然,這也致使了無法引用 ES6 模塊自己,由於它不是對象。
和class
同樣 ES6 的模塊自動採用嚴格模式。
export
指令用來導出你想暴露的值。
export var name = 'Jackson';
export var year = 1958;
// 或
var name = 'Jackson';
var year = 1958;
export { name, year };
// -----------
export function multiply(x, y) {
return x * y;
};
// --------
function a() {}
export {
a as b,
a as c
}
// 默認導出的名字和內部變量名相同。
// 可使用 as 關鍵字重命名
複製代碼
export
命令規定的是對外的接口,必須與模塊內部的變量創建一一對應關係。
// 報錯
export 1;
// 報錯
var m = 1;
export m;
// 報錯
function f() {}
export f;
// ------------- 正確寫法
// 寫法一
export var m = 1;
// 寫法二
var m = 1;
export {m};
// 寫法三
var n = 1;
export {n as m};
複製代碼
export
語句輸出的接口,與其對應的值是動態綁定關係,即經過該接口,能夠取到模塊內部實時的值。
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
// 上面代碼輸出變量foo,值爲bar,500 毫秒以後變成baz
複製代碼
這一點與 CommonJS 規範徹底不一樣。CommonJS 模塊輸出的是值的緩存,不存在動態更新。
export
命令不能被嵌套。
function foo() {
export let a = 'bar' // SyntaxError
}
複製代碼
import
命令用於加載模塊。
import
命令具備提高效果,會提高到整個模塊的頭部,首先執行。
import 'lodash'; // 執行文件但不須要其導出值
import { name as mz, year } from './profile.js';
// import 後面跟當前目錄下的 profile.js 中,導出的變量, from 後面是模塊路徑。
// 一樣可使用 as 關鍵字重命名
複製代碼
import
命令輸入的變量都是隻讀的,由於它的本質是輸入接口,修改它會報錯,若是它是一個對象則能夠修改它的屬性。
import
後面的from
指定模塊文件的位置,能夠是相對路徑,也能夠是絕對路徑,.js後綴能夠省略。若是隻是模塊名,不帶有路徑,那麼必須有配置文件,告訴 JavaScript 引擎該模塊的位置。
因爲import
是靜態執行,因此不能使用表達式和變量,這些只有在運行時才能獲得結果的語法結構。
// 報錯
import { 'f' + 'oo' } from 'my_module';
// 報錯
let module = 'my_module';
import { foo } from module;
// 報錯
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
複製代碼
可使用*
加載,一個模塊的全部導出值。
import * as a from './a'
// 將 a.js 文件的全部導出值,都賦值到一個 a 對象變量上
// 可是不能修改它的屬性
複製代碼
export default
指令用來設置默認導出值。
export default 1
// 使用 export default 就無需額外使用一個變量了
// 它其實像 CommonJS 的 module.exports
複製代碼
對於使用export default
的值,import
將它導入就無需使用大括號。
import config from './a.js'
// a.js 文件使用了 export default 導出值
// config 名字是本身隨便寫的
複製代碼
它其實至關於使用一個叫default
的變量
function add(x, y) {
return x * y;
}
export {add as default};
// 等同於
// export default add;
import { default as foo } from 'modules';
// 等同於
// import foo from 'modules';
複製代碼
固然一個文件中能夠同時有export
和export default
,使用import
導入時,也可使用逗號分隔默認值和其餘導出值。
import _, { each, forEach } from 'lodash';
複製代碼
若是須要先輸入後輸出同一個模塊,就可使用這種寫法。
export { foo, bar } from 'my_module';
複製代碼
foo
和bar
實際上並無被導入當前模塊,只是至關於對外轉發了這兩個接口,致使當前模塊不能直接使用foo
和bar
。
一樣還可使用as * default
。
// 接口更名
export { foo as myFoo } from 'my_module';
// 總體輸出
export * from 'my_module';
export { default } from 'foo';
// 替換默認接口
export { es6 as default } from './someModule';
export { default as es6 } from './someModule';
複製代碼
export ... from ...
的寫法主要是用在,一個文件好比index.js
,將其餘的文件的導出值結合起來一次導出。
import
命令會被 JavaScript 引擎靜態分析,因此嵌套是會報錯。
if (needA) {
import A from './a' // 報錯
}
// -----------
if (needA) {
const A = require('./a') // CommonJS 徹底沒問題
}
複製代碼
這時候就須要動態加載功能,import()
就是用來解決這個問題。
import()
函數能夠用在任何地方,不只僅是模塊,非模塊的腳本也可使用。它是運行時執行。
import()
返回一個 Promise 對象,then 方法的回調參數就是模塊的導出對象。
import('./myModule.js')
.then(({export1, export2}) => {
// ...·
});
Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
])
.then(([module1, module2, module3]) => {
···
});
async function main() {
const myModule = await import('./myModule.js');
const {export1, export2} = await import('./myModule.js');
const [module1, module2, module3] =
await Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
]);
}
複製代碼
import()
主要用在 webpack 的代碼分隔功能。好比 react 的 react-loadable 庫。
import Loadable from 'react-loadable';
const LoadableOtherComponent = Loadable({
loader: () => import('./OtherComponent'),
loading: () => <div>Loading...</div>,
});
const MyComponent = () => (
<LoadableOtherComponent/>
);
複製代碼