1. 程式人生 > >前端入門22-講講模組化

前端入門22-講講模組化

宣告

本篇內容梳理自以下來源:

感謝各位大佬的分享,解惑了很多。

正文-模組化

現在回過頭來想想,也許選擇以《JavaScript權威指南》一書來作為入門有些不好,因為這本書畢竟是很早之前的,書中所講的思想、標準也基本都只是 ES5 及那時代的相關技術。

這也就導致了,在書中看到的很多例子,雖然覺得所用到的思想很奇妙,比如臨時名稱空間之類的處理,但其實,有些技術到現在已經有了更為強大的技術替代了。

就像這篇要講的模組化,目前,以我看到的各資料後,所收穫的知識是,大概有四種較為常用且熱門的模組化技術,也許還有更新的技術,也許還有我不知道的技術,無所謂,慢慢來,這篇的內容已經夠我消化了。

目前四種模組化技術:

  • CommonJS規範&node.js
  • AMD規範&Require.js
  • CMD規範&Sea.js
  • ES6標準

前面是規範,規範就是類似於 ES5,ES6 只是提出來作為一種標準規範,而不同規範有具體的實現,比如 nodeJS 實現了 CommonJS 規範。

模組化歷程

在宣告部分中的第二、第三連結裡那兩篇,以時間線介紹了模組化的相關技術的發展歷程,我覺得很有必要一看,對於掌握和理解目前的模組化技術挺有幫助的。

這裡,就稍微摘抄其中一些內容,詳細內容還是需要到原文閱讀。

1.全域性變數、全域性函式(1999年)

這時候的多個 js 指令碼檔案之間,直接通過全域性變數或全域性函式的方式進行通訊,這種方式叫做:直接定義依賴。

雖然做的好一些的會對這些 js 檔案做一些目錄規劃,將資源歸類整理,但仍無法解決全域性名稱空間被大量汙染,極其容易導致變數衝突問題。

2.物件作為名稱空間(2002年)

為了解決遍地的全域性變數問題,這時候提出一種名稱空間模式的思路,即將本要定義成全域性變數、全域性函式的這些全都作為一個物件的屬性存在,這個物件的角色充當名稱空間,不同模組的 JS 檔案中通過訪問這個物件的屬性來進行通訊。

3.立即執行的函式作為臨時名稱空間 + 閉包(2003年)

雖然提出利用一個物件來作為名稱空間的思路,一定程度解決了大量的全域性變數的問題,但仍舊存在很多侷限,比如沒有模組的隱藏性,所以針對這些問題,這時候又新提出一種思路:利用立即執行的函式來作為臨時名稱空間,這樣就可以避免汙染全域性名稱空間,同時,結合閉包特性,來實現隱藏細節,只對外暴露指定介面。

雖然這種思路,解決了很多問題,但仍舊有一些侷限,比如,缺乏管理者,什麼意思,就是說,在前端裡,開發人員得手動將不同 JS 指令碼檔案按照它們之間的依賴關係,以被依賴在前面的順序來手動書寫 <script> 載入這些檔案。

也就是不同 <script> 的前後順序實際上表示著它們之間的依賴關係,一旦檔案過多,將會很難維護,這是上述方案都存在的問題。

4.動態建立 <script> 工具(2009年)

針對上述問題,也就衍生出了一些載入 js 檔案的工具,先看個例子:

$LAB.script("greeting.js").wait()
    .script("x.js")
    .script("y.js").wait()
    .script("run.js");

LAB.js 這類載入工具的原理實際上是動態的建立 <script>,達到作為不同 JS 指令碼檔案的管理者作用,來決定 JS 檔案的載入和執行順序。

雖然我沒用過這種工具,但我覺得它的侷限還是有很多,其實就是將開發人員本來需要手動書寫在 HTML 文件裡的 <script> 程式碼換成寫在 JS 檔案中,不同 JS 檔案之間的依賴關係仍舊需要按照前後順序手動維護。

5.CommonJS規範&node.js(2009年)

中間跳過了一些過程,比如 YUI 的沙箱模式等,因為不熟,想了解更詳細的可以去原文閱讀。

當 CommonJS 規範出來時,模組化算是進入了真正的革命,因為在此之前的探索,都是基於語言層面的優化,也就是利用函式特性、物件特性等來在執行期間模擬出模組的作用,而從這個時候起,模組化的探索就大量的使用了預編譯。

CommonJS 模組化規範有如下特點:

  • 所有程式碼都執行在模組作用域,不會汙染全域性作用域。
  • 模組可以多次載入,但是隻會在第一次載入時執行一次,然後執行結果就被快取了,以後再載入,就直接讀取快取結果。要想讓模組再次執行,必須清除快取。
  • 模組載入的順序,按照其在程式碼中出現的順序。

不同的模組間的依賴關係通過 require 來控制,而每個模組需要對外暴露的介面通過 exports 來決定。

由於 CommonJS 規範本身就只是為了服務端的 node.js 而考慮的,node.js 實現了 CommonJS 規範,所以執行在 node.js 環境中的 js 程式碼可以使用 requireexports 這兩個命令,但在前端裡,瀏覽器的 js 執行引擎並不認識 require 這些命令,所以需要進行一次轉換工作,後續介紹。

再來看看 require 命令的工作原理:

require 命令是 CommonJS 規範之中,用來載入其他模組的命令。它其實不是一個全域性命令,而是指向當前模組的 module.require 命令,而後者又呼叫 Node 的內部命令 Module._load

Module._load = function(request, parent, isMain) {
  // 1. 檢查 Module._cache,是否快取之中有指定模組
  // 2. 如果快取之中沒有,就建立一個新的Module例項
  // 3. 將它儲存到快取
  // 4. 使用 module.load() 載入指定的模組檔案,
  //    讀取檔案內容之後,使用 module.compile() 執行檔案程式碼
  // 5. 如果載入/解析過程報錯,就從快取刪除該模組
  // 6. 返回該模組的 module.exports
};

Module.prototype._compile = function(content, filename) {
  // 1. 生成一個require函式,指向module.require
  // 2. 載入其他輔助方法到require
  // 3. 將檔案內容放到一個函式之中,該函式可呼叫 require
  // 4. 執行該函式
};

所以,其實 CommonJS 的模組化規範之所以可以實現控制作用域、模組依賴、模組通訊,其實本質上就是將模組內的程式碼都放在一個函式內來執行,這過程中會建立一個物件 Module,然後將模組的相關資訊包括對外的介面資訊都新增到物件 Module 中,供其他模組使用。

6.AMD規範&Require.js(2009年)

CommonJS 規範載入模組是同步的,也就是說,只有載入完成,才能執行後面的操作。由於 Node.js 主要用於伺服器程式設計,模組檔案一般都已經存在於本地硬碟,所以載入起來比較快,不用考慮非同步載入的方式,所以CommonJS 規範比較適用。

但是,如果是瀏覽器環境,這種同步載入檔案的模式就會導致瀏覽器陷入卡死狀態,因為網路原因是不可控的。所以,針對瀏覽器環境的模組化,新提出了一種規範:AMD(Asynchronous Modules Definition)非同步模組定義。

也就是說,對於 Node.js,對於服務端而言,模組化規範就是按照 CommonJS 規範即可。

但對於瀏覽器,對於前端而言,CommonJS 不適用,需要看看 AMD 規範。

AMD 規範中定義:

  • 定義一個模組時通過 define 命令,通過 return 宣告模組對外暴露的介面
  • 依賴其他模組時,通過 require 命令

而規範終歸只是規範,使用時還是需要有規範的具體實現,針對 AMD 規範,具體的實現是 Require.js,在前端裡,如果基於 Require.js 來使用 AMD 規範的模組化技術,後續介紹。

7.CMD規範&Sea.js(2013年)

CMD(Common Module Definition)也是專門針對瀏覽器、針對前端而提出的一種模組化規範。它跟 AMD 規範都是用途都是一樣,用途解決前端裡的模組化技術。

但兩種規範各有各的優缺點,各有各的適用場景和侷限性吧,我還沒使用過這兩種規範,所以無從比較,但網上關於這兩種規範比較的文章倒是不少。

CMD 規範中定義:

  • 使用 define 命令定義一個模組,使用 exports 來暴露模組對外的介面
  • 使用 require 來同步載入依賴的模組,但也可使用 require.async 來非同步載入依賴的模組

總之,雖然兩種規範都是用於解決前端裡的模組化技術,但實現的本質上還是有些不同,後續介紹。

對於 CMD 規範的具體實現是 Sea.js,前端裡如果想使用 CMD 規範的模組化技術,需要藉助 Sea.js。

8.ES6標準(2015年)

2015 年釋出的 ES6 標準規範中,新增了 Module 特性,也就是官方在 ES6 中,在語言本身層面上,添加了模組的機制支援,讓 JavaScript 語言本身終於可以支援模組特性了。

在 ES6 之前的各種方案,包括 CommonJS,AMD,CMD,本質上其實都是利用函式具有的本地變數的特性進行封裝從而模擬出模組機制。也就是說,這些方案都是需要在執行期間,才會動態的創建出某個模組,並暴露模組的相關介面。這種方案其實也存在一些侷限性。

而 ES6 新增了模組的機制後,在程式碼的解析階段,就能夠確定模組以及模組對外的介面,而不用等到執行期。這種本質上的區別,在藉助開發工具寫程式碼階段的影響很明顯就是,終於可以讓開發工具智慧的提示依賴的模組都對外提供了哪些介面。

但 ES6 由於是新增的特性,在支援方面目前好像還不是很理想,並不是所有環境都支援 ES6 的模組機制好像,所以會看到某些大佬的文章裡會介紹一些諸如:Babel、Browserify。

Babel 用於將 ES6 轉換為 ES5 程式碼,好讓不支援 ES6 特性的環境可以執行 ES5 程式碼。

Browserify 用於將編譯打包 js,處理 require 這些瀏覽器並不認識的命令。

上面只是簡單介紹了下模組化的發展歷程,而且中間略過一些階段,想看詳細的可以去原文閱讀。下面就具體介紹下,目前四個比較穩定的模組技術:

CommonJS規範

由於 CommonJS 是針對服務端設計的模組化規範,對於 Node.js 來說,一個 JS 檔案就是一個模組,所以並不需要類似 AMD 或 CMD 中的 define 來宣告一個模組。這是我的理解。

exports

既然在 Node.js 中,每個 JS 檔案就是一個模組,那麼這個模組檔案內的變數都是對外隱藏的,外部無權訪問,只能訪問 exports 對外暴露的介面,如:

//module.js
var name = "dasu";
var wx = "dasuAndroidTv";
var getBlog = function() {
    return "http://www.cnblogs.com/dasusu/";
}
//以上變數和函式,外部都無權訪問,外部只能訪問到下面通過 exports 暴露的介面
module.exports.name = name;
exports.getWx = () => wx;

模組內,可以通過對 module.exports 或 exports 新增屬性來達到對外暴露指定介面的目的,當然,從程式上來說,你可以直接改變 module.exports 的指向,讓它指向一個新物件而不是在原本物件上新增屬性,這個就類似於對建構函式 prototype 屬性的修改。

但建議使用方式還是儘可能在 exports 物件上新增屬性。

如果有想探究它的原理的話,可以嘗試利用 Browserify 來轉換這段模組程式碼,看看最後生成的是什麼:

function(require,module,exports){
    //module.js
    var name = "dasu";
    var wx = "dasuAndroidTv";
    var getBlog = function() {
        return "http://www.cnblogs.com/dasusu/";
    }
    //以上變數和函式,外部都無權訪問,外部只能訪問到下面通過 exports 暴露的介面
    module.exports.name = name;
    exports.getWx = () => wx;
}

雖然,對於 Node.js 來說,它其實對待每個 JS 檔案,本質上,會將檔案內的程式碼都放於一個函式內,如果該模組首次被其他模組引用了,那麼就會去執行這個函式,也就是執行模組內的程式碼,由於函式本身有三個引數,其中有兩個分別是:module 和 exports,這也是內部為什麼可以直接通過 module.exports 或 exports 來操作的原因。

Module 物件是 Node.js 會為每個模組檔案建立的一個物件,模組之間的通訊,其實就是通過訪問每個 Module 物件的屬性來實現。

所以,說白了,CommonJS 模組化技術的本質,其實就是利用了函式的區域性作用域的特性來實現模組作用域,然後結合一個物件作為名稱空間的方式,來儲存模組內部需要對外暴露的資訊方式。最後,通過 require 命令來組織各模組之間的依賴關係,解決以前方案沒有管理者角色的侷限,解決誰先載入誰後加載的問題。

require

每個 JS 檔案其實都被當做一個模組處理,也就是檔案內的程式碼都會被放入到一個函式內,那這個函式什麼時候執行呢?也就是說,模組什麼時候應該被載入呢?

這就是由 require 命令決定,當某個模組內使用了 require 命令去載入其他模組,那麼這時候,被載入的模組如果是首次被呼叫,它是沒有對應的 Module 物件的,所以會去呼叫它的函式,執行模組內程式碼,這個過程是同步的,這期間會完善這個模組的 Module 物件資訊。之後,其他模組如果也引用了這個模組,因為模組內程式碼已經被執行過了,已經存在對應的 Module 物件,所以此時就不會再重複去載入這個模組了,直接返回 Module 物件。

以上,基本也就是模組間依賴的載入處理過程。而 require 命令用法很簡單:

//main.js
var module1 = require("./module");

module1.name;  //輸出=> dasu
module1.getWx(); //輸出 => dasuAndroidTv
//module1.getBlog(); //沒有許可權訪問

其實,Node.js 對 main.js 的處理也是認為它是個模組,所以檔案內的程式碼也都放入一個函式內,還記得函式的第一個引數就是 require 麼,這也就是為什麼模組內可以直接使用 require() 的原因,require 其實本質上是一個函式,具體的實現是 Node.js 的一個內建方法:Module._load(),主要工作上一節有介紹過了。

說得稍微嚴謹點,Node.js 其實才是作為各模組之間的管理者,由它來管控著哪個模組先載入,哪個後加載,維護著各模組對外暴露的資訊。

到這裡再來理解,有些文章中對 Module 物件的介紹:

  • module.id 模組的識別符,通常是帶有絕對路徑的模組檔名。
  • module.filename 模組的檔名,帶有絕對路徑。
  • module.loaded 返回一個布林值,表示模組是否已經完成載入。
  • module.parent 返回一個物件,表示呼叫該模組的模組。
  • module.children 返回一個數組,表示該模組要用到的其他模組。
  • module.exports 表示模組對外輸出的值。

這時,對於 Module 物件內的各屬性用途,理解應該會比較容易點了。

最後說一點,CommonJS 只是一種模組化的規範,而 Node.js 才是這個規範的具體實現者,但 Node.js 通常用於服務端的執行環境,對於前端而言,對於瀏覽器而言,因為不存在 Node.js 這東西,所以 require 或 exports 這類在前端裡是無法執行的,但可以藉助 Browerify 來進行程式碼轉換。

AMD規範

AMD規範和規範實現者:Require.js

前端裡沒有 Node.js 的存在,即使有類似的存在,但由於 CommonJS 的模組化規範中,各模組的載入行為是同步的,也就是被依賴的模組必須執行結束,當前模組才能繼續處理,這對於前端而言,模組的載入就多了一個下載的過程,而網路是不可靠的,所以 CommonJS 並不適用於前端的場景。

所以,針對前端,提出了另一種模組化規範:AMD,即非同步模組載入,通過增加回調處理的方式,來移除被依賴模組和當前模組的前後關聯,兩個模組可同時下載,執行。當前模組內,需要依賴於其他模組資訊的程式碼放置於回撥函式內,這樣就可以先行處理當前模組內其他程式碼。

define

前端裡是通過 <script> 來載入 JS 檔案程式碼的,不能像 Node.js 那樣從源頭上處理 JS 檔案,所以它多了一個 define 來定義模組,如:

//module.js
define(function(){
    var name = "dasu";
    var wx = "dasuAndroidTv";
    var getBlog = function() {
        return "http://www.cnblogs.com/dasusu/";
    }
    
    return {
        name: name,
        getWx: function() {
            return wx;
        }
    }
})

如果定義的模組又依賴了其他模組時,此時 define 需要接收兩個引數,如:

//兩個引數,第一個引數是陣列,數組裡是當前模組依賴的所有模組,第二個引數是函式,函式需要引數,引數個數跟陣列個數一直,也跟數組裡依賴的模組一一對應,該模組內部就是通過引數來訪問依賴的模組。
define(['module2'], function(module2){
    //module2.xxx  使用模組 module2 提供的介面
    
    //本模組的內部邏輯
    
    return {
        //對外暴露的介面
    }
})

define 有兩個引數,第一個引數是陣列,數組裡是當前模組依賴的所有模組,第二個引數是函式,函式需要引數,引數個數跟陣列個數一直,也跟數組裡依賴的模組一一對應,該模組內部就是通過引數來訪問依賴的模組。

require

如果其他地方有需要使用到某個模組提供的功能時,此時就需要通過 require 來宣告依賴關係,但宣告依賴前,需要先通過 requirejs.config 來配置各個模組的路徑資訊,方便 Require.js 能夠獲得正確路徑自動去下載。

requirejs.config({
    paths: {
        module1: './module'
    }
})

var module1 = require(['module1'], function(module1){
    console.log(module1.name);    //訪問模組內的 name 介面
    console.log(module1.getWx()); //訪問模組內的 getWx 介面
});

//其他不依賴於模組的程式碼

這種方式的話,require 的第一個陣列引數內的值就可以模組的別稱,而第二個引數是個函式,同樣,函式的引數就是載入後的模組,該 JS 檔案內需要依賴到模組資訊的程式碼都可以放到回撥函式中,通過回撥函式的引數來訪問依賴的模組資訊。

使用步驟

1.下載 Require.js,並放到專案中

Require.js:https://requirejs.org/docs/download.html#requirejs

2.新建作為模組的檔案,如 module.js,並通過 define 定義模組

//module.js
define(function(){
    var name = "dasu";
    var wx = "dasuAndroidTv";
    var getBlog = function() {
        return "http://www.cnblogs.com/dasusu/";
    }
    
    return {
        name: name,
        getWx: function() {
            return wx;
        }
    }
})

3.在其他 js 檔案內先配置所有的模組來源資訊

//main.js
requirejs.config({
    paths: {
        module1: './module'
    }
})

4.配置完模組資訊後,通過 require 宣告需要依賴的模組

//main.js
var module1 = require(['module1'], function(module1){
    console.log(module1.name);    //訪問模組內的 name 介面
    console.log(module1.getWx()); //訪問模組內的 getWx 介面
});

//其他不依賴於模組的程式碼

5.最後也最重要的一步,在 html 中宣告 require.js 和 入口 js 如 main.js 的載入關係

<script src="js/lib/require.js" data-main="js/src/main.js"></script>

當然,這只是基礎用法的步驟,其中第 3 步的模組初始化步驟也可通過其他方式,如直接利用 require 的不同引數型別來實現等等,但大體上需要這幾個過程,尤其是最後一步,也是最重要一步,因為 AMD 在前端的具體實現都依靠於 Require.js,所以必須等到 Require.js 下載並執行結束後會開始處理其他 js。

以上例子的專案結構如圖:

小結

最後小結一下,AMD 規範的具體實現 Require.js 其實從使用上來看,已經比較容易明白它的原理是什麼了。

本質上,也還是利用了函式的特性,作為模組存在的那些程式碼本身已經通過 define 規範被定義在函式內了,所以模組內的程式碼自然對外是隱藏的,外部能訪問到的只有函式內 return 的介面,那麼這裡其實也就利用了閉包的特性。

所以,模組化的實現,無非就是讓函式作為臨時名稱空間結合閉包或者物件作為名稱空間方式, 這種方式即使沒有 CommonJS 規範,沒有 AMD 規範,自己寫程式碼很可以容易的實現。那麼為什麼會有這些規範技術的出現呢?

無非就是為了引入一個管理者的角色,沒有管理者的角色,模組之間的依賴關係,哪個檔案先載入,哪個後加載,<script> 的書寫順序都只能依靠人工來維護、管理。

而這些模組化規範,其實目的就在於解決這些問題,CommonJS 是由 Node.js 作為管理者角色,來維護、控制各模組的依賴關係,檔案的載入順序。而 AMD 則是由 Require.js 來作為管理者角色,開發者不用在 HTML 裡寫那麼多 <script>,而且也沒必要去關心這些檔案誰寫前誰寫後,Require.js 會自動根據 require 來維護這些依賴關係,自動根據 requirejs.config 的配置資訊去決定先載入哪個檔案後加載哪個檔案。

CMD規範

CMD 規範,類似於 AMD,同樣也是用於解決前端裡的模組化技術問題。而有了 AMD 為什麼還會有 CMD,我個人的理解是,AMD 的適用場景並沒有覆蓋整個前端裡的需求,或者說 AMD 本身也有一些缺點,導致了新的 CMD 規範的出現。

比如說,從使用方式上,AMD 就有很多跟 CommonJS 規範不一致的地方,對於從 CommonJS 轉過來的這部分人來說,可能就會很不習慣;再比如說,AMD 考慮的場景可能太多,又要適配 jQurey,又要適配 Node 等等;

總之,CMD 規範總有它出現和存在的理由,下面就大概來看看 CMD 的用法:

define&exports

類似於 AMD,CMD 規範中,也是通過 define 定義一個模組:

//module.js
define(function (require, exports, module) {
    var name = "dasu";
    var wx = "dasuAndroidTv";
    var getBlog = function() {
        return "http://www.cnblogs.com/dasusu/";
    }
    
    exports.name = name;
    exports.getWx = () => wx;
})

跟 AMD 不一樣的地方是,CMD 中 define 只接收一個引數,引數型別是一個函式,函式的引數也很重要,有三個,按順序分別是 require,exports,module,作用就是 CommonJS 規範中三個命令的用途。

如果當前模組需要依賴其他模組,那麼在內部,使用 require 命令即可,所以,函式的三個引數很重要。

當前模組如果不依賴其他模組,也沒有對外提供任何介面,那麼,函式可以沒有引數,因為有了內部也沒有使用。

而如果當前模組需要依賴其他模組,那麼就需要使用到 require,所以函式第一個引數就是必須的;如果當前模組需要對外暴露介面,那麼後兩個引數也是需要的;

總之,建議使用 define 定義模組時,將函式三個引數都帶上,用不用再說,規範一點總沒錯。

require

在有需要使用某個模組提供的功能時,通過 require 來宣告依賴關係:

//main.js
define(function (require, exports, module) {
    console.log("=====main.js=======");

    var module1 = require("./module");//同步載入模組
    console.log(module1.name);

    require.async("./module2", function (module2) {//非同步載入模組
        console.log(module2.wx);
    })
})

require 預設是以同步方式載入模組,如果需要非同步載入,需要使用 require.async

使用步驟

1.下載 Sea.js,並放到專案中

Sea.js:https://github.com/seajs/seajs/releases

2.新建作為模組的檔案,如 module.js,並通過 define 定義模組

//module.js
define(function (require, exports, module) {
    var name = "dasu";
    var wx = "dasuAndroidTv";
    var getBlog = function() {
        return "http://www.cnblogs.com/dasusu/";
    }
    
    exports.name = name;
    exports.getWx = () => wx;
})

3.其他需要依賴到該模組的地方通過 require 宣告

//main.js
define(function (require, exports, module) {
    console.log("=====main.js=======");

    var module1 = require("./module");//同步載入模組
    console.log(module1.name);

    require.async("./module2", function (module2) {//非同步載入模組
        console.log(module2.wx);
    })
})

4.最後也最重要的一步,在 html 中先載入 sea.js 並指定主模組的 js

<script src="js/lib/require.js"></script>
<script>
     seajs.use("./js/src/main.js");
</script>

使用步驟跟 AMD 很類似,首先是需要依賴於 Sea.js,所以必須先下載它。

然後定義模組、依賴模組、使用模組的方式就跟 CommonJS 很類似,這幾個操作跟 AMD 會有些不同,也許這點也正是 CMD 存在的原因之一。

最後一步也是最重要一步,需要在 HTML 文件中載入 sea.js 文件,並指定入口的 js,注意做的事雖然跟 AMD 一樣,但實現方式不一樣。

小結

其實,CMD 跟 CommonJS 很類似,甚至在模組化方面的工作,可以很通俗的將 sea.js 理解成 node.js 所做的事,只是有些 node.js 能完成但卻無法通過 sea.js 來負責的工作需要開發人員手動處理,比如定義一個模組、通過 <script> 載入 sea.js 和指定主入口的 js 的工作。

CommonJS, AMD, CMD 三者間區別

下面分別從適用場景、使用步驟、使用方式、特性等幾個方面來對比這些不同的規範:

適用場景

CommonJS 用於服務端,基於 Node.js 的執行環境;

AMD 和 CMD 用於前端,基於瀏覽器的執行環境;

使用方式

CommonJS 通過 require 來依賴其他模組,通過 exports 來為當前模組暴露介面;

AMD 通過 define 來定義模組,通過 requirejs.config 來配置模組路徑資訊,通過 require 來依賴其他模組,通過 retrurn 來暴露模組介面;

CMD 通過 define 來定義模組,通過 require 來依賴其他模組,通過 exports 來為當前模組暴露介面;

使用步驟

CommonJS 適用的 Node.js 執行環境,無需其他步驟,正常使用模組技術即可;

AMD 適用的前端瀏覽器的執行環境沒有 Require.js,所以專案中需要先載入 Require.js,然後再執行主入口的 js 程式碼,需要在 HTML 中使用類似如下命令:

<script src="js/lib/require.js" data-main="js/src/main.js"></script>

CMD 適用的前端瀏覽器的執行環境也沒有 Sea.js,所以專案中也需要先載入 Sea.js,然後再執行主入口的 js 程式碼,需要在 HTML 中使用類似如下命令:

<script src="js/lib/sea.js"></script>
<script>
    seajs.use("./js/src/main.js");
</script>

#### 特性

AMD:依賴前置、提前執行,如:

require(['module1','module2'], function(m1, m2){
    //...
})
define(['module1','module2'], function(m1, m2){
    //...
})

需要先將所有的依賴的載入完畢後,才會去處理回撥中的程式碼,這叫依賴前置、提前執行;

CMD:依賴就近、延遲執行,如:

define(function(require, exports, module){
    //...
    var module1 = require("./module1");
    //...
    require("./module2", function(m2){
        //...
    });
})

等程式碼執行到 require 這行程式碼時才去載入對應的模組

ES6標準

ES6 中新增的模組特性,在上一篇中已經稍微介紹了點,這裡也不具體展開介紹了,需要的話開頭的宣告部分有給出連結,自行查閱。

這裡就簡單說下,在前端瀏覽器中使用 ES6 模組特性的步驟:

1.定義模組,通過指定 <script type="module"> 方式

2.依賴其他模組使用 import,模組對外暴露介面時使用 export;

需要注意的一點是,當 JS 檔案內出現 import 或者 export 時,這份 JS 檔案必須宣告為模組檔案,即在 HTML 文件中通過指定 <script> 標籤的 type 為 module,這樣 import 或 export 才能夠正常執行。

也就是說,使用其他模組的功能時,當前的 JS 檔案也必須是模組。

另外,有一點,ES6 的模組新特性,所有作為模組的檔案都需要開發人員手動去 HTML 文件中宣告並載入,這是與其他方案不同的一點,ES6 中 import 只負責匯入指定模組的介面而已,宣告模組和載入模組都需要藉助 <script> 實現。

這裡不詳細講 ES6 的模組特性,但想來講講,一些轉換工作的配置,因為:

  • 有些瀏覽器不支援 ES6 的語法,寫完 ES6 的程式碼後,需要通過 Babel 將 ES6 轉化為 ES5。
  • 生成了 ES5 之後,裡面仍然有 require 語法,而瀏覽器並不認識 require 這個關鍵字。此時,可以用 Browserify 編譯打包 js,進行再次轉換。

而我是選擇使用 WebStrom 作為開發工具的,所以就來講講如何配置

WebStrom 的 Babel 配置

教程部分摘抄自:ES6的介紹和環境配置

1.新建專案
2.通過 npm 初始化專案

在安裝 Babel 之前,需要先用 npm 初始化我們的專案。開啟終端或者通過 cmd 開啟命令列工具,進入專案目錄,輸入如下命令: npm init -y,命令執行結束後,會在專案根目錄下生成 package.json 檔案

3.(首次)全域性安裝 Babel-cli

在終端裡執行如下命令:

npm install -g babel-cli

4.本地安裝 babel-preset-es2015 和 babel-cli

npm install --save-dev babel-preset-es2015 babel-cli

本地是指在專案的根目錄下開啟終端執行以上命令,執行結束,專案根目錄的 package.json 檔案中會多了 devDependencies 選項

5.新建 .babelrc 檔案

在根目錄下新建檔案 .babelrc,輸入如下內容:

{
    "presets":[
        "es2015"
    ],
    "plugins":[]
}

6.執行命令轉換

babel js/src/main.js -o js/dist/main.js

-o 前是原檔案,後面是轉換後的目標檔案

這是我的專案結構:

.json 檔案和 node_modules 資料夾都是操作完上述步驟後會自動生成的,最後執行完命令後,會在 dist 目錄下生成目標檔案。

7.(可選)如果嫌每次執行的命令太過複雜,可利用 npm 指令碼

babel js/src/main.js -o js/dist/main.js 這行程式碼複製到 package.json 裡的 scripts 欄位中:

以後每次都點一下 build 左邊的三角形按鈕執行一下指令碼就可以了,省去了手動輸命令的時間。

8.(可選)如果還嫌每次手動點選按鈕執行指令碼麻煩,可配置監聽檔案改動自動進行轉換

開啟 WebStrom 的 Setting -> Tools -> File Watchers,然後點選 + 按鈕,選擇 Babel 選項,然後進行配置:

9.最後,以後每次新的專案,除了第 3 步不用了之外,其餘步驟仍舊需要進行。

WebStrom 的 Browserify 配置

步驟跟上述很類似,區別僅在於一個下載 babel,這裡下載的是 browserify,以及轉換的命令不同而已:

1.新建專案
2.通過 npm 初始化專案

開啟終端,進入到專案的根目錄,執行 npm init -y,執行結束後會在根目錄生成 package.json 檔案

3.(首次)全域性安裝 browserify

在終端裡執行如下命令:

npm install browserify -g

4.執行命令轉換

browserify js/src/main.js -o js/dist/main.js --debug

-o 前是原檔案,後面是轉換後的目標檔案

5.(可選)如果嫌每次執行的命令太過複雜,可利用 npm 指令碼

browserify js/src/main.js -o js/dist/main.js --debug 這行程式碼複製到 package.json 裡的 scripts 欄位中:

以後每次都點一下 build 左邊的三角形按鈕執行一下指令碼就可以了,省去了手動輸命令的時間。

6.(可選)如果還嫌每次手動點選按鈕執行指令碼麻煩,可配置監聽檔案改動自動進行轉換

開啟 WebStrom 的 Setting -> Tools -> File Watchers,然後點選 + 按鈕,選擇 <custom> 選項,然後進行配置:

7.最後,以後每次新的專案,除了第 3 步不用了之外,其餘步驟仍舊需要進行。


大家好,我是 dasu,歡迎關注我的公眾號(dasuAndroidTv),公眾號中有我的聯絡方式,歡迎有事沒事來嘮嗑一下,如果你覺得本篇內容有幫助到你,可以轉載但記得要關注,要標明原文哦,謝謝支援~
dasuAndroidTv2.png