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
的值是什麼,兩個模塊都會被加載。要實現動態加載,或者說運行時加載,一個可行的方案是在上面的基礎上,提供一個新的聲明依賴的關鍵字,而後這個關鍵字表明的函數,在執行的時候再建立模塊,加載代碼。
還有,當模塊之間存在循環依賴的狀況,尚未處理。理論上,經過分析模塊之間的靜態依賴關係,就能夠發現循環依賴的狀況。也能夠在運行的時候,根據模塊的狀態決定模塊是否返回空來結束循環依賴。
還有跨語言的支持也是一個頗有意思的問題。