此次要記錄的是一個很簡單的可是基本符合AMD規範的瀏覽器端模塊加載工具的開發流程。由於自從使用過require.js、webpack等模塊化加載工具以後就一直對它的實現原理很好奇,因而稍微研究了一下。webpack
實現的方法有許多,但簡單實現的話大體都會實現出如下的兩個方法:git
1 實現模塊的加載。從主模塊提及,咱們須要經過一個入口來加載咱們的主模塊的依賴模塊,同時在加載完依賴以後,可以取得所依賴模塊的返回值,並將它們傳入主模塊代碼中,再去執行咱們的主模塊代碼。函數入口相似於這樣的形式:github
require([ dependents ], function( ){ // 主模塊代碼 })
至於如何去加載咱們的依賴模塊,這裏通常能夠有兩種處理方式,一種是經過Ajax請求依賴模塊,一種是爲依賴模塊動態建立 script 標籤加載依賴模塊,在這裏我只選擇第二種方式,不過若是你須要加載文本文件或者JSON文件的話,仍是須要採用Ajax加載的方式,但這裏爲了簡單處理咱們不考慮這種狀況。web
因此咱們會遍歷主模塊的依賴數組,對依賴模塊的路徑作簡單的處理以後,動態建立 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 的方式作出標記,如今咱們打開頁面觀察控制檯:
沒毛病,模塊加載工具的整個加載流程在控制檯裏咱們均可以觀察獲得,清晰明瞭。至此,這篇文章就結束啦,最後祝你們新年快樂!