异步模块模式——AMD(Asynchronous Module Definition)
模块化
:将复杂的系统分解成高内聚、低耦合的模块,使系统开发变得可控、可维护、可拓展,提高模块的复用率请求发出后,继续其他业务逻辑,直到模块加载完成执行后续的逻辑,实现模块开发中对模块加载完成后的引用
异步加载文件中的模块
index.js
/** * 同步模块模式-SMD */ // 向闭包中传入模块管理器对象F(~屏蔽压缩文件时,前面漏写;报错) ~(function (F) { /** * 定义模块管理器 * @param {string} str 模块路由 * @param {Function} fn 模块方法 * @returns F模块管理器对象 */ F.define = function (str, fn) { // 过滤掉第一个元素为F的字符串数组 let keys = str.replace(/^F\./, '').split('.'); // 屏蔽对define与module模块方法的重写 if (keys[0] === 'define' || keys[0] === 'module') return this; // 遍历字符串数组,赋值到当前对象内 let obj = this; for (let i = 0; i < keys.length; i++) { // 防止后续模块覆盖以前定义的模块 if (typeof obj[keys[i]] === 'undefined') { obj[keys[i]] = { }; if (i === keys.length - 1) { obj[keys[i]] = fn && fn(); } } obj = obj[keys[i]]; } // 当前对象 return this; }; /** * 模块调用方法 * @param {...any} args 需要最后的参数为function */ F.module = function (...args) { // 依赖模块列表 let modules = []; // 判断最后一个参数是否为函数 let fn = typeof args[args.length - 1] === 'function' ? args.pop() : null; // 将参数转换为数组 let arr = (args.length === 1 && Array.isArray(args[0])) ? args[0] : args; // 遍历参数 let i = 0; while (i < arr.length) { // 判断参数是否是字符串(通过define创建的) if (typeof arr[i] === 'string') { let keys = arr[i].replace(/^F\./, '').split('.'); // 获取当前F对象 let mod = this; for (let j = 0; j < keys.length; j++) { // 替换当前的mod,查询到最里层对象 mod = mod[keys[j]]; } modules.push(mod); } else { modules.push(arr[i]) } i++; } fn && fn.apply(this, modules); }; }((function () { return window.F = { }; }())));
demo.js
F.define('demo.say', function () { return function (name) { console.log(`Hello ${ name}`); } });
index.html
<script src="./index.js"></script> <script> /** * 加载脚本文件 * @param {string} src 脚本路径 */ function loadScript(src) { let _script = document.createElement('script'); // 创建脚本元素 _script.type = 'text/javascript'; // 设置类型 _script.charset = 'UTF-8'; // 确认编码 _script.async = true; // 异步加载 _script.src = src; // 设置加载路径 document.getElementsByTagName('head')[0].appendChild(_script); // 将元素插入到页面中 } // 加载demo.js脚本 loadScript('./demo.js'); // 调用demo.say模块方法 (Error ---> Uncaught TypeError: Cannot read properties of undefined (reading 'say')) F.module('demo.say', function (say) { say('Lee'); }); setTimeout(() => { // 调用demo.say模块方法 (Success ---> Hello Lee) F.module('demo.say', function (say) { say('Lee'); }); }, 2000); </script>
报错原因:
由于浏览器中的文件是异步加载的,虽然现在开始加载demo.js文件,不过在文件没有加载完之前你可以继续做其他的事情,并且你写的方法,对于文件什么时候加载完成,你是无法获知的。
同步模块模式会立即引用该模块,此时文件加载尚未加载完成,因此你是引用不到该模块的。
当延迟了2s后,demo.js文件已经加载完毕,所以能得到demo.say模块,所以访问无问题。
异步模块(使用require.js
)
https://requirejs.org/docs/release/2.3.6/comments/require.js
<script src="https://requirejs.org/docs/release/2.3.6/comments/require.js"></script>
<script>
/**
* 异步模块(使用require.js)
*/
require(['./lib/a', './lib/b'], function (a, b) {
console.log(a, b);
a('Lee'); // Hello Lee
b.demo.getName(); // demo
});
</script>
// a.js
define([], function () {
return function (name) {
console.log(`Hello ${
name}`);
}
});
// b.js 依赖于demo.js
define(['./demo'], function (demo) {
return {
name: 'b',
demo
};
});
// demo.js
define([], function () {
return {
name: 'demo',
getName(){
console.log(this.name);
}
}
});
仿require.js
的实现
核心思想:
每开始进行加载一个js脚本,计数器加1;每加载完成一个脚本,计数器减1;当全部脚本加载完成,计数器为0,此时执行回调函数,告诉控制台脚本已经加载完成
// index.js
/**
* 异步模块 - 创建与调度模块
*/
// 向闭包中传入模块管理器对象R(~屏蔽压缩文件时,前面漏写;报错)
~(function (R) {
// 缓存所有加载的模块
const cache = {
};
// 最终的回调
let cb = function () {
};
// 脚本加载数量(每添加一个js脚本累加1;加载完成累减1;全部完成变为0)
let depCount = 0;
/**
* 获取脚本路径
* @param {string} name 路径
* @returns 路径
*/
function getScriptSrc(name) {
// 拼接完整的文件路径字符串,如'lib/ajax' => 'lib/ajax.js'
return String(name).replace(/\.js$/g, '') + '.js';
}
/**
* 加载脚本文件
* @param {string} src 路径
*/
function loadScript(src) {
let _script = document.createElement('script');
_script.type = 'text/javascript'; // 文件类型
_script.charset = 'UTF-8'; // 确认编码
_script.async = true; // 异步加载
_script.src = src; // 文件路径
document.getElementsByTagName('head')[0].appendChild(_script); // 插入页面中
}
/**
* 定义模块(当引入的js加载完成,就会自动执行此方法)
* @param {string} name 模块名称
* @param {Array} deps 依赖项 ([{name: 模块名称, path: 模块路径}, ...])
* @param {Function} callback 回调
*/
R.define = function (name, deps, callback) {
depCount--;
// 添加依赖脚本
deps.forEach(dep => {
depCount++;
loadScript(getScriptSrc(dep.path));
});
// 是否存在此模块,避免重复赋值
if (!cache[name]) {
cache[name] = {
depNames: deps.length ? deps.map(dep => dep.name) : [], // 脚本的依赖脚本模块名称(作为缓存对象的key,用于最终整理)
hasDep: !!deps.length, // 是否存在脚本的依赖脚本
export: deps.length ? callback : callback(), // 最终导出的数据(存在依赖脚本则返回函数,用于后续判断调用;不存在依赖脚本,返回当前函数的执行)
};
}
// 全部加载完成
if (depCount === 0) {
Object.values(cache).forEach(item => {
if (item.hasDep) {
let params = item.depNames.map(key => cache[key]['export']);
item.export = item.export.apply(null, params);
}
});
let obj = {
};
Object.keys(cache).forEach(key => {
obj[key] = cache[key].export;
});
cb.call(null, obj);
}
};
/**
* 执行各个脚本js模块
* @param {Array} deps 当前页面的依赖项,js脚本路径集合
* @param {Function} callback 回调
*/
R.module = function (deps, callback) {
// 保存当前的回调,用于脚本完全加载完成后调用
cb = callback;
// 添加依赖脚本
deps.forEach(dep => {
depCount++;
loadScript(getScriptSrc(dep));
});
};
}((function () {
return window.R = {
};
}())));
html.html
<script src="./index.js"></script> <script> console.log(R); /** * 异步模块 */ R.module(['./lib/a', './lib/b'], function (mod) { /** * { * a: ƒ (name), * b: { * name: 'b', * demo1: {name: 'demo1', getName: ƒ}, * demo2: {name: 'demo2', getName: ƒ}, * }, * demo1: {name: 'demo1', getName: ƒ}, * demo2: {name: 'demo2', getName: ƒ}, * } */ console.log(mod); mod.a('Lee'); // Hello Lee mod.b.demo1.getName(); // demo1 mod.b.demo2.getName(); // demo2 mod.demo1.getName(); // demo1 mod.demo2.getName(); // demo2 }); </script>
lib/a.js
// a.js R.define('a', [], function () { return function (name) { console.log(`Hello ${ name}`); } });
lib/b.js
// b.js 依赖于 demo1.js demo2.js R.define('b', [{ name: 'demo1', path: './lib/demo1' }, { name: 'demo2', path: './lib/demo2' }], function (demo1, demo2) { console.log(demo1, demo2); return { name: 'b', demo1, demo2 }; });
lib/demo1.js
// demo1.js R.define('demo1', [], function () { return { name: 'demo1', getName(){ console.log(this.name); } } });
lib/demo2.js
// demo2.js R.define('demo2', [], function () { return { name: 'demo2', getName(){ console.log(this.name); } } });
特点
模块化开发不仅解决了系统的复杂性问题,而且减少了多人开发中变量、方法名被覆盖的问题。通过其强大的命名空间管理,使模块的结构更合理。
通过对模块的引用,提高了模块代码复用率。异步模块模式在此基础上增加了模块依赖,使开发者不必担心某些方法尚未加载或未加载完全造成的无法使用问题。
异步加载部分功能也可将更多首屏不必要的功能剥离出去,减少首屏加载成本。