07 模块
模块
在 JavaScript 中,模块化是一种将代码组织成独立、可重用的单元的方法,有助于提高代码的可维护性和可读性。ES6(ECMAScript 2015)引入了原生的模块支持,但在此之前,社区已经发展出了多种模块化模式,如 CommonJS 和 AMD。以下是 JavaScript 模块化的一些主要方式及其特点。
1. ES6 模块(ECMAScript 模块)
ES6 模块是 JavaScript 的官方模块系统,提供了 import 和 export 语法来导入和导出模块。
导出模块(export)
- 命名导出:可以导出多个值(变量、函数、类等)。
1
2
3
4
5
6
7
8
9// mathUtils.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export class Calculator {
constructor() {}
add(a, b) {
return a + b;
}
} - 默认导出:每个模块只能有一个默认导出。
1
2
3
4
5
6
7// mainCalculator.js
export default class MainCalculator {
constructor() {}
add(a, b) {
return a + b;
}
}
导入模块(import)
- 导入命名导出:需要使用与导出时相同的名称。
1
2
3
4
5
6// app.js
import { add, subtract, Calculator } from './mathUtils.js';
console.log(add(2, 3)); // 5
console.log(subtract(5, 2)); // 3
const calc = new Calculator();
console.log(calc.add(4, 5)); // 9 - 导入默认导出:可以使用任意名称。
1
2
3
4// app.js
import MainCalc from './mainCalculator.js';
const mainCalc = new MainCalc();
console.log(mainCalc.add(6, 7)); // 13
2. CommonJS 模块
CommonJS 模块系统主要用于 Node. js 环境,使用 require 和 module.exports 来导入和导出模块。
导出模块(module.exports)
- 导出单个值:
1
2
3
4
5
6
7
8
9
10// mathUtils.js
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
class Calculator {
constructor() {}
add(a, b) {
return a + b;
}
}
module.exports = { add, subtract, Calculator }; - 导出默认值:
1
2
3
4
5
6
7
8// mainCalculator.js
class MainCalculator {
constructor() {}
add(a, b) {
return a + b;
}
}
module.exports = MainCalculator;
导入模块(require)
- 导入模块:
1
2
3
4
5
6
7
8
9// app.js
const { add, subtract, Calculator } = require('./mathUtils.js');
console.log(add(2, 3)); // 5
console.log(subtract(5, 2)); // 3
const calc = new Calculator();
console.log(calc.add(4, 5)); // 9
const MainCalc = require('./mainCalculator.js');
const mainCalc = new MainCalc();
console.log(mainCalc.add(6, 7)); // 13
3. AMD 模块(Asynchronous Module Definition)
AMD 模块系统主要用于浏览器环境,支持异步加载模块,使用 define 和 require 来定义和加载模块。
定义模块(define)
- 定义模块:
1
2
3
4
5
6
7
8
9
10
11
12// mathUtils.js
define('mathUtils', [], function() {
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
class Calculator {
constructor() {}
add(a, b) {
return a + b;
}
}
return { add, subtract, Calculator };
});
加载模块(require)
- 加载模块:
1
2
3
4
5
6
7// app.js
require(['mathUtils'], function(mathUtils) {
console.log(mathUtils.add(2, 3)); // 5
console.log(mathUtils.subtract(5, 2)); // 3
const calc = new mathUtils.Calculator();
console.log(calc.add(4, 5)); // 9
});
4. UMD 模块(Universal Module Definition)
UMD 模块系统是一种通用模块定义方式,旨在兼容 AMD、CommonJS 和全局变量的模块加载方式。它可以在浏览器和 Node. js 环境中使用。
定义 UMD 模块
1 | // mathUtils.js |
总结
- ES6 模块:现代 JavaScript 的官方模块系统,支持
import和export语法,适用于浏览器和 Node. js 环境。 - CommonJS 模块:主要用于 Node. js 环境,支持
require和module.exports语法。 - AMD 模块:主要用于浏览器环境,支持异步加载模块,使用
define和require语法。 - UMD 模块:通用模块定义方式,兼容 AMD、CommonJS 和全局变量的模块加载方式。
在实际开发中,选择哪种模块系统取决于你的项目需求和运行环境。ES6 模块是现代 JavaScript 开发的首选,但在某些情况下,CommonJS 和 AMD 仍然有其用武之地。
ES6 模块和 CommonJS 模块有什么区别?
ES6 模块(ECMAScript 模块)和 CommonJS 模块是 JavaScript 中两种主要的模块系统,它们在语法、功能和使用场景上有一些关键区别。以下是它们的主要区别:
1. 语法
- ES6 模块:
- 使用
import和export语法。 - 支持命名导出和默认导出。
- 导入时可以使用任意名称导入默认导出,但命名导出必须使用相同的名称。
1
2
3
4
5
6
7
8
9// mathUtils.js (ES6 模块)
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export default class Calculator {
constructor() {}
add(a, b) {
return a + b;
}
}1
2
3
4
5
6
7// app.js (ES6 模块)
import { add, subtract } from './mathUtils.js';
import Calculator from './mathUtils.js';
console.log(add(2, 3)); // 5
console.log(subtract(5, 2)); // 3
const calc = new Calculator();
console.log(calc.add(4, 5)); // 9 - 使用
- CommonJS 模块:
- 使用
require和module.exports语法。 - 只支持默认导出,但可以通过对象形式导出多个值。
1
2
3
4
5
6
7
8
9
10// mathUtils.js (CommonJS 模块)
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
class Calculator {
constructor() {}
add(a, b) {
return a + b;
}
}
module.exports = { add, subtract, Calculator };1
2
3
4
5
6// app.js (CommonJS 模块)
const { add, subtract, Calculator } = require('./mathUtils.js');
console.log(add(2, 3)); // 5
console.log(subtract(5, 2)); // 3
const calc = new Calculator();
console.log(calc.add(4, 5)); // 9 - 使用
2. 功能
- ES6 模块:
- 静态分析:
import和export语句是静态的,可以在编译时进行分析,这使得工具(如 Webpack)可以进行代码分割和树摇优化。 - 支持异步加载:ES6 模块支持异步加载,可以通过
import()返回一个 Promise 来动态加载模块。 - 支持命名导出和默认导出:可以同时使用命名导出和默认导出。
- 静态分析:
- CommonJS 模块:
- 动态加载:
require是动态的,只能在运行时解析,不支持静态分析。 - 只支持默认导出:虽然可以通过对象形式导出多个值,但没有命名导出的概念。
- 同步加载:
require是同步的,模块在调用时立即加载,不支持异步加载。
- 动态加载:
3. 使用场景
- ES6 模块:
- 浏览器环境:ES6 模块原生支持浏览器环境,可以直接在 HTML 文件中使用
<script type="module">标签。 - 现代 JavaScript 开发:适用于现代 JavaScript 项目,特别是使用构建工具(如 Webpack、Rollup)的项目。
- 支持动态导入:适用于需要按需加载模块的场景,如代码分割和懒加载。
1
2
3
4<script type="module">
import { add } from './mathUtils.js';
console.log(add(2, 3)); // 5
</script> - 浏览器环境:ES6 模块原生支持浏览器环境,可以直接在 HTML 文件中使用
- CommonJS 模块:
- Node. js 环境:CommonJS 是 Node. js 的默认模块系统,适用于服务器端 JavaScript 开发。
- 同步加载:适用于不需要异步加载的场景,模块在启动时一次性加载。
4. 性能
- ES6 模块:
- 静态分析优化:由于
import和export是静态的,构建工具可以进行代码分割和树摇优化,减少最终打包文件的大小。 - 异步加载:支持异步加载,可以按需加载模块,提高应用的加载性能。
- 静态分析优化:由于
- CommonJS 模块:
- 同步加载:模块在启动时一次性加载,可能会导致初始加载时间较长。
- 缓存机制:Node. js 有模块缓存机制,重复
require同一个模块时会返回缓存的实例,提高性能。
5. 兼容性
- ES6 模块:
- 现代浏览器:现代浏览器原生支持 ES6 模块,但需要使用
<script type="module">标签。 - Node. js:Node. js 从 v13.2.0 开始支持 ES6 模块,但需要使用
.mjs文件扩展名或在package.json中设置"type": "module"。
- 现代浏览器:现代浏览器原生支持 ES6 模块,但需要使用
- CommonJS 模块:
- Node. js:Node. js 默认支持 CommonJS 模块,使用
.js文件扩展名。 - 浏览器:浏览器不原生支持 CommonJS 模块,需要使用构建工具(如 Browserify)转换为浏览器兼容的格式。
- Node. js:Node. js 默认支持 CommonJS 模块,使用
总结
- ES6 模块:适用于现代 JavaScript 开发,特别是需要静态分析、代码分割和异步加载的场景。原生支持浏览器环境,也适用于 Node. js 环境。
- CommonJS 模块:适用于 Node. js 环境,特别是不需要异步加载的服务器端开发。不支持浏览器环境,需要构建工具转换。
在实际开发中,选择哪种模块系统取决于你的项目需求和运行环境。对于现代前端项目,ES6 模块是首选;对于 Node. js 项目,CommonJS 模块仍然是主流选择,但 ES6 模块也在逐渐普及。
vue 使用的模块
Vue 主要使用 ES6 模块系统来实现模块化开发。ES6 模块提供了 import 和 export 语法,使得代码的组织和管理更加清晰和高效。以下是 Vue 中使用 ES6 模块的一些常见方式:
1. 单文件组件(Single File Components, SFC)
Vue 的单文件组件(.vue 文件)是模块化开发的核心。每个 .vue 文件包含 <template>、<script> 和 <style> 三个部分,分别定义组件的模板、脚本和样式。
1 | <template> |
2. 组件导入
在 Vue 项目中,可以使用 import 语句导入其他组件或模块。
1 | // MyComponent.vue |
1 | // App.vue |
3. Vuex 模块化
Vuex 是 Vue 的状态管理库,支持模块化开发。可以通过创建多个模块来管理不同的状态。
1 | // store/modules/cart.js |
1 | // store/index.js |
4. Vue Router 模块化
Vue Router 用于管理 Vue 应用的路由,也可以通过模块化的方式组织路由配置。
1 | // router/modules/home.js |
1 | // router/index.js |
5. 项目结构
Vue 项目通常采用以下结构,每个模块有独立的目录和文件:
1 | src/ |
6. 构建工具
Vue 项目通常使用构建工具(如 Webpack、Vite)来处理模块化代码。这些工具可以将多个模块打包成一个或多个文件,优化加载性能。
1 | // 使用 Vite |
总结
Vue 通过 ES6 模块系统实现了模块化开发,支持单文件组件、Vuex 状态管理、Vue Router 路由管理等多种方式。这些模块化技术使得 Vue 项目更加清晰、易于维护和扩展。
esm和cmj的区别
ESM(EcmaScript Modules)和CMJ(CommonJS)是两种不同的JavaScript模块规范,它们在多个方面存在区别:
- 定义和引入方式:
- ESM使用
import和export关键字来定义和引入模块。 - CJS使用
require来引入模块,使用module.exports或exports来导出模块。
- ESM使用
- 加载机制:
- ESM的加载机制是静态的,即在编译阶段就确定模块依赖关系,这使得浏览器可以并行加载模块。
- CJS的加载机制是动态的,在代码运行时解析模块路径,因此更灵活。
- 作用域和全局变量:
- ESM模块始终运行在严格模式下,并且每个模块都有自己的作用域,顶层变量外部不可见。
- CJS模块共享一个全局作用域,可能引发作用域污染的问题。
- 输出的可变性:
- ESM的导出是不可变的,导出值本质上是一个绑定,修改导出值会影响到所有导入该值的模块。
- CJS的导出是一个值的拷贝,修改导出值不会影响到导入该值的模块。
- this的指向:
- 在ESM中,
this指向undefined。 - 在CJS中,
this指向当前模块的exports对象。
- 在ESM中,
- 文件路径和扩展名:
- ESM要求明确使用文件扩展名,例如
.js或.mjs。 - CJS通常不需要指定文件扩展名,因为
require函数会自动处理。
- ESM要求明确使用文件扩展名,例如
- 异步加载和编译时加载:
- ESM支持异步加载,允许使用
import()语法动态加载模块。 - CJS是同步加载,不适合在浏览器环境中使用,因为同步加载会阻塞主线程。
- ESM支持异步加载,允许使用
- 支持的JavaScript环境:
- ESM是JavaScript的官方标准,在现代浏览器和Node.js环境中原生支持。
- CJS是Node.js的默认模块系统,生态系统成熟,兼容性强。
- 顶级await:
- ESM支持在模块顶层使用
await,而CJS不支持。
- ESM支持在模块顶层使用
- __filename和__dirname:
- 在CJS中,模块的执行需要用函数包起来,并指定一些常用的值,如
__filename和__dirname。 - 在ESM中,不存在
__filename和__dirname这两个变量。
这些区别使得ESM和CJS在不同的应用场景下各有优势,开发者可以根据项目需求和环境选择合适的模块规范。
- 在CJS中,模块的执行需要用函数包起来,并指定一些常用的值,如
命名空间导入
在 ES6 模块系统中,命名空间导入(Namespace Import) 是一种将整个模块的导出内容作为一个对象导入的方式。通过这种方式,你可以通过一个命名空间对象访问模块中的所有导出成员。以下是详细的解释和用法:
1. 基本语法
使用 import * as Namespace 语法将模块的所有导出内容绑定到一个命名空间对象:
1 | // 导入整个模块内容到命名空间对象 'mathUtils' |
2. 核心作用
- 避免命名冲突:将模块内容封装到一个命名空间对象中,减少全局命名污染。
- 统一管理导出内容:当模块导出多个成员时,通过命名空间对象集中访问。
- 动态访问成员:可通过对象属性语法动态选择成员(如
mathUtils[funcName])。
3. 模块导出示例
假设 math-utils.js 导出多个函数:
1 | // math-utils.js |
4. 命名空间对象的结构
导入后的 mathUtils 对象结构如下:
1 | { |
5. 适用场景
(1)模块导出大量成员
当模块有多个导出项时,命名空间导入可简化访问:
1 | // 模块导出多个工具函数 |
(2)需要动态调用导出成员
通过字符串动态选择方法:
1 | const operation = 'add'; |
(3)第三方库的全局工具集
某些库推荐以命名空间形式使用(如早期 Lodash):
1 | import * as _ from 'lodash'; |
6. 与其他导入方式的对比
| 导入方式 | 语法 | 特点 |
|---|---|---|
| 命名空间导入 | import * as Namespace |
所有导出绑定到一个对象,适合多成员模块 |
| 命名导入(Named Import) | import { add, PI } |
仅导入指定成员,代码更简洁,推荐常用方式 |
| 默认导入(Default Import) | import ModuleDefault |
仅导入默认导出(需模块有 export default) |
7. 注意事项
(1)不包含默认导出
若模块同时存在 export default 和命名导出,命名空间对象不包含默认导出:
1 | // 模块代码:math-utils.js |
2)Tree-Shaking 限制
命名空间导入可能导致打包工具(如 Webpack)无法优化未使用的代码(无法 Tree-Shaking),因为所有导出都被视为已使用。
优化建议:优先使用命名导入(import { add })以明确依赖。
(3)循环引用问题
避免在模块间循环引用时通过命名空间动态访问成员,可能导致未定义错误。
8. 综合示例
模块文件:string-utils.js
1 | export function capitalize(str) { |
使用命名空间导入
1 | import * as stringUtils from './string-utils.js'; |
总结
命名空间导入(import * as Namespace)是 ES6 模块中管理多成员模块的有效方式,尤其适用于需要动态访问或避免命名冲突的场景。然而,在追求代码简洁性和打包优化时,更推荐优先使用命名导入(import { add })或默认导入(import ModuleDefault)。根据实际需求灵活选择导入方式,可以让代码更清晰、高效。