最近大半年也写了不少通用模块,却从来没使用过一种通过书写格式。
目前公司的前端通用库还是jQuery, 但也不是所有的模块都适合以jQuery 插件的形式开发。平时写的时候一般以立即执行函数或 OO 形式开发,以目前项目情况来看也不是很需要用require.jssea.js之类的加载器。

其实我的需求很简单,就是抹去模块在全局作用域的定义,以通用的形式定义和引用。

最终目标

从结果出发分析需求,先列出来最终想要的代码形式。

// 形式1:匿名模块,只有一个注入exports参数的函数体
module(function (exports) {
console.log(require('m1').msg);
});
// 形式2:具名模块,包含一个模块名称参数和一个注入exports参数的函数体
module('m1', function (exports) {
exports.msg = 'hello';
});
// 形式3:具名模块,包含一个模块名称参数、依赖模块数组、和一个注入依赖模块、exports参数的函数体
module('m2', ['m1'], function (m1, exports) {
exports.msg = m1.msg + "World";
});
// 形式4:匿名模块,与形式3相比没有模块名称
module(['m2'], function (m2, exports) {
console.log(m2.msg);
});

使用module定义一个模块,拥有一个函数上下文,可以定义模块名称和引用其他模块,也可以通过注入的exports参数输出模块开放接口。
某些时候也许需要require('模块名')这种形式来直接引用模块。

1. 注入环境变量

从以上四种模块形式来看,modulerequire两个变量的作用域是全局,需要注入到window中。

(function(w, f){
w.module = function () {
}
w.require = function (namne) {
}
}(window, undefined));

一个简单的立即执行函数就可以做到。

2. 解析 module 参数

module参数的数量是不固定的,最少1个(形式1)、最多3个(形式3)。
可以需要通过判断参数数量和类型加以区分。

w.module = function () {
var modName = arguments[0], // 模块名 String
mods = arguments[1], // 引用模块 Array
context = arguments[arguments.length - 1]; // 模块函数 Function
if(modName instanceof Array) {
mods = modName;
modName = f;
}
}

参数数量不固定,也就不必定义形参了,直接从arguments中获取。

arguments[0]是模块名,模块名存在时永远是参数第一位;
arguments[1]是引用模块,模块名不存在时arguments[0]为引用模块,所以判断模块名是数组类型时重新赋值;
不管何种形式模块函数都是最后一个参数,直接使用arguments[arguments.length - 1]获取;

3. 实现 module

模块函数的参数数量也是不固定的,但至少需要注入exports来输出开放接口。
每多引用一个模块,就需要向模块函数中注入一个对应参数。

var modules = {}; // 模块容器
w.module = function () {
var modName = arguments[0], // 模块名 String
mods = arguments[1], // 引用模块 Array
context = arguments[arguments.length - 1]; // 模块函数 Function
if(modName instanceof Array) {
mods = modName;
modName = f;
}
// 1. 取出模块
var args = [];
if(mods instanceof Array) {
for(var i = 0; i < mods.length; i++) {
args.push(modules[mods[i]]);
}
}
// 2. 注册模块
if(typeof modName === 'string') {
modules[modName] = {};
args.push(modules[modName]);
}
// 3. 注入参数,执行模块
context.apply(this, args);
}

遍历mods取出模块:定义了一个变量modules存放模块,那么假如一个模块引用了模块ab,则mods = ['a', 'b'];ab两个模块的真值即为modules['a']modules['b']。当模块为形式2时,参数有两个,参数2为模块函数。此时 arguments[1] === arguments[arguments.length - 1],引用模块(mods)也指向了模块函数,所以要判断mods类型。
注册模块:每一个具名模块都需要注册到modules中,以备其他模块引用。当模块为匿名模块时,不需要注册,所以要判断modName类型。
注入参数,执行模块:对于不定数量的参数注入,显然是使用apply最简单。

4. 实现 require

var modules = {}; // 模块容器
w.module = function () {
//...
}
w.require = function (namne) {
return modules[namne];
}

暂时不考虑异步加载、加载路径解析等各种情况了,直接从容器中取到模块…

5. 完整代码

(function(w, f){
var modules = {}; // 模块容器
w.module = function () {
var modName = arguments[0], // 模块名 String
mods = arguments[1], // 引用模块 Array
context = arguments[arguments.length - 1]; // 模块函数 Function
if(modName instanceof Array) {
mods = modName;
modName = f;
}
var args = [];
if(mods instanceof Array) {
for(var i = 0; i < mods.length; i++) {
args.push(modules[mods[i]]);
}
}
if(typeof modName === 'string') {
modules[modName] = {};
args.push(modules[modName]);
}
context.apply(this, args);
}
w.require = function (namne) {
return modules[namne];
}
}(window, undefined));
// 形式1:匿名模块,只有一个注入exports参数的函数体
module(function (exports) {
console.log(require('m1').msg);
});
// 形式2:具名模块,包含一个模块名称参数和一个注入exports参数的函数体
module('m1', function (exports) {
exports.msg = 'hello';
});
// 形式3:具名模块,包含一个模块名称参数、依赖模块数组、和一个注入依赖模块、exports参数的函数体
module('m2', ['m1'], function (m1, exports) {
exports.msg = m1.msg + "World";
});
// 形式4:匿名模块,与形式3相比没有模块名称
module(['m2'], function (m2, exports) {
console.log(m2.msg);
});

执行以上代码会报错,代码是自上而下同步执行的,形式1运行阶段模块m1还未定义。
把形式1放到形式4之后执行,输出:

helloWorld
hello