前端模块化的前世今生

本博客 hjy-xh,转载请申明出处

近年来 Web 应用变得更加复杂与庞大,Web 前端技术的应用范围也更加广泛。通过直接编写 JavaScript、CSS、HTML 开发 Web 应用的方式已经无法应对当前 Web 应用的发展。

站在巨人的肩膀往回看,好在 JavaScript 模块化出现,解决了前端痛点,并且推动前端工程化。

那什么是模块化呢? 为什么需要它呢?

模块化可以理解成是将一个复杂的系统分解为多个模块以方便组织和编码。

很久以前,网页开发要通过命名空间的方式来组织代码。像 jQuery 就是将他的 API 都放在 window.$ 下,在加载完 jQuery 后,其它模块再通过 window.$ 去使用。

那这么做会有一些问题,其中包括:

  • 命名空间冲突
  • 无法合理地管理项目地依赖和版本
  • 无法方便地控制依赖的加载顺序

可以预见,当项目越大,维护成本也越高,因此用模块化的思想来组织代码。

回顾前端模块化的前世今生,按时间先后可以总结为以下几个过程

  • 刀耕火种时代
  • CommonJS 模块规范及在 Node.js 里的实现
  • AMD 异步模块定义
  • ESM ECMAScript 模块系统

刀耕火种时代

我们都知道 HTML 的 <script> 元素用于嵌入或引用可执行脚本。

在互联网早期,Web1.0 时代,页面比较简单,大多时候只是展示内容,使用内嵌的方式或者引用单个 JavaScript 文件就可以满足业务需求。

当功能变得复杂时,单个 JavaScript 文件代码量也变多,此时可以将 JavaScript 分为多个文件,但需要处理好各个 <script> 标签的书写顺序。

这个时期针对 JavaScript 源码的组织,谈不上模块化。即便采用了文件拼接(concat)这样的处理技术,其先后顺序也需要人工维护。

CommonJS 模块规范及在 Node.js 里的实现

这是一种被广泛使用的 JavaScript 模块化规范,其核心思想是通过require方法来同步加载依赖的其他模块,通过module.export导出需要暴露的接口。

它的流行得益于 Node.js 采用了这种方法。

它的优点在于:

  • 代码可复用于 Node.js 环境下并运行
  • 有很多遵循此规范的 Npm 包

它的缺点在于:

  • 代码无法直接运行在浏览器环境下,需要通过工具转换成标准的 ES5

CommonJS 还可以细分为 CommonJS1 和 CommonJS2,区别在于 CommonJS1 只能通过export.xx = xx的方式导出,而 CommonJS2 在 CommonJS1 的基础上加入了module.export = xx的导出方式。CommonJS 通常指 CommonJS2。

Node 应用由模块组成,采用 CommonJS 模块规范。每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。

Node.js 在解析与执行每个模块之前,会先加上一层包装,类似于:

1
2
3
(function (exports, require, module, __filename, __dirname) {
// 模块代码...
});

所以,模块作用域实际上是一个函数作用域;而 __dirname__filenamerequiremodule 等模块常量/变量都是外部传入的参数。

AMD 异步模块定义

AMD(Asynchronous Module Definition)即异步模块定义。AMD 规范中,各个依赖可以异步加载而不影响正常逻辑,非常适用于浏览器环境。AMD 规范的核心 API 只有一个简单的 define()函数。

AMD 模块系统的经典实现库是 require.js。

例子:

1
2
3
define(id?: String, dependencies?: String[], factory: Function|Object);

define(function(require, exports, module) {})

id 是模块的名字,它是可选的参数。

dependencies 指定了所要依赖的模块列表,它是一个数组,也是可选的参数,每个依赖的模块的输出将作为参数一次传入 factory 中。如果没有指定 dependencies,那么它的默认值是 ["require", "exports", "module"]

factory 是最后一个参数,它包裹了模块的具体实现,它是一个函数或者对象。如果是函数,那么它的返回值就是模块的输出接口或值。

AMD 规范平时用得很少,这里举一个例子:

1
2
3
4
5
6
define("myModule", ["jquery"], function ($) {
// $ 是 jquery 模块的输出
$("body").text("hello world");
});
// 使用
require(["myModule"], function (myModule) {});

这里我们再来看看它的优点:

  • 可在不转换代码的情况下直接在浏览器中运行
  • 可异步加载依赖
  • 可并行加载多个依赖
  • 代码可运行在浏览器环境和 Node.js 环境下

AMD 的缺点在于 JavaScript 运行环境没有原生支持 AMD,需要先导入实现了 AMD 的库后才能正常使用。

ESM ECMAScript 模块系统

前面所提到的所有模块化解决方案,都是利用 JavaScript 语言本身的特性,实现的封装。而鉴于模块系统的重要性、必要性,TC39 委员会也对其标准化极为上心。2015 年推出的 ECMAScript 6 标准正式定义了 JavaScript 的模块系统。

需要记住的是:

  • 它在语言层面上实现了模块化。
  • 浏览器厂商和 Node.js 都宣布要原生支持该规范。它将逐渐取代 CommonJS、AMD 规范,成为浏览器和服务器通用的模块解决方法

它的工作原理是模块文件只加载、执行一次。

虽然 ES6 模块是终极模块化方案,但它目前无法直接运行在大部分 JavaScript 运行环境下,必须通过工具转换成标准的 ES5 后才能正常运行。