從零開始的NodeJS 第二章(前端模塊化之路)

JavaScript模塊機制

JS模塊化

自我使用JS這門語言以來,JS在整個發展歷史中不斷的變遷和優化,隨着使用者的增多和瀏覽器的支持規範的發展,JS的發展大體我劃分了六個階段。 表單校驗 --> 工具類庫 --> 組件庫 --> 前端框架 --> 前端應用 --> 前端微服務javascript

avatar
)

前端模塊化的發展是把 函數 做爲第一步的:模塊的本質就是實現特定功能的一組方法,在JavaScript中,函數是建立局部做用域的惟一方式,所以採用了函數做爲JS模塊化的第一步。html

function cut(){
    //具體實現
}
複製代碼

Cut 方法能夠做爲模塊被直接調用,這樣的好處是實現了代碼的複用,缺點是很容易形成命名衝突,而且污染了全局變量。當頁面抽離出的函數模塊較多時,看不出它們之間的組織關係。前端

函數掛載到對象 上是前端模塊化邁出的第二步。把功能函數掛載到一個對象上,有須要的時候,直接經過調用對象屬性的方式進行函數調用。 這裏隱隱有股Java類的感受,學過Java的可能會感受熟悉,本質上就是namespace模式的使用。C++的應該很瞭解。java

const FileUnit= {
  path:undefined,
  read: function(){},
  write: function(){}
}
FileUnit.read(path);
複製代碼

FileUnit對象掛載了模塊上的成員函數和變量。在進行使用的時候,就是調用這個對象的屬性。這樣的寫法解決了第一步的問題,可是,由此衍生出了新的問題,對象會暴露出全部的成員變量和函數,而且掛載的函數和變量有可能被改寫。json

當即執行函數(IIFE) 是爲了解決暴露對象而邁出的第三步。後端

//什麼是當即執行函數?-->就是聲明完成馬上執行的函數
function x(){} //聲明函數x
x();//調用函數x

//IIFE 聲明完成馬上調用 (fn(){})()
const module= (function(){
    let _path = "./xxx.json";
    let cut = function(){
      console.log(_private)
      }
    return {
        cut: cut
    }
})()
module.cut();
module._path; // undefined
複製代碼

當即執行函數(IIFE) 容許在函數內部使用局部變量,而不會意外覆蓋同名全局變量,但仍然可以訪問到全局變量,而在模塊外部沒法修改未暴露的私有變量和函數。但缺點也很明顯,不少時候,咱們的模塊都是架設在別的模塊的依賴上進行封裝,那當這個module模塊依賴另外一個模塊怎麼辦?在進行探索以後,前端模塊化邁出了第四步。數組

引入依賴瀏覽器

const module= (function(window,$){
    let cut = function(){
      console.log(window,$)
      }
    return {
        cut: cut
    }
})(window,jQuery)
複製代碼

將要引入的依賴做爲匿名函數的參數進行傳入,由此就能夠在函數內部訪問到依賴的下級模塊進行邏輯的封裝,由此開始,前端模塊化正式開始了漫漫長路。也由此,前端模塊化題提出了幾個不得不解決的問題:緩存

  • 如何安全的包裝一個模塊的代碼?(不污染模塊外的任何代碼)
  • 如何惟一標識一個模塊?
  • 如何優雅的把模塊的API暴漏出去?(不能增長全局變量)
  • 如何方便的使用所依賴的模塊?

前端的工程師對於上述問題給出的全部解決方案都有一個共同點:使用單個全局變量來把全部的代碼包含在一個函數內,由此來建立私有的命名空間和閉包做用域。由此,前端模塊規範誕生了。安全

JS的模塊化之路

  • CommonJS 規範
  • AMD
  • CMD
  • ES6模塊

CommonJS 規範

CommonJS 規範爲Javascript制定了一個美好的願景 ——— Javascript可以在任何地方運行。

出發點

在Web的發展過程當中,前端的規範化在穩步推行,但後端的JS規範卻遠遠落後,對後端的JS自身而言,還有不少缺陷

  • 沒有模塊系統
  • 標準版較少
  • 缺少包管理系統
  • 沒有標準化的接口

CommonJS 規範的提出,主要是爲了彌補後端JS沒有標準的缺陷,但願JS具備開發相似Java或者Python的大型應用的基礎能力。

模塊規範

CommonJS 對模塊的定義分爲三個部分

  • 模塊引用
  • 模塊定義
  • 模塊標識
//模塊引用
const math=require('math')
複製代碼

require方法接收一個 模塊標識 ,以此引入一個模塊的API到上下文環境中。

//模塊定義
exports.add =function(a,b){
  return a+b
}
//module.exports = value
//exports.xxx = value
複製代碼

CommonJS規範規定,一個文件就是一個模塊。每一個模塊內部,module變量表明當前模塊。這個變量是一個對象,它的exports屬性(即module.exports)是對外的接口。加載某個模塊,實際上是加載該模塊的module.exports屬性。

//模塊標識
模塊標識就是傳遞給require()方法的參數。參數必須是符合小駝峯命名的字符串,或者以./ .. 開頭的相對路徑或絕對路徑,能夠省略js後綴。
複製代碼

CommonJS模塊定義簡單,接口簡潔,成功的將類聚的方法和變量等限定在私有做用域的中,同時支持導入和導出功能以順暢的鏈接上下游依賴。

模塊的加載機制

在服務器端,CommonJS模塊的加載是同步的,而且模塊加載的順序,按照其在代碼中出現的順序加載,模塊能夠屢次加載,可是隻會在第一次加載時運行一次,而後運行結果就被緩存了,之後再加載,就直接讀取緩存結果。要想讓模塊再次運行,必須清除緩存。和其餘模塊加載機制有重大不一樣的是CommonJS模塊的加載輸入的是被輸出的值的拷貝。也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。這點與ES6模塊化有重大差別。

// lib.js
let counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

// main.js
const lib = require('./lib');
console.log(lib.counter);  // 3
lib.incCounter();
console.log(lib.counter); // 3
複製代碼

counter輸出之後,lib.js模塊內部的變化就影響不到counter了。這是由於counter是一個原始類型的值,會被緩存。除非寫成一個函數,才能獲得內部變更後的值。

AMD

CommonJS模塊規範的推出使得服務端Js開始模塊化,可是CommonJS規範對瀏覽器端並不適用,CommonJS模塊的加載是同步加載,而且按照其在代碼中出現的順序加載,瀏覽器端的模塊都存放在服務器上,加載模塊的等待時間取決於網速快慢,若是網速較差,會致使瀏覽器處於"假死"狀態。所以,瀏覽器端的模塊,不能採用"同步加載"(synchronous),只能採用"異步加載"(asynchronous)。這就是AMD規範誕生的背景。()

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

模塊定義和使用

AMD一開始是CommonJS規範中的一個草案,全稱是Asynchronous Module Definition,即異步模塊加載機制。後來由該草案的做者以RequireJS實現了AMD規範,因此通常說AMD也是指RequireJS。AMD規範的實現源於想要一種比當今「編寫一堆必須手動排序的具備隱式依賴項的腳本標籤」更好的模塊依賴解決方案而產生,AMD模塊但願能夠在瀏覽器中直接使用,而且具備良好的調試性且不依賴與特定化的服務器。AMD規範設計源於Dojo使用XHR + eval的真實經驗。

RequireJS的基本思想是,經過define方法,將代碼定義爲模塊;經過require方法,實現代碼的模塊加載。

//定義沒有依賴的模塊
define(id?, dependencies?, factory);
//id :可選參數,它指的是模塊的名字。
//dependencies:可選參數,定義中模塊所依賴模塊的數組。
//factory:模塊初始化要執行的函數或對象
//
define("alpha", ["require", "exports", "module"], function (require, exports, module) {  
      //三種暴露API的方式
      // exports.xxx=xxx
      // module.exports=xxx 
      // return xxx; 
        
});

//模塊引入
require([module], callback);
//module:一個數組,裏面的成員就是要加載的模塊.
//callback:模塊加載成功以後的回調函數。
require(["a","b","c"],function(a,b,c){
  //Code 
});
複製代碼

從整體上說,AMD規範修正了不少CMD規範的細節問題,並在模塊化的路子上進行了並行的邁進。這裏給出一個我以爲講的不錯的AMD規範的文章:Why AMD ? 有興趣的能夠讀一讀詳細的瞭解AMD規範作了哪些改進。

CMD

CMD 即Common Module Definition, CMD是sea.js的做者在推廣sea.js時提出的一種規範.SeaJS與RequireJS並稱,SeaJS做者爲阿里的玉伯。CMD規範專門用於瀏覽器端,模塊的加載是異步的,模塊使用時纔會加載執行。CMD規範整合了CommonJS和AMD規範的特色。在 Sea.js 中,全部 JavaScript 模塊都遵循 CMD模塊定義規範。

在CMD規範中,一個模塊就是一個文件。代碼的書寫格式以下:

define(function(require, exports, module) {
    // 模塊代碼
    // 使用require獲取依賴模塊的接口
    // 使用exports或者module或者return來暴露該模塊的對外接口
})
複製代碼

CMD規範採用全局的define函數定義模塊, 無需羅列依賴數組,在factory函數中需傳入形參require,exports,module. require用來加載一個 js 文件模塊和獲取指定模塊的接口對象module.exports

Sea.js加載依賴的方式分爲兩個時期:

  • 加載期:即在執行一個模塊以前,將其直接或間接依賴的模塊從服務器端同步到瀏覽器端;
  • 執行期:在確認該模塊直接或間接依賴的模塊都加載完畢以後,執行該模塊。

那做爲異步加載模塊的兩種規範,AMD和CMD各有千秋。

  1. AMD推崇依賴前置,在定義模塊的時候就要聲明其依賴的模塊,CMD推崇就近依賴,只有在用到某個模塊的時候再去require。
  2. AMD和CMD最大的區別是對依賴模塊的執行時機處理不一樣,一樣都是異步加載模塊,AMD在加載模塊完成後就會執行改模塊,全部模塊都加載執行完後會進入require的回調函數,執行主邏輯。CMD加載完某個依賴模塊後並不執行,只是下載而已,在全部依賴模塊加載完成後進入主邏輯,遇到require語句的時候才執行對應的模塊,這樣模塊的執行順序和書寫順序是徹底一致的。 所以AMD模塊的使用者用戶體驗好,由於沒有延遲,依賴模塊提早執行了,CMD性能好,只有用戶須要的時候才執行對應的模塊。

ES6

上述提到的模塊規範的都不屬於JS原生支持的, 在ECMAScript 6 (ES6)中,引入了模塊功能, ES6 的模塊功能汲取了CommonJS 和 AMD 的優勢,擁有簡潔的語法並支持異步加載,而且還有其餘諸多更好的支持。

ES6 模塊的設計思想就是:一個 JS 文件就表明一個 JS 模塊。在模塊中你可使用 import 和 export 關鍵字來導入或導出模塊中的東西。

ES6 模塊主要具有如下幾個基本特色:

  • 自動開啓嚴格模式,即便你沒有寫 use strict
  • 每一個模塊都有本身的上下文,每個模塊內聲明的變量都是局部變量,不會污染全局做用域
  • 模塊中能夠導入和導出各類類型的變量,如函數,對象,字符串,數字,布爾值,類等
  • 每個模塊只加載一次,每個 JS 只執行一次, 若是下次再去加載同目錄下同文件,直接從內存中讀取。
/** 定義模塊 math.js **/
let basicNum = 0;
let add = function (a, b) {
    return a + b;
};
export { basicNum, add };
/** 引用模塊 **/
import { basicNum, add } from './math';
function test(item) {
    item.textContent = add(99 + basicNum);
}
複製代碼

上述的例子採用了math.js定義了模塊,經過export導出了一個掛在了basicNum, add兩個函數的對象。在進行引用的時候,經過對對象進行析構獲得導出的對應函數。這種加載方式,使得在採用import命令進行導入的時候,用戶須要知道所要加載的變量名或函數名,不然沒法加載出對應的函數。爲了給用戶提供方便,讓他們不用閱讀文檔就能加載模塊,就要用到export default命令,爲模塊指定默認輸出。

// export-default.js
export default function () {
  console.log('foo');
}
// import-default.js
import customName from './export-default';
customName(); // 'foo'
複製代碼

這種導入方式使得用戶能夠爲匿名函數指定任意名字進行使用。

ES6 模塊與 CommonJS 模塊的差別

① CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。

  • commonJS模塊一旦輸出一個值,模塊內部的變化就影響不到這個值。
  • ES6模塊若是使用import從一個模塊加載變量,那些變量不會被緩存,而是成爲一個指向被加載模塊的引用,原始值變了,import加載的值也會跟着變。須要開發者本身保證,真正取值的時候可以取到值。

② CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。

-運行時加載:commonJS 模塊就是對象;即在輸入時是先加載整個模塊,生成一個對象,而後再從這個對象上讀取方法,這種加載稱爲「運行時加載」。commonJS腳本代碼在require的時候,就會所有執行。一旦出現某個模板被「循環加載」,就只能輸出已經執行的部分,還未執行的部分不會輸出。

  • 編譯時加載:ES6 模塊不是對象,而是經過export命令顯式指定輸出的代碼,import時指定加載某個輸出值,而不是加載整個模塊,這種加載稱爲「編譯時加載」。

③ ES6 模塊的運行機制與 CommonJS 不同。ES6 模塊是動態引用,而且不會緩存值,模塊裏面的變量綁定其所在的模塊。

總結

本片簡述了JS的一路發展以來,模塊化的歷程,從最開始模塊化探索,到橫空出世的CommonJS規範,從草案開始的AMD規範到Sea.js推廣的CMD規範,最後官方的ES6模塊化爲模塊化立下了一個階段的里程碑。下篇將會回到NodejS談談Node中模塊化和核心模塊。 由於筆者水平有限,若是有錯誤或者遺漏,歡迎留言進行建議。

相關文章
相關標籤/搜索