動手實現一個AMD模組載入器(二)

灰風GreyWind發表於2017-11-04

在上一篇文章中,我們已經基本完成了模組載入器的基本功能,接下來來完成一下路徑解析的問題。

在之前的功能中,我們所有的模組預設只能放在同級目錄下,而在實際專案中,我們的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
1111

說明我們的所做的路徑解析工作是正確的。

系列文章:
動手實現一個AMD模組載入器(一)
動手實現一個AMD模組載入器(二)
動手實現一個AMD模組載入器(三)

相關文章