JS-模块化编程机制

Javascript 不是一种模块化编程语言,ES5 标准不支持"类"(class),"模块"(module), ES6 标准将正式支持"类"和"模块"。

为什么模块很重要?

因为有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块。

Javascript 社区做了很多努力,在现有的运行环境中,实现"模块"的效果。目前,通行的 Javascript 模块规范共有两种:CommonJS 和 AMD。

CommonJS

node.js 的模块系统,就是参照 CommonJS 规范实现的。在 CommonJS 中,有一个全局性方法 require(),用于加载模块。假定有一个数学模块 math.js,就可以像下面这样同步加载"(synchronous)和调用模块提供的方法:

var math = require("math");
math.add(2, 3); // 5
1
2

但是,由于一个重大的局限,使得服务器端 CommonJS 规范不适用于浏览器环境。第二行 math.add(2, 3),在第一行 require('math') 同步加载"(synchronous)之后运行,因此必须等 math.js 加载完成,浏览器处于"假死"状态。

因此,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。这就是 AMD 规范诞生的背景。

AMD 规范

AMD 是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

AMD 也采用 require()语句加载模块,但是不同于 CommonJS,它要求两个参数:

require([module], callback);
1

第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数 callback,则是加载成功之后的回调函数。如果将前面的代码改写成 AMD 形式,就是下面这样:

require(["math"], function (math) {
    math.add(2, 3);
});
1
2
3

math.add()与 math 模块加载不是同步的,浏览器不会发生假死。所以很显然,AMD 比较适合浏览器环境。

目前,主要有两个 Javascript 库实现了 AMD 规范:require.jscurl.js

require.js

require.js 的诞生,就是为了解决这两个问题:

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

require.js 的加载

/**
 加载这个js文件可能造成网页失去响应。解决办法有两个:
 1. 把它放在网页底部加载,
 2. 使用 async,defer
    - async 表明这个文件需要异步加载,避免网页失去响应。IE不支持这个属性,只支持defer。
    - defer 表明外部脚本将在页面完成解析时执。
 */

<script src="js/require.js" defer async="true"></script>

/**
 加载require.js以后,下一步就要加载我们自己的代码了。
 假定我们自己的代码文件是main.js,也放在js目录下面:
 */

<script src="js/require.js" data-main="js/main"></script>
// data-main 属性的作用是,指定网页程序的主模块。
// 在上例中,就是 js 目录下面的 main.js,这个文件会第一个被 require.js 加载。
// 由于 require.js 默认的文件后缀名是 js,所以可以把 main.js 简写成 main。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

主模块的写法

主模块 main.js 依赖于其他模块,这时就要使用 AMD 规范定义的的 require() 函数。

// main.js

require(["module1", "module2", "module3"], function (m1, m2, m3) {
    // some code here
});

// require() 函数接受两个参数。
// 第一个参数是一个数组,表示所依赖的模块,
// 上例就是 ["module1", "module2", "module3"], 即主模块依赖这三个模块;
// 第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用。
// 加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块。

// 其他配置
require.config({
    baseUrl: "js/lib",
    paths: {
        jquery: "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min",
        underscore: "underscore.min",
        backbone: "backbone.min"
    }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

require() 异步加载 module1,module1 和 module1,浏览器不会失去响应;它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。

AMD 模块的写法

模块必须采用特定的 define()函数来定义

  • 如果一个模块不依赖其他模块,那么可以直接定义在 define()函数之中。
  • 如果这个模块还依赖其他模块,那么 define()函数的第一个参数,必须是一个数组,指明该模块的依赖性。
// 不依赖:
// define(function () {})

// 依赖 myLib
define(["myLib"], function (myLib) {
    function foo() {
        myLib.doSomething();
    }
    return {
        foo: foo
    };
});
1
2
3
4
5
6
7
8
9
10
11
12

加载非 AMD 规范的模块

加载非 AMD 规范的模块在用 require()加载之前,要先用 require.config()方法,定义它们的一些特征。

require.config({
    shim: {
        "jquery.scroll": {
            deps: ["jquery"],
            exports: "jQuery.fn.scroll"
        },
        underscore: {
            exports: "_"
        },
        backbone: {
            deps: ["underscore", "jquery"],
            exports: "Backbone"
        }
    }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

CMD 规范

CMD 推崇依赖就近,

// CMD 依赖就近 代码在运行时,首先是不知道依赖的,需要遍历所有的require关键字,找出后面的依赖。具体做法是将function toString后,用正则匹配出require关键字后面的依赖。显然,这是一种牺牲性能来换取更多开发便利的方法。
define(function (require, exports, module) {
    var a = require("./a");
    a.doSomething();
    var b = require("./b");
    b.doSomething();
});

// AMD 依赖前置 在解析和执行当前模块之前,模块作者必须指明当前模块所依赖的模块
define(["./a", "./b"], function (a, b) {
    a.doSomething();
    b.doSomething();
});
1
2
3
4
5
6
7
8
9
10
11
12
13

ES6 模块

在 ES6 前, 实现模块化使用的是 RequireJS 或者 seaJS(分别是基于 AMD 规范的模块化库, 和基于 CMD 规范的模块化库)。

ES6 引入了模块化,其设计思想是在编译时就能确定模块的依赖关系,以及输入和输出的变量。

  • ES6 的模块化分为导出(export) @与导入(import)两个模块。
  • ES6 的模块自动开启严格模式,不管你有没有在模块头部加上 use strict;。
  • 模块中可以导入和导出各种类型的变量,如函数,对象,字符串,数字,布尔值,类等。
  • 每个模块都有自己的上下文,每一个模块内声明的变量都是局部变量,不会污染全局作用域。
  • 每一个模块只加载一次(是单例的), 若再去加载同目录下同文件,直接从内存中读取。
  • 静态执行特性:import 是静态执行,所以不能使用表达式和变量。
  • 只读属性:不允许在加载模块的脚本里面,改写接口的引用指向,即可以改写 import 变量类型为对象的属性值,不能改写 import 变量类型为基本类型的值。
// 导入
import m from "./xxx.js";
import { fn as fn2 } from "./xxx.js";

const module = "ES6 module";

// 导出
export * from "./xxx.js";
export { m1, m2 as m3 } from "./xxx.js";
// export { default as foo } from "./xxx.js";
// export { foo as default } from "./xxx.js";

export const str = module;
export const fn = function () {};

export { module };
export { module as m4 };
export default { module };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

参考资料