背景
自es6以前,JavaScript是天生模塊化缺失的,即缺少類似后端語言的class,
作用域也只以函數作為區分。這與早期js的語言定位有關,
作為一個只需要在網頁中嵌入幾十上百行代碼來實現一些基本的交互效果的腳本語言,
確實用不着嚴格的組織代碼規范。但是隨着時代的發展,js承擔的任務越來越重,
從原先的script引入幾十行代碼即可的狀態變成現在多人協作文件眾多的地步,
管理和組織代碼的難度越來越大,模塊化的需求也越來越迫切。
在此背景下,眾多的模塊化加載器便應運而生。
模塊化規范和實現
前文提到在es6模塊化出現之前,為了解決模塊化的需求,出現了眾多的模塊化機制例如cmd,amd等。遵循不同規范有sea.js, require.js等實現。
- AMD:
Asynchronous Module Definition 異步模塊定義。瀏覽器端模塊化開發的規范,
模塊將被異步加載,模塊加載不影響后面語句的運行。所有依賴某些模塊的語句均放置在回調函數中。
AMD 是 RequireJS 在推廣過程中對模塊定義的規范化的產出。require.js詳情參考
//依賴前置,jquery模塊先聲明
define(['jquery'], function ($) {
/***/
})
- CommonJS:
CommonJS是服務器端模塊的規范,Node.js采用了這個規范。Node.JS首先采用了js模塊化的概念。CommonJS規范參考
//同步加載
var $ = require('jquery');
/****/
module.exports = myFunc;
- CMD:
CMD(Common Module Definition) 通用模塊定義。該規范是SeaJS推廣過程中發展出來的。
與AMD區別:
AMD是依賴關系前置,CMD是按需加載。更多參考
define(function (require, exports, module) {
// 就近依賴
var $ = require('jquery');
/****/
})
- UMD:
Universal Module Definition 通用模塊規范。
基於統一規范的目的出現,看起來沒那么簡約,但是支持amd和commonjs以及全局模塊模式。
//做的工作其實就是這么粗暴,判斷當前用的什么就以當前規范來定義
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['jquery'], factory);
} else if (typeof exports === 'object') {
// CommonJS
module.exports = factory(require('jquery'));
} else {
// 全局變量
root.returnExports = factory(root.jQuery);
}
}(this, function ($) {
// methods
function myFunc(){};
// exposed public method
return myFunc;
}));
綜上所訴,各個不同的規范都有各自的優點,具體使用需要是各自項目情況而定。沒有好不好只有適用與否。
模塊加載器實現原理淺析
以上各種模塊加載器關於模塊化的實現都有各自的特點,並且是比較成型完善的體系。本文也無意去重新實現一個大而全的模塊加載器。
本着學習的態度,簡單對其中的部分原理進行部分探究。
所有的模塊加載器的實現都要有以下步驟:
- 動態創建腳本節點
模塊化歸根到底還是要在瀏覽器中加載腳本。 - 解析依賴模塊路徑
模塊化的初衷就是解決各個文件的依賴問題,所以各個模塊之間的依賴分析必不可少。
該部分需要控制腳本加載隊列,對遞歸依賴進行分析,按順序進行加載 - 模塊緩存
將每個模塊的內容根據特定規則保存起來,為調用做准備。
對於沒有聲明name的模塊要匹配的話就需要根據currentScript獲取文件名。然后進行緩存.
基礎方法聲明
既然是有個加載器,當然是會指定一些規則來讓使用者遵循。否則也實現不了相應的方法。不同的框架的實現方式也是不同的,不過速途同歸。
作為一個模塊加載器(簡單歸簡單),基本的接口如下:
- 定義模塊(define):
define(deps,func,name)參數如下
1 deps: 依賴模塊,支持數組,或者省略
2 func: 自身func,即接受依賴模塊作為參數的回調
3 name: 模塊對應的name,如果不存在則根據當前路徑來命名。
功能無非是根據不同的狀態將該模塊處理后的屬性,例如name等。存入模塊隊列(modules變量)中。
modules[ src ] = {
name : name || src,
src : src,
dps : [],
exports : (typeof fn === "function")&&fn(),
state : "complete"
};
- 依賴模塊接口(require):
require(deps,func)參數同define。這里就不實現支持commonjs的方式了,即依賴必須前置聲明。
//不支持
var a = require('a');
//支持
require( ['a'], function(a) {
var a1 = a;
});
這樣而來require模塊的完全可以通過define來實現了。
動態創建腳本
歸根到底前端模塊化的實質還是通過script標簽來引入相應文件(基於服務端的模塊加載器非此類實現,例如webpack等)。
所以必不可少的需要進行創建和引入。主要用到的createElement方法來創建以及appendChild插入dom中。
/**
* @param src string
* 此處的src為路徑,即define里的字段
* */
var loadScript = function(src) {
/**
* 進一步處理,是否網絡路徑或者本地路徑
* */
var scriptSrc = getUrl(src);
/**
* 接下來實現大同小異,無非是腳本加載變化時的處理函數的做法
* */
var sc = document.createElement("script");
var head = document.getElementsByTagName("head")[0];
sc.src = scriptSrc;
sc.onload = function() {
console.log("script tag is load, the url is : " + src);
};
head.appendChild( sc );
};
解析依賴模塊路徑
由前面創建腳本可知,需要解析腳本路徑來分別區分當前不同路徑。
路徑和模塊的對應關系遵循id=路徑的原則
//此處的a對應的路徑即為base+a.js.
require('a', function(){
//abcc
} )
當然實際情況中的匹配是很復雜的,簡單實現就不考慮那么多。
對於匿名模塊的存在,是可以通過document.currentScript獲取當前路徑手動給其增加標識的。
腳本路徑無外乎一下幾種情況:
- 相對路徑:
此種路徑只需要獲取當前跟路徑拼接處理即可。(為了簡化處理,此處入口文件在項目根目錄下) - http網絡路徑:
此路徑直接不變即可. - npm依賴的各種包,此處就先不處理這種了畢竟是簡單實現。
var getUrl = function(src) {
var scriptSrc = "";
//判斷URL是否是
//相對路徑'/'或者'./'開頭的,獲取當前根路徑替換掉其他字符即可。
if( src.indexOf("/") === 0 || src.indexOf("./") === 0 ) {
scriptSrc = require.config.base + src.replace(/(^\/|^\.\/)/,"");
}else if( src.indexOf("http:") === 0 ) {
//直接獲取
scriptSrc = src;
}else if( src.match(/^[a-zA-Z1-9]/) ){
//不以路徑符開頭的直接憑借
scriptSrc = require.config.base + src;
}else if(true) {
alert("src錯誤!");
};
if (scriptSrc.lastIndexOf(".js") === -1) {
scriptSrc += ".js";
};
return scriptSrc;
};
此處還需要獲取當前的根路徑,模塊化加載必定會有script來加載加載器js。所以可以據此來判斷當前路徑。
關於兼容性的處理,這里就不在講述。
//去除&?等字符
var repStr = function(str) {
return (str || "").replace(/[\&\?]{1}.+/g,"") || "";
};
if(document.currentScript) return repStr(document.currentScript.src);
模塊緩存
腳本加載之后,需要根據模塊不同的狀態進行處理。模塊主要分以下狀態:
1 init:
初始化,即剛進行模塊相關屬性的處理,未進行模塊解析。即將進行模塊加載處理
2 loading:
模塊解析中,即將完成
3 complete:
模塊解析完成,將參數對象,exports接口存在緩存中。依賴模塊解析完成之后進行執行。
至此,關於模塊化的探究就基本結束了。說來原理大家都知道。無非就是解析一下模塊路徑,然后動態創建腳本,控制下加載就可以了。實現以下還是有很多收獲的