模块化导入的背景与需求
代码可维护性与复用性
随着前端应用程序日益复杂,模块化成为提升代码组织、可维护性和复用性的关键技术。模块化允许我们开发者可以将代码分割成独立的、可管理的部分,每个部分负责特定的功能。这不仅提高了代码的可读性和可维护性,还促进了团队协和,减少了代码冲突的可能性。
全局明明空间污染问题
在模块化出现之前,JavaScript文件通过<script> 标签直接引入,所有变量和函数都暴露在全局命名空间中。这种方式简单直接,但随着项目规模的扩大,容易引起命名冲突导致代码维护困难。模块化通过创建私有作用域,封装模块内部的变量和函数,避免了全局命名空间的污染,减少了冲突。
依赖管理的挑战
在复杂的前端项目中,模块之间的依赖关系往往非常复杂。手动管理这些依赖关系不仅容易出错,还会导致代码难以维护。模块化通过明确的导入和导出机制,使得依赖关系清晰可见。自动化工具可以更有效的处理依赖关系,提升代码的可维护性和可靠性。
背景总结
模块化说到底就是解决了因代码量的增多,而引发的代码维护性和可读性下降等问题。有了模块化之后,开发工作能够按照不同模块分别开展,而且依赖管理相关的方法也可被抽取出来实现复用,可以提高开发效率和代码质量,让整个前端项目结构清晰,开发和维护更加有序、高效。
主要的模块系统
CommonJS(CJS)
CommonJS(CJS) 是一种同步加载模块的规范,最早由服务器端(Node.js)开发者提出。CJS 是为了让 Node 可以有类似其他后端语言如 Java、Python 的模块系统。使代码组织和复用更加便捷。CJS 的出现主要是为了解决服务器端模块化需求,特别是在 Node.js 中实现高效的模块加载和管理。
使用方法
// math.cjs
function add(a, b) {
return a + b;
};
module.exports = { add }; // 模块导出使用 module.exports 或 exports 对象
// app.cjs
const math = require('./math.cjs'); // 同步加载:模块在运行时通过 require 函数同步加载,适合服务器端环境
console.log(math.add(2, 3)) // 输出 5
主要应用场景
- Node.js 环境: CJS 是 Node.js 默认的模块系统,几乎所有的 Node.js 模块(尤其是早期模块)都采用 CJS。
- 旧的前端项目: 一些早期采用模块化的前端项目仍在使用 CJS,尤其是通过打包工具(如 Browserify)进行模块打包的项目
Asynchronous Module Definition(AMD)
Asynchronous Module Definition(AMD) 是一种异步加载模块的规范,主要为了解决浏览器环境中模块加载的性能问题。AMD 由 RequireJS 等库广泛应用,就是为了通过异步加载模块,优化前端应用的性能和用户体验。AMD 的出现填补了浏览器端模块化的空白,使得前端开发能够更加模块化和高效。
使用方法
// index.js
require.config({
baseUrl: 'js/lib',
paths: {
lodash: 'lodash.min.js' // 实际路径 js/lib/lodash.min.js
}
})
// math.js
define(['lodash'], function (_) { // 引入 lodash 依赖
function add(a, b) {
return _.add(6, 4); // 使用 lodash 依赖
}
return { add }
})
require(['math'], function (math) {
console.log(math.add(2, 3))
})
主要应用场景
旧的浏览器项目: 在 RequireJS 等库的支持下,AMD 被广泛应用于需要异步加载模块的浏览器项目。
Universal Module Definition(UMD)
Universal Module Definition(UMD) 是一种兼容多种模块系统的规范,为了在使模块能够在不同环境(浏览器环境、Node 环境)中通用。UMD 通过检测当前还进个,选择合适的模块加载方式(如 CJS、AMD 或全局变量),提高了模块的通用性和兼容性。UMD 的出现使为了解决跨环境模块化需求,尤其是在构建需要再多种环境运行的库和框架时提供便利。
使用方法
// 自动检测运行环境,根据当前运行环境选择合适的模块加载方式
// math.js
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD 环境
define(['lodash'], factory);
} else if (typeof module === 'object' && module.exports) {
// CommonJS 环境
module.exports = factory(require('lodash'));
} else {
// 浏览器全局环境
root.math = factory(root._);
}
}(typeof self !== 'undefined' ? self : this, function (lodash) {
function add(a, b) {
return lodash.add(a, b);
}
return {
add: add
};
}));
AMD 环境中使用
// main.js
require.config({
paths: {
'lodash': 'lib/lodash.min'
}
});
require(['math'], function(math) {
console.log(math.add(2, 3)); // 输出: 5
});
CommonJS 环境中使用
// main.js
const math = require('./math');
const _ = require('lodash');
console.log(math.add(2, 3)); // 输出: 5
浏览器环境中使用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>UMD Example with Lodash</title>
<script src="lib/lodash.min.js"></script>
<script src="math.js"></script>
</head>
<body>
<h1>UMD Example with Lodash</h1>
<script>
console.log(math.add(2, 3)); // 输出: 5
</script>
</body>
</html>
主要应用场景
- 跨环境库和框架: UMD被涉及为兼容多种模块系统,使得库和框架能够在浏览器和 Node.js 环境中无缝运行
- 需要在浏览器和 Node.js 中通用的库: 例如,许多第三方库(如 Underscore ) 采用 UMD,以确保在各种环境中均可使用
ECMAScript Modules(ESM)
ECMAScript Modules(ESM) 是 JavaScript 官方标准的模块化系统,于 ES6(ECMAScript2015) 中正式引入。ESM 为了统一前端和后端的模块化方式,支持静态分析和现代优化技术,如 tree shaking 和代码分割。ESM 的出现是为了取代 CJS 和 AMD,提供一个更现代化、标准化的模块系统,适应日益复杂的前端开发需求
使用方法
// math.js
import _ from 'lodash';
export function add(a, b) {
return _.add(a, b);
}
// main.js
import { add } from './math.js';
console.log(add(2, 3)); // 输出: 5
主要应用场景
- 现代前端项目: 几乎所有现代前端框架(如 React、Vue、Angular)。
- 支持 ESM 的构建工具: 如 Webpack、Rollup、Vite 都全面支持 ESM。
- Node.js 环境: 从 Node v8 支持 ESM,允许在服务器端使用 ESM 语法。
CommonJS 与 ESM 的深入对比
引入方式的不同
上面已经介绍过了,就不赘述了,一个require一个import
不同的加载方式
-
CJS 的同步加载
- CJS 使用
requier进行模块加载,这种方式是同步的,即在模块加载完成之前,程序会阻塞执行(但得益于服务端读取文件的快速,可以忽略) - 适用于服务端,如 Node.js,因为同步加载效率较高。
- CJS 使用
-
ESM 的异步加载
- ESM 使用
import语句进行模块加载,设计为异步,以避免阻塞主线程,特别适合浏览器环境。 - 支持动态导入
import(),返回一个 Promise,允许在运行时异步加载模块。 - 在 Node.js 中,ESM 也是异步加载的。
- ESM 使用
-
打包工具的影响
- 打包工具如 Webpack 或 Rollup 在构建时同步处理 ESM 模块,生成一个或几个打包文件
- 虽然打包文件可能在浏览器中同步加载,但模块内部的 ESM 行为仍然时异步的。
- 打包工具优化模块依赖,提高交付效率,但不改变 ESM 的异步本质。
-
模块解析与优化
- CJS 基于文件路径解析模块,适合同步环境。
- ESM 基于 URL 解析模块,适合异步环境,并且其静态的
import和export语句使得 tree shaking 更有效。
值的引用和值的拷贝
- CommonJS:导入的的是值的拷贝(Copy)。
- ESM:导入的是值的引用
CommonJS的值拷贝行为
特点
- 使用
require导入模块 - 导入的变量(如数字、字符串)是拷贝,不会随着模块内部的值的变化而更新。
- 如果导入的是对象或数组等引用类型,它们的引用会保持同步
示例 1:导入基本类型(拷贝行为)
// cjsModule.js
// 定义一个变量和一个修改函数
let count = 0;
function increamentCount() {
count++;
}
module.exports = {
count,
incrementCount
}
// cjsTest.js
// 导入模块
const { count, incrementCount } = requier('./cjsModule');
// 初始值
console.log(‘初始值’, count) // 输出 0
// 调用模块的函数修改 count
incrementCount();
// 再次查看 count
console.log('更新后的count', count); // 输出 0(未更新)
分析
count是一个数字(基本类型),被拷贝到使用方。incrementCount修改了模块内部的count,但导入方的count不会随之更新,因为他是一个拷贝的值。
示例 2:导入引用类型(保持同步)
// cjsModule.js
// 定义一个对象和一个修改函数
const counter = { value: 0 };
function incrementCounter() {
counter.value++
}
module.export = {
counter,
incrementCounter
}
// cjsTest.js
// 导入模块
const { counter, incrementCounter } = require('./cjsModule');
// 初始值
console.log('Initial counter value:', counter.value); // 输出 0
// 调用模块的函数修改 counter
incrementCounter();
// 再次查看 counter.value
console.log('Updated counter value:', counter.value); // 输出 1(已更新!)
分析
counter是一个对象(引用类型),导入方和模块内部共享同一个引用。- 当模块内的
counter.value被修改时,导入方可以立即反应变化
ESM的引用行为
特点
- 使用
import导入模块 - 导入的所有导出值,无论是基本类型还是引用类型,都是引用。
- 模块的任何修改都会实时反映在导入方。
示例 1:导入基本类型(引用行为)
// esmModule.js
// 定义一个变量和一个修改函数
export let count = 0;
export function incrementCount() {
count++;
}
// esmTest.js
// 导入模块
import { count, incrementCount } from './esmModule.js';
// 初始值
console.log('Initial count:', count); // 输出 0
// 调用模块的函数修改 count
incrementCount();
// 再次查看 count
console.log('Updated count:', count); // 输出 1(已更新!)
分析
count是一个引用,直接指向模块内部的变量incrementCount修改模块的count值,导入方的count立即反映变化
示例 2:导入引用类型(保持同步)
// esmModule.js
// 定义一个对象和一个修改函数
export const counter = { value: 0 };
export function incrementCounter() {
counter.value++;
}
// esmTest.js
// 导入模块
import { counter, incrementCounter } from './esmModule.js';
// 初始值
console.log('Initial counter value:', counter.value); // 输出 0
// 调用模块的函数修改 counter
incrementCounter();
// 再次查看 counter.value
console.log('Updated counter value:', counter.value); // 输出 1(已更新!)
分析
- 和 CommonJS 一样,引用类型(如对象)的导入保持同步。
- 但不同的是,ESM 对所有导出都是引用,包括基本类型。
总结
| 特性 | CommonJS(CJS) | ECMAScript Module(ESM) |
|---|---|---|
| 导入语法 | const mod = require('./mode') | import mode from './mod' |
| 导出语法 | module.exports = { foo }; 或 exports.foo = foo; | export default foo 或 export const foo = foo |
| **** | ||
| 加载时机 | 运行时加载: 依赖是在代码执行时动态解析的 | 编译时加载: 依赖关系在编译阶段就被解析 |
| 导入类型 | 值拷贝(基本类型),引用(引用类型) | 引用,所有导入均实时同步 |
| 是否支持 Tree-shaking | 不支持:所有导出的代码都会被引入 | 支持:未使用的导出代码可以被打包工具剔除 |
| 兼容性 | Node.js 原生支持 | Node.js 12+ 和现代浏览器原生支持 |
| 动态导入 | 动态导入可以直接用require | 支持通过import()实现动态导入 |
| 异步加载 | 不支持: require是同步加载 | 支持: 通过import()可以实现异步加载 |
| 作用域 | 模块内是独立作用域 | 模块内是独立作用域 |
| 适用场景 | Node.js 后端开发,老旧工具链 | 前端开发,现代工具链,浏览器环境 |
| 输出文件结构 | module.exports 是整个导出的对象 | 多个命名导入和默认导出,静态结构 |
| 性能 | 模块加载时同步的,适合后端 | 静态加载 + 异步支持,适合现代前端需求 |
ESM一统江湖
在现代 JavaScript 的江湖里,ESM(ECMAScript Module)早已挑起了“一统江湖”的大梁,从前端到后端,ESM 正以迅雷不及掩耳盗铃之势的速度接管模块化的地位,逐步取代CommonJS(CJS)。
- 标准出身,根正苗红,ESM 是 ECMAScript 的官方标准,由 TC39 提出,从 ES6 开始引入,正统性无可置疑
- 原生支持,现代浏览器原生支持
<script type="module"> - Node.js 自 12+ 版本全面支持,通过
.mjs或者package.json中"type": "module"声明 - ESM 的静态加载特性让打包工具(如 Rollup、Webpack)能够在编译阶段解析模块依赖关系。
- 支持 Tree-Shaking,未使用的代码可以被打包工具剔除,生成更小的产物。
- 支持 import() 动态导入,适合懒加载和按需加载场景。
- 前后端统一模块规范,减少开发的心智负担。
主要也是面试的时候被问过几次,就看了一下,如果文中有不对的地方,欢迎指正