模块化

为啥要模块化

降低代码耦合度, 功能模块之间不会互相影响, 优点:

  • 可维护性
    减少与外部代码的联系,且整个项目使用同一份代码, 维护方便。
  • 命名空间
    全局污染:避免定义许多全局变量,造成全局污染。
    命名空间污染:避免不同模块之间的变量命名发生冲突。
  • 可复用性
    模块化后可以一次定义,多次使用,避免复制粘贴,多个项目中都可以使用。

模块化需要解决的几个问题

  • 如何方便地管理和使用模块的依赖。
  • 如何安全地包装模块的代码,避免全局污染。
  • 如何优雅地设计和暴露API,尽量使得每个API都是纯函数。
  • 如何在使用时简单快速地获取模块(如模块id)。

模块的设计有哪些关键点

好的模块封装应该是高度独立的,任何一个模块的加入或移除不会对已有的模块功能造成影响,为了良好的解耦,模块化需要关注以下几点:

  • 依赖设计
  • 内部实现隔离
  • 接口设计

模块化的历史

模块化最初是由 CommonJS 发展起来的,而后分化出 AMD 和 CMD,UMD 的出现实现了服务端和浏览器端的统一,ES2015 将模块化写入 js 标准,使得模块化成为标准。而 webpack 的出现使得模块化不仅仅是 js 层面的东西,它提倡一切皆模块,css 甚至图片也加入了模块化的领域。

早期js的模块化有以下几种方式

1、 全局引入

也就是jQuery的设计方式:通过将全局变量传入匿名函数的方式进行放大。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// jquery: globalVariable === $;
(function (globalVariable) {
// 在函数的作用域中下面的变量是私有的
var privateFunction = function() {
console.log('Shhhh, this is private!');
}
// 通过全局变量设置下列方法的外部访问接口
// 与此同时这些方法又都在函数内部
globalVariable.each = function(collection, iterator) {
if (Array.isArray(collection)) {
for (var i = 0; i < collection.length; i++) {
iterator(collection[i], i, collection);
}
} else {
for (var key in collection) {
iterator(collection[key], key, collection);
}
}
};
globalVariable.filter = function(collection, test) {
var filtered = [];
globalVariable.each(collection, function(item) {
if (test(item)) {
filtered.push(item);
}
});
return filtered;
};
globalVariable.map = function(collection, iterator) {
var mapped = [];
globalUtils.each(collection, function(value, key, collection) {
mapped.push(iterator(value));
});
return mapped;
};
globalVariable.reduce = function(collection, iterator, accumulator) {
var startingValueMissing = accumulator === undefined;
globalVariable.each(collection, function(item) {
if(startingValueMissing) {
accumulator = item;
startingValueMissing = false;
} else {
accumulator = iterator(accumulator, item);
}
});
return accumulator;
};
}(globalVariable));

2、 对象接口

  • 从模块内部返回一个模块接口供外部使用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    var myGradesCalculate = (function () {
    // 在函数的作用域中下面的变量是私有的
    var myGrades = [93, 95, 88, 0, 55, 91];
    // 通过接口在外部访问下列方法
    // 与此同时这些方法又都在函数内部
    return {
    average: function() {
    var total = myGrades.reduce(function(accumulator, item) {
    return accumulator + item;
    }, 0);
    return'Your average grade is ' + total / myGrades.length + '.';
    },
    failing: function() {
    var failingGrades = myGrades.filter(function(item) {
    return item < 70;
    });
    return 'You failed ' + failingGrades.length + ' times.';
    }
    }
    })();
    myGradesCalculate.failing(); // 'You failed 2 times.'
    myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'

以上两种方式的模块化都需要直接或者间接地依赖全局变量,就算模块化的架构经过深思熟虑,设计了各种冲突策略,也难以改变全局污染的事实,除此之外,随着项目复杂性的增加,全面变量将变得越来越大,越来越难以维护。

3、 ES Module

一年一个版本的迭代不是吹的,随着 ES2015 的落地,模块化也开始有约可寻。ES Module 为开发者们提供了 class 写法的 js 模块开发方式,将模块内变量限制与外部方法隔离,虽然只是个语法糖,但是对于后端转型的开发以及面向对象的开发者们简直不能太友好。

  • ECMA对于模块的加载过程并未做太多的限制,不过定了执行的一些规约:
    Runtime Semantics: TopLevelModuleEvaluationJob ( sourceText)
    文中realm的存在就决定了这种模块定义方式的目标是跨平台的统一写法。
  • ES2015 与 AMD 和 CMD 不同的地方在于:
    • import 和 export 是静态的。即通过语法解析即可得到,不需要实际运行,这样,在import一个外部模块变量之后在语法解析阶段 js 就会认识这个变量的存在,不会报错。
    • import 的对象叫做 exports 对象,这个对象内管理了模块的接口层面的所有东西,
    • exports 的是一个引用,指向的是 模块的 exports 属性。
  • ES2015 的优点:

    • 通过语法分析确定模块间关系,为构建等工作奠定基础
    • 可以结合 webpack 等打包工具在检测到依赖之后立即进行打包,减少网络传输代价
    • 使用引用作为 export 有效解决不少情况下循环引用的问题。
  • 关于模块,可以参照 模块 Modules - InfoQ


关于CommonJS、AMD和CMD

CommonJS: NodeJS
  • 老祖宗级别的存在,出现是初衷是为了弥补 js 标准库过少的缺点,帮助js实现模块的功能,后来的 NodeJS 就是 CommonJS 规范的一个实现。
  • 用法:
    模块定义部分使用 exports 开放模块接口。
    require(“xxx”) 的形式导入(使用)模块
  • 缺点:同步阻塞的加载过程不适合浏览器端使用 ( AMD 出现的原因)

AMD: RequireJS

  • 异步加载,模块的加载不会影响其他代码的运行,所有依赖于某个模块的代码全都模块加载的回调函数中去。
  • 用法:define(id?, dependencies?, factory); require([module], callback)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 定义
    define('foo', ['math'], function(math) {
    return {
    increase: function(x) {
    return math.add(x, 1);
    }
    };
    });
    // 匿名模块
    define(function(require, exports, module) {
    var math = require('math');
    exports.increase = function(x) {
    return math(x, 1);
    };
    });
    // 加载
    require(['math'], function(math) {
    math.add(1, 3);
    });
CMD: SeaJS
  • 与 CommonJS 规范是兼容的。遵循 CMD 的模块,可以在 Node.js 中运行。
  • CMD 规范中不使用 id 和 deps 参数, 只保留 factory 。(也可以带上 id 和 deps , 但是不属于规范)

  • 用法:
    define(function(require, exports, module){});
    require(“xxx”);
    require.async(“xxx”, cb); (注意,如果要完全改写模块接口,则需要使用module.exports={});

UMD
  • 优先判断是否支持AMD环境, 再检验是否支持CommonJS环境,否则认为当前环境是浏览器环境(this 为 window);
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    (function (root, factory) {
    if (typeof define === 'function' && define.amd) {
    // AMD
    define(['myModule', 'myOtherModule'], factory);
    } else if (typeof exports === 'object') {
    // CommonJS
    module.exports = factory(require('myModule'), require('myOtherModule'));
    } else {
    // Browser globals (Note: root is window)
    root.returnExports = factory(root.myModule, root.myOtherModule);
    }
    }(this, function (myModule, myOtherModule) {
    // Methods
    function notHelloOrGoodbye(){}; // A private method
    function hello(){}; // A public method because it's returned (see below)
    function goodbye(){}; // A public method because it's returned (see below)
    // Exposed public methods
    return {
    hello: hello,
    goodbye: goodbye
    }
    }));

总结

  • CommonJS 是老祖宗,而且非常适合用在服务器端。
  • AMD 和 CMD 分别是RequireJS 和 SeaJS 在推广过程中对模块定义的规范化产出。但都是为了解决 CommonJS 在浏览器端的局限性。目的都是浏览器模块化开发。
  • 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible.
  • CMD 推崇依赖就近,AMD 推崇依赖前置。
  • UMD 是服务器端和浏览器端的一个封装。
AMD
  • AMD 是在发现 CommonJS 的局限性之后产生的一种解决方案,这种方案要求所有的依赖都要在 require 的时候写好,回调函数的参数则是各个被引用模块的引用。在引入新模块时,被引用模块会预先下载并执行,然后才会执行回调函数中的逻辑,那么问题来了:

    • 万一有些被依赖模块在回调中根本用不到,那这个模块的执行就毫无意义了,是个多余的负担。
    • 两大串引用列表和参数列表逼死强迫症。

    好,AMD 支持在用到的时候 require, 比如这样:
    AMD_require_while_used
    b.js 确实是要等到 click 事件触发才会去下载然后执行,好了,网络爆炸,b 根本下不下来,button 点到死没用。

CMD
  • CMD 是在发现 CommonJS 的局限性后在 CommonJS 上做了改进而产生的。
  • CMD 的两个特性: 延迟执行,依赖就近,解决了 AMD 等到要用才去下载的依赖于网速的 bug。
AMD & CMD

参考