模块化开发 —— 当下最重要的前端开发范式之一
# 背景
随着前端应用的日益复杂,我们的项目代码已经逐渐膨胀到了不得不花大量时间去管理的程度,而模块化就是最主流的代码组织方式。它通过将复杂代码按照功能的不同,去划分不同的模块单独维护的这种方式,去提高开发效率,降低维护成本
模块化只是一种思想,一种开发的理论,并不是一个具体的实现方式
# 模块化的演进过程
早期的前端标准,根本没有预料到前端行业会有今天的规模,所以有很多设计上的遗留问题,就导致了实现前端模块化会有一些问题,虽然说现如今这些问题都已经被一些工具或标准解决了,但是解决这些问题的过程我们需要了解。
# Stage 1 - 基于文件划分方式
最早期的js模块化是基于文件划分的方式去实现的,这也是web中最原始的模块系统 *
- 约定一个js文件就是一个模块,然后通过script标签引入到html中,一个script就对应一个模块
- 在代码中直接调用模块中的全局成员,这个成员可能是一个变量也可能是一个函数
这种方式的缺点:
- 所有的模块都在全局范围工作,没有一个独立的私有空间,也就造成了 ——
全局作用域污染
(即模块内部所有的成员都可以在外部任意访问) - 同时模块过多时容易产生
命名冲突
问题 - 无法管理模块与模块之间的依赖关系
总的来说这种方式完全依赖于约定,项目体量一旦大起来,就很麻烦
# Stage 2 - 命名空间方式
- 约定每一个模块指暴露一个全局的对象,所有的模块成员,都挂载在这个全局对象下
- 这种方式的优点:可以减小命名冲突的可能;缺点:依然没有私有空间,也没有解决依赖关系
# Stage 3 IIFE
使用IIFE(立即执行函数),为模块提供私有空间
- 具体做法就是将模块中每一个成员,都放在一个函数中,对于需要暴露给外部的成员,可以通过window.XXX挂载到全局对象上
- 这种方式的优点
- 实现了私有成员,私用成员只能通过闭包的形式访问,外部无法访问
- 还可以利用自执行函数的参数, 作为依赖声明去使用,例如
;(function ($) { })(JQuery)
上述方案就是早期前端开发者在没有工具和规范的情况下对模块化的落地实现方式,这些方式确实解决了各种各样的问题, 但是仍然存在一些没有解决的问题, 针对这些问题, 下面单独细说。
# 模块化规范
上面的方式都是以原始的模块系统为基础,通过约定的方式去实现模块化的代码组织,这些方式在不同的开发者去实施的时候会有一些细微的差别,为了统一不同开发者和不同项目之间的差异,就需要一些标准去规范模块化的实现方式;另外,在模块化当中针对模块加载的问题,在上面几种中都是通过script标签,手动的去引入需要使用的模块,也就是说模块的加载并不受代码的控制。
例如,项目中依赖一个模块,但是在html中忘记引用该模块,就会出问题,又或者说我们移除了一个模块,但是在html中又忘了删除,也会出问题。
因此我们需要一个基础的代码,去实现通过代码自动的加载模块,也就是需要:模块化标准
+ 模块加载器
# CommonJS规范
提到模块化首先想到的就是CommonJS规范,它是Node提出的一个模块化标准,Node中的所有模块代码,都必须遵循CommonJS规范。
它的约定如下:
- 一个文件就是一个模块
- 每个模块都有单独的作用域
- 通过module.exports导出成员
- 通过require函数载入模块
但是如果想在浏览器端也使用这个规范会出现一些问题
CommonJS约定是以同步的方式加载模块,因为Node的机制就是在启动的时候加载模块,执行过程中是不需要加载的,只会去使用。所以这样的方式在node中没有问题,但是在浏览器端CommonJS规范,每次页面加载都会导致大量的同步模式请求出现,所以在在早期的浏览器前端模块化中并没有选择CommonJS规范,而是在CommonJS的基础上结合浏览器的特性,重新设计了一个规范,叫做AMD(Asynchronous Module Definition),也就是异步的模块定义规范。
# AMD
# 使用(Require.js)
在AMD规范出现后,为它设计了一个库,它实现了AMD规范;另外其本身又是一个非常强大的模块加载器
- define
AMD中定义所有的模块都要通过define
这个函数去定义,默认接收两个参数,也可以传递3个参数,如下面这个例子。
// 定义模块
// 传递3个参数时
/**
* @param {string/number} 模块的名字,可以后续在加载模块时使用
* @param {array} 用来声明模块的依赖性
* @param {function} 为当前模块提供一个私有的空间,其参数就是上面数组传入的依赖项
* @return {object} 通过return导出成员
*/
define("module1", ["jquery", "./module2"], function($, module2) {
return {
start: function() {
$("body").animate({marin: "200px"})
module2()
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- require
可以通过require
去载入一个模块,一旦其需要加载模块,内部就会创建一个script标签,去发送对应的脚本文件的请求,并且执行相应的模块代码
// 载入模块
require(['./module1'], function(module1) {
module1.start()
})
2
3
4
# AMD缺点
目前绝大多数第三方库都支持AMD规范,它的生态比较好,但是它也有很多缺点:
- AMD使用起来相对复杂
- 模块划分的过于细致,会造成模块JS文件请求频繁,导致页面效率比较低下
因此AMD只能算是一种妥协的方式,并不能算是一种最终的解决方案,但是在当时的环境下,它还是非常有意义的,它毕竟给前端模块化提供了标准。
# Sea.js + CMD
CMD规范是淘宝提出的,借鉴了AMD类型的模块化规范,提出就是为了让CMD写出的代码尽量跟CommonJS类似,现在已经被AMD所兼容了。
define(function (require, exports, module) {
// 通过require引入依赖
var $ = require("jquery")
// 通过exports或者module.exports对外层暴露成员
module.exports = function () {
console.log("module 2 ~")
$("body").append("<p>module2</p>")
}
})
2
3
4
5
6
7
8
9
# 模块化标准规范
随着技术的发展,JavaScript模块化的标准也在逐渐完善,现在的前端模块化已经非常成熟了,并且针对前端模块化也已经非常的统一,就是在Node.js环境中,使用CommonJs
规范,在浏览器中使用ES Modules
ES Modules是ES2015提出的新标准,也就是说它比较新,并且可能有一些环境兼容问题,最早这个标准刚出的时候,当时的主要浏览器都不支持这一特性,都是随着webpack这些打包工具的流行,它也被各大浏览器厂商逐渐接受,并且它比社区规范更加完善,因为他在语言层面实现了模块化, 同时现如今的大部分浏览器都是原生就支持ES Modules, 意味着以后也许可以直接使用这个特性开发网页应用。