1 模块化出现的背景
在 Web 发展的早期,前端还没有作为一个单独的工种分离出来,JavaScript 的作用也只是在 HTML 上 用来实现简单的表单验证,到后来无非实现一些轮播图等简单的视觉效果。一句话来讲,很长时间以来我们的前端很简单,JS 代码也很少。
随着 Web 的发展,Web 应用越来越复杂,诸如淘宝、京东、美团等 Web 应用,其复杂度是极高的。前端的交互、视觉效果、逻辑控制需要编写大量的 JS 代码。前端急需使用工程化的方式来降低 Web 开发和维护成本,对 JS 模块化开发的需求越来越迫切。
2 模块化的意义
1、避免命名冲突。模块化开发中,每个文件是一个模块,模块中定义的变量、函数、类都是该模块的私有变量,不会污染全局变量。同样也不会被全局变量污染。
2、更清晰的依赖关系。在使用或定义一个模块的时候,会显式的声明对模块的引用,文件直接的依赖关系非常清晰,结合 Webpack 等构建工具,更是无需手动处理复杂的依赖关系。
3、高可维护性。每个模块的功能职责单一,需要升级改动部分功能只需要针对具体的模块进行改动,大大提高可维护性。
4、高可复用性。每个功能模块实现后,可以供开发者重复使用,开发者只需要关注本身的业务逻辑的开发,大大提高开发效率。
5、降低复杂度。通过将复杂的应用拆解成多个容易实现的模块,模块化可以降低项目本身的复杂度。
3 主流的模块化规范
随着前端的不断发展演进,目前出现了 CommonJS、AMD、CMD、ES6 module 等几种主要的模块化规范。我们分别进行介绍。
3.1 CommonJS CommonJS 规范指在非浏览器端定义一套通用的 JS 模块化规范。CommonJS 规范的应用目前也主要是在服务器端和桌面端应用。
CommonJS 规范中,每个文件就是一个模块,每个模块中定义的变量,函数,类等都是私有的,对外不可见。
CommonJS 规范中,通过 require 方法来加载依赖的模块,通过 exports 或者 module.exports 来导出模块。
例如我们定义一个 calc 模块:
// ./calc.js
const { PI } = Math;
exports.area = (r) => PI * r ** 2;
exports.circumference = (r) => 2 * PI * r;
我们在 calc.js 中定义了 area 方法用来计算圆的面积,定义了 circumference 方法用来计算圆的周长。两个方法定义在了 exports 对象上,对外暴露,模块内部的 PI 常量从是无法访问到的。
在 main.js 中引入 calc 模块:
// ./main.js
const calc = require('./calc.js')
const r = 10;
console.log(calc.area(r))
console.log(calc.circumference(r))
// 方式二
// const {area, circumference} = require('./calc.js')
// console.log(area(r))
// console.log(circumference(r))
在 main.js 中,通过 require 方法引入 calc 模块。调用 calc 模块中的方法进行计算。两个模块之间相互独立,依赖关系通过 require 方法体现。
适用场景 CommonJS 规范中模块的加载采取同步加载的方式,比较适用于 Node.js 端。因为服务端的文件都是存在本地的,加载起来很快。
浏览器环境下,所有的资源都需要从服务器下载后才能使用。同步加载会阻塞 JS 的运行。所以 CommonJS 在浏览器端不太适合。
3.2 AMD 鉴于浏览器的特殊情况,随后又衍生了 AMD(异步模块定义)规范。AMD 通过异步的方式加载依赖的模块,依赖的模块需要通过一个数组指明,模块以异步的方式并行加载。
AMD 模块的定义通过 define 这个全局包裹函数来实现。模块的引用通过全局方法 require 来加载。RequireJS 是 AMD 的实现。我们以 RequireJS 为例来看一下 AMD 模块的使用。
定义没有外部依赖的模块:
// ./calc.js
define(function () {
const { PI } = Math;
const area = (r) => PI * r ** 2;
const circumference = (r) => 2 * PI * r;
return {area, circumference}
});
calc 模块不依赖任何外部模块,因此只需要通过在回调函数中,定义模块的逻辑即可。需要使用 return 将对外暴露的方法返回出来。
定义有依赖的模块:
// ./setBodyColor.js
define(['jquery'], function ($) {
const setBodyColor = color => {
$('body').css('background-color', color)
}
return setBodyColor
});
如果模块依赖外部模块,需要在依赖数组中指出依赖的模块。这里指定的是模块的名称,具体的文件路径需要在 require.config 中进行配置。
在入口文件中加载使用模块:
// ./main.js
require.config({
baseUrl: '/',
paths: {
setBodyColor: './setBodyColor',
jquery: './jquery-3.4.0'
}
})
require(['calc','setBodyColor'], function (calc, setBodyColor) {
const r = 10;
const c = '#000';
console.log(calc.area(r));
console.log(calc.circumference(r));
setBodyColor(c);
});
在入口文件中,我们通过 require.config 指定模块的路径映射,此外我们使用到了 calc 和 setBodyColor,我们通过 require 方法指定对他们的依赖,我们是在模块加载完成后才去执行回调函数中的代码。
适用场景 AMD 在浏览器环境中异步并行加载依赖模块,并且可以有效地管理文件依赖关系和加载顺序。适合在浏览器场景使用,当然其初心也是聚焦浏览器。但是 AMD 在书写上多了多余的包裹函数,在代码的阅读和书写上相对繁琐。