实现 AMD 模块加载器

只处理了两个模块直接循环依赖,还没有解决模块间间接循环依赖的问题。

(function (global) {

  global.define = define
  global.define.amd = {}
  global.require = require

  let _curDeps = []   // 记录 require 方法调用后,当前主模块的所有依赖(包含子模块的依赖)
  const _modules = {} // 所有的模块信息
  global._modules = _modules

  /**   * 定义一个模块   * @param {String} name - 模块名称(可省略)   * @param {Array} deps - 依赖模块数组(可省略)   * @param {Function} callback - 成功回调函数, 必须带return   */
  function define (name, deps, callback) {
    // define([], function() {})
    if (Array.isArray(name)) {
      callback = deps
      deps = name
    } else if (typeof name === 'function') {
      // define(function () {})
      deps = []
      callback = name
    }
    new Module(deps, helpers.modulePathToModuleName(helpers.getCurrentJsSrc()), callback)
  }

  /**   * 入口文件的函数   * @param {Array} dep - 依赖模块数组(可省略)   * @param {Function} cb - 成功回调函数   */
  function require (dep, callback) {
    _curDeps = [] // 每次调用前清空
    let deps = typeof dep === 'string' ? [dep] : dep
    let moduleName = helpers.getModuleName()
    let allDeps = []
    // 主模块
    let mainModule = new Module(deps, moduleName, callback).loadDeps(onload)
    // 每次依赖的模块加载完成都会执行
    function onload (module) {
      allDeps.push(module.name)
      // 当数量一致时表示已经加载完主模块的所有依赖
      if (_curDeps.length === allDeps.length) {
        // 执行所有依赖模块的接口函数
        allDeps.forEach(moduleName => {
          _modules[moduleName].execute()
        })
        // 执行主模块的接口函数
        setTimeout(() => {
          mainModule.execute()
        }, 0)
      }
    }
  }

  /**   * 模块对象   * @param {Array} deps - 依赖数组   * @param {String} name - 模块名   * @param {Function} callback - 模块接口函数   */
  function Module (deps, name, callback) {
    this.deps = deps
    this.name = name
    this.callback = callback
    // 将每个模块实例存储在_modules对象上
    if (!_modules[this.name]) {
      _modules[this.name] = this
    }
    return this
  }

  Module.STATUS = {
    LOADED: 2,     // 加载成功
    ERROR: 5,      // 加载失败
  }

  // 加载模块依赖文件(包含子模块的依赖)
  Module.prototype.loadDeps = function (callback) {
    this.deps.forEach(moduleName => {
      // 依赖模块还没有在当前主模块依赖中则加载(防止重复加载)
      if (_curDeps.indexOf(moduleName) === -1) {
        _curDeps.push(moduleName)
        // 如果还没有加载过
        if (!_modules[moduleName]) {
          helpers.loadJS(helpers.moduleNameToModulePath(moduleName), () => {
            let module = _modules[moduleName]
            // 加载子模块的依赖文件
            module.loadDeps(callback)
            // 加载成功
            module.status = 2
            // 循环依赖检查
            helpers.checkDeps(moduleName)
            // 加载完成回调
            callback && callback(module)
          }, () => {
            let module = _modules[moduleName]
            // 加载失败
            module.status = 5
          })
        } else {
          // 已经加载过,直接执行回调
          callback && callback(_modules[moduleName])
        }
      }
    })
    return this
  }

  // 执行模块的回调函数
  Module.prototype.execute = function () {
    let params = []
    if (typeof this.exports !== 'undefined') return
      // 遍历所有依赖模块
    this.deps.forEach(moduleName => {
      let module = _modules[moduleName]
      // 递归调用
      module.execute()
      params.push(module.exports)
    })
    // 执行函数,得到模块接口
    if (typeof this.callback === 'function') {
      this.exports = this.callback.apply(global, params)
    }
  }

  // 协助函数
  const helpers = {
    /**     * 加载JS文件到页面上     * @param {String} src - JS文件绝对路径     * @param {Function} callback - 成功回调     */
    loadJS (src, callback, errorCallback) {
      let script = document.createElement('script')
      script.async = true
      script.onload = callback || function () { }
      script.onerror = errorCallback || function () { console.warn(`${src}加载失败`) }
      script.src = src
      document.getElementsByTagName('head')[0].appendChild(script)
    },
    // 获取当前运行的js文件路径
    getCurrentJsSrc () {
      return document.currentScript && document.currentScript.src
    },
    // 获取当前运行的JS element
    getCurrentJS () {
      return document.currentScript
    },
    /**     * 将模块名转换成模块路径     * @param {String} name - 模块名     * @returns {String} - 模块路径     */
    moduleNameToModulePath (name) {
      let reg = /\w*.js/
      let output = reg.exec(name)
      if (!output) {
        return `./${name}.js`
      } else {
        return name
      }
    },
    /**     * 将模块的路径转换成模块名     * @param {String} path - 模块路径     * @returns {String} - 模块名     */
    modulePathToModuleName (path) {
      let reg = /\w*.js/
      let output = reg.exec(path)
      if (!output) {
        return path
      } else {
        return output[0].split('.')[0]
      }
    },
    /**     * 获取模块名,没有的话随机分配一个     */
    getModuleName () {
      return this.getCurrentJsSrc() ? this.modulePathToModuleName(this.getCurrentJsSrc()) : ~~new Date()
    },
    /**     * 检查模块之间是否出现循环依赖(只处理了两个模块直接的依赖,间接相互依赖没有处理)     */
    checkDeps (curModuleName) {
      for (moduleName in _modules) {
        if (_modules[curModuleName].status !== 2) break
        if (curModuleName !== moduleName && _modules[moduleName].status === 2) {
          //当前模块在遍历模块的依赖里有出现 && 遍历模块的依赖有出现当前模块
          if (
            _modules[moduleName].deps.indexOf(curModuleName) !== -1 && 
            _modules[curModuleName].deps.indexOf(moduleName) !== -1
          ) {
            throw Error(`${curModuleName}${moduleName}存在循环依赖`)
          }
        }
      }
    }
  }

  /**   * 主入口,如果有指定data-main属性则执行   */
  function entry () {
    let moduleName = helpers.getCurrentJS().getAttribute('data-main')
    if (moduleName) {
      helpers.loadJS(helpers.moduleNameToModulePath(moduleName))
    }
  }

  entry()

})(this)