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

发布于 2022-10-04 09:47:29 字数 10287 浏览 103 评论 0

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

在之前的功能中,我们所有的模块默认只能放在同级目录下,而在实际项目中,我们的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;
    }
  }
});

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

1111

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

纵性

暂无简介

0 文章
0 评论
24 人气
更多

推荐作者

醉城メ夜风

文章 0 评论 0

远昼

文章 0 评论 0

平生欢

文章 0 评论 0

微凉

文章 0 评论 0

Honwey

文章 0 评论 0

qq_ikhFfg

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文