一個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的值是什麼,兩個模塊都會被加載。要實現動態加載,或者說運行時加載,一個可行的方案是在上面的基礎上,提供一個新的聲明依賴的關鍵字,而後這個關鍵字表明的函數,在執行的時候再建立模塊,加載代碼。

還有,當模塊之間存在循環依賴的狀況,尚未處理。理論上,經過分析模塊之間的靜態依賴關係,就能夠發現循環依賴的狀況。也能夠在運行的時候,根據模塊的狀態決定模塊是否返回空來結束循環依賴。

還有跨語言的支持也是一個頗有意思的問題。

相關文章
相關標籤/搜索