前端模塊化發展歷史(commonJS、AMD、CMD、UMD、ES6不含webpack)

寫在前面

JavaScript發展之初,只是爲了解決基礎的表單驗證問題,以及基礎的頁面交互,代碼很是簡單,不存在模塊化的概念和問題。可是隨着ajax的發展,web進入2.0時代,JavaScript成爲一門應用很是普遍的語言。html

這個時候js做爲一門嵌入型語言,劣勢就展現出來了,沒有一個權威的規範,問題老是要解決,在前端發展的這幾十年,也就順勢而爲的產生了不少的js規範。前端

前端模塊化

1、函數

在最先的js中,想要實現分模塊開發,最簡單的就是函數,由於函數能造成一個相對封閉的空間,經過函數來實現簡單的模塊化也是最先的解決方案vue

function model1 = {
    
}

function model2 = {

}
複製代碼

缺點:

一、污染全局做用域
二、維護成本高(命名容易衝突)
三、依賴關係不明顯
複製代碼

2、對象

對象裏面能夠包含屬性和方法,就至關於一個容器了,咱們能夠把每一個模塊的代碼寫到一個對象裏面,從而實現模塊化的目的node

var model1 = {
    age: 11,
    say() {
        console.log(age)
    }
}

var model2 = {
    age: 15,
    say() {
        console.log(age)
    }
}
複製代碼

缺點

外部能夠修改模塊內部狀態,能夠隨意修改每一個模塊的某個屬性,有至關的安全隱患
複製代碼

3、自執行函數

IIFE(immediately invoked function expression),也就是咱們說的自執行函數,經過定義一個匿名函數,建立了一個「私有」的命名空間,該命名空間的變量和方法,不會破壞全局的命名空間jquery

var module = (function(){
  var age = 11
    var say = function(){
        console.log(age)
    }
    return {say};
})();

module.say();  //11
console.log(module.age)  //undefined
複製代碼

缺點

外部沒法訪問內部私有變量
複製代碼

4、commonJs

前端真正提出模塊化的概念,就是從commonJs的誕生開始的, 由於js做爲一門嵌入型語言,處理頁面邏輯和交互,即便沒有模塊化也能運行,並不會出什麼問題,可是服務端卻必需要有模塊的概念。因此commonJs的發揚光大和nodejs相關,尤爲是近幾年nodejs的應用愈來愈普遍,npm統治整個前端之後,commonJs規範所以被你們熟知。webpack

一、定義模塊

根據CommonJS規範,一個單獨的文件就是一個模塊。每個模塊都是一個單獨的做用域,也就是說,在該模塊內部定義的變量,沒法被其餘模塊讀取,除非定義爲global對象的屬性es6

二、模塊輸出

模塊只有一個出口,module.exports對象,咱們須要把模塊但願輸出的內容放入該對象web

三、加載模塊

加載模塊使用require方法,該方法讀取一個文件並執行,返回文件內部的module.exports對象ajax

// model1.js
var age = 11

function say(){
	console.log(age);
}
module.exports = {
    say
}

// index.html
var wu = require('./index.js');

console.log(wu.say)
複製代碼

四、優勢

解決了依賴、全局變量污染的問題express

五、缺點

一、同步加載

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

二、瀏覽器不能用

5、AMD

AMD 即Asynchronous Module Definition,中文名是異步模塊定義的意思。

CommonJS 規範主要是爲服務器端的 NodeJS 服務,服務器端加載模塊文件無延時,可是在瀏覽器上就大不相同了。AMD 便是爲了在瀏覽器宿主環境中實現模塊化方案的規範之一。

因爲不是JavaScript原生支持,使用AMD規範進行頁面開發須要用到對應的庫函數,也就是大名鼎鼎RequireJS,實際上AMD 是 RequireJS 在推廣過程當中對模塊定義的規範化的產出。

官網地址不能用,能夠直接在這個地址下載下來引用

requirejs.org/docs/releas…

一、引入依賴

<script src="js/require.js" data-main="./main"></script>
複製代碼

二、模塊定義

由 define 方法來定義,在 define API 中:

id:模塊名稱,或者模塊加載器請求的指定腳本的名字;

dependencies:是個定義中模塊所依賴模塊的數組,默認爲 [「require」, 「exports」, 「module」]

factory:爲模塊初始化要執行的函數或對象。若是爲函數,它應該只被執行一次。若是是對象,此對象應該爲模塊的輸出值;

// hello.js
define('hello', function (x, y){
  var add = function (x,y){
    console.log(x, y) // 1, 2
   return x+y;
 };
  return {
   add: add
 };
});
複製代碼

三、模塊引入

require()函數接受兩個參數

第一個參數是一個數組,表示所依賴的模塊

第二個參數是一個回調函數,當前面指定的模塊都加載成功後,它將被調用。加載的模塊會以參數形式傳入該函數,從而在回調函數內部就可使用這些模塊

// main.js

require.config({
    'baseUrl': './js',
    'paths': {
        'hello': './hello'
    }
})

define('main', function() {
    require(['hello'], function(hello) {
        console.log(hello.add(1, 2)) // 3
    })
})
複製代碼

require()函數在加載依賴的函數的時候是異步加載的,這樣瀏覽器不會失去響應,它指定的回調函數,只有前面的模塊都加載成功後,纔會運行,解決了依賴性的問題。

6、CMD

CMD 全稱爲 Common Module Definition,是 Sea.js 所推廣的一個模塊化方案的輸出。在 CMD define 的入參中,雖然也支持包含 id, deps 以及 factory 三個參數的形式,但推薦的是接受 factory 一個入參,而後在入參執行時,填入三個參數 require、exports 和 module:

一、定義模塊

require是能夠把其餘模塊導入進來的一個參數;

而exports是能夠把模塊內的一些屬性和方法導出的;

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

define(function(require, exports, module) {
  // 每一個函數單獨導出
  exports.add = function(x, y) {
    return x + y;
  }
});
複製代碼

二、引用模塊

define(function(require, exports, module) {
    var hello = require('hello');
    console.log(hello.add(2,3));
  
    // 單獨導出
    exports.init = function init() {
      console.log('init');
    }
});
複製代碼

三、html調用

<script src="./js/sea.js"></script>
<script>
seajs.config({
  base: './js', // 後續引用基於此路徑
  alias: {  // 別名,能夠用一個名稱 替代路徑(基於base路徑)
    hello: './js/hello.js'
  },
});

// 加載入口模塊
seajs.use("./main.js", function(main) {
  main.init(); // init
});
</script>
複製代碼

四、AMD和CMD的區別

關於這兩種的區別網上有不少版本,大致意思差很少:

AMD推崇依賴前置,在定義模塊的時候就要聲明其依賴的模塊

CMD推崇就近依賴,只有在用到某個模塊的時候再去require
複製代碼

因此從這一點上來看,二者在性能上並無太多差別。由於最影響頁面渲染速度的固然是資源的加載速度,既然都是預加載,那麼加載模塊資源的耗時是同樣的(網絡狀況相同時)。

7、UMD

UMD,全稱 Universal Module Definition,即通用模塊規範。既然 CommonJs 和 AMD 風格同樣流行,那麼須要一個能夠統一瀏覽器端以及非瀏覽器端的模塊化方案的規範。

如今主流框架的源碼都是用的UMD規範,由於它既能夠兼容瀏覽器端又能夠兼容node。

UMD的實現:

先判斷是否支持 AMD(define 是否存在),存在則使用 AMD 方式加載模塊;

再判斷是否支持 Node.js 模塊格式(exports 是否存在),存在則使用 Node.js 模塊格式;

前兩個都不存在,則將模塊公開到全局(window 或 global);

全局對象掛載屬性

(function(root, factory) {
    console.log('沒有模塊環境,直接掛載在全局對象上')
    console.log(factory())
    root.umdModule = factory();
}(this, function() {
    return {
        name: '我是一個umd模塊'
    }
}))
複製代碼

咱們把factory寫成一個匿名函數,利用IIFE(當即執行函數)去執行工廠函數,返回的對象賦值給root.umdModule,這裏的root就是指向全局對象this,其值多是window或者global,視運行環境而定。

兼容AMD環境

(function(root, factory) {
    if (typeof define === 'function' && define.amd) {
        // 若是環境中有define函數,而且define函數具有amd屬性,則能夠判斷當前環境知足AMD規範
        console.log('是AMD模塊規範,如require.js')
        define(factory)
    } else {
        console.log('沒有模塊環境,直接掛載在全局對象上')
        root.umdModule = factory();
    }
}(this, function() {
    return {
        name: '我是一個umd模塊'
    }
}))
複製代碼

兼容commonJs和CMD

(function(root, factory) {
  if (typeof module === 'object' && typeof module.exports === 'object') {
      console.log('是commonjs模塊規範,nodejs環境')
      module.exports = factory();
  } else if (typeof define === 'function' && define.amd) {
      console.log('是AMD模塊規範,如require.js')
      define(factory)
  } else if (typeof define === 'function' && define.cmd) {
      console.log('是CMD模塊規範,如sea.js')
      define(function(require, exports, module) {
          module.exports = factory()
      })
  } else {
      console.log('沒有模塊環境,直接掛載在全局對象上')
      root.umdModule = factory();
  }
}(this, function() {
  return {
      name: '我是一個umd模塊'
  }
}))
複製代碼

jQuery 模塊如何用 UMD 規範:

(function (factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(['jquery'], factory);
    } else if (typeof module === 'object' && module.exports) {
        // Node/CommonJS
        module.exports = function( root, jQuery ) {
            if ( jQuery === undefined ) {
                // require('jQuery') returns a factory that requires window to
                // build a jQuery instance, we normalize how we use modules
                // that require this pattern but the window provided is a noop
                // if it's defined (how jquery works)
                if ( typeof window !== 'undefined' ) {
                    jQuery = require('jquery');
                }
                else {
                    jQuery = require('jquery')(root);
                }
            }
            factory(jQuery);
            return jQuery;
        };
    } else {
        // Browser globals
        factory(jQuery);
    }
}(function ($) {
    $.fn.jqueryPlugin = function () { return true; };
}));
複製代碼

vue源碼UMD規範:

(function (global, factory) {
  // 遵循UMD規範
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  typeof define === 'function' && define.amd ? define(factory) :
  (global = global || self, global.Vue = factory());
}(this, function () { 'use strict';
  ···
  // Vue 構造函數
  function Vue (options) {
    // 保證了沒法直接經過Vue()去調用,只能經過new的方式去建立實例
    if (!(this instanceof Vue)
    ) {
      warn('Vue is a constructor and should be called with the `new` keyword');
    }
    this._init(options);
  }
  return Vue
})
複製代碼

8、es6模塊規範

前端的模塊化發展如此複雜,ECMAScript 標準的起草者 TC39 委員會不能再坐視不理,推出了ES2015 Modules(import、export),最後有了ES6模塊化規範。

導入的值也是隻讀不可變對象,不像CommonJS是一個內存的拷貝,看一個栗子就能明白es6相比commonJs優勢在什麼地方。

commonJs代碼

// 模塊定義代碼:lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};
// 模塊使用代碼:main.js
var mod = require('./lib');
console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3
var mod2 = require('./lib');
console.log(mod2.counter);  // 3
複製代碼

爲何都是3?

CommonJS 規範是一種動態加載、拷貝值對象執行的模塊規範。每一個模塊在被使用時,都是在運行時被動態拉取並被拷貝使用的,模塊定義是惟一的,但有幾處引用便有幾處拷貝。因此,對於不一樣的 require 調用,生成的是不一樣的運行時對象。

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  get counter() {
    return counter
  },
  incCounter: incCounter,
};
// main.js
var mod = require('./lib');
console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 4
複製代碼

爲何加了一個getter函數就行了?

這是因爲 CommonJS 的拷貝機制形成的。因爲 CommonJS 規範的拷貝運行機制,在 lib.js 中使用 module.exports 輸出的對象,是從 lib 模塊內拷貝而得,當時 counter 的值是幾,便拷貝了幾。不管執行 incCounter 多少次,改變的都不是輸出對象的 counter 變量。

而當定義了 getter 屬性以後,該屬性指向了被 incCounter 方法以閉包形式囊括的 counter 變量,這個變量是輸出的模塊對象的一部分。

ES6模塊化

/** 定義模塊 hello.js **/
var age = 10;
var add = function (a, b) {
    return age + a + b
};
export { age, add };

/** 引用模塊 **/
import { age, add } from './js/hello.js';
function test() {
    console.log(age)
    return add(20, age);
}
console.log(test())
複製代碼

在 ES6 模塊規範中,只有 export 與 import 兩個關鍵字。

也可使用default關鍵字

/** 定義模塊 hello.js **/
var age = 10;
var add = function (a, b) {
    return age + a + b
};
export default { age, add };

/** 引用模塊 **/
import hello from './js/hello.js';
function test() {
    console.log(hello.age)
    return hello.add(20, hello.age);
}
console.log(test())
複製代碼

ES6 模塊規範與 CommonJS 規範不一樣:

(1)ES6 模塊規範是解析(是解析不是編譯)時靜態加載、運行時動態引用,全部引用出去的模塊對象均指向同一個模塊對象。在上面使用 CommonJS 規範聲明的 lib 模塊,若是使用 ES6 模塊規範聲明,根本不會出現 counter 變量含糊不清的問題。

(2)CommonJS 規範是運行時動態加載、拷貝值對象使用。每個引用出去的模塊對象,都是一個獨立的對象。

寫在最後

前端模塊化有這麼多的標準,爲何咱們實際開發中用的那麼少呢,由於後來出了webpack這個神器,把模塊化編程須要作的都幫咱們解決了。

webpack 本身實現了一套模塊機制,不管是 CommonJS 模塊的 require 語法仍是 ES6 模塊的 import 語法,都可以被解析並轉換成指定環境的可運行代碼。隨着webpack打包工具的流行,ES6語法普遍手中,後來的開發者對於 AMD CMD的感知愈來愈少。

參考連接

https://juejin.cn/post/6844903927104667662
https://zhuanlan.zhihu.com/p/55407719
https://www.cnblogs.com/dolphinX/p/4381855.html
https://juejin.cn/post/6844903848511799303
複製代碼

公衆號:小Jerry有話說

相關文章
相關標籤/搜索