JavaScript 模块化编程(一):模块的写法
JavaScript 模块化编程(二):规范
JavaScript 模块化编程(三):实现一个RequireJS
JavaScript 模块化编程(四):结合Node源码分析CommonJs规范
虽然RequireJS是一个过时的JS模块化解决方案,但是其对我们了解JS模块化的发展依然很重要。下面将参考RequireJS源码写了一个简单的JavaScript模块加载器。只有300行代码,是源码的六分之一,已经放在我的github上
--------点击查看代码--------
1.RequireJS
先回顾一下RequireJS的两个重要api
定义模块
define(id?, dependencies?, factory)
加载模块
require([module], callback)
require和define函数的关系
(1)require和define函数接收同样的参数,define和require在依赖处理和回调执行上都是一样的
(2)define的回调函数需要有return语句返回模块对象
,这样define定义的模块才能被其他模块引用
,所以用它来定义模块(建议在一个文件中使用一次);require的回调函数不需要return语句
。其实在我看来,require函数可以看做是特殊的define函数,它用来定义一个顶层匿名模块,用来加载和使用模块,这个模块不需要被其他模块加载。
RequireJS中的执行流程
(1)RequireJS首先找到data-main属性,然后根据属性值(通过新建一个script标签)加载并且解析入口文件
(2)先调用 require 函数和define函数,将所有的依赖转化为script 节点插入到dom 中,然后每一个 节点的onload事件中,将该模块作为实体保存起来,并检查所有模块是否加载完成,如果加载完成,递归执行所有回调
2.实现一个简单的RequireJS
首先这里先说明一点,下面我们主要了解实现的整体流程,会粘出一些主要代码,里面可能会包含一些辅助函数,具体可以查看我github上的源码
(1)定义一个全局变量
首先,需要定义几个全局变量,用来保存已经加载的模块,尚未加载的模块,所有模块等全局信息
var context={
topModule:"", //顶层模块
waitings:[], // 尚未加载的模块
loadeds:[], // 已经加载的模块
baseUrl:"",
/**
* 每一个模块都有下面的几个属性:
* moduleNmae 模块名称
* deps 模块依赖
* factory 模块工厂函数 .
* args 该模块的依赖模块的返回值
* returnValue 该模块工厂函数的返回值
*/
modules:[] // 模块集合
} ;
(2)加载 require 顶层模块
在require.js 里面都是用 data-main 属性来指定入口文件,所以先寻找 data-main 属性,并插入到 head中 。这里将 data-main 作为的路径作为 baseUrl
/**
* 查找data-main属性的script标签,
* 根据属性值(通过新建一个script标签)加载并且解析入口文件
*/
if (isBrowser) {
var scripts=document.getElementsByTagName('script');
var head,src,subPath,mainScript;
eachList(scripts,function(script){
var dataMain = script.getAttribute('data-main');
if (dataMain) {
if (!head) {
head = script.parentNode;
}
if (!context.baseUrl) {
src = dataMain.split('/');
mainScript = src.pop();
subPath = src.length ? src.join('/') + '/' : './';
context.baseUrl = subPath;
}
// 创建顶层节点
var dataMainNode = document.createElement('script');
dataMainNode.async = true;
head.appendChild(dataMainNode);
dataMainNode.src = dataMain+ ".js";
dataMainNode.onload = function() {
// 将顶层模块 从waitings里面除去,并添加到loadeds数组中
removeByEle(context.waitings, context.topModule)
context.loadeds.push(context.topModule);
}
return true;
}
});
(3)定义 require 方法
require 方法用来使用模块,也就是定义一个顶层模块,这个模块不需要被其他模块加载
/**
* require方法,加载一个模块
* @param {[type]} deps [依赖数组]
* @param {Function} callback [工厂函数]
* @return {[type]}
*/
requireJs.require=function(deps,callback){
if (typeof name !== 'string') {
callback = deps;
deps = name;
name = null;
}
if (!isArray(deps)) {
callback = deps;
deps = [];
}
// 生成随机模块名,方法
let moduleName = getUnqName();
context.topModule = moduleName;
context.waitings.push(moduleName);
// 生成一个模块配置
context.modules[moduleName] = {
moduleName: moduleName,
deps: deps,
factory: callback,
args: [],
returnValue: ""
}
deps.forEach(function(dep) {
var scriptNode = document.createElement("script");
scriptNode.setAttribute("data-module-name", dep);
scriptNode.async = true;
scriptNode.src = context.baseUrl + dep + ".js";
document.querySelector("head").appendChild(scriptNode);
scriptNode.onload = scriptOnload;
context.waitings.push(dep);
});
}
这里需要注意一个函数scriptOnload
,在script 节点加载完成后触发。将对应模块从waitings 里面删除,同时往loadeds里面添加该模块,如果发现 waitings为空,那么就开始递归执行工厂函数 。
/**
* [每一个脚本插入head中,都会执行这个事件 。这个函数完成两件事:
* 1. 如果是一个匿名模块加载,那么取得这个匿名模块,并完成模块命名,
* 2. 当节点加载完毕,判断context.waitings是否为空,如果不为空,返回,如果为空,说明已经全部加载完毕,现在就可以执行所有的工厂函数]
* @param {[object]} e [事件对象]
* @return {[type]}
*/
function scriptOnload(e) {
e = e || window.event;
let node = e.target;
let moduleName = node.getAttribute('data-module-name');
tempModule.moduleName = moduleName;
context.modules[moduleName] = tempModule;
removeByEle(context.waitings, moduleName);
context.loadeds.push(moduleName);
if (!context.waitings.length) {
console.log(context.modules);
exec(context.topModule);
}
}
(4)定义 define 方法
其实define函数和上面的require函数做了差不多相同的事,差别在于require自动生成了一个模块名。并且require中设置了context.topModule
/**
* [define和 require 做的工作几乎相同]
* @param {[array]} deps [依赖数组]
* @param {[function]} callback [工厂函数]
* @return {[type]}
*/
requireJs.define=function(name,deps,callback){
if (typeof name !== 'string') {
callback = deps;
deps = name;
name = null;
}
if (!isArray(deps)) {
callback = deps;
deps = [];
}
//生成一个模块配置
tempModule = {
deps: deps,
factory: callback,
args: [],
returnValue: ""
}
// 递归遍历所有依赖,添加到 `head` 中,并设置 这个节点的一个属性`data-module-name`标识模块名
deps.forEach(function(dep) {
var scriptNode = document.createElement("script");
scriptNode.setAttribute("data-module-name", dep);
scriptNode.async = true;
scriptNode.src = context.baseUrl + dep + ".js";
document.querySelector("head").appendChild(scriptNode);
scriptNode.onload = scriptOnload;
context.waitings.push(dep);
});
}
(5)执行回调
我们再回到scriptOnload
函数,每个模块加载完成,就会在 waitings 里面去掉,然后检查waitings 数组,如果为空,说明全部加载完,就可以执行 exec
函数,在这里函数中,递归执行所有的回调 。
/**
* 所有模块加载完毕,递归执行工程函数 , 核心方法
* @param {[string]} moduleName [模块名]
* @return {[type]}
*/
function exec(moduleName) {
let module = context.modules[moduleName];
let deps = module.deps;
let args = [];
if(deps){
deps.forEach(function(dep) {
exec(dep);
args.push(context.modules[dep].returnValue);
});
module.args = args;
module.returnValue = context.modules[moduleName].factory.apply(context.modules[moduleName], args);
}
}
3.测试
//user.js
define([], function () {
return {
checkLogin: function (name,pwd) {
return name==="xxx"&&pwd==="yyy"
}
}
})
//math.js
define(function () {
return {
add: function (a,b){
return a+b;
},
sub:function(a,b){
return a-b;
}
}
})
//main.js
require(["math","user"], function(math,user) {
if(user.checkLogin("xxx","yyz")){
console.log("12+21=" + math.add(12,21));
}else{
console.log('please sign in or register first');
}
})
//test.html
结果