一个cmd模块加载器toy


基本信息

CMD风格的模块加载器是解决javascript模块化加载的一个方案,典型表明是sea.js,它的规范也很简洁。在研究原理的时候,能够本身动手写一个简单的loader。为了方便,就叫它testLoaderjavascript

基本实现

testLoader有这样的功能:html

  1. 经过testLoader.config({...})来配置一些全局的信息
  2. 经过testLoader.define(function(){})来定义模块,可是不指定模块的id,用模块所在文件的路径来做为模块的id
  3. 在模块内部经过require来加载别的模块
  4. 在模块内部经过exportsmodule.exports来对外暴露接口
  5. 经过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}是能够达到效果的。

测试例子
  1. 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
   }
 })复制代码
  1. a.js
testLoader.define(function(requrie, exports, module) {
   module.exports = function(msg) {
     console.log('in the a.js')
     document.body.innerHTML = msg
   }
 })复制代码
  1. b.js
testLoader.define(function(require, exports, module) {
   console.log('in the b.js')
   module.exports = 'Wonderful Tonight'
 })复制代码
  1. 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>复制代码
  1. 用浏览器打开,在调试窗口能看到打印的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的值是什么,两个模块都会被加载。要实现动态加载,或者说运行时加载,一个可行的方案是在上面的基础上,提供一个新的声明依赖的关键字,而后这个关键字表明的函数,在执行的时候再建立模块,加载代码。

还有,当模块之间存在循环依赖的状况,尚未处理。理论上,经过分析模块之间的静态依赖关系,就能够发现循环依赖的状况。也能够在运行的时候,根据模块的状态决定模块是否返回空来结束循环依赖。

还有跨语言的支持也是一个颇有意思的问题。

相关文章
相关标签/搜索