Open creeperyang opened 8 years ago
好,上面大概聊完了模块化的背景,顺便安利了模块化七日谈(写的真的很好),下面步入正题:怎么实现一个AMD Loader?
读读Amd的规范,结合我们使用require.js
的经验,其实核心就是要实现define
,require
两个函数。
当然在这之前,我们先设定一下目标,或者说手撸一个_Amd loader_的背景。
理解_Amd loader_的原理,让新手去除对require.js
等loader的神秘感,理解模块化运作机制。这是我写这篇文章的目的。
在写这篇文章的过程中,我阅读了一些相关文章,看了require.js
的某些实现,这些都对本文有所帮助,非常感谢🙏。
首先把一些工具函数,一些前置工作先拎出来讲。
通过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
,用于指明当前加载执行的是哪个脚本。
define(id, deps, factory)
定义模块对于define而言,id如果出现,必须是“顶级”的和绝对的(不允许相对名字)。比如jquery
是合法的,./jquery
是非法的。
deps里也是id,但和require(deps, callback)
中deps情形一致,所以放到下面讲。
依赖模块数组中的id有以下几个情形:
require(['/lib/util', 'http://cdn.com/lib.js'], callback)
;require(['./lib/util', '../a/b'], callback)
;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;
}
首先我们必然要暴露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类的实例:
设计中最核心的一点是分治思想。我们知道,要把一个模块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 有比较完整的注释,结合上面所讲的,应该比较容易理解。
什么是AMD(不是做显卡的:joy:)?如果不熟的话,
require.js
总应该比较熟。AMD是_Asynchronous Module Definition_的缩写,字面上即异步模块定义。
require.js
是模块加载器,实现了AMD的规范。本文想说的就是怎么实现一个类似
require.js
的加载器。但在这之前,我们应该了解下JS模块化的历史。https://github.com/Huxpro/js-module-7day
这个Slides讲的比我好的多,所以想了解前端模块化的前世今生的可以去看看。这里简单总结下:
为什么需要模块化?
前端模块历史?
但模块化还需要解决加载问题:
define
包裹,在打包时解决模块化;