前端模塊化的理解(AMD,CMD,CommonJs,ES6)

前言

初期的web端交互還是很簡單,不需要太多的js就能實現。隨著時代的的發展,用戶對Web瀏覽器的性能也提出了越來越高的要求,瀏覽器也越來越多的承擔了更多的交互,不再是寥寥數語的js就能解決的,那么就造成了前端代碼的日益膨脹,js之間的相互依賴也會越來越多,此時就需要使用一定的規范來管理js之間的依賴。
本文主要是什么是模塊化,為什么需要模塊化以及現下流行的模塊化規范:AMD,CMD,CommonJs,ES6。

什么是模塊化

要想理解模塊化是什么可以先理解模塊是什么?
模塊:能夠獨立命名并且能夠獨立完成一定功能的集合。
因此在js中就可以理解為模塊就是能夠實現特定功能獨立的一個個js文件。
模塊化:就可以簡單的理解為將原來繁重復雜的整個js文件按功能或者按模塊拆成一個個單獨的js文件,然后將每一個js文件中的某些方法拋出去,給別的js文件去引用和依賴。

為什么需要模塊化

一、模塊化的進程

1、全局function模式:將不同的功能封裝為不同的函數
缺點:污染全局命名空間,容易引起命名沖突,看不出模塊間的依賴
2、namespace模式:封裝為對象模式
作用:減少全局變量,解決命名沖突
缺點:數據不安全(外部函數可以修改模塊內的數據),看不出模塊之間的依賴

const module = {
    data:1,
  getData(){console.log(this.data)}
}
module.data = 2; //這樣會直接修改模塊內部的數據

3、IIFE模式:匿名函數自調用(閉包)
作用:解決了數據安全,數據是私有的,外部只能調用暴露的信息
缺點:需要綁定到一個全局變量上例如window向外暴露,這樣也會有命名沖突的問題
4、IIFE增強模式:引入依賴
作用:解決了模塊直接的依賴問題
缺點:引入js的時候需要注意引入的順序,并且當依賴很多的時候也會有弊端

// IIFE模式:匿名函數自調用(閉包)
(function(window){
    let data = '這是IIFE模式';
  getData(){
    console.log(data);
  }
  window.module = { getData }
})(window)
// IIFE增強模式
(function(window,$){
    let data = '這是IIFE模式';
  getData(){
    console.log(data);
    $('body').css('background', 'red')
  }
  window.module = { getData }
})(window,jQuery);
//index.html
//需要注意引入的順序
<script type="text/javascript" src="jquery-1.10.1.js"></script>
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
  myModule.foo()
</script>
//當<script>過多的時候的缺點:
// 1. 請求過多
// 2. 依賴模糊:不清楚依賴直接是什么關系,很容易因為引入的順序導致出錯
// 3. 難以維護

從以上的發展歷程來看雖然模塊化還不是那么的完善,但是也不難能發現模塊化的優點:

二、模塊化的優點:

  • 避免命名沖突
  • 更好的分離模塊,按需加載
  • 高復用性
  • 高維護性

模塊的規范

AMD

AMD(Asynchronous Module Definition):異步模塊定義。采用異步方式加載模塊,模塊的加載不影響后續語句的執行。所有依賴這個模塊的語句,都定義在一個回調函數中,等到加載完成之后,這個回調函數才會運行。瀏覽器環境要從服務器端加載模塊,這時就必須采用非同步模式,因此瀏覽器端一般采用AMD規范。
RequireJs是AMD規范的最佳實踐,所以我們用AMD規范的時候要引入requirejs。

AMD的語法:

define用來定義模塊;
require用來加載模塊,通常AMD框架會以require方法作為入口,進行依賴關系分析并依次有序地進行加載。
AMD是依賴前置,就是說,在define或require中方法里傳入的模塊數組會在一開始就下載并執行

//define定義模塊
//第一個參數:依賴的模塊,沒有可以不寫
define(['module1','module2'],function(m1,m2){
  //如果引入了module,但是沒有使用,模塊的內容也會執行
    return 模塊;
})
//引入模塊,相當于主函數的引用
require(['module1','module2'],function(m1,m2){
  //使用m1,m2
  //如果引入了module,但是沒有使用,模塊的內容也會執行
})
AMD具體使用流程
  1. 引入requirejs
    a. requirejs官網下載:requirejs
    b. github:requirejs
    c. 或者直接用require的js鏈接放在index.html中引用(只建議在學習時候用): https://requirejs.org/docs/release/2.3.6/minified/require.js
  2. 定義模塊
//module1.js
define(function(){
    const msg='module1';
  return {
    msg
  }
})
//module2.js
define(function(){
    const msg="module2";
  return {
    msg
  }
})
//module3.js
define(['module2'],function(m2) {
    const msg="module3";
    const msg2 = m2.msg
    return {
        msg,
        msg2
    };
});
//main.js,入口文件不需要module2,因此就不需要引入
require.config({
    paths:{
        jquery:'jquery' //用于引入第三方庫,此處填寫的是路徑,文件的路徑;當然也可以在index.html用script標簽引入
    }
})
require(['module1','module3','jquery'],function(m1,m3,$){
    console.log(m1,'module1');
    console.log(m3,'module3');
    console.log($,'第三方庫');
});
console.log('模塊加載'); //會優先打印
  1. index.html的引入
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
  <!--data-main:文件入口-->
    <script data-main="main" src="https://requirejs.org/docs/release/2.3.6/minified/require.js"></script>
</body>
</html>
  1. 輸出結果


    image

CMD

CMD(Common Module Definition):通用模塊定義。用于瀏覽器端,是除AMD以外的另一種模塊組織規范。結合了AMD與CommonJs(后面會講到)的特點。也是異步加載模塊。
與AMD不同的是:AMD推崇的是依賴前置,而CMD是依賴就近,延遲執行。
依賴前置&&依賴就近,延遲執行

//依賴前置:AMD
require(['module1','module2'],function(m1,m2){
    //依賴的模塊首先加載,無論后續是否會用到
})
//依賴就近,延遲執行
define(funciton(require){
    const module1 = require('./module1'); //用到的時候再申明,不需要就不用申明,也就不會加載進來       
})
CMD具體使用步驟
  1. 引入sea.js
    a. 官網:https://seajs.github.io/seajs/docs/#downloads
    b. github:https://github.com/seajs/seajs
  2. 模塊定義
//module1.js
define(function(require,exports) {
    const msg='這是模塊1';
    // const module2 = require('./module2');
    exports.msg = msg; // 注意這里是用的exports
    // exports.module2 = module2; // 如果是多個就得這么寫,所以如果暴露多個接口不建議用exports
});
//module2.js
define(function(require,exports,module) {
    const msg="這是模塊2";
    module.exports = { //這里用的是module.exports,跟exports作用一樣
        msg
    }
});
//module3.js
define(function(require,exports,module) {
    const msg="這是模塊3";
    const module2 = require('./module2');
    module.exports = { //這里用的是module.exports,跟exports作用一樣
        msg,
        module2
    }
});
//main.js
define(function(require, exports,module) {
    const module1 = require('./module1');
    const module3 = require('./module3');
    console.log(module1);
    console.log(module3);
});
console.log('模塊加載');
  1. index.html引入
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script src="sea.js"></script>
    <script>
        seajs.use('main');
    </script>
</body>
</html>
  1. 輸出結果
    image

CommonJS

Node 應用由模塊組成,采用 CommonJS 模塊規范。每個文件就是一個模塊,有自己的作用域。在一個文件里面定義的變量、函數、類,都是私有的,對其他文件不可見。在服務器端,模塊的加載是運行時同步加載的;在瀏覽器端,模塊需要提前編譯打包處理。
CommonJs有4個畢竟重要的變量:modulerequireexportsglobal

CommonJs的特點:
  1. 所有代碼都運行在模塊作用域,不會污染全局作用域;如果想要多個模塊共享一個變量,需要給global添加屬性(不建議這么用)
//module.js
global.data = '共享變量'; //添加到global的屬性是多個文件可以共享的
let x = {
    a:5
}
let b=0;
const add = function(val){
    x.a+=x.a;
  b=++b;
}
module.exports.x=x; //只有對外暴露了變量,在外部引用的時候才能獲取到,否則x,b就是模塊內部的私有變量
module.exports.add = add;
  1. 模塊可以多次加載,但是只有再第一次加載的時候才會執行,后續的加載都是使用的緩存結果;如果想要再次加載需要清除緩存,或者對外暴露一個函數,加載之后執行暴露的函數
// test1.js
let msg="測試緩存";
console.log(msg);
exports.msg=msg;
// test2.js
const msg = require('./test1');
const msg2 = require('./test1');
// 上面的輸出結果是:測試緩存
// 會發現只打印了一次結果,以此可以證明只有第一次加載的時候才會執行,第二次加載的時候使用的是緩存的結果
// 刪除指定模塊的緩存
delete require.cache[moduleName] // moduleName必須是絕對路徑
// 刪除所有模塊緩存
Object.keys(require.cache).forEach(function(key){
    delete require.cache[key]
})
  1. 模塊加載的順序是按照再代碼中書寫的順序加載的,同步加載模塊。
  2. 引入的值其實是輸出值的拷貝(淺拷貝)。也就是說一旦值輸出之后,模塊內的改變就不會影響這個值的改變(引用類型除外)
// example.js
let x= {
    a:5
};
let b=0;
const add = function(val){
    x.a+=x.a;
    b=++b;
    // return x.a++;
}
module.exports.x = x;
module.exports.b=b;
module.exports.add = add;
// main.js
const example = require('./example');
const add = require('./example').add;
example.add();
console.log(example.x) // {a:10}
console.log(example.b) // b:0 
CommonJs的語法
//對外暴露接口
module.exports
exports
//引入模塊
require(模塊的路徑) //模塊的路徑有多種寫法,后續補充

module.exports&&exports
module.exports: 每個模塊內部,module代表了當前這個模塊,module是一個對象,對外暴露的就是exports這個屬性,加載某個模塊其實就相當于加載的module.exports這個屬性
exports: 其實就是module.exports的引用。為了使用方便,node為每個模塊創建了一個exports變量,這個變量就指向了module.exports。因此以下兩種做法是錯誤的。

//錯誤一
exports='msg'; // 此時相當于改變了exports的指向,失去了與module.exports的聯系,也就失去了對外暴露接口的能力
//錯誤二
exports.msg='msg';
module.exports = 'Hello world'; // 上面的msg是無法對外暴露的,因為module.exports被重新賦值了;此時對外暴露的就是『Hello world』

ES6

ES modules(ESM)是 JavaScript 官方的標準化模塊系統。ES6模塊設計的思想是盡量的靜態化,使得編譯時就能知道模塊的依賴關系,以及輸入和輸出的變量。有兩個主要的命令:export和import。export用于對外暴露接口,import用于引入其他模塊。

ES6模塊的特點:
  • 嚴格模式:ES6 的模塊自動采用嚴格模式
  • import read-only特性: import的屬性是只讀的,不能賦值,類似于const的特性
  • export/import提升: import/export必須位于模塊頂級,不能位于作用域內;其次對于模塊內的import/export會提升到模塊頂部,這是在編譯階段完成的
  • 兼容在node環境下運行
  • ES modules 輸出的是值的引用,輸出接口動態綁定,而 CommonJS 輸出的是值的拷貝
//ES6模塊值的引用 .mjs 主要是為了能用node環境運行:node --experimental-modules
// example.mjs
let x= {
    a:5
};
let b=0;
const add = function(val){
    x.a+=x.a;
    b=++b;
}
export {x,b,add}
// main.mjs
import { x,b, add} from './example.mjs';
add();
console.log(x,b); // {a:10} 1 

export&&import用法
export:用于向外暴露接口
import:用于引入外部接口

// 方法一:
//export單個向外暴露接口
export const x = 1;
export const y = {a:1}
export const add = function(){console.log(123)}
//export一起向外暴露接口
const x=1;
const y={a:1};
const add = function(){console.log(123)};
export {x,y,add}
//import引入外部接口
//針對以上兩種方式import可以寫成如下兩種情況
import {x,y,add} from './exmaple'; //使用哪個就引入哪個
import * as moduleName from './exmaple'; // 全部引入,使用的時候使用moduleName.x
// 方法二:
//除了使用export 向外暴露接口外還可以使用export default向外暴露接口:同一個模塊中export可以有多個,但是export default只能有一個
const x=1;
const y={a:1};
export default { x,y}
// 對應的import
import moduleName from './exmaple'; //全部引入,使用方式moduleName.x

總結

  • AMD:異步加載模塊,允許指定回調函數。AMD規范是依賴前置的。一般瀏覽器端會采用AMD規范。但是開發成本高,代碼閱讀和書寫比較困難。
  • CMD:異步加載模塊。CMD規范是依賴就近,延遲加載。一般也是用于瀏覽器端。
  • CommonJs:同步加載模塊,一般用于服務器端。對外暴露的接口是值的拷貝
  • ES6:實現簡單。對外暴露的接口是值的引用。可以用于瀏覽器端和服務端。

后記

模塊化的一次有一次的變更,讓系統模塊化變得也越來越好,但是響應的也引起了一些問題。例如使用模塊的時候,發現有些模塊引入了但是并沒有真正的使用到,這樣就造成了代碼的冗余,多了一些不必要的代碼。這些模塊有時通過檢查是很難發現的,因此就要想如何能夠快速的去除這些無用的模塊呢,此時Tree Shaking就出現了;又比如如何能夠在開發代碼的時候比較便捷,然后在生產中又有高強度的兼容性呢,此時就出現了babel;又比如如何預處理模塊,此時出現了webpack……
出現了解決問題的辦法,就得繼續學習啦……

參考文章

前端模塊化的十年征程
徹底理清 AMD,CommonJS,CMD,UMD,ES6 modules
前端模塊化—CommonJS、CMD、AMD、UMD和ESM
前端模塊化詳解(完整版)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • ~出來包括,嗯,每一項每一項工作,然後需要做什麼?在什麼節點做,啊,怎麼做?把這些全部都羅列出來,羅列出來之後我們...
    戈壁女神閱讀 486評論 0 0
  • 詩配畫是兒童畫的一種,只不過在兒童畫的基礎上配以古詩(現代詩)文字內容,以這種獨特的方式來簡簡單單表現中國文...
    前郭225王冬芹閱讀 199評論 0 0
  • 就算是自己的日記本吧,畢竟還是太懶根本就不可能堅持下來 ,天哪,好像就是這個樣子的。 繼續堅持自己一貫的作風,起了...
    給一柄劍卍許一座城閱讀 262評論 0 1
  • 欣入梅嶺鑒清雪 移履不為春瓶折 貪賞失寒猶戀戀 省卻對窗嘆早謝 昆南于己亥端月十八
    昆南閱讀 365評論 0 5
  • 最近在使用Masonry出現了一個問題,以前居然沒有遇到過,也是神奇了,就是我在一個View上面添加一個label...
    coder_feng閱讀 376評論 0 0