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

作者: 忽如寄 | 来源:发表于2017-11-21 16:12 被阅读41次

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

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

    相关文章

      网友评论

        本文标题:动手实现一个AMD模块加载器(二)

        本文链接:https://www.haomeiwen.com/subject/njewvxtx.html