07 模块
loyalvi Lv7

07 模块

模块

在 JavaScript 中,模块化是一种将代码组织成独立、可重用的单元的方法,有助于提高代码的可维护性和可读性。ES6(ECMAScript 2015)引入了原生的模块支持,但在此之前,社区已经发展出了多种模块化模式,如 CommonJS 和 AMD。以下是 JavaScript 模块化的一些主要方式及其特点。

1. ES6 模块(ECMAScript 模块)

ES6 模块是 JavaScript 的官方模块系统,提供了 importexport 语法来导入和导出模块。

导出模块(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 环境,使用 requiremodule.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 模块系统主要用于浏览器环境,支持异步加载模块,使用 definerequire 来定义和加载模块。

定义模块(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// mathUtils.js
(function(factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define([], factory);
} else if (typeof module === 'object' && module.exports) {
// Node.js
module.exports = factory();
} else {
// Browser globals
window.mathUtils = factory();
}
}(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 };
}));

总结

  • ES6 模块:现代 JavaScript 的官方模块系统,支持 importexport 语法,适用于浏览器和 Node. js 环境。
  • CommonJS 模块:主要用于 Node. js 环境,支持 requiremodule.exports 语法。
  • AMD 模块:主要用于浏览器环境,支持异步加载模块,使用 definerequire 语法。
  • UMD 模块:通用模块定义方式,兼容 AMD、CommonJS 和全局变量的模块加载方式。
    在实际开发中,选择哪种模块系统取决于你的项目需求和运行环境。ES6 模块是现代 JavaScript 开发的首选,但在某些情况下,CommonJS 和 AMD 仍然有其用武之地。

ES6 模块和 CommonJS 模块有什么区别?

ES6 模块(ECMAScript 模块)和 CommonJS 模块是 JavaScript 中两种主要的模块系统,它们在语法、功能和使用场景上有一些关键区别。以下是它们的主要区别:

1. 语法

  • ES6 模块
    • 使用 importexport 语法。
    • 支持命名导出和默认导出。
    • 导入时可以使用任意名称导入默认导出,但命名导出必须使用相同的名称。
    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 模块
    • 使用 requiremodule.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 模块
    • 静态分析importexport 语句是静态的,可以在编译时进行分析,这使得工具(如 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>
  • CommonJS 模块
    • Node. js 环境:CommonJS 是 Node. js 的默认模块系统,适用于服务器端 JavaScript 开发。
    • 同步加载:适用于不需要异步加载的场景,模块在启动时一次性加载。

4. 性能

  • ES6 模块
    • 静态分析优化:由于 importexport 是静态的,构建工具可以进行代码分割和树摇优化,减少最终打包文件的大小。
    • 异步加载:支持异步加载,可以按需加载模块,提高应用的加载性能。
  • CommonJS 模块
    • 同步加载:模块在启动时一次性加载,可能会导致初始加载时间较长。
    • 缓存机制:Node. js 有模块缓存机制,重复 require 同一个模块时会返回缓存的实例,提高性能。

5. 兼容性

  • ES6 模块
    • 现代浏览器:现代浏览器原生支持 ES6 模块,但需要使用 <script type="module"> 标签。
    • Node. js:Node. js 从 v13.2.0 开始支持 ES6 模块,但需要使用 .mjs 文件扩展名或在 package.json 中设置 "type": "module"
  • CommonJS 模块
    • Node. js:Node. js 默认支持 CommonJS 模块,使用 .js 文件扩展名。
    • 浏览器:浏览器不原生支持 CommonJS 模块,需要使用构建工具(如 Browserify)转换为浏览器兼容的格式。

总结

  • ES6 模块:适用于现代 JavaScript 开发,特别是需要静态分析、代码分割和异步加载的场景。原生支持浏览器环境,也适用于 Node. js 环境。
  • CommonJS 模块:适用于 Node. js 环境,特别是不需要异步加载的服务器端开发。不支持浏览器环境,需要构建工具转换。
    在实际开发中,选择哪种模块系统取决于你的项目需求和运行环境。对于现代前端项目,ES6 模块是首选;对于 Node. js 项目,CommonJS 模块仍然是主流选择,但 ES6 模块也在逐渐普及。

vue 使用的模块

Vue 主要使用 ES6 模块系统来实现模块化开发。ES6 模块提供了 importexport 语法,使得代码的组织和管理更加清晰和高效。以下是 Vue 中使用 ES6 模块的一些常见方式:

1. 单文件组件(Single File Components, SFC)

Vue 的单文件组件(.vue 文件)是模块化开发的核心。每个 .vue 文件包含 <template><script><style> 三个部分,分别定义组件的模板、脚本和样式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div>{{ message }}</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello Vue!'
};
}
}
</script>
<style>
/* CSS 样式 */
</style>

2. 组件导入

在 Vue 项目中,可以使用 import 语句导入其他组件或模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
// MyComponent.vue
<template>
<div>Count is: {{ count }}</div>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const count = ref(0);
return { count };
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
// App.vue
<template>
<MyComponent />
</template>
<script>
import MyComponent from './MyComponent.vue';
export default {
components: {
MyComponent
}
}
</script>

3. Vuex 模块化

Vuex 是 Vue 的状态管理库,支持模块化开发。可以通过创建多个模块来管理不同的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
// store/modules/cart.js
const state = {
items: []
};
const mutations = {
ADD_ITEM(state, item) {
state.items.push(item);
}
};
export default {
state,
mutations
};
1
2
3
4
5
6
7
8
// store/index.js
import { createStore } from 'vuex';
import cart from './modules/cart';
export default createStore({
modules: {
cart
}
});

4. Vue Router 模块化

Vue Router 用于管理 Vue 应用的路由,也可以通过模块化的方式组织路由配置。

1
2
3
4
5
// router/modules/home.js
const routes = [
{ path: '/', component: () => import('../views/Home.vue') }
];
export default routes;
1
2
3
4
5
6
7
8
9
10
11
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import homeRoutes from './modules/home';
const routes = [
...homeRoutes
];
const router = createRouter({
history: createWebHistory(),
routes
});
export default router;

5. 项目结构

Vue 项目通常采用以下结构,每个模块有独立的目录和文件:

1
2
3
4
5
6
7
8
src/
├── assets/ # 静态资源
├── components/ # 公共组件
├── views/ # 页面组件
├── store/ # Vuex 状态管理
├── router/ # Vue Router 路由管理
├── main.js # 入口文件
└── App.vue # 根组件

6. 构建工具

Vue 项目通常使用构建工具(如 Webpack、Vite)来处理模块化代码。这些工具可以将多个模块打包成一个或多个文件,优化加载性能。

1
2
3
4
5
6
// 使用 Vite
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()]
});

总结

Vue 通过 ES6 模块系统实现了模块化开发,支持单文件组件、Vuex 状态管理、Vue Router 路由管理等多种方式。这些模块化技术使得 Vue 项目更加清晰、易于维护和扩展。

esm和cmj的区别

ESM(EcmaScript Modules)和CMJ(CommonJS)是两种不同的JavaScript模块规范,它们在多个方面存在区别:

  1. 定义和引入方式
    • ESM使用importexport关键字来定义和引入模块。
    • CJS使用require来引入模块,使用module.exportsexports来导出模块。
  2. 加载机制
    • ESM的加载机制是静态的,即在编译阶段就确定模块依赖关系,这使得浏览器可以并行加载模块。
    • CJS的加载机制是动态的,在代码运行时解析模块路径,因此更灵活。
  3. 作用域和全局变量
    • ESM模块始终运行在严格模式下,并且每个模块都有自己的作用域,顶层变量外部不可见。
    • CJS模块共享一个全局作用域,可能引发作用域污染的问题。
  4. 输出的可变性
    • ESM的导出是不可变的,导出值本质上是一个绑定,修改导出值会影响到所有导入该值的模块。
    • CJS的导出是一个值的拷贝,修改导出值不会影响到导入该值的模块。
  5. this的指向
    • 在ESM中,this指向undefined
    • 在CJS中,this指向当前模块的exports对象。
  6. 文件路径和扩展名
    • ESM要求明确使用文件扩展名,例如.js.mjs
    • CJS通常不需要指定文件扩展名,因为require函数会自动处理。
  7. 异步加载和编译时加载
    • ESM支持异步加载,允许使用import()语法动态加载模块。
    • CJS是同步加载,不适合在浏览器环境中使用,因为同步加载会阻塞主线程。
  8. 支持的JavaScript环境
    • ESM是JavaScript的官方标准,在现代浏览器和Node.js环境中原生支持。
    • CJS是Node.js的默认模块系统,生态系统成熟,兼容性强。
  9. 顶级await
    • ESM支持在模块顶层使用await,而CJS不支持。
  10. __filename和__dirname
    • 在CJS中,模块的执行需要用函数包起来,并指定一些常用的值,如__filename__dirname
    • 在ESM中,不存在__filename__dirname这两个变量。
      这些区别使得ESM和CJS在不同的应用场景下各有优势,开发者可以根据项目需求和环境选择合适的模块规范。

命名空间导入

在 ES6 模块系统中,命名空间导入(Namespace Import) 是一种将整个模块的导出内容作为一个对象导入的方式。通过这种方式,你可以通过一个命名空间对象访问模块中的所有导出成员。以下是详细的解释和用法:

1. 基本语法

使用 import * as Namespace 语法将模块的所有导出内容绑定到一个命名空间对象:

1
2
3
4
5
// 导入整个模块内容到命名空间对象 'mathUtils'
import * as mathUtils from './math-utils.js';
// 使用命名空间访问导出成员
const sum = mathUtils.add(2, 3);
const product = mathUtils.multiply(4, 5);

2. 核心作用

  • 避免命名冲突:将模块内容封装到一个命名空间对象中,减少全局命名污染。
  • 统一管理导出内容:当模块导出多个成员时,通过命名空间对象集中访问。
  • 动态访问成员:可通过对象属性语法动态选择成员(如 mathUtils[funcName])。

3. 模块导出示例

假设 math-utils.js 导出多个函数:

1
2
3
4
// math-utils.js
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
export const PI = 3.14159;

4. 命名空间对象的结构

导入后的 mathUtils 对象结构如下:

1
2
3
4
5
{
add: function add(a, b) { ... },
multiply: function multiply(a, b) { ... },
PI: 3.14159
}

5. 适用场景

(1)模块导出大量成员

当模块有多个导出项时,命名空间导入可简化访问:

1
2
3
4
// 模块导出多个工具函数
import * as utils from './utils.js';
utils.formatDate();
utils.validateEmail();

(2)需要动态调用导出成员

通过字符串动态选择方法:

1
2
const operation = 'add';
const result = mathUtils[operation](2, 3); // 调用 mathUtils.add(2, 3)

(3)第三方库的全局工具集

某些库推荐以命名空间形式使用(如早期 Lodash):

1
2
import * as _ from 'lodash';
_.debounce(() => {}, 100);

6. 与其他导入方式的对比

导入方式 语法 特点
命名空间导入 import * as Namespace 所有导出绑定到一个对象,适合多成员模块
命名导入(Named Import) import { add, PI } 仅导入指定成员,代码更简洁,推荐常用方式
默认导入(Default Import) import ModuleDefault 仅导入默认导出(需模块有 export default

7. 注意事项

(1)不包含默认导出

若模块同时存在 export default 和命名导出,命名空间对象不包含默认导出

1
2
3
4
5
6
7
// 模块代码:math-utils.js
export default function log() { console.log('default'); }
export function add() { /* ... */ }
// 导入后
import * as mathUtils from './math-utils.js';
console.log(mathUtils.default); // 默认导出作为 'default' 属性
mathUtils.default(); // 调用默认导出函数

2)Tree-Shaking 限制

命名空间导入可能导致打包工具(如 Webpack)无法优化未使用的代码(无法 Tree-Shaking),因为所有导出都被视为已使用。
优化建议:优先使用命名导入(import { add })以明确依赖。

(3)循环引用问题

避免在模块间循环引用时通过命名空间动态访问成员,可能导致未定义错误。

8. 综合示例

模块文件:string-utils.js

1
2
3
4
5
6
export function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function reverse(str) {
return str.split('').reverse().join('');
}

使用命名空间导入

1
2
3
4
import * as stringUtils from './string-utils.js';
const name = 'alice';
console.log(stringUtils.capitalize(name)); // "Alice"
console.log(stringUtils.reverse(name)); // "ecila"

总结

命名空间导入(import * as Namespace)是 ES6 模块中管理多成员模块的有效方式,尤其适用于需要动态访问或避免命名冲突的场景。然而,在追求代码简洁性和打包优化时,更推荐优先使用命名导入import { add })或默认导入import ModuleDefault)。根据实际需求灵活选择导入方式,可以让代码更清晰、高效。

由 Hexo 驱动 & 主题 Keep
访客数 访问量