导语:本文带你去了解一下JavaScript模块化的前世今生,包括但不限于JavaScript模块化、模块化规范、模块加载器和模块打包工具等。本文不是一个深度剖析JavaScript模块相关话题的文章,仅是一个能够让你10分钟快速了解JavaScript模块化相关知识的介绍。
作者:徐江伟--腾讯前端工程师
@IMWeb前端社区
现在JavaScript技术的发展可能会让你应接不暇。身为一线搬砖工的你对雨后春笋般的前端工具和框架越来越疲于学习和暇接。有时候,你可能不自主的问,webpack是什么玩意?browserify又是什么东东?AMD和CommonJS这都是啥,又有啥关系和区别?
模块化是什么?我们首先看一下模块化的定义是什么,以下定义来自百度百科:
模块化:
模块化是指解决一个复杂问题时自顶向下逐层把系统划分成若干模块的过程,有多种属性,分别反映其内部特性。
乍看起来有点高深,简单地讲就是模块化可以让我们将代码分功能和业务等维度来切分成小的组织单元,然后根据需要来更好的组织代码,增加代码的可维护性、复用性等。其实,确切的讲,我们写代码完全可以不用模块化。但正如提到的,模块化可以显著地增加代码的复用性、可维护性和扩展性等优点,是自上世纪开始写代码后人们在实践总结出来的优秀实践原则。
具体到JavaScript中来讲,模块化带来了以下几点优点:
ES5之前的模块化
代码抽象:将可复用的代码逻辑抽象到通用的库,屏蔽实现的复杂性;
代码封装:将代码的实现封装到模块里,对外暴露公用的API,从而使调用模块不必关心实现的复杂性;
代码复用:也即是DRY( Dont Repeat Yourself)原则,减少代码的冗余,增加代码的可复用性;
代码管理:模块化后的代码更加易于组织代码结构和管理。
遗憾的讲,在ES5版本和之前的版本中,JavaScript并没有模块化的概念。行业优秀的工程师们通过各种各样的方式来模拟JavaScript的模块化。来,下面我们就看看常用的两种模拟方法是怎么样的。
1、立即执行函数
立即执行函数,英文为Immediately Invoked Function Expression,简称为IIFE。通常结构如下:
(function(){ // ...})()
从中可以看出,IIFE是一个一旦被声明就会被立即执行的匿名函数。那么匿名的立即执行函数带来了哪些特点呢:
将逻辑的复杂性实现都封装在IIFE函数内;
由于function会隔离作用域,因此IIFE内声明的任何变量都会被当做IIFE的局部变量,从而不会污染到全局变量。
因此,我们可以说,IIFE为一个小的模块。但是,从中我们可以看出,它并没有提供良好的依赖管理的实现。
2、模块化模式
模块化的模式是在立即执行函数上更进了一步,我们将需要提供给第三方的变量return出来,如下:
// 将封装的模块赋值给一个变量
var helloApi = function(){
// 内部实现
function sayHello(){
console.log('Hello');
}
// 对外暴露API
return { sayHello: sayHello
}
}()
然后,我们可以按照如下方式来调用模块的API了:
helloApi.sayHello(); // Hello
以上,实现了简单的代码模块化。随着JavaScript模块化实践的不断推进,企业和社区冒出了很多不同的模块化定义和使用的库,如seaJS、moduleJS、requireJS等,各自语法大同小异,也各有优缺点。
模块化规范
模块化的规范定义了我们如何来写模块化的代码。其实,在ES6发布之前,JavaScript语言并没有推出官方的规范来确定模块化定义语法。上面我们提到了,JavaScript模块化实践的不断推进,企业和社区冒出了很多不同的模块化定义和使用的库,因此也有不同的规范版本。包括ES6等一些比较优秀的规范包括:
Asynchronous Module Definition (AMD)
Common Module Definition (CMD)
CommonJS
Universal Module Definition (UMD)
ES6 module format
我们挨个看看,每一个是如何来定义和实现模块化的。
1、Asynchronous Module Definition (AMD)
AMD 是 RequireJS 在推广过程中对模块定义的规范化产出的。看一下AMD的语法:
define(['./a', './b'], function(a, b) {
// 依赖必须一开始就写好
a.doSomething();
// 此处略去 100 行
b.doSomething();
...
})
2、Common Module Definition (CMD)
CMD是seaJS在推广过程中对模块化定义的规范产出的。与上面的例子类似,我们看看CMD的语法:
define(function(require, exports, module) {
var a = require('./a') ;
a.doSomething() ;
var b = require('./b');
// 依赖可以就近书写
b.doSomething();
})
3、CommonJS
CommonJS是在Nodejs中定义模块中使用非常广泛的规范,它使用require和module.exports定义模块的依赖和API导出,简单的例子如下:
var dep1 = require('./dep1');
var dep2 = require('./dep2');
module.exports = function(){
// ...
}
4、Universal Module Definition (UMD)
UMD基本上是统一了浏览器端和后端JS(Nodejs)的模块化定义,看下它的定义便可窥知:5、ES6 module
ES6的发布基本上第一次在JavaScript语言层面增加了对模块化的支持,它使用了import和export来定义依赖和模块导出。看一个简单的例子:
// hello.js
// 导出sayHello方法
export function sayHello(){
console.log('Hello');
}
// other.js
// 引入hello模块的sayHello方法
import { sayHello } from './hello';
sayHello(); // => Hello
模块加载器说白了,模块加载器来解释和加载模块化的代码。它的职责包括:
加载并解释主引导main.js或者app.js;
根据需要加载其它需要的模块化js文件。
国内用的比较多的包括requireJS、moduleJS、seaJS等。特别需要说明的一点是,模块加载器是执行在浏览器端,需要加载到浏览器里,在webapp运行阶段执行。
模块打包器模块化打包器或者说模块化打包工具的出现,基本上说可以替代了模块加载器,可以完全将模块加载器的事都给干了。但是,有一点与模块加载器不同的是,它运行在webapp构建阶段。它的特点如下:
在运行阶段,将各类依赖s文件构建为一个单独的js文件,如app.js或者build.js;
浏览器只需要加载build.js或者app.js这个单一文件即可。
所有的文件都在构建的时候打包为一个文件,浏览器端只加载这一个文件完全够了。比较火的模块打包工具包括:webpack和browserify等。
总结到这里,相信你对JavaScript的模块化的前世今生和关键的名词都比较了解了。这里给出各个概念的解释:
模块: 模块是指将一段功能代码的具体实现封装在单独的模块当中,并对外暴露出可供调用的API,从而方便其它模块加载和调用;
模块化规范:模块化规范是定义了如何定义和使用模块的语法;
模块加载器:模块加载器用来加载和解释由遵循特定规范定义的模块,要注意的是在浏览器端执行;
模块打包器:模块打包器用来在构建阶段替代模块加载器的工作并且构建出唯一的js文件的工具,要注意的是在构建阶段执行。