目前,一個典型的前端專案技術框架的選型主要包括以下三個方面:
- JS模組化框架。(Require/Sea/ES6 Module/NEJ)
- 前端模板框架。(React/Vue/Regular)
- 狀態管理框架。(Flux/Redux)
系列文章將從上面三個方面來介紹相關原理,並且嘗試自己造一個簡單的輪子。
本篇介紹的是JS模組化。
JS模組化是隨著前端技術的發展,前端程式碼爆炸式增長後,工程化所採取的必然措施。目前模組化的思想分為CommonJS、AMD和CMD。有關三者的區別,大家基本都多少有所瞭解,而且資料很多,這裡就不再贅述。
模組化的核心思想:
- 拆分。將js程式碼按功能邏輯拆分成多個可複用的js程式碼檔案(模組)。
- 載入。如何將模組進行載入執行和輸出。
- 注入。能夠將一個js模組的輸出注入到另一個js模組中。
- 依賴管理。前端工程模組數量眾多,需要來管理模組之間的依賴關係。
根據上面的核心思想,可以看出要設計一個模組化工具框架的關鍵問題有兩個:一個是如何將一個模組執行並可以將結果輸出注入到另一個模組中;另一個是,在大型專案中模組之間的依賴關係很複雜,如何使模組按正確的依賴順序進行注入,這就是依賴管理。
下面以具體的例子來實現一個簡單的基於瀏覽器端的AMD模組化框架(類似NEJ),對外暴露一個define函式,在回撥函式中注入依賴,並返回模組輸出。要實現的如下面程式碼所示。
define([
'/lib/util.js', //絕對路徑
'./modal/modal.js', //相對路徑
'./modal/modal.html',//文字檔案
], function(Util, Modal, tpl) {
/*
* 模組邏輯
*/
return Module;
})複製程式碼
1. 模組如何載入和執行
先不考慮一個模組的依賴如何處理。假設一個模組的依賴已經注入,那麼如何載入和執行該模組,並輸出呢?
在瀏覽器端,我們可以藉助瀏覽器的script標籤來實現JS模組檔案的引入和執行,對於文字模組檔案則可以直接利用ajax請求實現。
具體步驟如下:
- 第一步,獲取模組檔案的絕對路徑。
要在瀏覽器內載入檔案,首先要獲得對應模組檔案的完整網路絕對地址。由於a標籤的href屬性總是會返回絕對路徑,也就是說它具有把相對路徑轉成絕對路徑的能力,所以這裡可以利用該特性來獲取模組的絕對網路路徑。需要指出的是,對於使用相對路徑的依賴模組檔案,還需要遞迴先獲取當前模組的網路絕對地址,然後和相對路徑拼接成完整的絕對地址。程式碼如下:
var a = document.createElement('a');
a.id = '_defineAbsoluteUrl_';
a.style.display = 'none';
document.body.appendChild(a);
function getModuleAbsoluteUrl(path) {
a.href = path;
return a.href;
}
function parseAbsoluteUrl(url, parentDir) {
var relativePrefix = '.',
parentPrefix = '..',
result;
if (parentDir && url.indexOf(relativePrefix) === 0) {
// 以'./'開頭的相對路徑
return getModuleAbsoluteUrl(parentDir.replace(/[^\/]*$/, '') + url);
}
if (parentDir && url.indexOf(parentPrefix) === 0) {
// 以'../'開頭的相對路徑
return getModuleAbsoluteUrl(parentDir.replace(/[\/]*$/, '').replace(/[\/]$/, '').replace(/[^\/]*$/, '') + url);
}
return getModuleAbsoluteUrl(url);
}複製程式碼
- 第二步,載入和執行模組檔案。
對於JS檔案,利用script標籤實現。程式碼如下:
var head = document.getElementsByTagName('head')[0] || document.body;
function loadJsModule(url) {
var script = document.createElement('script');
script.charset = 'utf-8';
script.type = 'text/javascript';
script.onload = script.onreadystatechange = function() {
if (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete') {
/*
* 載入邏輯, callback為define的回撥函式, args為所有依賴模組的陣列
* callback.apply(window, args);
*/
script.onload = script.onreadystatechange = null;
}
};
}複製程式碼
對於文字檔案,直接用ajax實現。程式碼如下:
var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP'),
textContent = '';
xhr.onreadystatechange = function(){
var DONE = 4, OK = 200;
if(xhr.readyState === DONE){
if(xhr.status === OK){
textContent = xhr.responseText; // 返回的文字檔案
} else{
console.log("Error: "+ xhr.status); // 載入失敗
}
}
}
xhr.open('GET', url, true);// url為文字檔案的絕對路徑
xhr.send(null);複製程式碼
2. 模組依賴管理
一個模組的載入過程如下圖所示。
- 狀態管理
從上面可以看出,一個模組的載入可能存在以下幾種可能的狀態。
- 載入(load)狀態,包括未載入(preload)狀態、載入(loading)狀態和載入完畢(loaded)狀態。
- 正在載入依賴(pending)狀態。
- 模組回撥完成(finished)狀態。
因此,需要為每個載入的模組加上狀態標誌(status),來識別目前模組的狀態。
依賴分析
在模組載入後,我們需要解析出每個模組的絕對路徑(path)、依賴模組(deps)和回撥函式(callback),然後也放在模組資訊中。模組物件管理邏輯的資料模型如下所示。{ path: 'http://asdas/asda/a.js', deps: [{}, {}, {}], callback: function(){ }, status: 'pending' }複製程式碼
依賴迴圈
模組很可能出現迴圈依賴的情況。也就是a模組和b模組相互依賴。依賴分為強依賴和弱依賴。強依賴是指,在模組回撥執行時就會使用到的依賴;反之,就是弱依賴。對於強依賴,會造成死鎖,這種情況是無法解決的。但弱依賴可以通過現將一個空的模組引用注入讓一個模組先執行,等依賴模組執行完後,再替換掉就可以了。強依賴和弱依賴的例子如下:
//強依賴的例子
//A模組
define(['b.js'], function(B) {
// 回撥執行時需要直接用到依賴模組
B.demo = 1;
// 其他邏輯
});
//B模組
define(['a.js'], function(A) {
// 回撥執行時需要直接用到依賴模組
A.demo = 1;
// 其他邏輯
});複製程式碼
// 弱依賴的例子
// A模組
define(['b.js'], function(B) {
// 回撥執行時不會直接執行依賴模組
function test() {
B.demo = 1;
}
return {testFunc: test}
});
//B模組
define(['a.js'], function(A) {
// 回撥執行時不會直接執行依賴模組
function test() {
A.demo = 1;
}
return {testFunc: test}
});複製程式碼
3. 對外暴露define方法
對於define函式,需要遍歷所有的未處理js指令碼(包括內聯和外聯),然後執行模組的載入。這裡對於內聯和外聯指令碼中的define,要做分別處理。主要原因有兩點:
- 內斂指令碼不需要載入操作。
- 內斂指令碼中define的模組的回撥輸出是不能作為其他模組的依賴的。
var handledScriptList = [];
window.define = function(deps, callback) {
var scripts = document.getElementsByTagName('script'),
defineReg = /s*define\s*\(\[.*\]\s*\,\s*function\s*\(.*\)\s*\{/,
script;
for (var i = scripts.length - 1; i >= 0; i--) {
script = list[i];
if (handledScriptList.indexOf(script.src) < 0) {
handledScriptList.push(script.src);
if (script.innerHTML.search(defineReg) >= 0) {
// 內斂指令碼直接進行模組依賴檢查。
} else {
// 外聯指令碼的首先要監聽指令碼載入
}
}
}
};複製程式碼
上面就是對實現一個模組化工具所涉及核心問題的描述。完整的程式碼點我。