JavaScript 模块化01

2018-12-04

最早期JavaScript代码的问题

最早期的时候我们通过<script>标签来载入JavaScript文件,通过全局变量来区分不同的功能代码, 而全局变量之间的依赖关系需要显式地通过<script>标签的的加载顺序来解决。

但是过多的<script>标签数会增加HTTP请求的数目, 从而延长网页的加载时间。 所以一般情况下,我们会在发布时将所有代码压缩到同一个文件中。

当Web项目变得非常庞大,所依赖的前端模块非常多的时候, 通过手动管理这是全局变量就变得不切实际了。

什么是模块化?

Modular programming is a software design technique that emphasizes separating the functionality of a program into independent, interchangeable modules, such that each contains every thing necessary to execute only one aspect of the desired functionality.

为什么要模块化?

  • 模块化后的代码可以重复使用, 无需copy&paste相同的代码; 只需要引入相同的模块。
  • 高度解耦的模块,更改模块的实现方式时,只需要保证接口一致。
  • 避免污染全局空间。

一个模块的基本要求

  1. 定义封装的模块
  2. 定义其对其他模块的依赖
  3. 支持其他模块引入

最早期的JavaScript模块的写法

1
2
3
4
5
6
7
8
9
10
11
var module = (function() {
var privateVar = "Nick";

function setName( name ) {
privateVar = name;
}

return {
setName: setName,
}
})();

这种方法是jQuery, zepto.js等早期JavaScript库采用的方法。

这种方式通过闭包的方式封装了私有变量/方法,返回的对象暴露了相应的公共对象和方法。 但它不支持异步加载。所有模块加载都是同步进行的。

CommonJS

CommonJS 是一个同步加载模块规范,通过requireexports来引入和输出模块内容。require function引入其他模块的内容, exports则是一个代表文件输出内容的对象。

  • CommonJS定义的模块分为:
    • 模块引用(require)
    • 模块导出对象(exports)
    • 模块标识(module)
1
2
3
4
5
var customerStore = require('store/customer'); // import a module

epxorts = function() {
return customer.get('store');
}

Node Module

Node.js的模块系统基本上是遵守CommonJS的规范来实现的。在Node的模块系统中, 每一个文件都是一个独立的模块, 有自己的作用于,在一个文件里定义的变量、函数、类、都是私有的、对其他文件都是不可见的。如下:

1
2
3
4
5
6
7
8
// foo.js
const circle = require('./circle.js');
console.log(`The area of a circle of radius 4 is ${circle.area(4)}`);

// circle.js
const { PI } = Math;
exports.area = (r) => PI * r ** 2;
exports.circumference = (r) => 2 * PI * r;

foo.js 文件引入了 circle.js模块, circle.js 模块只导出了areacircumference 方法。而变量PI则是circle.js 模块的私有变量, 对foo.js模块不可见。 这是因为在模块被 module wrapper方法处理。

1
2
3
(function(exports, require, module, __filename, __dirname) {
// Module code actually lives in here
});

Node内部提供了一个Module构建函数,所有模块都是Module的实例

1
2
3
4
5
function Module(id, parent) {
this.id = id;
this.exprots = {};
this.parent = parent;
}

每个模块内部都有一个module对象,代表当前模块,它有以下属性:

module.id: 模块的识别符,通常是带有绝对路径的文件名

module.filename: 模块的文件名,带有绝对路径

module.loaded: 返回一个布尔值,表示模块是否完成加载

module.parent: 返回一个对象,表示调用该模块的的模块

module.children: 返回一个数组, 表示该模块要用到的其他模块

module.exports: 表示模块对外的输出值

AMD规范

前面提到CommonJS是一个同步模块加载规范, 只有当引用的模块按顺序加载完成后才会执行后面的操作。 由于CommonJS这一特性, CommonJS只适用于服务器端(模块文件存在于本地硬盘,所以加载起来比较快), 如果是在浏览器环境,需要从服务器端下载响应模块,这种情况下同步加载会阻塞渲染, 所以必须采用非同步加载模块的方式,也就是AMD规范。

AMD使用define方法定义模块:

1
2
3
define(['module1', 'module2'], function(module1, module2) {
console.log(module1.setName());
})

define方法接受两个参数, 第一个参数是所依赖的模块组成的数组,这些模块以异步非阻塞地方式加载, 当加载完成时,·define`方法的第二个参数——callback function被调用, 加载好的模块作为参数传入callback function.

我们可以注意到,上面的所有模块机制都不是JavaScript的内置的。我们通过CommonJS和AMD来模拟一个模块系统。最终,幸运地是ES6引入了内置的模块系统。

ECMAScript 6 Modules

ECMAScript 6 内置了模块机制,即支持同步也支持异步的模块加载。

1
2
3
4
5
6
7
8
9
10
11
12
// lib.js
export const sqrt = Math.sqrt;
export function square(x) {
return x * x;
}
export function diag(x, y) {
return sqrt(square(x) + square(y));
}
// main.js
import { square, diag } from 'lib';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5

通过import和export来引入和导出模块,不能动态加载,在模块中的位置固定在文件的首部和尾部。通过静态分析的方法来构建模块依赖的树。

这种内置模块在一些老的浏览器中还不能使用,需要通过Babel进行转译。