AMD 加载器分析与实现
什么是 AMD(不是做显卡的)?如果不熟的话,require.js
总应该比较熟。
AMD 是 Asynchronous Module Definition 的缩写,字面上即异步模块定义。require.js
是模块加载器,实现了AMD的规范。
本文想说的就是怎么实现一个类似 require.js
的加载器。但在这之前,我们应该了解下JS模块化的历史。
https://github.com/Huxpro/js-module-7day
这个 Slides 讲的比我好的多,所以想了解前端模块化的前世今生的可以去看看。这里简单总结下:
为什么需要模块化?
- Web Pages正在变成 Web App,应用越大那么代码也越复杂;
- 模块化利于解耦,降低复杂性和提高可维护性;
- 应用部署可以优化代码,减少 http 请求(避免多模块文件造成的多请求)。
前端模块历史?
- 无模块,全局作用域冲突;
- namespace封装,减少暴露给全局作用域的变量,本质是对象,不安全;
- IIFE;
- 添加依赖的IIFE,即模块化,也是现代模块化的基础;
但模块化还需要解决加载问题:
- 原始的script tag,有难以维护,依赖模糊,请求过多的问题;
- script loader,如Lab.js,基于文件的依赖管理;
- module loader,YUI;
- CommonJS,node提供的模块化和加载方案,由于是同步/阻塞加载,所以只适合服务器/本地;
- AMD/CMD,异步加载;
- Browserify/Webpack,去掉
define
包裹,在打包时解决模块化; - ES6带来语言原生的模块化方案。
好,上面大概聊完了模块化的背景,顺便安利了_模块化七日谈_(写的真的很好),下面步入正题:怎么实现一个AMD Loader?
读读Amd的规范,结合我们使用require.js
的经验,其实核心就是要实现define
,require
两个函数。
当然在这之前,我们先设定一下目标,或者说手撸一个_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有以下几个情形:
- id是绝对路径,如
require(['/lib/util', 'http://cdn.com/lib.js'], callback)
; - id是相对路径,如
require(['./lib/util', '../a/b'], callback)
; - 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. 整体设计
首先我们必然要暴露define
,require
函数给全局对象,加载的模块也应该缓存,那么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类的实例:
- 当我们需要获取一个模块时,首先尝试从缓存中查找,没有则以url和deps(可选)创建一个模块实例。
- 模块开始初始化。
- 模块按deps获取自己的所有依赖模块,获取方式按第一步开始。
- 模块把自己添加到deps中各个依赖的引用模块列表中。
- 如果所有的依赖模块加载完毕,则模块自身运行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 技术交流群。
上一篇: JavaScript 与简单算法
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论