web系列之模組化——AMD、CMD、CommonJS、ES6 整理&&比較

_Bruce發表於2018-10-24

AMD、CMD、CommonJS、ES6

一、AMD

AMD,Asynchronous Module Definition, 非同步模組定義。它是一個在瀏覽器端模組化開發的規範。 它不是javascript原生支援,所以使用AMD規範進行頁面開發需要用到對應的庫,也就是RequireJS,AMD其實是RequireJS在推廣的過程中對模組定義的範圍化的產出。

requireJS主要解決兩個問題:

  • 多個js檔案存在依賴關係時,被依賴的檔案需要早於依賴它的檔案載入到瀏覽器
  • js載入的時候瀏覽器會阻塞渲染執行緒,載入檔案越多,頁面失去響應的時間越長

用法: require需要一個root來作為搜尋依賴的開始(類似package.jsonmain),data-main來指定這個root

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

這樣就指定了rootapp.js,只有直接或者間接與app.js有依賴關係的module才會被插入到html中。

  • define()函式:用來定義模組的函式。args0: 需引入模組的名字陣列,arg1:依賴引入之後的callbackcallback的引數就是引入的東西。如果有多個依賴,則引數按照引入的順序依次傳入。
define(['dependence_name'], (args) => {
// args就是從dependence_name引入的東西
// ... Your fucking code ...
   return your_export;
});
複製程式碼
  • require()函式: 用來引入模組的函式。
require(['import_module_name'], (args) => {
// args就是從import_module_name引入的東西
// ... Your fucking code ...
});
複製程式碼
  • require.config配置:
    • baseUrl:載入module的根路徑
    • paths:用於對映不存在根路徑下面的模組路徑
    • shimes:載入非AMD規範的js

二、CMD

CMD, Common Module Definition, 通用模組定義。 CMD是在sea.js推廣的過程中產生的。在CMD規範中,一個模組就是一個檔案。

define(function(require, exprots, module) {
	const fs = require('fs');  //接受模組標識作為唯一引數
	// exports,module則和CommonJS類似
	exports.module = {
		props: 'value'
	};
});

seajs.use(['test.js'], function(test_exports) {
// ....
});
複製程式碼
null AMD CMD
定義module時對依賴的處理 推崇依賴前置,在定義的時候就要宣告其依賴的模組 推崇就近依賴,只有在用到這個module的時候才去require
載入方式 async async
執行module的方式 載入module完成後就會執行該module,所有module都載入執行完成後會進入require的回撥函式,執行主邏輯。依賴的執行順序和書寫的順序不一定一致,誰先下載完誰先執行,但是主邏輯 一定在所有的依賴載入完成後才執行(有點類似Promise.all)。 載入完某個依賴後並不執行,只是下載而已。在所有的module載入完成後進入主邏輯,遇到require語句的時候才會執行對應的module。module的執行順序和書寫的順序是完全一致的。

三、CommonJS

English time: Common -- 常識 W3C官方定義的API都只能基於Browser,而CommonJS則彌補了javascript這方面的不足。

NodeJSCommonJS規範的主要實踐者。它有四個重要的環境變數為模組化的實現提供支援:module、exports、require、global。 實際用時,使用module.exports(不推薦使用exports)定義對外輸出的API,用require來引用模組。CommonJS用同步的方式載入模組。在Server上模組檔案都在本地磁碟,所以讀取非常快沒什麼不妥,但是在Browser由於網路的原因,更合理的方案是非同步載入。 CommonJS對模組的定義主要分為:模組引用、模組定義、模組標識3個部分。

1、模組引用:

const fs = require('fs');
複製程式碼

require的執行步驟:

  1. 如果是核心模組, 如fs,則直接返回模組
  2. 如果是路徑,則拼接成一個絕對路徑,然後先讀取快取require.cache再讀取檔案。(如果沒有副檔名,則以js => json => node(以二進位制外掛模組的方式去讀取)的順序去識別)
  3. 首次載入後的模組會在require.cache中,所以多次require,得到的物件是同一個(引用的同一個物件)
  4. 在執行模組程式碼的時候,會將模組包裝成以下模式,以便於作用域在模組範圍之內。
(function (exports, require, module, __filename, __dirname) {
  // module codes
});
複製程式碼
  1. 包裝之後的程式碼同過vm原生模組的runInThisContext()方法執行(類似eval,不過具有明確上下文不會汙染環境),返回一個function物件。 最後將當前模組物件的exportsrequire方法、module以及檔案定位中得到的完整檔案路徑(包括檔名)和檔案目錄傳遞給這個function執行。

2、模組定義:

function fn() {}
exports.propName = fn;
module.exports = fn;
複製程式碼

一個module物件代表模組本身,exportsmodule的屬性。一般通過在exports上掛載屬性即可定義匯出,也可以直接給module.exports賦值來定義匯出(推薦)。

3、模組標識:

模組標識就是傳遞給require()方法的引數,可以是相對路徑或者絕對路徑,也可以是符合小駝峰命名的字串。 NodeJSCommonJS的實現:Node中模組分為Node提供的核心模組和使用者編寫的檔案模組

核心模組Node原始碼的編譯過程中,編譯進了二進位制執行檔案。在Node啟動的時候部分核心模組就載入到了memory中,所以在引用核心模組的時候,檔案定位和編譯執行步驟可以省略,並且在路徑判斷中優先判斷,所以它的載入速度是最快的。 檔案模組則是在執行時動態載入,需要完整的路徑分析,檔案定位、編譯執行等過程,速度較核心模組慢。 在NodeJS中引入模組需要經歷如下3個步驟:

  1. 路徑分析:module.paths = [‘當前目錄下的node_modules’, ‘父目錄下的node_modules’, …, ‘跟目錄下的node_modules’]

  2. 檔案定位:副檔名分析、目錄和包的處理

    • 副檔名分析:Node會按.js => .json => .node的次序補足副檔名依次嘗試。(在嘗試的過程中會呼叫同步的fs模組來檢視檔案是否存在)
    • 目錄和包的處理:可能沒有對應的檔案,但是存在相應的目錄。這時Node會在此目錄中查詢package.json,並JSON.parsemain(入口檔案)對應的檔案。如果main屬性錯誤或者沒有package.json,則將index作為main。如果沒有定位成功任何檔案,則到下一個模組路徑重複上述工作,如果整個module.paths都遍歷完都沒有找到目標檔案,則跑出查詢失敗錯誤。
  3. 編譯執行:在Node中每個模組檔案都是一個物件,編譯執行是引入檔案模組的最後一個階段。定位到檔案後,Node會新建一個模組物件,然後根據路徑載入並編譯。對於不同的副檔名,其載入的方式也有所不同:

    • .js: 通過fs模組同步讀取檔案後編譯執行
    • .node:這是C++編寫的擴充套件檔案,通過dlopen()載入最後編譯生成的檔案。
    • .json:同.js檔案,之後用JSON.parse解析返回結果。 其餘檔案: 都按js的方式解析。
null CommonJS ES6
keywords exports, require, module, __filename. __dirname import, export
匯入 const path = require('fs'); 必須將一個模組匯出的所有屬性都引入 import path from 'path'; 可以只引入某個
匯出 module.exports = App; export default App;
匯入的物件 隨意修改 值的copy 不能隨意修改 值的reference
匯入次數 可以任意次require,除了第一次,之後的require都是從require.cache中取得 在頭部匯入,只能匯入一次
載入 執行時載入 編譯時輸出介面

ES6模組

ES6的模組已經比較熟悉了,用法不多贅述,直接上碼:

import { prop } from 'app';   //從app中匯入prop
import { prop as newProp } from 'app';   // 功能和上面一樣,不過是將匯入的prop重新命名為newProp

import App from 'App';   // 匯入App的default
import * as App from 'App';  // 匯入App的所有屬性到App物件中

export const variable = 'value'; // 匯出一個名為variable的常量
export {variable as newVar};   // 和import 的重新命名類似,將variable作為newVar匯出

export default variable = 'value';  // 將variable作為預設匯出
export {variable as default};   //  和上面的寫法基本一樣

export {variable} from 'module';  // 匯出module的variable ,該模組中無法訪問
export {variable as newVar} from 'module';  // 下面的自己看  不解釋了
export {variable as newVar} from 'module';
export * from 'module';
複製程式碼

ps:ES6模組匯入的變數(其實應該叫常量更準確)具有以下特點: 變數提升、相當於被Object.freeze()包裝過一樣、import/export只能在頂級作用域

ES6模組區別於CommonJS的執行時載入,import 命令會被JavaScript引擎靜態分析,優先於模組內的其他內容執行(類似於函式宣告優先於其他語句那樣), 也就是說在檔案的任何位置import引入模組都會被提前到檔案頂部。

ES6的模組 自動開啟嚴格模式,即使沒有寫'use strict'; 。 執行一個包含import宣告的模組時,被引入的模組先匯入並載入,然後根據依賴關係,每個模組的內容會使用深度優先的原則進行遍歷。跳過已經執行過的模組,避免依賴迴圈。

okey~接下來老哥再看看(查查)import到底幹啥了: 標準幾乎沒有談到import該做什麼,ES6將模組的載入細節完全交給了實現。 大致來說,js引擎執行一個模組的時候,其行為大致可歸納為以下四步:

  1. 解析:engine去解析模組的程式碼,檢查語法等。
  2. 載入:遞迴載入所有被引入的模組,深度優先
  3. 連結:為每個新載入的模組建立一個作用域,並將模組中的宣告綁入其中(包括從其他模組中引入的)。 當js引擎開始執行載入進來的模組中的程式碼的時候,import的處理過程已經完了,所以js引擎執行到一行import宣告的時候什麼也不會幹。引入都是靜態實現的,等到程式碼執行的時候就啥都不幹了。

既然說到了模組(module),那就順便提一下它和指令碼(script)的區別(注意,我這裡說的區別僅限於在Web瀏覽器中):

- module script
使用方式 (當然還有其他的執行方式,在這裡不做過多討論) <script src="./source.js type="module" /> <script src="./source.js type="text/javascript" />
下載 ①遇到<script>時,會自動應用defer。
②下載 && 解析module。
③遞迴下載module中匯入的資源。下載階段完成。
遇到<script>時預設阻塞文件渲染,開啟下載。
執行方式 ①下載完成後會遞迴執行module中匯入的資源。
②然後執行module本身。
ps:內聯module少了下載module本身的步驟,其他步驟和引入的module相同。
預設是下載完成立即執行

參考連結:

前端模組化:CommonJS,AMD,CMD,ES6

ES6 的模組系統

深入淺出NodeJS

相關文章