JS 中的模塊管理 CommonJS AMD CMD ES6

之前 JS 主要用在瀏覽器,它是沒有模塊系統的。若是咱們作的項目有點大,那麼管理項目的依賴就很是困難,好比 A 依賴 BC,而 CB 有依賴其餘的庫。這時人爲的用script標籤的前後順序來讓項目依賴正常是很是困難的。這時就有了不少解決方案。javascript

CommonJS

CommonJS 是以在瀏覽器環境以外構建 JavaScript 生態系統爲目標而產生的項目,好比在服務器和桌面環境中。html

好比nodejs就是用的 CommonJS 規範,可是它也沒徹底接受規範。java

CommonJS 中,每一個文件就是一個模塊,有本身的做用域。在一個文件裏面定義的變量、函數、類,都是私有的,對其餘文件不可見。node

module

每一個模塊(文件)內部都有一個 module 對象,它有如下屬性。react

  1. id 模塊的識別符,一般是帶有絕對路徑的模塊文件名。
  2. exports 表示模塊對外輸出的值。
  3. parent 一個對象,表示調用該模塊的模塊。(沒有則是undefined
  4. filename 模塊的文件名,帶有絕對路徑。
  5. loaded 一個布爾值,表示模塊是否已經完成加載。
  6. children 一個數組,表示該模塊要用到的其餘模塊
  7. 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

它使用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參數是一個路徑字符串,

  1. 若是以/表示加載的是一個位於絕對路徑的模塊文件。
  2. 若是以./表示加載的是一個位於相對路徑(跟當前執行腳本的位置相比)的模塊文件。
  3. 若是都不是則表示加載 nodejs 的核心模塊。
  4. 若是沒有找到的話,nodejs 會經過module.paths屬性查找node_modules文件夾中的第三方庫文件。
  5. 若是還沒找到就報錯。

若是沒有帶後綴 nodejs 會以.js、.json、.node的順序查找模塊文件。

require 上也有一些屬性和方法。

  1. resolve() 獲得require命令加載的確切文件名。
  2. cache nodejs 會將已經加載過的模塊緩存起來,方便下次加載,已經緩存的模塊就在這個對象中,可使用delete require.cache[moduleName] 刪除緩存。
  3. main 屬性用來判斷模塊是直接執行,仍是被調用執行。直接執行的時候(node module.js),require.main屬性指向模塊自己require.main === module // true
  4. 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 */
複製代碼

AMD

首先是 Asynchronous Module Definition 規範,它在適合在瀏覽器環境中異步加載模塊,而且能夠併發的加載。

它主要只有一個接口define(id?, dependencies[]?, factory)函數。它只要有三個參數,前兩個可選,它用來定義一個模塊。

id 模塊名,不推薦使用,通常被工具自動生成。

dependencies 是一個字符串數組,當前這個模塊要依賴的模塊。

factory 是函數或者對象,若是是對象,那麼這個對象就是向外暴露的值,若是是一個函數那麼這個函數的返回值就是對外暴露的值。若是dependencies參數爲空,那麼這個函數的參數默認是require, exportsmodule

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 () {};
    }
);
複製代碼

require.js

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 依賴 bb 依賴 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();
    };
});
複製代碼

UMD

對於第三方庫通常會判斷當前的環境,決定使用 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(CMD 模塊定義規範) 和 AMD 很是相似。seajs 是對 CMD 實現。

新版的 requirejs 幾乎和 seajs 寫法一摸同樣。

define(function(require, exports, module) {

  // 模塊代碼
  // requirejs 也支持這種寫法,固然 seajs 也接受 define 三參數寫法,

});
複製代碼

CMD 和 AMD 的最大區別就是,AMD 是依賴提早執行(或許你沒用到這個依賴),CMD 是延遲執行。新版本的 requirejs 也改成了延遲執行。

ES6 Module

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 指令用來導出你想暴露的值。

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命令具備提高效果,會提高到整個模塊的頭部,首先執行。

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指令用來設置默認導出值。

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';
複製代碼

固然一個文件中能夠同時有exportexport default,使用import導入時,也可使用逗號分隔默認值和其餘導出值。

import _, { each, forEach } from 'lodash';
複製代碼

export ... from ... 寫法

若是須要先輸入後輸出同一個模塊,就可使用這種寫法。

export { foo, bar } from 'my_module';
複製代碼

foobar實際上並無被導入當前模塊,只是至關於對外轉發了這兩個接口,致使當前模塊不能直接使用foobar

一樣還可使用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()

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/>
);
複製代碼
相關文章
相關標籤/搜索