CMD风格的模块加载器是解决javascript
模块化加载的一个方案,典型表明是sea.js,它的规范也很简洁。在研究原理的时候,能够本身动手写一个简单的loader
。为了方便,就叫它testLoader
。javascript
testLoader
有这样的功能:html
testLoader.config({...})
来配置一些全局的信息testLoader.define(function(){})
来定义模块,可是不指定模块的id
,用模块所在文件的路径来做为模块的id
require
来加载别的模块exports
和module.exports
来对外暴露接口testLoader.bootstrap(id, callback)
来做为入口启动首先,定义testLoader
的基本信息:java
window.testLoader = {
define: define,
bootstrap: bootstrap,
require: require,
modules: {},
config: {
root: ''
},
config: function(obj) {
this.config = {...this.config, ...obj}
},
MODULE_STATUS: {
PENDING: 0,
LOADING: 1,
COMPLETED: 2,
ERROR: 3
}
}复制代码
从testLoader.bootstrap
开始看。testLoader.bootstrap(id, callback)
在执行时,首先是根据id
来加载模块,加载模块完成后,将模块暴露出的对象做为参数,执行callback
。在加载模块的时候,首先是从testLoader
的模块缓存中查找有没有相应的模块,若是有,就直接取,不然,就建立一个新的模块,并将这个模块缓存。代码以下:git
const generatePath = (id) => `${testLoader.config.root}${id}`
const load = (id) => new Promise((resolve, reject) => {
let mod = testLoader.modules[id] || (new Module(id))
mod.on('complete', () => {
let exp = getModuleExports(mod)
resolve(exp)
})
mod.on('error', reject)
})
const bootstrap = (ids, callback) => {
ids = Array.isArray(ids) ? ids : [ids]
Promise.all(ids.map((id) => load(generatePath(id))))
.then((list) => {
callback.apply(window, list)
}).catch((error) => {
throw error
})
}复制代码
getModuleExports
时是用于获取模块暴露出的接口,实现以下:github
const getModuleExports = (mod) => {
if (!mod.exports) {
mod.exports = {}
mod.factory(testLoader.require, mod.exports, mod)
}
return mod.exports
}复制代码
当模块的exports
属性为空的时候,执行mod.factory(testLoader.require, mod.exports, mod)
,由于传入的mod.exports
是一个引用类型,在factory
执行的过程当中会由于反作用,为mod.exports
提供值。bootstrap
而Module
则是一个用来生成模块对象的Class
,定义以下:浏览器
class Module {
constructor(id) {
this.id = id
testLoader.modules[id] = this
this.status = testLoader.MODULE_STATUS.PENDING
this.factory = null
this.dependences = null
this.callbacks = {}
this.load()
}
load() {
let id = this.id
let script = document.createElement('script')
script.src = id
script.onerror = (event) => {
this.setStatus(testLoader.MODULE_STATUS.ERROR, {
id: id,
error: new Error('module can not load')
})
}
document.head.appendChild(script)
this.setStatus(testLoader.MODULE_STATUS.LOADING)
}
on(event, callback) {
(this.callbacks[event] || (this.callbacks[event] = [])).push(callback)
if (
(this.status === testLoader.MODULE_STATUS.LOADING && event === 'load') ||
(this.status === testLoader.MODULE_STATUS.COMPLETED && event === 'complete')
) {
callback(this)
}
if (this.status === testLoader.MODULE_STATUS.ERROR && event === 'error') {
callback(this, this.error)
}
}
emit(event, arg) {
(this.callbacks[event] || []).forEach((callback) => {
callback(arg || this)
})
}
setStatus(status, info) {
if (this.status === status) return
if (status === testLoader.MODULE_STATUS.LOADING) {
this.emit('load')
}
else if (status === testLoader.MODULE_STATUS.COMPLETED) {
this.emit('complete')
}
else if (status === testLoader.MODULE_STATUS.ERROR) {
this.emit('error', info)
}
else return
}
}复制代码
在建立一个模块对象的时候,首先是给模块赋予一些基本的信息,而后经过script
标签来加载模块的内容。这个模块对象只是提供了一个模块的基本的属性和简单的事件通讯机制,可是模块的内容,模块的依赖这些信息,须要经过define
来提供。define
为开发者提供了定义模块的能力,Module
则是提供了testLoader
描述表示模块的方式。缓存
经过define
定义模块,在define
执行的时候,首先须要为模块定义一个id
,这个id
是模块在testLoader
中的惟一标识。在前面已经说明了,在testLoader
中,不能指定id
,只是经过路径来生成id
,那么经过获取当前正在运行的script
代码的路径来生成id
。获取到id
以后,从testLoader
的缓存中取出对应的模块表示,而后解析模块的依赖。因为define
的时候,不能指定id
和依赖,对依赖的解析是经过匹配关键字require
来实现的,经过解析require('x')
获取全部的依赖模块的id
,而后加载全部依赖。就完成了模块的定义,代码以下:sass
const getCurrentScript = () => document.currentScript.src
const getDependence = (factoryString) => {
let list = factoryString.match(/require\(.+?\)/g) || []
return list.map((dep) => dep.replace(/(^require\(['"])|(['"]\)$)/g, '')) } const define = (factory) => { let id = getCurrentScript().replace(location.origin, '') let mod = testLoader.modules[id] mod.dependences = getDependence(factory.toString()) mod.factory = factory if(mod.dependences.length === 0) { mod.setStatus(testLoader.MODULE_STATUS.COMPLETED) return } Promise.all(mod.dependences.map((id) => new Promise((resolve, reject) => { id = generatePath(id) let depModule = testLoader.modules[id] || (new Module(id)) depModule.on('complete', resolve) depModule.on('error', reject) }) )).then((list) => { mod.setStatus(testLoader.MODULE_STATUS.COMPLETED) }).catch((error) => { mod.setStatus(testLoader.MODULE_STATUS.ERROR, error) }) }复制代码
那么依赖别的模块是经过require
来实现的,它核心的功能是获取一个模块暴露出来的接口,代码以下:bash
const require = (id) => {
id = generatePath(id)
let mod = testLoader.modules[id]
if (mod) {
return getModuleExports(mod)
}
else {
throw 'can not get module by id: ' + id
}
}复制代码
从上面解析依赖的方式能够看出,在经过define
定义模块的时候,匿名函数有三个参数
testLoader.define(function(requrie, exports, module){})复制代码
exports
本质上是module.exports
的引用,因此经过exports.a=x
是能够暴露接口的,可是exports={a:x}
则不行,由于后一种方式本质上是改变了将exports
做为一个值类型的参数,修改它的值,这种操做,在函数调用结束后,是不会生效的。按照这种原理,module.exports={a:x}
是能够达到效果的。
index.js
testLoader.define(function(require, exports, module) {
var a = require('a.js')
var b = require('b.js')
a(b)
module.exports = {
a: a,
b: b
}
})复制代码
a.js
testLoader.define(function(requrie, exports, module) {
module.exports = function(msg) {
console.log('in the a.js')
document.body.innerHTML = msg
}
})复制代码
b.js
testLoader.define(function(require, exports, module) {
console.log('in the b.js')
module.exports = 'Wonderful Tonight'
})复制代码
index.html
<html lang="en">
<head>
<title></title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src='./test-cmd-loader.js'></script>
<script>
testLoader.config({
root: '/Users/guangyi/code/javascript/sass/lib/test/'
})
testLoader.bootstrap('index.js', (list) => {
console.log(list)
})
</script>
</head>
<body></body>
</html>复制代码
log
,页面上也渲染出了Wonderful Tonight
。<html lang="en"></html>
经过这个简单的loader
,能够了解CMD
的规范,以及CMD
规范的loader
工做的基本流程。可是,和专业的loader
相比,还有不少没有考虑到,好比define
的时候,支持指定模块的id
和依赖,不过在上面的基础上,也很容易实现,在生成id
的时候将自动生成的id
做为默认值,在决定依赖的时候,将参数中定义的依赖和解析生成的依赖执行一次merge
处理便可。可是,这些能力本质上仍是同样的,由于这种机制定义的依赖是静态依赖,在这个模块的内容执行以前,依赖的模块已经被加载了,因此相似
if (condition) {
require('./a.js')
}
else {
require('./b.js) }复制代码
这种加载依赖的方式是不生效的,不论condition
的值是什么,两个模块都会被加载。要实现动态加载,或者说运行时加载,一个可行的方案是在上面的基础上,提供一个新的声明依赖的关键字,而后这个关键字表明的函数,在执行的时候再建立模块,加载代码。
还有,当模块之间存在循环依赖的状况,尚未处理。理论上,经过分析模块之间的静态依赖关系,就能够发现循环依赖的状况。也能够在运行的时候,根据模块的状态决定模块是否返回空来结束循环依赖。
还有跨语言的支持也是一个颇有意思的问题。