js模塊化規範

什麼是模塊化?

模塊化是組織代碼的一種方式。將全部的js業務邏輯代碼寫在一個文件裏面,不只致使文件龐大,並且難以管理和維護。javascript

好比:html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>js</title>
</head>
<body>
    <script> ...// 一些業務邏輯 </script>
</body>
</html>
複製代碼

爲了方便維護,能夠經過外部引入的方式:前端

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>js</title>
</head>
<body>
    ...
<script type="javascript" src="...(file path)"></script>
</body>
</html>
複製代碼

這種方式我的看來也是一種模塊化的方式,只不過這種方式存在許多弊端。vue

  • 文件必須順序引入。在大型項目開發中,因爲引入的文件較多,文件之間的依賴關係也較爲複雜,文件引入順序難以明確。
// test.js是基於jQuery.js開發的,也就是說test.js是依賴於jQuery.js的,因此jQuery必須先於test.js引入。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>js</title>
</head>
<body>
    ...
<script type="javascript" src="jQuery.js"></script>
<script type="javascript" src="test.js"></script>
</body>
</html>
複製代碼
  • 命名空間污染問題。
// a.js
var a=1;

// b.js
var a=2;

//test.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <script src="a.js"></script>
    <script src="b.js"></script>
    <title>js</title>
</head>
<body>
    <script> alert('a的值爲'+a); </script>
</body>
</html>
複製代碼

執行結果以下:java

執行結果

爲了解決這些問題,一些模塊化的規範就出現了。jquery

模塊的發展歷程

  先想想,爲何模塊很重要?git

  由於有了模塊,咱們就能夠更方便地使用別人的代碼,想要什麼功能,就加載什麼模塊。github

  可是,這樣作有一個前提,那就是你們必須以一樣的方式編寫模塊,不然你有你的寫法,我有個人寫法,豈不是亂了套!考慮到 Javascript模塊如今尚未官方規範,這一點就更重要了。面試


  • js不像其餘高級語言有模塊系統,標準庫較少和更缺少包管理系統express

  • js 起初只有全局對象的形式,經過一個個小函數來實現不一樣的模塊功能

    function m1(){
    &emsp;&emsp;//...
    }
    function m2(){
    &emsp;&emsp;//...
    } 
    複製代碼
  • 漸漸發展,經過構建對象的形式,來武裝不一樣的功能

    var module1 = new Object ({
    &emsp;&emsp;_count : 0,
    &emsp;&emsp;m1 : function (){
    &emsp;&emsp;&emsp;&emsp;//...
    &emsp;&emsp;},
    &emsp;&emsp;m2 : function (){
    &emsp;&emsp;&emsp;&emsp;//...
    &emsp;&emsp;}
    }); 
    複製代碼

    上面的函數 m1()m2(),都封裝在 module1 對象裏。使用的時候,就是調用這個對象的屬性:

    module1.m1();

    可是,這樣的寫法會暴露全部模塊成員,內部狀態能夠被外部改寫。好比,外部代碼能夠直接改變內部計數器的值。

    module1._count = 5; 
    複製代碼
  • 繼續發展,經過當即執行函數和閉包的形式來分離一個又一個的小組件

    3、當即執行函數寫法

      使用"當即執行函數"(Immediately-Invoked Function Expression,IIFE),能夠達到不暴露私有成員的目的。

    var module1 = (function(){
    &emsp;&emsp;var _count = 0;
    &emsp;&emsp;var m1 = function(){
    &emsp;&emsp;&emsp;&emsp;//...
    &emsp;&emsp;};
    &emsp;&emsp;var m2 = function(){
    &emsp;&emsp;&emsp;&emsp;//...
    &emsp;&emsp;};
    &emsp;&emsp;return {
    &emsp;&emsp;m1 : m1,
    &emsp;&emsp;m2 : m2
    &emsp;&emsp;};
    })(); 
    複製代碼

      使用上面的寫法,外部代碼沒法讀取內部的_count 變量。

    console.info (module1._count); //undefined
    複製代碼

      module1就是Javascript模塊的基本寫法。

     下面,再對這種寫法進行加工。

    放大模式( augmentation)

      若是一個模塊很大,必須分紅幾個部分,或者一個模塊須要繼承另外一個模塊,這時就有必要採用"放大模式"(augmentation)。

    var module1 = (function (mod){
    &emsp;&emsp;mod.m3 = function () {
    &emsp;&emsp;&emsp;&emsp;//...
    &emsp;&emsp;};
    &emsp;&emsp;return mod;
    })(module1); 
    複製代碼

      上面的代碼爲 module1 模塊添加了一個新方法 m3(),而後返回新的 module1 模塊。

    寬放大模式(Loose augmentation)

      在瀏覽器環境中,模塊的各個部分一般都是從網上獲取的,有時沒法知道哪一個部分會先加載。若是採用上一節的寫法,第一個執行的部分有可能加載一個不存在的空對象,這時就要採用"寬放大模式"。

    var module1 = ( function (mod){
    &emsp;&emsp;//...
    &emsp;&emsp;return mod;
    })(window.module1); 
    複製代碼

      與"放大模式"相比,"寬放大模式"就是"當即執行函數"的參數能夠是空對象。

  • 當對象多起來的時候,又開始經過命名空間,來實現分級管理

    輸入全局變量

  獨立性是模塊的重要特色,模塊內部最好不與程序的其餘部分直接交互。

  爲了在模塊內部調用全局變量,必須顯式地將其餘變量輸入模塊。

var module1 = (function ($, YAHOO) {
&emsp;&emsp;//...
})(jQuery, YAHOO); 
複製代碼

  上面的 module1 模塊須要使用 jQuery庫和 YUI庫,就把這兩個庫(實際上是兩個模塊)看成參數輸入 module1。這樣作除了保證模塊的獨立性,還使得模塊之間的依賴關係變得明顯

  • 最終,歷經十多年,社區漸漸發展壯大,commonJS規範的提出成了javascript歷史上最重要的里程碑。
  • Best Wishes : hope javascript can run everywhere!

主流的模塊化的規範有:

  • commonJs 規範
  • AMD 規範(Asynchronous Module Definition) (異步模塊定義)
  • CMD 規範
  • ES6 module

四大主流規範

CommonJS規範

CommonJS規範的使用

Node.jscommonJS規範的主要實踐者,它有四個重要的環境變量爲模塊化的實現提供支持:moduleexportsrequireglobal。實際使用時,用module.exports定義當前模塊對外輸出的接口(不推薦直接用exports),用require加載模塊。

// 定義模塊math.js
var basicNum = 0;
function add(a, b) {
  return a + b;
}
module.exports = { //在這裏寫上須要向外暴露的函數、變量
  add: add,
  basicNum: basicNum
}

// 引用自定義的模塊時,參數包含路徑,可省略.js
var math = require('./math');
math.add(2, 5);

// 引用核心模塊時,不須要帶路徑
var http = require('http');
http.createService(...).listen(3000);
複製代碼

commonJS用**同步**的方式加載模塊。在服務端,模塊文件都存在本地磁盤,讀取很是快,因此這樣作不會有問題。可是在瀏覽器端,限於網絡緣由,更合理的方案是使用異步加載。

commomJS的實現原理

commonJS簡化版源碼

function Module(id, parent){
    this.id = id;
    this.exports = {};
    this.parent = parent;
    this.filename = null;
    this.loaded = false;
    this.children = []
}

Module.prototype.require = function(path){
  return Module._load(path, this)  
}
//由此可知,require 並非全局命令,而是每一個模塊提供的一個內部方法,也就是說,只有在模塊內部才能使用require命令,(惟一的例外是REPL 環境)。另外,require 其實內部調用 Module._load 方法。

Module._load = function(request, parent, isMain) {

  // 計算絕對路徑
  var filename = Module._resolveFilename(request, parent);

  // 第一步:若是有緩存,取出緩存
  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    return cachedModule.exports;
  }
  
  // 第二步:是否爲內置模塊
  if (NativeModule.exists(filename)) {
    return NativeModule.require(filename);
  }

  // 第三步:生成模塊實例,存入緩存
  var module = new Module(filename, parent);
  Module._cache[filename] = module;

  // 第四步:加載模塊
  try {
    module.load(filename);
    hadException = false;
  } finally {
    if (hadException) {
      delete Module._cache[filename];
    }
  }

  // 第五步:輸出模塊的exports屬性
  return module.exports;
};
module.exports = Module;
複製代碼
  • 每一個文件就是一個模塊,每一個模塊都是Module類的一個實例。

1561816770931

  • 從上面圖中,能夠知道moduleglobal全局對象的一個屬性。
  • 能夠觀察到在global對象也有一個全局函數require(),global對象的require()是對module.require()函數的進一步抽象和封裝。

AMD規範

AMD是"Asynchronous Module Definition"的縮寫,意思就是"異步模塊定義"。它採用異步方式加載模塊,模塊的加載不影響它後面語句的運行。全部依賴這個模塊的語句,都定義在一個回調函數中,等到加載完成以後,這個回調函數纔會運行。


爲何會有AMD規範

有了服務器端模塊之後,很天然地,你們就想要客戶端模塊。並且最好二者可以兼容,一個模塊不用修改,在服務器和瀏覽器均可以運行。

可是,因爲一個重大的侷限,使得CommonJS規範不適用於瀏覽器環境。仍是上一節的代碼,若是在瀏覽器中運行,會有一個很大的問題。

var math = require('math');
math.add(2, 3);
複製代碼

第二行math.add(2, 3),在第一行require('math')以後運行,所以必須等math.js加載完成。也就是說,若是加載時間很長,整個應用就會停在那裏等。

這對服務器端不是一個問題,由於全部的模塊都存放在本地硬盤,能夠同步加載完成,等待時間就是硬盤的讀取時間。可是,對於瀏覽器,這倒是一個大問題,由於模塊都放在服務器端,等待時間取決於網速的快慢,可能要等很長時間,瀏覽器處於"假死"狀態。

所以,瀏覽器端的模塊,不能採用"同步加載"(synchronous),只能採用"異步加載"(asynchronous)。這就是AMD規範誕生的背景。

AMD規範的使用

AMD也採用require()語句加載模塊,可是不一樣於CommonJS,它要求兩個參數:

require([module], callback);
複製代碼

第一個參數[module],是一個數組,裏面的成員就是要加載的模塊;第二個參數callback,則是加載成功以後的回調函數。若是將前面的代碼改寫成AMD形式,就是下面這樣:

require(['math'], function (math) {
    math.add(2, 3);
});
複製代碼

math.add()math模塊加載不是同步的,瀏覽器不會發生假死。因此很顯然,AMD比較適合瀏覽器環境。

目前,主要有兩個Javascript庫實現了AMD規範:require.jscurl.js

AMD規範的寫法

require.js 加載的模塊,採用 AMD規範。也就是說,模塊必須按照 AMD的規定來寫。 具體來講,就是模塊必須採用特定的 define()函數來定義。若是一個模塊不依賴其餘模塊。那麼能夠直接定義在 define() 函數之中。 假定如今有一個 math.js 文件,它定義了一個math模塊。那麼,math.js 就要這樣寫:

// math.js
define(function (){
&emsp;var add = function (x,y){
&emsp;&emsp;return x+y;
&emsp;};
&emsp;return {
&emsp;&emsp;add: add
&emsp;};
});
複製代碼

加載方法以下:

// main.js
require(['math'], function (math){
&emsp;alert(math.add(1,1));
});
複製代碼

若是這個模塊還依賴其餘模塊,那麼define() 函數的第一個參數,必須是一個數組,指明該模塊的依賴性。

define(['myLib'], function(myLib){
&emsp;function foo(){
&emsp;&emsp;myLib.doSomething();
&emsp;}
&emsp;return {
&emsp;&emsp;foo : foo
&emsp;};
});
複製代碼

require() 函數加載上面這個模塊的時候,就會先加載myLib.js文件。

加載非規範的模塊

理論上,require.js 加載的模塊,必須是按照 AMD 規範、用 define()函數定義的模塊。可是實際上,雖然已經有一部分流行的函數庫(好比jQuery)符合 AMD規範,更多的庫並不符合。那麼,require.js 是否可以加載非規範的模塊呢? 回答是能夠的。 這樣的模塊在用 require() 加載以前,要先用 require.config()方法,定義它們的一些特徵。 舉例來講,underscorebackbone 這兩個庫,都沒有采用 AMD規範編寫。若是要加載它們的話,必須先定義它們的特徵。

require.config({
&emsp;shim: {
&emsp;&emsp;'underscore': {
&emsp;&emsp;&emsp;exports: '_'
&emsp;&emsp;},
&emsp;&emsp;'backbone': {
&emsp;&emsp;&emsp;deps: ['underscore', 'jquery'],
&emsp;&emsp;&emsp;exports: 'Backbone'
&emsp;&emsp;}
&emsp;}
});
複製代碼

require.config() 接受一個配置對象,這個對象除了有前面說過的paths 屬性以外,還有一個 shim屬性,專門用來配置不兼容的模塊。具體來講,每一個模塊要定義: (1)exports 值(輸出的變量名),代表這個模塊外部調用時的名稱; (2)deps 數組,代表該模塊的依賴性。 好比,jQuery 的插件能夠這樣定義:

shim: {&emsp;
    'jquery.scroll': {&emsp;&emsp;
        deps: ['jquery'],&emsp;
        exports: 'jQuery.fn.scroll'
    }
}
複製代碼

CMD 規範

CMDCommon Module definition的縮寫,即通用模塊定義。CMD規範是國內發展出來的,就像AMD有個requireJS,CMD有個瀏覽器的實現SeaJS,SeaJS要解決的問題和requireJS同樣,只不過在模塊定義方式和模塊加載(能夠說運行、解析)時機上有所不一樣

CMD規範的寫法

  • 全局函數define,用來定義模塊。
  • 參數 factory 能夠是一個函數,也能夠爲對象或者字符串。
  • 當 factory 爲對象、字符串時,表示模塊的接口就是該對象、字符串。

CMD中,一個模塊就是一個文件,格式爲:

define( factory );
複製代碼
  1. 定義JSON數據模塊:
define({ "foo": "bar" });
複製代碼

2.經過字符串定義模板模塊:

define('this is `data`.');
複製代碼
  1. factory 爲函數的時候,表示模塊的構造方法,執行構造方法即可以獲得模塊向外提供的接口。

    • require 是一個方法,接受模塊標識做爲惟一參數,用來獲取其餘模塊提供的接口:require(id)


      define(function( require, exports ){ 
          var a = require('./a');
          a.doSomething(); 
      });
      複製代碼

      require是同步往下執行的,須要的異步加載模塊可使用 require.async 來進行加載:

      define( function(require, exports, module) { 
          require.async('.a', function(a){ 
              a.doSomething(); 
          }); 
      });
      複製代碼

      require.resolve( id )可使用模塊內部的路徑機制來返回模塊路徑,不會加載模塊。


    • exports 是一個對象,用來向外提供模塊接口

    • module 是一個對象,上面存儲了與當前模塊相關聯的一些屬性和方法

    define( function(require, exports, module) { 
     // 模塊代碼 
    });
    複製代碼

示例:

// math.js

define(function(require,exports,module) {
  exports.add = function() {
    var sum = 0,i = 0,args = arguments,l = args.length;
    while (i < l) {
      sum += args[i++];
    }
    return sum;
  };
});
複製代碼
// increment.js

define(function(require,exports,module) {
  var add = require('math').add;
  exports.increment = function(val) {
    return add(val,1);
  };
});
複製代碼
// program.js

define(function(require ,exports,module) {
  var inc = require('increment').increment;
  var a = 1;
  inc(a); // 2

  module.id == "program";
});
複製代碼

AMD規範和CMD規範的比較

  1. 對於依賴的模塊,AMD 是提早執行,CMD是延遲執行。CMD 推崇 as lazy as possible
  2. CMD 推崇依賴就近,AMD 推崇依賴前置。
// CMD
define(function(require, exports, module) { 
    var a = require('./a') 
    a.doSomething() 
    // 此處略去 100 行 
    var b = require('./b') // 依賴能夠就近書寫 
    b.doSomething() 
    // ... 
});

// AMD 默認推薦的是
define(['./a', './b'], function(a, b) { // 依賴必須一開始就寫好 
    a.doSomething() 
    // 此處略去 100 行 
    b.doSomething() 
    // ... 
})
複製代碼
  1. 二者的使用加載機制不一樣,也就致使了AMD(requirejs)模塊會提早執行**,用戶體驗好,**而CMD(seajs)性能好,由於只有在須要時候才執行。

ES6模塊規範

ES6的語言規格中引入了模塊化功能,也就很好的取代了以前的commonjsAMD規範,成爲了瀏覽器和服務器的通用的模塊解決方案,在現今(vuejs,ReactJS)等框架大行其道中,都引入了ES6中的模塊化(Module)機制。

Es6中模塊導出的基本語法

模塊的導出,export關鍵字用於暴露數據,暴露給其餘模塊

使用方式是,能夠將export放在任何變量,函數或類聲明的前面,從而將他們從模塊導出,而import用於引入數據,例如以下所示:

將下面這些js存儲到exportExample.js中,分別導出的是數據,函數,類:

exportExample.js

// 1. 導出數據,變量前面加上export關鍵字
export var  name = "隨筆川跡"; // 導出暴露name變量
export let  weChatPublic = "itclanCoder"; // 暴露weChatPublic
export const time = 2018; // 暴露time


// 2. 導出函數,函數前面加上export關鍵字
export function sum(num1,num2){
      return num1+num2;
 }
/* * * 以上等價於 * function sum(num1,num2){ * return num1+num2; * } * export sum; * 也能夠這樣:在定義它時沒有立刻導出它,因爲沒必要老是導出聲明,能夠導出引用,所以下面這段代碼也是能夠運行的 */

// 3. 導出類,類前面加上export關鍵字
export class People{
      constructor(name,age){
         this.name = name;
         this.age = age;
      }
      info(){
         return `${this.name}${this.age}歲了`;
      }
 }
複製代碼

注意:一個模塊就是一個獨立的文件,該文件內部的全部變量,外部沒法獲取,一樣,任何未顯示導出的變量,函數或類都是模塊私有的,若沒有用export對外暴露,是沒法從模塊外部訪問的 例如:

function countResult(num1,num2){
       return num1-num2;
 }
// 沒有經過export關鍵字導出,在外部是沒法訪問該模塊的變量或者函數的
複製代碼

對應在另外一個模塊中經過import導入以下所示,模塊命名爲importExample.js

import { name, weChatPublic,time,sum,People} from "../modelTest1/exportExampleEs5.js"

var people = new People("小美",18); // 實例化perople對象
console.log(name);
console.log(weChatPublic);
console.log(time);
console.log(sum(1,2));
console.log(people.info());
複製代碼

注意:在上面的示例中,除了export關鍵字外,每個聲明與腳本中的如出一轍,由於導出的函數和類聲明須要有一個名稱,因此代碼中的每個函數或類也確實有這個名稱,除非用default關鍵字,不然不能用這個語法導出匿名函數或類。

Es6中模塊導入的基本語法

若是想從一個文件(模塊)訪問另外一個文件(模塊)的功能,則須要經過import關鍵字在另外一個模塊中引入數據,import語句的兩個部分組成分別是**:要導入的標識符標識符應當從那個模塊導入,**另外,導入的標識符的順序能夠是任意位置,可是導入的標識符(也就是大括號裏面的變量)與export暴露出的變量名應該是一致的。具體的寫法以下:

import {identifer1,indentifer2}  from "./example.js"  // import {標識符1,標識符2} from "本地
複製代碼

1. 導入單個綁定

// 只導入一個
 import {sum} from "./example.js"
  console.log(sum(1,2));  // 3
  sum = 1; // 拋出一個錯誤,是不能對導入的綁定變量對象進行改寫操做的
複製代碼

2. 導入多個綁定

若是想從示例模塊中導入多個綁定,與單個綁定類似,多個綁定值之間用逗號隔開便可:

// 導入多個
import {sum,multiply,time} from "./exportExample.js"
console.log(sum(1,2)); // 3
console.log(multiply(1,2)); // 3
console.log(time);  // 2018
複製代碼

在這段代碼中,從exportExample.js模塊導入3個綁定,sum,multiplytime以後使用它們,就像使用本地定義的同樣 等價於下面這個: **無論在import語句中把一個模塊寫了多少次,該模塊將只執行一次,導入模塊的代碼執行後,實例化過的模塊被保存在內存中,**只要另外一個import語句使用它就能夠重複使用它.

import {sum} from "./exportExample.js"
import {multiply} from "./exportExample.js"
import {time} from "./exportExample.js
複製代碼

3. Es6中導入整個模塊

特殊狀況下,能夠導入整個模塊做爲一個單一的對象,而後全部的導出均可以做爲對象的屬性使用,例如:

// 導入一整個模塊
import * as example from "./exportExample.js"
console.log(example.sum(1,example.time));
consoole.log(example.multiply(1,2));// multiply與sum函數功能同樣
複製代碼

在上面這段代碼中,從本地模塊的exportExample.js中導出的全部綁定被加載到一個被稱做爲example的對象中,指定的導出sum()函數,multiply()函數和time以後做爲example的屬性被訪問,這種導入格式被稱爲命名空間導入,由於exportExample.js文件中不存在example對象,因此它被做爲exportExample.js中全部導出成員的命名空間對象而被建立

Es6中如何給導入導出時標識符重命名

從一個模塊導入變量,函數或者類時,咱們可能不但願使用他們的原始名稱,就是導入導出時模塊內的標識符(變量名,函數,或者類)能夠不用一一對應,保持一致**,**能夠在導出和導入過程當中改變導出變量對象的名稱

使用方式①: 使用as關鍵字來指定變量,函數,或者類在模塊外應該被稱爲何名稱,例如以下一函數:

function sum(num1,num2){
     return num1+num2;
}
export {sum as add} // as後面是從新指定的函數名
複製代碼

如上代碼,函數sum是本地名稱,add是導出時使用的名稱,換句話說,當另外一個模塊要導入這個函數時,必須使用add這個名稱:

若在importExample.js一模塊中,則導入的變量對象應是add而不是sum,是由它導出時變量對象決定的

import {add} from "./exportExample.js"
複製代碼

使用方式②: 使用as關鍵字來指定變量,函數,或者類在主模塊內應該被稱爲何名稱,例如以下一函數:

// exportExample.js
export function sum(num1,num2){
     return num1+num2;
}
複製代碼
// importExample.js
import {sum as add} from "./exportExample.js"
console.log(sum(1,2)); // 3
複製代碼

如上代碼導入add函數時使用了一個導入名稱來重命名sum函數,注意這種寫法與前面導出export時的區別,使用import方式時,從新命名的標識符在前面,as後面是本地名稱,可是這種方式,即便導入時改變函數的本地名稱,即便模塊導入了add函數,在當前模塊中也沒有add()標識符,如上對add的類型檢測就是很好的驗證.

ES6匿名方式的導入和導出

若是在不給導出的標識符(變量,函數,類)呢,那麼能夠經過導出default關鍵字指定單個變量,函數或者類, 在import的時候, 名字隨便寫, 由於每個模塊的默認接口就一個。

//a.js 
let sex = "boy"; 
export default sex //(sex不能加大括號) 
//本來直接export sex外部是沒法識別的,加上default就能夠了.可是一個文件內最多隻能有一個export default。 其實此處至關於爲sex變量值"boy"起了一個系統默認的變量名default,天然default只能有一個值,因此一個文件內不能有多個export default。
複製代碼
// b.js 
//本質上,a.js文件的export default輸出一個叫作default的變量,而後系統容許你爲它取任意名字。因此能夠爲import的模塊起任何變量名,且不須要用大括號包含 
import any from "./a.js" 
import any12 from "./a.js" 
console.log(any,any12) // boy,boy
複製代碼

參考連接

javascript模塊化編程

前端模塊化:CommonJS,AMD,CMD,ES6

深刻了解CommonJS的模塊實現原理

面試官說:說一說CommonJS的實現原理

詳解Node全局變量global模塊

JavaScript AMD 與 CMD 規範

AMD,CMD 規範詳解

30分鐘學會前端模塊化開發

分析 Babel 轉換 ES6 module 的原理

相關文章
相關標籤/搜索