一个简单的JavaScript模块加载器

大型网站项目中,JavaScript 按需加载是一个常见的需求。几年前,LABjs 曾经流行过一段时间,它的主要原理是创建一个 type="text/cache"script 标签,并在需要的时候将其更改为 type="text/javascript",从而动态并行地加载 JS 并控制其执行时间。

使用 LABjs 时,被引入的 JS 几乎不需要更改,使用非常方便。但它也有不足,最大的问题是它只是一个加载器,没有模块管理功能,而后者对大型前端项目非常重要。很快,随着 CommonJSAMDCMD 等规范的流行,Require.JSSeaJS 等兼顾了 JS 文件按需加载以及模块化的加载器占据了更大的市场。LABjs 也在两三年前宣布停止开发,后来又说还会维护,只是不再添加新功能。

CommonJS 规范主要在 Node.JS 环境中使用,当然,现在也有 browserifywebpack 等工具可以让浏览器端的 JS 直接使用 CommonJS 规范。它们的原理一般是分析依赖关系,然后将所有依赖的 JS 打包为一个文件。(webpack 也可以实现动态按需加载。)

AMD、CMD 规范则是完全为浏览器端 JS 设计的。它们的设计细节不同,不过最基本的原理一样:通过类似 JSONP 的方式加载 JS 并隔离不同模块的变量。当然,在具体实现过程中还有很多问题需要考虑,比如模块依赖关系等。另外,SeaJS 等还会用正则匹配出用户在代码中直接用 require 等“关键字”加载的模块并自动加入依赖。

下面是我参考 AMD 规范实现的一个极简的 JavaScript 模块加载器(源码),去掉注释和空行差不多100行的样子。

(function (global) {
	"use strict";

	var LOADER_NAME = "myloader";
	var LOADER_FN_DEFINE = "define";
	if (global[LOADER_NAME]) return;

	var loader = {};
	var registered_modules = {};
	var loaded_modules = {};
	var on_modules_loaded = {};
	var doc = document;
	var node_script = doc.getElementsByTagName("script")[0];
	var _idx = 0;

	function Loader(name, deps, callback) {
		console.log("name", name);
		this.name = name;
		this.deps = deps;
		this.callback = callback;
		this.deps_left = deps.length;

		this.init();
	}

	Loader.prototype = {
		init: function () {
			if (this.deps_left == 0) {
				// 没有依赖,直接加载
				this.loaded(this.name);
			}

			for (var i = 0; i < this.deps.length; i++) {
				this.loadModule(this.deps[i]);
			}
		},

		loadModule: function (name) {
			var _this = this;
			if (loaded_modules.hasOwnProperty(name)) {
				// 该模块已经加载了
				this.loaded(name);
				return;
			}

			var m = registered_modules[name];
			if (!m) {
				throw new Error("unregisted module: " + name);
			}
			var el = doc.createElement("script");
			el.src = m.url;
			node_script.parentNode.insertBefore(el, node_script);

			on_modules_loaded[name] = on_modules_loaded[name] || [];
			on_modules_loaded[name].push(function () {
				_this.loaded();
			});
		},

		loaded: function () {
			this.deps_left--;
			if (this.deps_left <= 0) {
				this.run();
			}
		},

		run: function () {
			if (loaded_modules[this.name]) return;

			var modules = [];
			var i;

			for (i = 0; i < this.deps.length; i++) {
				modules.push(loaded_modules[this.deps[i]]);
			}
			loaded_modules[this.name] = this.callback.apply(null, modules) || {};

			var fns = on_modules_loaded[this.name] || [];
			var fn;
			while (fn = fns.shift()) {
				fn.call();
			}
		}
	};

	global[LOADER_NAME] = loader;
	global[LOADER_FN_DEFINE] = function (module_name, dependences, fn) {
		if (typeof dependences == "function") {
			fn = dependences;
			dependences = [];
		}
		new Loader(module_name, dependences, fn);
	};

	/**
	 *
	 * @param configs
	 *      configs 格式:
	 *      {
	 *          name: "a",
	 *          url: "http://xxx/libs/a.js"
	 *      }
	 */
	loader.register = function (configs) {
		if (Object.prototype.toString.call(configs) === "[object Array]") {
			// 传入的是一个数组
			for (var i = 0; i < configs.length; i++) {
				loader.register(configs[i]);
			}
		} else {
			registered_modules[configs.name] = configs;
		}
	};

	loader.use = function (modules, callback) {
		if (typeof modules == "string") {
			modules = [modules];
		}

		new Loader(_idx++, modules, callback);
	};

})(window)

你可以在 GitHub 上查看它的源码及示例 。需要说明的是,它只实现了 AMD 规范的一个子集,并且把 require 改为了 myloader.use,同时对循环依赖等情况也没有做处理。不过除了这些,它已经是一个可以使用并且兼容各大浏览器的 JavaScript 模块加载器了。

最后,现在流行的各种 AMD、CMD 加载器,在不久的将来也会像 LABjs 一样被人慢慢忘记,因为它们都只是为了解决某一个特定历史阶段的某一类技术问题而诞生的,随着相关技术的发展,它们也将慢慢完成历史使命,退出前端舞台。比如,基于 jspm 等项目,我们现在已经可以使用 ES6 中的模块加载方法:

System.import('buffer').then(function(buffer) {
	console.log(new buffer.Buffer('base64 encoded').toString('base64'));
});

One Reply to “一个简单的JavaScript模块加载器”

  1. 下面方法可以优化,否则会在head插入多余script节点
    loadModule: function (name) {
    var _this = this;
    if (loaded_modules.hasOwnProperty(name)) {
    // 该模块已经加载了
    this.loaded(name);
    return;
    }
    var m = registered_modules[name];
    if (!m) {
    throw new Error(“unregisted module: ” + name);
    }
    on_modules_loaded[name] = on_modules_loaded[name] || [];
    if(!on_modules_loaded[name].length){
    var el = doc.createElement(“script”);
    el.src = m.url;
    var node_script = doc.getElementsByTagName(“script”)[0];
    node_script.parentNode.insertBefore(el, node_script);
    }
    on_modules_loaded[name].push(function () {
    _this.loaded();
    });
    },

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s