此次要记录的是一个很简单的可是基本符合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 的方式作出标记,如今咱们打开页面观察控制台:
没毛病,模块加载工具的整个加载流程在控制台里咱们均可以观察获得,清晰明了。至此,这篇文章就结束啦,最后祝你们新年快乐!