js中的模塊化——commonjs,AMD,CMD,UMD,ES6

前言

歷史上,js沒有模塊化的概念,不能把一個大工程分解成很多小模塊。這對於多人開發大型,複雜的項目形成了巨大的障礙,明顯降低了開發效率,java,Python有import,甚至連css都有@import,但是令人費解的是js居然沒有這方面的支持。es6出現之後才解決了這個問題,在這之前,各大社區也都出現了很多解決方法,比較出色的被大家廣爲流傳的就有AMD,CMD,commonjs,UMD,今天我們就來分析這幾個模塊化的解決方案。

模塊加載

上面提到的幾種模塊化的方案的模塊加載有何異同呢?
先來說下es6模塊,es6模塊的設計思想是儘量靜態化,使得編譯時就能確定依賴關係,被稱爲編譯時加載。其餘的都只能在運行時確定依賴關係,這種被稱爲運行時加載。下面來看下例子就明白了,比如下面這段代碼

let {a,b,c} = require("util");//會加載util裏的所有方法,使用時只用到3個方法
import {a,b,c} from 'util';//從util中加載3個方法,其餘不加載

模塊化的幾種方案

下面簡單介紹一下AMD,CMD,commonjs,UMD這幾種模塊化方案。

commonjs

commonjs是服務端模塊化採用的規範,nodejs就採用了這個規範。
根據commonjs的規範,一個單獨的文件就是一個模塊,加載模塊使用require方法,該方法讀取文件並執行,返回export對象。

// foobar.js
//私有變量
var test = 123;
//公有方法
function foobar () {
 
    this.foo = function () {
        // do someing ...
    }
    this.bar = function () {
        //do someing ...
    }
}
//exports對象上的方法和變量是公有的
var foobar = new foobar();
exports.foobar = foobar;
//讀取
var test = require('./foobar').foobar;
test.bar();

CommonJS 加載模塊是同步的,所以只有加載完成才能執行後面的操作。像Node.js主要用於服務器的編程,加載的模塊文件一般都已經存在本地硬盤,所以加載起來比較快,不用考慮異步加載的方式,所以CommonJS規範比較適用。但如果是瀏覽器環境,要從服務器加載模塊,這是就必須採用異步模式。所以就有了 AMD CMD 解決方案。

AMD

AMD是"Asynchronous Module Definition"的縮寫,意思就是"異步模塊定義"
AMD設計出一個簡潔的寫模塊API:

define(id?, dependencies?, factory);

第一個參數 id 爲字符串類型,表示了模塊標識,爲可選參數。若不存在則模塊標識應該默認定義爲在加載器中被請求腳本的標識。如果存在,那麼模塊標識必須爲頂層的或者一個絕對的標識。
第二個參數,dependencies ,是一個當前模塊依賴的,已被模塊定義的模塊標識的數組字面量。
第三個參數,factory,是一個需要進行實例化的函數或者一個對象。

看下下面的例子就明白了

define("alpha", [ "require", "exports", "beta" ], function( require, exports, beta ){
    export.verb = function(){
        return beta.verb();
        // or:
        return require("beta").verb();
    }
});

提到AMD就不得不提requirejs。
RequireJS 是一個前端的模塊化管理的工具庫,遵循AMD規範,它的作者就是AMD規範的創始人 James Burke。
AMD的基本思想就是先加載需要的模塊,然後返回一個新的函數,所有的操作都在這個函數內部操作,之前加載的模塊在這個函數裏是可以調用的。

CMD

CMD是seajs在推廣的過程中對模塊的定義的規範化產出
和AMD提前執行不同的是,CMD是延遲執行,不過requirejs從2.0開始也開始支持延遲執行了,這取決於寫法。
AMD推薦的是依賴前置,CMD推薦的是依賴就近。
看下AMD和CMD的代碼

//AMD
define(['./a','./b'], function (a, b) {
    //依賴一開始就寫好
    a.test();
    b.test();
});
 
//CMD
define(function (requie, exports, module) {
    //依賴可以就近書寫
    var a = require('./a');
    a.test();
    ...
    //軟依賴
    if (status) {
        var b = requie('./b');
        b.test();
    }
});

UMD

UMD是AMD和commonjs的結合
AMD適用瀏覽器,commonjs適用服務端,如果結合了兩者就達到了跨平臺的解決方案。
UMD先判斷是否支持AMD(define是否存在),存在用AMD模塊的方式加載模塊,再判斷是否支持nodejs的模塊(exports是否存在),存在用nodejs模塊的方式,否則掛在window上,當全局變量使用。
這也是目前很多插件頭部的寫法,就是用來兼容各種不同模塊化的寫法。

(function(window, factory) {
    //amd
    if (typeof define === 'function' && define.amd) {
        define(factory);
    } else if (typeof exports === 'object') { //umd
        module.exports = factory();
    } else {
        window.jeDate = factory();
    }
})(this, function() {  
...module..code...
})

ES6

es6的模塊自動採用嚴格模式,不管有沒有在頭部加上'use strict'
模塊是由export和import兩個命令構成。

export命令

  1. export命令可以出現在模塊的任何位置,只要處於模塊的頂層(不在塊級作用域內)即可。如果處於塊級作用域內,會報錯。
  2. export語句輸出的值是動態綁定的,綁定其所在的模塊。

export default命令

//a.js
export default function(){
  console.log('aaa');
}
//b.js
import aaa from 'a.js';

1.使用export default的時候,對應的import不需要使用大括號,import命令可以爲default指定任意的名字。
2.不適用export default的時候,對應的import是需要使用大括號的
3.一個export default只能使用一次

import命令

  1. import命令具有提升效果,會提升到整個模塊的頭部首先執行,所以建議直接寫在頭部,這樣也方便查看和管理。
  2. import語句會執行所加載的模塊,因爲有以下的寫法
  import 'lodash;

上面的代碼僅僅執行了lodash模塊,沒有輸入任何值

整體加載

整體加載有兩種方式

//import
import * as circle from './circle'
//module
//module後面跟一個變量,表示輸入的模塊定義在該變量上
module circle from './circle'

循環加載

在講循環加載前,先了解下commonjs和es6模塊加載的原理

commonjs模塊加載的原理

commonjs的一個模塊就是一個腳本文件,require命令第一次加載腳本的時候就會執行整個腳本,然後在內存中生成一個對象

{
  id:"...",
  exports: {...},
  loaded: true,
  ...
}

上面的對象中,id是模塊名,exports是模塊輸出的各個接口,loaded是一個布爾值,表示該模塊的腳本是否執行完畢.
之後要用到這個模塊時,就會到exports上取值,即使再次執行require命令,也不會執行該模塊,而是到緩存中取值

es6模塊加載的

commonjs模塊輸入的是被輸出值的拷貝,也就是說一旦輸出一個值,模塊內部的變化就影響不到這個值
es6的運行機制和commonjs不一樣,它遇到模塊加載命令import不會去執行模塊,只會生成一個動態的只讀引用,等到真正要用的時候,再到模塊中去取值,由於es6輸入的模塊變量只是一個‘符號鏈接’,所以這個變量是隻讀的,對他進行重新賦值會報錯。

import {obj} from 'a.js';
obj.a = 'qqq';//ok
obj = {}//typeError

分析完兩者的加載原理,來看下兩者的循環加載

commonjs的循環加載

commonjs模塊的重要特性是加載時執行,即代碼在require的時候就會執行,commonjs的做法是一旦出現循環加載,就只輸出已經執行的部分,還未執行的部分不會輸出.
下面來看下commonjs中的循環加載的代碼

//a.js
exports.done = false;
var b = require('./b.js');
console.log('在a.js中,b.done=',b.done);
exports.done = true;
console.log('a.js執行完畢')
//b.js
exports.done = false;
var a = require('./a.js');
console.log('在b.js中,a.done=',a.done);
exports.done = true;
console.log('b.js執行完畢')
//main.js
var a = require('./a.js');
var b = require('./b.js');
console.log('在main.js中,a.done=',a.done,',b.done=',b.done);

上面的代碼中,執行a.js的時候,a.js先輸出done變量,然後加載另一個腳本b.js,此時a的代碼就停在這裏,等待b.js執行完畢,再往下執行。然後看下b.js的代碼,b.js也是先輸出done變量,然後加載a.js,這時發生了循環加載,按照commonjs的機制,系統會去a.js中的exports上取值,可是其實a.js是沒有執行完的,只能輸出已經執行的部分done=false,然後b.js繼續執行,執行完畢後將執行權返回給a.js,於是a.js繼續執行,直到執行完畢。
所以執行main.js,結果爲
在b.js中,a.done=false
b.js執行完畢
在a.js中,b=done=true
a.js執行完畢
在main.js中,a.done=true,b.done=true
上面這個例子說了兩點

  1. 在b.js中a.js沒有執行完,只執行了第一行

2.main.js中執行到第二行不會再次執行b.js,而是輸出緩存的b.js的執行結果,即第4行

es6的循環加載

es6處理循環加載和commonjs不同,es6是動態引用,遇到模塊加載命令import時不會去執行模塊,只會生成一個指向模塊的引用,需要開發者自己保證能取到輸出的值
看下面的例子

//a.js
import {odd} from 'b.js';
export counter = 0;
export function even(n){
  counter++;
  return n==0 || odd(n-1);
}
//b.js
import {even} from 'a.js';
export function odd(n){
  return n!=0 && even(n-1);
}
//main.js
import {event,counter } from './a.js';
event(10)
counter //6

執行main.js,按照commonjs的規範,上面的代碼是無法執行的,因爲a先加載b,b又加載a,但是a又沒有輸出值,b的even(n-1)會報錯
但是es6可以執行,結果是6

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章