1.1 什么是模块
我们总是希望将一份工程代码划分为多个模块。试想一下,如果你的工程是一块铁板,你想要在其中做一点简单的改动,就不得不重新编译运行整个工程。对于规模不断变大的工程软件来说,这对开发、debug到后期维护都是非常不友好的。好的模块具有非常好的自给自足(self-contained)的特性,新增、删除、修改它们都不会中断到整个工程的运作。
1.2 为什么要使用模块
正如上一小节所说,模块将工程软件很好地划分成多个部分,每个模块负责各自的功能,这很好地优化了开发流程,使得构建大型项目成为可能。简单来说,模块具有以下优点:
- 可维护性(maintainability):一个设计良好的模块会尽可能地减轻其与基础代码的耦合程度,这意味着你可以对模块进行单独的开发与维护而不用担心对工程里的其他部分产生副作用。
- 私有命名空间:在JS里,在top-level的函数作用域外侧的变量就属于全局变量,这非常容易产生“命名空间污染”的问题,这意味着你在跟毫不相关的代码共享全局变量。与毫不相关的代码共享全局变量是非常不好的。而模块会通过为变量构造一个私有空间来解决这个问题。
- 可复用性:这很好理解,一个有明确目的且独立的模块开发好之后,我们可以在工程中任何需要它的地方使用它,而不需要编写重复的逻辑。这将会为我们节约大量的时间!
1.3 怎么实现模块
有许多方法可以实现模块。
1.3.1 模块模式(Module pattern)
模块模式模仿类的概念,使得我们可以在一个对象中存储公有和私有的变量或方法。也就是说,我们对外为这些我们想要暴露的方法提供接口,同时又能将私有变量和方法封装在一个闭包里。通过匿名函数+IIFE实现。
1.3.2 CommonJS & AMD
上述方法的中心思想就是:创建一个全局变量,将相关代码包裹在一个函数中,从而利用闭包创建一个私有命名空间。
作为开发者,我们还需要知道引入依赖(在html文件中添加<script>标签)的顺序以及解决可能遇到的依赖名字重复的问题。我们能否拥有一个不通过全局作用域的模块接口。答案是可以!有两个流行的最佳实践方案:CommonJS & AMD。
CommonJS
CommonJS是一个志愿工作小组设计实现用于声明模块的JS APIs。一个CommonJS模块本质上是一个可重用的JS代码,它可以export特殊的对象,程序中的任何模块都可以获取。
//commonjs module
module.exports = myModule
//another file
var myModule = require('myModule')
这个方法的优点是:
- 避免了全局命名空间污染
- 使得我们的依赖更明确
另外需要注意:
- server-first
- CommonJS是同步的
CommonJS在服务端工作得很好,但是在浏览器就不尽然了。从web读取模块的速度是明显低于从硬盘读取的,而脚本的加载通常会阻塞渲染。这将会造成不好的用户体验。
AMD
AMD(Asynchronous Module Definition)支持异步加载模块。
define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) {
console.log(myModule.hello());
});
//myModule
define([], function() {
return {
hello: function() {
console.log('hello');
},
goodbye: function() {
console.log('goodbye');
}
};
});
UMD
同时支持AMD和CommonJS特性(Universal Module Definition)
1.3.3 Native JS
ES6引入了内置的模块。ES6模块提供了声明式语法和异步加载,以及对循环依赖的支持。对比live read-only views:
CommonJS:
// lib/counter.js
var counter = 1;
function increment() {
counter++;
}
function decrement() {
counter--;
}
module.exports = {
counter: counter,
increment: increment,
decrement: decrement
};
// src/main.js
//引入的是一个copy,不能通过方法修改
var counter = require('../../lib/counter');
counter.increment();
console.log(counter.counter); // 1
另一方面ES6:
// lib/counter.js
export let counter = 1;
export function increment() {
counter++;
}
export function decrement() {
counter--;
}
// src/main.js
import * as counter from '../../counter';
console.log(counter.counter); // 1
counter.increment();
console.log(counter.counter); // 2
本篇文章首先介绍了什么是模块以及为什么要使用模块。对程序划分模块增强了程序的可维护性,解决“命名空间污染”的问题,还能够增强代码的可复用性。然后介绍了实现模块的方式之一Module Pattern。该模式主要利用闭包实现私有变量和方法的封装。为了进一步解决模块引入顺序以及可能存在的模块名字重复的问题,出现了一些解决方案:CommonJS、AMD、UMD、ES6。其中CommonJS面向服务端且只能同步加载,AMD支持异步加载,UMD综合了前两者的特性。ES6内置了模块,拥有很多强大的特性。