目的:

了解AMD规范与CMD规范,写一个模块加载器雏形。

基本概念:

AMD是异步模块定义规范,而CMD是通用模块定义规范。其他的还有CommonJS Modules规范。

对于具体的规范,可以参考:

https://github.com/amdjs/amdjs-api/wiki/AMD AMD规范

https://github.com/seajs/seajs/issues/242 CMD规范

http://www.zhihu.com/question/20351507/answer/14859415 玉伯对于AMD和CMD规范的回答

无论是哪一种规范,都是为了解决前端模块依赖与加载的问题。

与后台语言不同的地方是,js并无法做到阻塞的模块/文件加载,这是我们需要这些模块加载、依赖管理工具的重要原因之一。

需求分析:

根据规范,我们需要实现require和define两个接口,define接口用于定义模块(并加载依赖),require接口用来加载模块并回调。

我们将实现简化的接口:

define
    define(deps, definition):依赖数组 与 模块定义函数
    define(definition):模块定义函数(模块无依赖)

require
    require(deps, callback):依赖数组 与 完成回调
    require(moduleId):传入模块id,获得模块实例(模块需提前通过define或require完成加载)

接下来我们进一步分析下任务:

代码(模块)状态:
未加载 -> 加载中 -> 加载完毕,等待依赖解决 -> 依赖完成,模块加载完成 ( -> 初次使用,执行模块定义生成模块实例)
非模块代码无依赖,加载后直接跳到完成状态,并且无模块定义无法生成模块实例

模块的加载时间线:
开始加载,添加加载完成的回调 -> 加载完成,执行define函数,注册模块依赖与模块定义 -> 执行代码加载完成回调(从这里拿到代码的url,或者说是模块ID),解析模块

我们需要在几个情况下分析依赖是否满足:
代码加载完成时以及模块依赖满足,状态为完成时

依赖满足的条件:
所有依赖的模块状态为完成

对于js的加载,我们创建script标签,在其上保存模块的id,并添加加载完成的时间侦听:

var scriptNode = document.createElement('script');
scriptNode.async = true;
scriptNode.moduleId = mId;
scriptNode.addEventListener('load', onScriptLoaded);
scriptNode.src = mId + '.js';
document.body.appendChild(scriptNode);

最终代码:

load.js:

(function (global) {
	var modules = {}, // 模块列表
		_modulesToLoad = [], // 从依赖中获得的要加载的js列表
		MODULE_STATUS = { // 定义模块状态
			LOADING: 1,
			PENDING: 2,
			DONE: 3
		},
		_modulesToHandler = [], // define函数运行时存入这个数组(在这里每次只有一个被存入),在onScriptLoaded中处理
		_requireCallbacks = []; // 待满足的依赖,每一项格式为[deps数组,回调函数]

	// 处理依赖,得到要加载的模块数组,并建立LOADING的模块状态
	function addDepsToModules (deps) {
		if (!deps) { return; }
		deps.forEach(function (mId) {
			if (!modules[mId]) {
				modules[mId] = {
					id: mId,
					status: MODULE_STATUS.LOADING,
					deps: null,
					defi: null,
					instance: null
				}
				_modulesToLoad.push(mId);
			}
		});
	}
	// 加载代码,在script标签节点上保存模块id
	function loadScripts () {
		var mId;
		while(mId = _modulesToLoad.pop()) {
			var scriptNode = document.createElement('script');
			scriptNode.async = true;
			scriptNode.moduleId = mId;
			scriptNode.addEventListener('load', onScriptLoaded);
			scriptNode.src = mId + '.js';
			document.body.appendChild(scriptNode);
		}
	}
	function onScriptLoaded (evt) {
		if (evt.type === 'load') {
			var scriptNode = evt.currentTarget,
				mId = scriptNode.moduleId;
			if (modules[mId]) { // 不处理作为data-main的入口代码
				if (_modulesToHandler.length === 0) { // 加载的并非模块代码(没有调用define),立即修改状态为DONE
					modules[mId].status = MODULE_STATUS.DONE;
				} else {
					var pair = _modulesToHandler.pop(),
						deps = pair[0],
						defi = pair[1];
					modules[mId].status = MODULE_STATUS.PENDING; // 修改状态问PENDING
					modules[mId].defi = defi; // 保存模块定义
					_requireCallbacks.push([deps, function () { // 建立待满足的依赖
						modules[mId].status = MODULE_STATUS.DONE; // 依赖满足后修改状态为DONE
						setTimeout(resolveRequireCallbacks, 0); // 并重新寻找已满足的依赖
					}]);
					addDepsToModules(deps); // 处理这个模块的依赖
					loadScripts(); // 加载依赖的js
				}
				resolveRequireCallbacks(); // 寻找已满足的依赖
			}
		}
	}
	// 寻找并处理已满足的依赖
	function resolveRequireCallbacks () {
		_requireCallbacks = _requireCallbacks.filter(function (pair) {
			var deps = pair[0],
				callback = pair[1],
				allLoaded = true;
			// 判断依赖是否都已满足
			deps && deps.some(function (mId) {
				if (modules[mId].status !== MODULE_STATUS.DONE) {
					allLoaded = false;
					return true;
				}
			});
			if (allLoaded) { // 如果满足执行回调,并剔除出待满足的依赖列表
				callback();
				return false;
			} else {
				return true;
			}
		});
	}
	// 定义require函数
	global.require = function (deps, callback) {
		if (typeof deps === 'string') { // 传入moduleId时返回模块实例
			if (modules[deps] && modules[deps].status === MODULE_STATUS.DONE) {
				if (!modules[deps].instance) { // 模块实例在第一次require(mId)时通过模块定义创建
					modules[deps].instance = modules[deps].defi();
				}
				return modules[deps].instance;
			} else {
				throw 'Module ['+deps+'] not loaded yet.';
			}
		}
		_requireCallbacks.push([deps, callback]); // 添加回调函数到待解决的依赖
		addDepsToModules(deps); // 寻找要加载的js
		loadScripts(); // 加载js
		resolveRequireCallbacks(); // 处理待满足的依赖
	}
	// 定义define函数
	global.define = function (deps, defi) {
		if (!defi) {
			defi = deps;
			deps = null;
		}
		// 将依赖、模块定义存入待处理数组,由onScriptLoaded处理
		_modulesToHandler.push([deps, defi]);
	}
	// 获得并加载main入口js
	var scripts = document.getElementsByTagName('script'),
		selfScript = scripts[scripts.length - 1], // 当前代码,也就是指向load.js的这个
		mainSrc = selfScript.getAttribute('data-main');
	global.onload = function () { // 等待body完成创建,应当在window.onload前就执行,这里就直接用window.onload了
		_modulesToLoad.push(mainSrc);
		loadScripts();
	}
})(window);

html:

<script data-main="main" src="load.js"></script>

模块文件:

// main.js
require(['d', 'd0'], function() {
    console.log('ready', require('d').mId, require('d0').mId);
});
// d.js
define(['d1', 'd2'], function () {
	return {mId: 'd' + require('d1').token};
});
// d0.js
define(['d1', 'd2'], function () {
	return {mId: 'd0' + require('d1').token};
});
// d1.js
define(function () {
	return {token: '|xyz123'};
});
// d2.js
console.log('d2');

可以做的改进:

1. 依赖锁死的情况,比如模块依赖自身,模块循环依赖的情况,改如何处理。

2. 单个文件中多个define的调用,调用形式为define(模块id,模块依赖,模块定义),这样做是为了使得多个模块代码可以合并到单文件中。

3. 更智能的模块依赖分析。

对于这个第三点,我们来做个小尝试:

function func (jquery, underscore) {
	var async = require('async');
}
func.toString();

我们可以看到利用function的toString方法,通过“分析代码”,我们可以从模块定义函数“智能”的分析出依赖,无论是angular的依赖注入还是sea.js的CMD编写格式,就都可以做到了。

扩展:

除了require.js之外,browserify和webpack都是采用了CommonJS规范,这样的好处是与nodejs以及ES6的模块规范保持一致。

browserify:http://browserify.org/

webpack:http://webpack.github.io/

与require不同,这两种工具都要求代码提前通过编译转换,特别是browserify,生成的文件添加的代码相对较少,加之本身丰富的功能,很适合单页面应用和多页面的项目,但这两者的定位并非解决动态加载和相应的模块依赖。

对于相对小型或简单的项目,webpack是很好的选择,一是配置简单,二是代码配重小。但对于大型的单页面应用,当我们需要动态加载大量模块时,require可以压缩“静态依赖”/框架代码,并动态加载模块代码,因此仍然是非常好的选择之一。

js与AMD模块加载的更多相关文章

  1. 一个简单的AMD模块加载器

    一个简单的AMD模块加载器 参考 https://github.com/JsAaron/NodeJs-Demo/tree/master/require PS Aaron大大的比我的完整 PS 这不是一 ...

  2. javascript Date 日期格式化 formatDate, require.js 模块 支持全局js引入 / amd方式加载

    * 引入AMD加载方式: require.js CDN https://cdn.bootcss.com/require.js/2.3.5/require.js *  创建模块文件./js/util/d ...

  3. JavaScript AMD 模块加载器原理与实现

    关于前端模块化,玉伯在其博文 前端模块化开发的价值 中有论述,有兴趣的同学可以去阅读一下. 1. 模块加载器 模块加载器目前比较流行的有 Requirejs 和 Seajs.前者遵循 AMD规范,后者 ...

  4. 对于requirejs AMD模块加载的理解

    个人人为使用模块化加载的优点有三: 1,以我的项目为例:90%为图表展示,使用的是echarts,此文件较大,requirejs可以在同一个版本号(urlArgs)之下缓存文件,那么我就可以在访问登陆 ...

  5. 【模块化编程】理解requireJS-实现一个简单的模块加载器

    在前文中我们不止一次强调过模块化编程的重要性,以及其可以解决的问题: ① 解决单文件变量命名冲突问题 ② 解决前端多人协作问题 ③ 解决文件依赖问题 ④ 按需加载(这个说法其实很假了) ⑤ ..... ...

  6. Dojo初探之1:AMD规范,编写符合AMD规范(异步模块加载机制)的模块化JS(其中dojo采用1.11.2版本)

    一.AMD规范探索 1.AMD规范(即异步模块加载机制) 我们在接触js的时候,一般都是通过各种function来定义一些方法,让它们帮我们做一些事情,一个js可以包含很多个js,而这些functio ...

  7. js模块加载之AMD和CMD

    当我写这篇文章的时候,sea.js已经逐渐退出历史的舞台,详细链接.不过任何新事物的出现都是对旧事物的取其精华,去其糟粕,所以了解一下以前模块的加载也是一件好事. js模块化的原因自不比多说,看看HU ...

  8. js模块定义——支持CMD&AMD&直接加载

    /* animate */ //直接加载 (function() { var animate = {} //balabala window.animate = animate; })(); //AMD ...

  9. 如何使用 require.js ,实现js文件的异步加载,避免网页失去响应,管理模块之间的依赖性,便于代码的编写和维护。

    一.为什么要用require.js? 最早的时候,所有Javascript代码都写在一个文件里面,只要加载这一个文件就够了.后来,代码越来越多,一个文件不够了,必须分成多个文件,依次加载.下面的网页代 ...

随机推荐

  1. 近期用过的Linux口令备份

    最近使用Ubuntu用到的一些口令: rm -rf directory 移除文件夹或者文件touch (create file)mkdirmv(move,rename)chown (change ow ...

  2. SqlServer批量导入

    SQL Server的Bulk Insert语句可以将本地或远程的数据文件批量导入到数据库中,速度非常的快.远程文件必须共享才行,文件路径须使用通用约定(UNC)名称,即"\\服务器名或IP ...

  3. Qt5:窗口居中显示

    QDesktopWidget* desktop = QApplication::desktop(); // =qApp->desktop();也可以move((desktop->width ...

  4. string数组转化成int数组

    string idsStr = "1,2,3,4,5"; int[] ids = idsStr.Split(',').Select(Int32.Parse).ToArray();

  5. base库插件---form

    $().extend('serialize', function () { for (var i = 0; i < this.elements.length; i ++) { var form ...

  6. mysql优化---优化sql

    一.通过show status和应用特点了解各种SQL的执行频率 通过SHOW STATUS可以提供服务器状态信息,也可以使用mysqladmin extended-status命令获得.SHOW S ...

  7. pcap filter

    今天用tshark抓包,本以为wireshark能用的filter,如“mysql”它也应该能用,其实不然:tshark -f只认识pcap filter,用-R的话,说要用-2,加上-2的话又说什么 ...

  8. 考分鄙视(exam)

    考分鄙视(exam) 题目描述 Whence这个学期考了n次试,每一次都有一个0-20000之间的整数分数.Whence本来的状态应该是每一次考试都比前一次多一分(除第一次),但由于他很不稳定,偏差可 ...

  9. CSS实现三角形方法一--rotate+relative

    方法说明:两个正方形,一个小的,一个大的,将大的正方向进行旋转,然后移动到小的正方形的合适位置,覆盖小正方形的一部分,使小正方形剩余部分为三角形,再把大正方形的背景色改为浏览器窗口的颜色. 用到知识: ...

  10. php中serialize、unserialize与json_encode、json_decode比较

    性能比较 同一个变量编码或解码10000次,每个函数执行10000次所需时间 php5.2.13 json : 190 serialize : 257 json_encode : 0.08364200 ...