AMD 加载器分析与实现

发布于 2022-05-10 12:50:30 字数 7544 浏览 1083 评论 0

什么是 AMD(不是做显卡的)?如果不熟的话,require.js 总应该比较熟。

AMD 是 Asynchronous Module Definition 的缩写,字面上即异步模块定义。require.js 是模块加载器,实现了AMD的规范。

本文想说的就是怎么实现一个类似 require.js 的加载器。但在这之前,我们应该了解下JS模块化的历史。

https://github.com/Huxpro/js-module-7day

这个 Slides 讲的比我好的多,所以想了解前端模块化的前世今生的可以去看看。这里简单总结下:

为什么需要模块化?

  1. Web Pages正在变成 Web App,应用越大那么代码也越复杂;
  2. 模块化利于解耦,降低复杂性和提高可维护性;
  3. 应用部署可以优化代码,减少 http 请求(避免多模块文件造成的多请求)。

前端模块历史?

  1. 无模块,全局作用域冲突;
  2. namespace封装,减少暴露给全局作用域的变量,本质是对象,不安全;
  3. IIFE;
  4. 添加依赖的IIFE,即模块化,也是现代模块化的基础;

但模块化还需要解决加载问题:

  1. 原始的script tag,有难以维护,依赖模糊,请求过多的问题;
  2. script loader,如Lab.js,基于文件的依赖管理;
  3. module loader,YUI;
  4. CommonJS,node提供的模块化和加载方案,由于是同步/阻塞加载,所以只适合服务器/本地;
  5. AMD/CMD,异步加载;
  6. Browserify/Webpack,去掉define包裹,在打包时解决模块化;
  7. ES6带来语言原生的模块化方案。

好,上面大概聊完了模块化的背景,顺便安利了_模块化七日谈_(写的真的很好),下面步入正题:怎么实现一个AMD Loader?

读读Amd的规范,结合我们使用require.js的经验,其实核心就是要实现definerequire两个函数。

当然在这之前,我们先设定一下目标,或者说手撸一个_Amd loader_的背景。

理解_Amd loader_的原理,让新手去除对require.js等loader的神秘感,理解模块化运作机制。这是我写这篇文章的目的。

在写这篇文章的过程中,我阅读了一些相关文章,看了require.js的某些实现,这些都对本文有所帮助,非常感谢🙏。

1. 准备工作

首先把一些工具函数,一些前置工作先拎出来讲。

1.1 怎么加载模块/文件?

通过script标签。这是最简单自然的方法,其它可以ajax加载源代码eval,利用worker等等。

  /**
   * load script
   * @param  {String}   url    script path
   * @param  {Function} callback function called after loaded
   */
  function loadScript(url, callback) {
    var node = document.createElement('script');
    var supportOnload = 'onload' in node;

    node.charset = CONFIG.charset || 'utf-8';
    node.setAttribute('data-module', url);

    // bind events
    if (supportOnload) {
      node.onload = function() {
        onload();
      };
      node.onerror = function() {
        onload(true);
      };
    } else {
      // https://github.com/requirejs/requirejs/blob/master/require.js#L1925-L1935
      node.onreadystatechange = function() {
        if (/loaded|complete/.test(node.readyState)) {
          onload();
        }
      }
    }

    node.async = true;
    node.src = url;

    // For some cache cases in IE 6-8, the script executes before the end
    // of the appendChild execution, so to tie an anonymous define
    // call to the module name (which is stored on the node), hold on
    // to a reference to this node, but clear after the DOM insertion.
    currentlyAddingScript = node;

    // ref: #185 & http://dev.jquery.com/ticket/2709
    baseElement ? head.insertBefore(node, baseElement) : head.appendChild(node);

    currentlyAddingScript = null;

    function onload(error) {
      // ensure only execute once
      node.onload = node.onerror = node.onreadystatechange = null;
      // remove node
      head.removeChild(node);
      node = null;
      callback(error);
    }
  }

代码没什么复杂逻辑,很好理解,就是创建<script>标签加载脚本,完成后删除标签,调用回调。

稍微需要注意的是这里设置了currentlyAddingScript,用于指明当前加载执行的是哪个脚本。

1.2. module id的命名规则,id和url的转换规则

define(id, deps, factory)定义模块

对于define而言,id如果出现,必须是“顶级”的和绝对的(不允许相对名字)。比如jquery是合法的,./jquery是非法的。

deps里也是id,但和require(deps, callback)中deps情形一致,所以放到下面讲。

依赖模块id

依赖模块数组中的id有以下几个情形:

  1. id是绝对路径,如require(['/lib/util', 'http://cdn.com/lib.js'], callback)
  2. id是相对路径,如require(['./lib/util', '../a/b'], callback)
  3. id是ID名,如jquery

对于1和2而言,id都是url,可以统一处理。对于2,以当前模块所在的目录来把相对路径转化成绝对路径。对于绝对路径,那么此时是合法的id,此时id和url相等。

对于3而言,一般需要设置config.paths,因为仅仅根据这个id无法获取url,即无法加载。

  var dotReg = /\/\.\//g;
  var doubleDotReg = /\/[^/]+\/\.\.\//;
  var multiSlashReg = /([^:/])\/+\//g;
  var ignorePartReg = /[?#].*$/;
  var suffixReg = /\.js$/;
  var dirnameReg = /[^?#]*\//;

  function fixPath(path) {
    // /a/b/./c/./d --> /a/b/c/d
    path = path.replace(dotReg, "/");

    // a//b/c --> a/b/c
    // a///b////c --> a/b/c
    path = path.replace(multiSlashReg, "$1/");

    // a/b/c/../../d --> a/b/../d --> a/d
    while (path.match(doubleDotReg)) {
      path = path.replace(doubleDotReg, "/");
    }

    // main/test?foo#bar  -->  main/test
    path = path.replace(ignorePartReg, '');

    if (!suffixReg.test(path)) {
      path += '.js';
    }

    return path;
  }

  function dirname(path) {
    var m = path.match(dirnameReg);
    return m ? m[0] : "./";
  }

  function id2Url(url, baseUrl) {
    url = fixPath(url);
    if (baseUrl) {
      url = fixPath(dirname(baseUrl) + url);
    }
    if (CONFIG.urlArgs) {
      url += CONFIG.urlArgs;
    }
    return url;
  }

2. 整体设计

首先我们必然要暴露definerequire函数给全局对象,加载的模块也应该缓存,那么loader的基本结构应该如下:

(function(root) {
  var CONFIG = {
    baseUrl: '',
    charset: '',
    paths: {},
    shim: {}
  };
  var MODULES = {};
  var cache = {
    modules: MODULES,
    config: CONFIG
  };

   ...

  var define = function(id, deps, factory) {};
  define.amd = {};

  var require = function(ids, callback) {};

  require.config = function(config) {};

  // export to root
  root.define = define;
  root.require = require;
})(this);

然后设计我们的模块系统。以面向对象的思维,把每个模块抽象成Module类的实例:

  1. 当我们需要获取一个模块时,首先尝试从缓存中查找,没有则以url和deps(可选)创建一个模块实例。
  2. 模块开始初始化。
  3. 模块按deps获取自己的所有依赖模块,获取方式按第一步开始。
  4. 模块把自己添加到deps中各个依赖的引用模块列表中。
  5. 如果所有的依赖模块加载完毕,则模块自身运行factory,设置exports,标志自己加载完成,并notify自己的引用模块列表。

设计中最核心的一点是分治思想。我们知道,要把一个模块A真正加载完成,必须确认它的所有依赖模块加载完成。然后模块A本身也可以作为其它模块的依赖模块。So,我们可以转换一下,把模块A设置为其依赖模块的引用模块,当依赖模块加载完成时通知A来执行工厂函数完成挂载exports。

最终我们的Module类设计成:

function Module(url, deps) {}

Module.prototype = {
  constructor: Module,

  load: function() {
    var mod = this;
    var args = [];

    if (mod.status >= STATUS.LOAD) return mod;

    mod.status = STATUS.LOAD;
    mod.resolve();
    mod.setDependents();
    mod.checkCircular();

    // about to execute/load dependencies
    each(mod.dependencies, function(dep) {
      if (dep.status < STATUS.FETCH) {
        dep.fetch();
      } else if (dep.status === STATUS.SAVE) {
        dep.load();
      } else if (dep.status >= STATUS.EXECUTED) {
        args.push(dep.exports);
      }
    });

    mod.status = STATUS.EXECUTING;

    // means load all dependencies
    if (args.length === mod.dependencies.length) {
      args.push(mod.exports);
      mod.makeExports(args);
      mod.status = STATUS.EXECUTED;
      mod.notifyDependents();
    }
  },

  resolve: function() {},

  setDependents: function() {},

  checkCircular: function() {},

  notifyDependents: function() {},

  fetch: function() {},

  onload: function(error) {},

  save: function(deps) {}
}

最终实现

https://github.com/creeperyang/amd-loader/blob/master/amd.js 有比较完整的注释,结合上面所讲的,应该比较容易理解。

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

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

发布评论

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

关于作者

葬心

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

沧笙踏歌

文章 0 评论 0

山田美奈子

文章 0 评论 0

佚名

文章 0 评论 0

岁月无声

文章 0 评论 0

暗藏城府

文章 0 评论 0

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