动手实现一个AMD模块加载器(二)

在上一篇文章中,我们已经基本完成了模块加载器的基本功能,接下来来完成一下路径解析的问题。

在之前的功能中,我们所有的模块默认只能放在同级目录下,而在实际项目中,我们的js很有可能位于多个目录,甚至是CDN中,所以现在这种路径解析是非常不合理的,因此我们需要将每个模块的name转化为一个绝对路径,这样才是一个比较完美的解决方案。

借鉴部分requirejs的思想,我们可以通过配置来配置一个baseUrl,当没有配置这个baseUrl的时候,我们认为这个baseUrl就是html页面的地址,所以我们需要对外暴露一个config方法,如下:

var cfg = {
  baseUrl: location.href.replace(/(\/)[^\/]+$/g, function(s, s1){
    return s1
  })
}

function config(obj) {
  obj && merge(cfg, obj);
}

function merge(obj1, obj2) {
  if(obj1 && obj2) {
    for(var key in obj2) {
      obj1[key] = obj2[key]
    }
  }
}

loadjs.config = config;

上面的代码中,我们定义了一个基本的全局配置对象cfg、一个用来合并对象属性的merge方法和一个用来支持配置的config方法。但是显然这个时候配置baseUrl的时候需要使用一个绝对路径。但是在实际中我们可能更会使用的是一个相对路径,例如../或者./或者/这个需求是非常正常的,因此我们需要也支持这些实现。首先我们先来写这些的匹配的正则表达式,为了之后的使用我们同时也写出检测完整路径(包括http、https和file协议)

var fullPathRegExp = /^[(https?\:\/\/) | (file\:\/\/\/)]/;
  var absoPathRegExp = /^\//;
  var relaPathRegExp = /^\.\//;
  var relaPathBackRegExp = /^\.\.\//;

同时将这些判断写进一个outputPath方法中。

    function outputPath(baseUrl, path) {
    if (relaPathRegExp.test(path)) {
      if(/\.\.\//g.test(path)) {
        var pathArr = baseUrl.split('/');
        var backPath = path.match(/\.\.\//g);
        var joinPath = path.replace(/[(^\./)|(\.\.\/)]+/g, '');
        var num = pathArr.length - backPath.length;
        return pathArr.splice(0, num).join('/').replace(/\/$/g, '') + '/' +joinPath;
      } else {
        return baseUrl.replace(/\/$/g, '') + '/' + path.replace(/[(^\./)]+/g, '');
      }
    } else if (fullPathRegExp.test(path)) {
      return path;
    } else if (absoPathRegExp.test(path)) {
      return baseUrl.replace(/\/$/g, '') + path;
    } else {
      return baseUrl.replace(/\/$/g, '') + '/' + path;
    }
  }

这里可能需要关注的一个相对路径的问题,因为有可能是需要返回上一级目录的,即形如./../../的形式,因此也应该处理这种情况。另外之所以在这里都是要匹配baseUrl的最后一个斜杠/,是因为提供的这个很有可能带有斜杠,也很有可能不带斜杠。
最后使用config方法配置的时候,通过判断提供的path来做相应的处理,修改config方法如下:

  function config(obj) {
    if(obj){
     if(obj.baseUrl) {
       obj.baseUrl = outputPath(cfg.baseUrl, obj.baseUrl);
     }
      merge(cfg, obj);
    } 
  }

最后我们修改一下每个模块名为这个模块的绝对路径,这样我们就不必再修改loadScript方法了,我们在loadMod方法中修改name参数,增加代码:

name = outputPath(cfg.baseUrl, name);

我们再来优化一下,毕竟如果我们每一个模块都要使用./或者../之类的,很多模块下这是要崩溃的,所以我们依旧是借鉴requirejs的方法,允许使用config方法来配置path属性这个问题,当我们配置了一个app的path之后我们认为在模块引用的时候,如果遇到app开头则需要替换这个path。
所以先来看config方法修改如下:

  function config(obj) {
    if(obj){
     if(obj.baseUrl) {
       obj.baseUrl = outputPath(cfg.baseUrl, obj.baseUrl);
     }
     if(obj.path) {
       var base = obj.baseUrl || cfg.baseUrl;
       for(var key in obj.path) {
         obj.path[key] = outputPath(base, obj.path[key]);
       }
     }
      merge(cfg, obj);
    }
  }

因此在loadMod方法中同时也应该检测cfg.path中是否含有这个属性,这时候会比较复杂,因此单独抽出为一个函数来说是比较好的处理方式,单独抽出为一个replaceName方法,如下:

  function replaceName(name) {
    if(fullPathRegExp.test(name) || absoPathRegExp.test(name) || relaPathRegExp.test(name)) {
      return outputPath(cfg.baseUrl, name);
    } else {
      var prefix = name.split('/')[0] || name;
      if(cfg.path[prefix]) {
        if(name.split('/').length === 0) {
         return cfg.path[prefix];
        } else {
          var endPath = name.split('/').slice(1).join('/');
          return outputPath(cfg.path[prefix], endPath);
        }
      }
    }
  }

这样,我们只需要在loadMod方法中调用这个方法就可以了。
我们再优化一下,我们完全可以在define中将name替换为一个绝对路径,同时在主模块加载依赖的时候,将依赖替换为绝对路径即可,因此我们可以在定义模块的时候就将这个这个路径替换好。
不过这个时候我们需要明白的是,在定义模块的时候是一个类似单词,而声明依赖的时候则有可能含有路径,如何在模块声明的时候正确解析路径呢?
很明显我们可以使用一个变量来做这个事情,这个变量存储着所有模块名和依赖这个模块时的声明。那么我们就应该在use方法加载模块的时候将这些变量名添加到这个变量名之下,之后再define中进行转化,那么最后我们的整个代码如下:


(function(root){
  var modMap = {};
  var moduleMap = {};
  var cfg = {
    baseUrl: location.href.replace(/(\/)[^\/]+$/g, function(s, s1){
      return s1
    }),
    path: {

    }
  };
  var fullPathRegExp = /^[(https?\:\/\/) | (file\:\/\/\/)]/;
  var absoPathRegExp = /^\//;
  var relaPathRegExp = /^\.\//;
  var relaPathBackRegExp = /^\.\.\//;


  function outputPath(baseUrl, path) {
    if (relaPathRegExp.test(path)) {
      if(/\.\.\//g.test(path)) {
        var pathArr = baseUrl.split('/');
        var backPath = path.match(/\.\.\//g);
        var joinPath = path.replace(/[(^\./)|(\.\.\/)]+/g, '');
        var num = pathArr.length - backPath.length;
        return pathArr.splice(0, num).join('/').replace(/\/$/g, '') + '/' +joinPath;
      } else {
        return baseUrl.replace(/\/$/g, '') + '/' + path.replace(/[(^\./)]+/g, '');
      }
    } else if (fullPathRegExp.test(path)) {
      return path;
    } else if (absoPathRegExp.test(path)) {
      return baseUrl.replace(/\/$/g, '') + path;
    } else {
      return baseUrl.replace(/\/$/g, '') + '/' + path;
    }
  }

  function replaceName(name) {
    if(fullPathRegExp.test(name) || absoPathRegExp.test(name) || relaPathRegExp.test(name) || relaPathBackRegExp.test(name)) {
      return outputPath(cfg.baseUrl, name);
    } else {
      var prefix = name.split('/')[0] || name;
      if(cfg.paths[prefix]) {
        if(name.split('/').length === 0) {
         return cfg.paths[prefix];
        } else {;
          var endPath = name.split('/').slice(1).join('/');
          return outputPath(cfg.paths[prefix], endPath);
        }
      } else {
        return outputPath(cfg.baseUrl, name);
      }
    }
  }

  function fixUrl(name) {
    return name.split('/')[name.split('/').length-1]
  }
  function config(obj) {
    if(obj){
     if(obj.baseUrl) {
       obj.baseUrl = outputPath(cfg.baseUrl, obj.baseUrl);
     }
     if(obj.paths) {
       var base = obj.baseUrl || cfg.baseUrl;
       for(var key in obj.paths) {
         obj.paths[key] = outputPath(base, obj.paths[key]);
       }
     }
      merge(cfg, obj);
    }
  }

  function merge(obj1, obj2) {
    if(obj1 && obj2) {
      for(var key in obj2) {
        obj1[key] = obj2[key]
      }
    }
  }
  function use(deps, callback) {
    if(deps.length === 0) {
      callback();
    }
    var depsLength = deps.length;
    var params = [];
    for(var i = 0; i < deps.length; i++) {
      moduleMap[fixUrl(deps[i])] = deps[i];
      deps[i] = replaceName(deps[i]);
      (function(j){
        loadMod(deps[j], function(param) {
          depsLength--;
          params[j] = param;
          if(depsLength === 0) {
            callback.apply(null, params);
          }
        })
      })(i)
    }
  }

  function loadMod(name, callback) {
    if(!modMap[name]) {
      modMap[name] = {
        status: 'loading',
        oncomplete: []
      };
      loadscript(name, function() {
        use(modMap[name].deps, function() {
          execMod(name, callback, Array.prototype.slice.call(arguments, 0));
        })
      });
    } else if(modMap[name].status === 'loading') {
      modMap[name].oncomplete.push(callback);
    } else if (!modMap[name].exports){
      use(modMap[name].deps, function() {
        execMod(name, callback, Array.prototype.slice.call(arguments, 0));
      })
    }else {
      callback(modMap[name].exports);
    }
  }

  function execMod(name, callback, params) {
    var exp = modMap[name].callback.apply(null, params);
    modMap[name].exports = exp;
    callback(exp);
    execComplete(name);
  }

  function execComplete(name) {
    for(var i = 0; i < modMap[name].oncomplete.length; i++) {
      modMap[name].oncomplete[i](modMap[name].exports);
    }
  }
  function loadscript(name, callback) {
    var doc = document;
    var node = doc.createElement('script');
    node.charset = 'utf-8';
    node.src = name + '.js';
    node.id = 'loadjs-js-' + (Math.random() * 100).toFixed(3);
    doc.body.appendChild(node);
    node.onload = function() {
      callback();
    }
  }

  function define(name, deps, callback) {
    if(moduleMap[name]) {
      name=moduleMap[name]
    } 
    name = replaceName(name);
    deps = deps.map(function(ele, i) {
      return replaceName(ele); 
    });
    modMap[name] = modMap[name] || {};
    modMap[name].deps = deps;
    modMap[name].status = 'loaded';
    modMap[name].callback = callback;
    modMap[name].oncomplete = modMap[name].oncomplete || [];
  }

  var loadjs = {
    define: define,
    use: use,
    config: config
  };

  root.define = define;
  root.loadjs = loadjs;
  root.modMap = modMap;
})(window);

我们进行一下测试:

    loadjs.config({
      baseUrl:'./static',
      paths: {
        app: './app'
      }
    });
    loadjs.use(['app/b', 'a'], function(b) {
      console.log('main');
      console.log(b.equil(1,2));
    })
define('a', ['app/c'], function(c) {
  console.log('a');
  console.log(c.sqrt(4));
  return {
    add: function(a, b) {
      return a + b;
    }
  }
});
define('c', ['http://ce.sysu.edu.cn/hope/Skin/js/jquery.min.js'], function() {
  console.log('c');
  return {
    sqrt: function(a) {
      return Math.sqrt(a)
    }
  }
});
define('b', ['c'], function(c) {
  console.log('b');
  console.log(c.sqrt(9));
  return {
    equil: function(a,b) {
      return a===b;
    }
  }
});

打开浏览器我们可以看到正常输出,如下:


说明我们的所做的路径解析工作是正确的。

系列文章:
动手实现一个AMD模块加载器(一)
动手实现一个AMD模块加载器(二)
动手实现一个AMD模块加载器(三)

最后是一个广告贴,最近新开了一个分享技术的公众号,欢迎大家关注👇

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • 随着前端业务复杂度的增加,模块化成为一个大的趋势。而在ES6还未被浏览器所支持的情况下,commonjs作为ES6...
    吴高亮阅读 1,053评论 0 3
  • 背景介绍:广东某211高校财务管理专业,一名爱写作爱摄影的会计人,校招投过50多家公司,拿到安永、华润、移动等of...
    失忆诗人阅读 393评论 1 5
  • 记不清具体哪天我开始颓废,几乎不出宿舍门。大概是上周二交了三方吧,感觉工作有着落了,也不去老师那里做毕设了,...
    狼二蛮阅读 243评论 3 0
  • ----------------------昨夜热醒,赋闲文记 迷糊之间,终于好像睡着了…………迷糊之间,挣扎着醒...
    有理性思想的浪漫诗人阅读 283评论 4 4