一文徹底搞懂JS前端5大模組化規範及其區別

Echoyya、發表於2021-03-29

碼文不易,轉載請帶上本文連結,感謝~ https://www.cnblogs.com/echoyya/p/14577243.html

在開發以及面試中,總是會遇到有關模組化相關的問題,始終不是很明白,不得要領,例如以下問題,回答起來也是模稜兩可,希望通過這篇文章,能夠讓大家瞭解十之一二,首先丟擲問題:

  • 匯出模組時使用module.exports/exports或者export/export default;
  • 有時載入一個模組會使用require奇怪的是也可以使用import??它們之間有何區別呢?

於是有了菜鳥解惑的搜嘍過程。。。。。。

模組化規範:即為 JavaScript 提供一種模組編寫、模組依賴和模組執行的方案。

Script 標籤

其實最原始的 JavaScript 檔案載入方式,就是Script 標籤,如果把每一個檔案看做是一個模組,那麼他們的介面通常是暴露在全域性作用域下,也就是定義在 window 物件中,不同模組的介面呼叫都是一個作用域中,一些複雜的框架,會使用名稱空間的概念來組織這些模組的介面。

缺點:

  1. 汙染全域性作用域
  2. 開發人員必須主觀解決模組和程式碼庫的依賴關係
  3. 檔案只能按照script標籤的書寫順序進行載入
  4. 在大型專案中各種資源難以管理,長期積累的問題導致程式碼庫混亂不堪

預設情況下,瀏覽器是同步載入 JavaScript 指令碼,即渲染引擎遇到<script>標籤就會停下來,等到執行完指令碼,再繼續向下渲染。如果是外部指令碼,還必須加入指令碼下載的時間。

如果指令碼體積很大,下載和執行的時間就會很長,因此造成瀏覽器堵塞,使用者會感覺到瀏覽器“卡死”了,沒有任何響應。這顯然是很不好的體驗,所以瀏覽器允許指令碼非同步載入

<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>

<script>標籤新增deferasync屬性,指令碼就會非同步載入。渲染引擎遇到這一行命令,就會開始下載外部指令碼,但不會等它下載和執行,而是直接執行後面的命令。

defer:要等到整個頁面在記憶體中正常渲染結束,才會執行;多個指令碼時,按順序執行

async:一旦下載完,渲染引擎就會中斷渲染,執行這個指令碼再繼續渲染。多個指令碼時,不能保證按執行順序

總結一句話:defer是“渲染完再執行”,async是“下載完就執行”。

CommonJS規範(同步載入模組)

  • Node.js 是目前 CommonJS 規範最熱門的一個實現
  • 通過require方法同步載入所依賴的模組,通過exportsmodule.exports匯出需要暴露的資料。
  • CommonJS 規範包括了模組(modules)、包(packages)、系統(system)、二進位制(binary)、控制檯(console)、編碼(encodings)、檔案系統(filesystems)、套接字(sockets)、單元測試(unit testing)等部分。

建立模組

在 Node.js 中,建立一個模組非常簡單,一個檔案就是一個模組

// module.js 模組
var name = "Echoyya";

// todo something...
exports.name = name 

載入模組

使用require函式 載入模組(即被依賴模組的 module.exports物件)。

  1. 按路徑載入模組
  2. 通過查詢 node_modules 目錄載入模組
  3. 載入快取:Node.js 是根據實際檔名快取,而不是 require() 提供的引數快取的,如 require('express')require('./node_modules/express')載入兩次,也不會重複載入,儘管兩次引數不同,解析到的檔案卻是同一個。
  4. 核心模組擁有最高的載入優先順序,換言之如果有模組與其命名衝突,Node.js 總是會載入核心模組。
  5. 更多關於require函式的用法和特點,博主此前另外總結過一篇博文,NodeJs 入門到放棄 — 入門基本介紹(一)

匯出模組

exports.屬性 = 值
exports.方法 = 函式

  • Node.js 為每個模組提供一個 exports 變數,指向 module.exports。相對於在每個模組頭部,有一行這樣的命令:var exports = module.exports;
  • exports物件 和 module.exports物件,指同一個記憶體空間, module.exports物件才是真正的暴露物件
  • exports物件 是 module.exports物件的引用,不能改變指向,只能新增屬性和方法,若直接改變exports 的指向,等於切斷了 exports 與 module.exports 的聯絡,返回空物件
  • console.log(module.exports === exports); // true
  • 更多關於exports函式的用法和特點,博主此前另外總結過一篇博文,NodeJs 入門到放棄 — 入門基本介紹(一)

另外的用法:

// singleobjct.js

function Hello() {
    var name;
    this.setName = function (thyName) {
        name = thyName;
    };
    this.sayHello = function () {
        console.log('Hello ' + name);
    };
}

exports.Hello = Hello;

此時獲取 Hello 物件require('./singleobject').Hello,略顯冗餘,可以用下面方法簡化。

// hello.js
function Hello() {
  var name;
  this.setName = function(thyName) {
    name = thyName;
  };
  this.sayHello = function() {
    console.log('Hello ' + name);
  };
}
module.exports = Hello;

就可以直接獲得這個物件:

// gethello.js
var Hello = require('./hello');
hello = new Hello();
hello.setName('Yu');
hello.sayHello();

CommonJS 特點

  1. 同步載入方式,適用於服務端,因為模組都放在伺服器端,對於服務端來說模組載入較快,不適合在瀏覽器環境中使用,因為同步意味著阻塞載入。
  2. 所有程式碼都執行在模組作用域,不會汙染全域性作用域。
  3. 模組可以多次載入,但只會在第一次載入時執行一次,然後執行結果就被快取了,以後再載入,就直接讀取快取結果。
  4. 模組載入的順序,按照其在程式碼中出現的順序。

AMD(Asynchronous Module Definition)

採用非同步方式載入模組,模組的載入不影響它後面語句的執行。所有依賴這個模組的語句,都定義在一個回撥函式中,等到載入完成之後,這個回撥函式才會執行。推崇依賴前置

require.js 是目前 AMD 規範最熱門的一個實現

AMD 也採用 require語句載入模組,但是不同於 CommonJS,它要求兩個引數:require([module], callback);

  • [module]:是一個陣列,成員就是要載入的模組

  • callback:載入成功之後的回撥函式;

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

建立模組

模組必須採用 define() 函式來定義。

  1. 若一個模組不依賴其他模組。可以直接定義在 define() 函式中。
// math.js

define(function (){
 var add = function (x,y){
  return x+y;
 };
 return {
  add: add
 };
});
  1. 若這個模組還依賴其他模組,那麼 define() 函式的第一個引數,必須是一個陣列,指明該模組的依賴性。當 require() 函式載入test模組時,就會先載入 myLib.js 模組。
// test.js

define(['myLib'], function(myLib){
 function foo(){
  myLib.doSomething();
 }
 return {
  foo : foo
 };
});

載入規範模組

// main.js

require(['math'], function (math){
 alert(math.add(1,1));
});

載入非規範的模組

理論上require.js載入的模組,必須是按照 AMD 規範define() 函式定義的模組。但實際上,雖然已經有一部分流行的函式庫(比如 jQuery )符合 AMD 規範,更多的庫並不符合。那麼require.js 如何能夠載入非規範的模組呢?

這樣的模組在用 require() 載入之前,要先用 require.config()方法,定義它們的一些特徵。
例如,underscore 和 backbone 這兩個庫,都沒有采用 AMD 規範編寫。如果要載入的話,必須先定義它們的特徵。

require.config({
 shim: {
  'underscore': {
   exports: '_'
  },
  'backbone': {
   deps: ['underscore', 'jquery'],
   exports: 'Backbone'
   }
 }
});

require.config() 接受一個配置物件,這個物件有一個 shim 屬性,專門用來配置不相容的模組。每個模組要定義:

  • exports :輸出的變數名,表示這個模組外部呼叫時的名稱;

  • deps: 陣列,表示該模組的依賴性。

如jQuery 的外掛還可以這樣定義:

shim: {
 'jquery.scroll': {
  deps: ['jquery'],
  exports: 'jQuery.fn.scroll'
 }
}

AMD特點

  1. AMD允許輸出的模組相容CommonJS
  2. 非同步並行載入,不阻塞 DOM 渲染。
  3. 推崇依賴前置,也就是提前執行(預執行),在模組使用之前就已經執行完畢。

CMD(Common Module Definition)

  • CMD 是通用模組載入,要解決的問題與 AMD 一樣,只不過是對依賴模組的執行時機不同 ,推崇就近依賴
  • sea.js 是 CMD 規範的一個實現代表庫
  • 定義模組使用全域性函式define,接收一個 factory 引數,可以是一個函式,也可以是一個物件或字串;
  1. factory 是函式時有三個引數,function(require, exports, module):

    • require:函式用來獲取其他模組提供的介面require(模組標識ID)

    • exports: 物件用來向外提供模組介面

    • module :物件,儲存了與當前模組相關聯的屬性和方法

    // 定義 a.js 模組
    define(function(require, exports, module) {
    
      var $ = require('jquery.js')
     
      exports.price= 200;  
    });
    
    // b.js 載入模組
    const a = require('./a.js')
    
  2. factory 為物件、字串時,表示模組的介面就是該物件、字串。比如可以定義一個 JSON 資料模組:

    define({"foo": "bar"});
    
  3. 通過字串模板定義模組:

    define('I am a template.My name is {{name}}.');
    

AMD 與 CMD 的區別

  1. AMD 是提前執行,CMD 是延遲執行

  2. AMD 是依賴前置,CMD 是依賴就近

    // AMD 
    define(['./a', './b'], function(a, b) {  // 在定義模組時 就要宣告其依賴的模組
        a.doSomething()
        // ....
        b.doSomething()
        // ....
    })
    
    // CMD
    define(function(require, exports, module) {
       var a = require('./a')
       a.doSomething()
       // ... 
       
       var b = require('./b') // 可以在用到某個模組時 再去require
       b.doSomething()
       // ... 
    })
    
    

UMD(Universal Module Definition)

  • UMD是AMD和CommonJS的糅合
  • UMD的實現很簡單:
    1. 先判斷是否支援Node.js模組(exports是否存在),存在則使用Node.js模組模式。
    2. 再判斷是否支援AMD(define是否存在),存在則使用AMD方式載入模組。
    3. 前兩個都不存在,則將模組公開到全域性(window或global)。
(function (window, factory) {
    if (typeof exports === 'object') {
    
        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {
    
        define([],factory);
    } else {
    
        window.eventUtil = factory();
    }
})(this, function () {
	return {};
});

ES6模組化

​ ES6 模組的設計思想,是儘量的靜態化,使得編譯時就能確定模組的依賴關係,以及輸入和輸出的變數。
ES6 中,import引用模組,使用export匯出模組。通過babel專案將還未被宿主環境(各瀏覽器、Node.js)直接支援的 ES6 模組 編譯為 ES5 的 CommonJS。因此Babel實際上是將不被支援的import/export翻譯成目前已被支援的require/exports

// 匯入
import Vue from 'vue'
import App from './App'


// 匯出
function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};
export function multiply() {...};
export var year = 2018;
export default ...

模組化規範大總結

CommonJS AMD CMD ES6
引用模組 require require require import
暴露介面 module.exports || exports define函式返回值 return exports export
載入方式 執行時載入,同步載入 並行載入,提前執行,非同步載入 並行載入,按需執行,非同步載入 編譯時載入,非同步載入
實現模組規範 NodeJS RequireJS SeaJS 原生JS
適用 伺服器 瀏覽器 瀏覽器 伺服器/瀏覽器

問題迴歸:"require"與"import"的區別

說了這麼多,還是要回到文章一開始提到的問題,"require"與"import"兩種引入模組方式,到底有神馬區別,大致可以分為以下幾個方面(可能總結的也不是很全面):

寫法上的區別

require/exports 的用法只有以下三種簡單的寫法:

const fs = require('fs')
exports.fs = fs
module.exports = fs

import/export 的寫法就多種多樣:

import fs from 'fs'
import {default as fs} from 'fs'
import * as fs from 'fs'
import {readFile} from 'fs'
import {readFile as read} from 'fs'
import fs, {readFile} from 'fs'

export default fs
export const fs
export function readFile
export {readFile, read}
export * from 'fs'

輸入值的區別

require輸入的變數,基本型別資料是賦值,引用型別為淺拷貝,可修改

import輸入的變數都是隻讀的,如果輸入 a 是一個物件,允許改寫物件屬性。

import {a} from './xxx.js'

a = {}; // Syntax Error : 'a' is read-only;

a.foo = 'hello'; // 合法操作

執行順序

require:不具有提升效果,到底載入哪一個模組,只有執行時才知道。

const path = './' + fileName;
const myModual = require(path);

import:具有提升效果,會提升到整個模組的頭部,首先執行。import的執行早於foo的呼叫。本質就是import命令是編譯階段執行的,在程式碼執行之前。

foo();

import { foo } from 'my_module';

import()函式:ES2020提案引入,支援動態載入模組。import()函式接受一個引數,指定所要載入的模組的位置,引數格式同import命令,兩者區別主要是import()為動態載入。可用於按需載入條件載入動態的模組路徑等。

它是執行時執行,也就是說,什麼時候執行到這一句,就會載入指定的模組,返回一個 Promise 物件。import()載入模組成功以後,該模組會作為一個物件,當作then方法的引數。可以使用物件解構賦值,獲取輸出介面。

// 按需載入
button.addEventListener('click', event => {
  import('./dialogBox.js')
  .then({export1, export2} => {   // export1和export2都是dialogBox.js的輸出介面,解構獲得
    // do something...
  })
  .catch(error => {})
});

// 條件載入
if (condition) {
  import('moduleA').then(...);
} else {
  import('moduleB').then(...);
}


// 動態的模組路徑
import(f()).then(...);    // 根據函式f的返回結果,載入不同的模組。

使用表示式和變數

require:很顯然是可以使用表示式和變數的

let a = require('./a.js')
a.add()

let b = require('./b.js')
b.getSum()

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';
}

而require/exports 和 import/export 本質上的區別,實際上也就是CommonJS規範與ES6模組化的區別

它們有三個重大差異。

  1. CommonJS 模組輸出的是一個值的拷貝,ES6 模組輸出的是值的引用。

  2. CommonJS 模組是執行時載入,ES6 模組是編譯時輸出介面。

  3. CommonJS 模組的require()同步載入模組,ES6 模組的import命令是非同步載入,有一個獨立的模組依賴的解析階段。

導致第二個差異是因為 CommonJS 載入的是一個物件(即module.exports屬性),該物件只有在指令碼執行完才會生成。而 ES6 模組不是物件,它的對外介面只是一種靜態定義,在程式碼靜態解析階段就會生成

CommonJS執行時載入

  • 只能在執行時確定模組的依賴關係,以及輸入和輸出的變數,一個模組就是一個物件,輸入時必須查詢物件屬性。
// CommonJS模組
let { stat, exists, readfile } = require('fs');

// 等同於
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

ES6編譯時載入或者靜態載入

  • ES6 模組不是物件,而是通過export命令顯式指定輸出的程式碼,再通過import命令輸入。
  • 可以在編譯時就完成模組載入,引用時只載入需要的方法,其他方法不載入。效率要比 CommonJS 模組的載入方式高。
import { stat, exists, readFile } from 'fs';

相關文章