什么是模块化

前端模块化是指将一个大型的前端应用程序分解为小的、独立的模块,每个模块都有自己的功能和接口,可以被其他模块使用。

  • 前端模块化的出现是为了解决前端开发中代码复杂度和可维护性的问题。
  • 在前端模块化的架构下,开发人员可以更加专注于各自的模块开发,提高了代码的复用性和可维护性。

为什么需要模块化

在传统的前端开发中,所有的代码都是写在同一个文件中。

所有的代码都是写在同一个文件中的问题:

  • 可维护性差:当应用程序变得越来越大时,代码变得越来越难以维护。
  • 可重用性差:相同的代码可能会被多次复制和粘贴到不同的文件中,这样会导致代码冗余,增加了代码量。
  • 可测试性差:在传统的前端开发中,很难对代码进行单元测试。
  • 可扩展性差:在传统的前端开发中,很难对应用程序进行扩展。

模块化的发展历程

全局function模式

将不同功能封装成不同的函数。

1
2
3
4
5
6
function fetchData() {
...
}
function handleData() {
...
}

缺陷: 这个是将方法挂在 window 下,会污染全局命名空间容易引起命名冲突数据不安全等问题。

全局namespace模式

通过对象来封装模块。

1
2
3
4
5
6
7
8
let myModule = {
fetchData() {
...
},
handleData() {
...
}
};

缺陷: 这个方案确实减少了全局变量,解决命名冲突的问题,但是外部可以直接修改模块内部的数据

IIFE模式(通过自执行函数创建闭包)

通过传入依赖的方式来解决模块间引用的问题。

1
2
3
4
5
6
7
8
9
10
11
function(global) {
let data = 1

function fetchData() {
...
}
function handleData() {
...
}
window.myModule = {fetchData, handleData}
}(window)

缺陷:这个方案下,数据是私有的,外部只能通过暴露的方法操作,但无法解决模块间相互依赖问题

IIFE模式增强(传入自定义依赖)

通过传入依赖的方式来解决模块间引用的问题。

1
2
3
4
5
6
7
8
9
10
11
function(global, otherModule) {
let data = 1

function fetchData() {
...
}
function handleData() {
...
}
window.myModule = {fetchData, handleData, otherApi: otherModule.api}
}(window, window.other_module)

缺陷

  • 多依赖传入时,代码阅读困难
  • 无法支持大规模模块化开发
  • 无特定语法支持,代码简陋
  • 各种模块化标准

模块化规范

经过以上过程的演进,我们确实可以实现前端模块化开发了,但是仍然有几个问题:

  1. 一是请求过多,我们都是通过 script 标签来引入各个模块文件的,依赖多个模块,那样就会发送多个请求。
  2. 二是依赖模糊,很容易因为不了解模块之间的依赖关系导致加载先后顺序出错,模块之间的依赖关系比较难以管理,也没有明确的接口和规范。

因此模块化规范应运而生。

CommonJS

CommonJS 是一个 JavaScript 模块化规范,它最初是为了解决 JavaScript 在服务器端的模块化问题而提出的。是 NodeJS 的默认模块饭规范,该规范定义了模块的基本结构、模块的加载方式以及模块的导出和导入方式等内容。

模块基本结构

  • 在 CommonJS 规范中,一个模块就是一个文件。每个文件都是一个独立的模块,文件内部定义的变量、函数和类等 只在该文件内部有效
  • 每个模块都有自己的作用域,模块内部的变量、函数和类等只在该模块内部有效。如果想在其他模块中使用该模块内部的变量、函数和类等,需要将其导出

模块加载方式

在 CommonJS 规范中,模块的加载方式是同步的:当一个模块被引入时,会立即执行该模块内部的代码,并将该模块导出的内容返回给引入该模块的代码

  • 模块可以多次加载,第一次加载时会运行模块,模块输出结果会被 缓存,再次加载时,会从缓存结果中直接读取模块输出结果。
  • 模块 加载的顺序 ,按照其在 代码中出现的顺序
  • 模块输出的值是 值的拷贝,类似 IIFE 方案中的内部变量。
  • 这种同步加载方式可以保证模块内部的代码执行完毕后再执行外部代码,从而避免了异步加载所带来的一些问题。
  • 但同时也会影响页面加载速度,因此在浏览器端使用时需要注意。

模块导入和导出方式

在 CommonJS 规范中,一个模块可以通过module.exports 或者 exports 对象来导出内容。

  • module.exports 是真正的导出对象,而 exports 对象只是对 module.exports 的一个引用。
  • 一个模块可以导出多个内容,可以通过 module.exports 或者 exports 对象分别导出。

注意: exports是指向module.exports的引用,module.exports的初始值是一个空对象{},require()返回的是module.exports而不是exports。所以一旦module.exports有了新的引用,exports就和module.exports失去联系,导不出了。

相当于是:

1
2
3
4
let module = {
exports:{}
}
let exports = module.exports
  • exports 对象是 module 对象的一个属性,在初始时 module.exportsexports 指向同一块内存区域
  • 模块导出的是 module.exports , exports 只是对它的引用,在不改变exports 内存的情况下,修改exports 的值可以改变 module.exports 的值
  • 导出时尽量使用 module.exports ,以免因为各种赋值导致的混乱

导出:

1
2
3
4
5
6
7
// 导出一个变量
module.exports.name = 'Tom';

// 导出一个函数
exports.sayHello = function() {
console.log('Hello!');
};

导入: 在另一个模块中,可以通过 require 函数来引入其他模块(是个对象) ,并访问其导出的内容。

1
2
3
4
5
6
7
8
// 引入其他模块
let moduleA = require('./moduleA');

// 访问其他模块导出的变量
console.log(moduleA.name);

// 访问其他模块导出的函数
moduleA.sayHello();

特点

  • CommonJS 模块由 JS 运行时实现
  • CommonJS 模块输出的是值的拷贝本质上导出的就是 exports 属性
  • CommonJS 是可以动态加载的,对每一个加载都存在缓存,可以有效的解决循环引用问题
  • CommonJS 模块同步 加载并执行模块文件

ES6模块化

ESModule 模块化规范是一种静态的模块化方案,它允许开发者将代码分割成小的、独立的模块,每个模块都有自己的作用域。ESModule 规范是基于文件的,每个文件都是一个独立的模块

ESModule 的模块解析规则是基于 URL 解析规则的。当我们使用 import 语句导入一个模块时,模块加载器会根据 import 语句中指定的路径解析出对应的 URL,并将其作为唯一标识符来加载对应的模块文件。

  • 浏览器中,URL 解析规则是基于当前页面的 URL 进行解析
  • Node.js 中,URL 解析规则是基于当前运行脚本的路径进行解析

模块加载方式

  • 在浏览器中,可以使用<script type="module">标签来加载 ESModule 模块
1
2
<!-- 在浏览器中加载ESModule模块 -->
<script type="module" src="./module.js"></script>
  • 在 Node.js 中,可以使用 import 关键字来加载 ESModule 模块
1
2
// 在Node.js中加载ESModule模块
import { name } from './module';

模块导入和导出方式

命名导出和命名导入

命名导出和命名导入是最常见的一种方式。

可以将多个变量或者函数命名导出,也可以将多个变量或者函数命名导入:

1
2
3
4
5
6
7
8
// module.js
export const name = '张三';
export function sayHello() {
console.log('Hello');
}

// app.js
import { name, sayHello } from './module';

默认导出和默认导入

默认导出和默认导入是一种简单的方式。

可以将一个变量或者函数作为默认导出,也可以将一个变量或者函数作为默认导入:(默认导出,在导入时不需要考虑导出的名字

1
2
3
4
5
// module.js
export default 'Hello World';

// app.js
import message from './module';

混合命名和默认导出

混合命名和默认导出也是一种常见的方式。

可以将多个变量或者函数命名导出,同时将一个变量或者函数作为默认导出:

1
2
3
4
5
6
7
8
9
// module.js
export const name = '张三';
export function sayHello() {
console.log('Hello');
}
export default 'Hello World';

// app.js
import message, { name, sayHello } from './module';

特点

  • ES6 Module 静态 不能放在块级作用域内代码发生在编译时
  • ES6 模块输出的是值的引用如果一个模块修改了另一个模块导出的值,那么这个修改会影响到原始模块
  • ES6 Module 可以导出多个属性和方法,可以单个导入导出,混合导入导出
  • ES6 模块提前加载并执行 模块文件

AMD

AMD 是 Asynchronous Module Definition 的缩写,即异步模块定义。

它是由 RequireJS 的作者 James Burke 提出的一种模块化规范。

AMD 规范的主要特点是:异步加载、提前执行

AMD 模式可以用于浏览器环境,并且允许非同步加载模块,也可以根据需要动态加载模块

语法: define(id?, dependencies?, factory)

  • id:可选参数,表示模块标识符一般为字符串类型
  • dependencies:可选参数,表示当前模块所依赖的其他模块。它是一个数组类型,每个元素表示一个依赖模块的标识符。
  • factory:必需参数,表示当前模块的工厂函数。它是一个函数类型用于定义当前模块的行为

导出:

1
2
3
4
5
6
define('module1', ['module2', 'module3'], function(module2, module3) {
// 模块1的代码逻辑
return {
// 暴露给外部的接口
};
});

导入: AMD 规范采用异步加载方式,它通过require函数来加载一个或多个模块。require函数接受一个数组类型的参数,每个元素表示一个待加载的模块标识符。当所有依赖模块加载完成后, require函数才会执行回调函数

1
2
3
require(['module1', 'module2'], function(module1, module2) {
// 所有依赖模块加载完成后执行的回调函数
});

CMD

CMD 是 Common Module Definition 的缩写,即通用模块定义。

CMD 规范的主要特点是:按需加载、延迟执行

CMD 规范专门用于浏览器端模块的加载是异步的模块使用时才会加载执行。CMD 规范整合了 CommonJS 和 AMD 规范的特点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//定义没有依赖的模块
define(function(require, exports, module){
exports.xxx = value
module.exports = value
})
//定义有依赖的模块
define(function(require, exports, module){
//引入依赖模块(同步)
const module2 = require('./module2')
//引入依赖模块(异步)
require.async('./module3', function (m3) {
})
//暴露模块
exports.xxx = value
})

// 引入该模块
define(function (require) {
const m1 = require('./module1')
const m4 = require('./module4')
m1.show()
m4.show()
})

UMD

UMD 是一种 JavaScript 通用模块定义规范,让模块能在 JavaScript 所有运行环境中发挥作用。

2014 年 9 月,美籍华裔 Homa Wong 提交了 UMD 第一个版本的代码。UMD 即 Universal Module Definition 的缩写,它本质上并不是一个真正的模块化方案,而是将 CommonJS 和 AMD 相结合。

  1. 优先判断是否存在 exports 方法,如果存在,则采用 CommonJS 方式加载模块;

  2. 其次判断是否存在 define 方法,如果存在,则采用 AMD 方式加载模块;

  3. 最后判断 global 对象上是否定义了所需依赖,如果存在,则直接使用;反之,则抛出异常。