打造史上最強模組載入工具
這次要記錄的是一個很簡單的但是基本符合AMD規範的瀏覽器端模組載入工具的開發流程。因為自從使用過require.js、webpack等模組化載入工具之後就一直對它的實現原理很好奇,於是稍微研究了一下。
實現的方法有許多,但簡單實現的話大致都會實現出以下的兩個方法:
1 實現模組的載入。從主模組說起,我們需要通過一個入口來載入我們的主模組的依賴模組,同時在載入完依賴之後,能夠取得所依賴模組的返回值,並將它們傳入主模組程式碼中,再去執行我們的主模組程式碼。函式入口類似於這樣的形式:
require([ dependents ], function( ){ // 主模組程式碼 })
至於如何去載入我們的依賴模組,這裡一般可以有兩種處理方式,一種是通過Ajax請求依賴模組,一種是為依賴模組動態建立 script 標籤載入依賴模組,在這裡我只選擇第二種方式,不過如果你需要載入文字檔案或者JSON檔案的話,還是需要採用Ajax載入的方式,但這裡為了簡單處理我們不考慮這種情況。
所以我們會遍歷主模組的依賴陣列,對依賴模組的路徑做簡單的處理之後,動態建立 script 標籤載入每一個依賴模組。所謂的載入模組,其本質便是通過網路請求將模組 Fetch 到本地。通過 script 標籤載入資源有兩個特點:
1.1 script 標籤載入到JS程式碼之後會立即執行這一段程式碼。JSONP也利用了 script 標籤的這個特點。
1.2 可以通過 script.onload 和 script.onerror 監聽模組的載入狀況。我們只需要快取對應模組的返回值即可,所以可以監聽 script 標籤的 onload 事件,在模組快取成功之後刪除對應的 script 標籤。
2 實現模組的定義。在AMD規範中,每一個模組的編寫我們需要遵循類似於這樣的形式:
define([ dependents ], factory( results ))
上面也說到,script 標籤會立即執行載入成功的模組,所以如果在此之前我們的 define 函式已經被掛載到全域性的話,define 函式會被立即執行,完成模組的定義工作。
關於模組定義的概念這裡需要說一下,我們的模組定義,是指成功將模組的返回值(或者該模組的全部程式碼) cache 到我們的本地快取當中,我們會使用一個變數負責去快取所有的依賴模組以及這些依賴模組所對應的模組ID,所以每次在執行 require 方法或者 define 方法之前我們都會去檢查一下所依賴的模組在快取中是否存在(根據模組ID查詢),即是否已經成功定義。如果已經成功定義過了,我們便會忽略對此模組的處理,否則就會去呼叫 require 方法載入並定義它。待依賴模組都已經成功定義過之後,我們再從快取中取出這些依賴模組的返回值傳入 factory 方法當中執行主模組或者 cache 我們當前定義的模組。
以上就是一個簡單的模組載入器的一般原理了,具體細節再在下面具體說明。
所以我們的關鍵是實現 require 和 define 方法。不過在這裡有一個重要的細節需要我們處理,前面有提到過,我們的每一次 require 或者 define 之前會去檢查所依賴模組是否都已經完全定義,再去定義未定義的依賴模組,那如果所有的依賴模組都已經全部完成定義,我們的 require 或者 define 怎麼樣才能即時的知曉到這一情報呢?
我們可以藉助於實現一個類似於 Nodejs 當中 EventEmiter 模組的事件發射器去完成我們的需求。
這個事件發射器有兩個主要的方法 watch 和 emit。
watch :我們在載入依賴模組的同時,會將我們的依賴模組陣列和回撥函式( factory )傳入事件發射器的 watch 方法,watch 方法會為我們建立一個任務,監聽所傳入依賴模組陣列的載入狀況,一旦檢測到依賴模組陣列中的模組全部都已經定義成功之後,主動觸發之前傳入的回撥函式( factory ),執行接下來的邏輯。
emit :每次有模組被定義成功,便會呼叫事件發射器的 emit 方法傳送一個模組定義成功的訊號,之後事件發射器會檢查一遍當前定義成功的模組所在的依賴模組陣列中的依賴模組是否全部已經定義成功,如果是的話,再去執行依賴模組陣列對應的回撥函式( factory )。
事件發射器的程式碼如下:
var utils = { ...... proxy : (function( ){ var tasks = { } var task_id = 0 var excute = function( task ){ console.log( "excute task" ) var urls = task.urls var callback = task.callback var results = [ ] for( var i = 0; i < urls.length; i ++ ){ results.push( modules[ urls[ i ] ] ) } callback( results ) } var deal_loaded = function( url ){ console.log( "deal_loaded " + url ) var i, k, sum = 0 for( k in tasks ){ if( tasks[ k ].urls.indexOf( url ) > -1 ){ for( i = 0; i < tasks[ k ].urls.length; i ++ ){ if( m_methods.isModuleCached( tasks[ k ].urls[ i ] ) ){ sum ++ } } if( sum == tasks[ k ].urls.length ){ excute( tasks[ k ] ) delete( tasks[ k ] ) } } } } var emit = function( m_id ){ console.log( m_id + " was loaded !" ) deal_loaded( m_id ) } var watch = function( urls, callback ){ console.log( "watch : " + urls ) var sum for( var i = 0; i < urls.length; i ++ ){ if( m_methods.isModuleCached( urls[ i ] ) ){ sum ++ } } if( sum == urls.length ){ excute({ urls : urls, callback : callback }) } else { console.log( "建立監聽任務 : " ) var task = { urls : urls, callback : callback } tasks[ task_id ] = task task_id ++ console.log( task ) } } return { emit : emit, watch : watch } })( ) }
define方法實現:
var define = function(deps, factory) { console.log("define...") var _deps = factory ? deps : [], _factory = factory ? factory : deps new Module(_deps, _factory) }
function Module(deps, factory) { var _this = this _this.m_id = doc.currentScript.src // 判斷模組是否定義成功 if (m_methods.isModuleCached(_this.m_id)) { return } if (arguments[0].length == 0) { // 沒有依賴模組 _this.factory = arguments[1] // 模糊定義成功,取返回值新增到快取中 m_methods.cacheModule(_this.m_id, _this.factory()) utils.proxy.emit(_this.m_id) } else { // 有依賴模組 _this.factory = arguments[1] // 載入依賴模組 require(arguments[0], function(results) { m_methods.cacheModule(_this.m_id, _this.factory(results)) utils.proxy.emit(_this.m_id) }) } }
require方法:
var require = function(deps, callback) { console.log("require " + deps) if (!Array.isArray(deps)) { deps = [deps] } var urls = [] for (var i = 0; i < deps.length; i++) { // 處理模組路徑 urls.push(utils.resolveUrl(deps[i])) } utils.proxy.watch(urls, callback) // 載入依賴模組 m_methods.fetchModules(urls) }
這裡有一個小細節,在處理依賴模組路徑的時候,可以藉助 a 標籤去獲取到我們需要的絕對路徑,a 標籤有一個特點,當我們通過 JS 去獲取它的 href 值時,它始終會給我們返回相對應的絕對路徑,即使我們之前給它的 href 值賦予的是相對路徑。
所以我們的路徑處理可以這麼實現:
......
var _script = document.getElementsByTagName("script")[0]
var _a = document.createElement("a")
_a.style.visibility = "hidden"
document.body.insertBefore(_a, _script)
......
var utils = {
resolveUrl: function(url) {
_a.href = url
var absolute_url = _a.href
_a.href = ""
return absolute_url
},
......
}
至此我們的模組載入工具的主要功能都已大致實現。完整程式碼在 https://github.com/KellyLy/loader.js
現在可以測試一下。假設我們現在有a、b、c、d四個模組,分別是:
以及主模組:
一切就緒,我們在關鍵區域都以列印 log 的方式做出標記,現在我們開啟頁面觀察控制檯:
沒毛病,模組載入工具的整個載入流程在控制檯裡我們都可以觀察得到,清晰明瞭。至此,這篇文章就結束啦,最後祝大家新年快樂!