JavaScript模块化编程实例:ES Module导入导出源代码

作者:异步漫步

时间:2026-10-30

浏览:578

本文通过完整源代码实例系统讲解 JavaScript ES Module(ESM)模块化规范,涵盖命名导出、默认导出、重命名导入、动态导入及浏览器与 Node.js 运行配置。

文章目录

在早期前端开发中,所有 JavaScript 脚本通常通过多个 <script> 标签依次载入 HTML 页面。随着项目体积扩大,全局变量命名冲突、代码加载依赖隐晦等问题频发。ECMAScript 2015(ES6)正式推出了官方标准的模块系统——ES Module(简称 ESM)。它通过原生的 export 和 import 语法,在语言层面上实现了作用域隔离与模块化拆分。本文通过完整可运行的工程源码实例,讲解命名导出、默认导出、重命名导入以及动态加载的核心语法,并提供浏览器与 Node.js 双宿主环境下的配置和验证步骤。

一、ES Module 核心机制与依赖加载生命周期

与 CommonJS 在运行时动态加载并执行模块不同,ES Module 是静态结构(Static Module Structure)的规范。模块之间的引用关系在编译与代码解析阶段即可确定,这一机制为现代构建工具的代码静态分析与无用代码剔除(Tree-shaking)提供了底层支持。

当宿主环境加载一个入口 ESM 文件时,会经历三个严格有序的阶段:构建解析、符号实例化以及求值执行。只有完整理解这三个步骤,才能在后续编写模块引用和处理循环依赖时准确定位问题。

JavaScript模块化编程实例:ES Module导入导出源代码
图 1:ES Module 依赖解析、实例化与求值的三个阶段

二、核心语法与源代码实例

ES Module 提供两种导出模式:命名导出(Named Exports)与默认导出(Default Export)。在实际项目开发中,通常根据模块职能合理配合使用。

JavaScript模块化编程实例:ES Module导入导出源代码
图 2:命名导出与默认导出的语法特征及使用对比

1. 命名导出示例:mathUtils.js

命名导出允许一个文件暴露出多个独立的变量或函数。导出有两种书写位置:声明时直接导出,或者在文件末尾使用统一导出语句。

src/mathUtils.js
// 声明时直接命名导出
export const PI = 3.141592653589793;

export function add(a, b) {
  return a + b;
}

function multiply(a, b) {
  return a * b;
}

function calculateArea(radius) {
  return PI * radius * radius;
}

// 统一列表命名导出,并演示 as 重命名
export {
  multiply as mult,
  calculateArea
};

2. 默认导出示例:UserSession.js

默认导出适用于模块只负责单个核心任务的场景。例如一个业务类、一个控制器或一个配置单例对象。

src/UserSession.js
// 默认导出一个类
export default class UserSession {
  constructor(username, role = 'guest') {
    this.username = username;
    this.role = role;
    this.loginTime = new Date().toISOString();
  }

  getProfile() {
    return {
      user: this.username,
      role: this.role,
      activeSince: this.loginTime
    };
  }
}

3. 综合导入示例:main.js

在消费模块时,可以将命名导入与默认导出整合在同一条语句中,也可以通过命名空间导入完整的符号对象。

src/main.js
// 1. 同时导入默认导出和命名导出
import UserSession, { add, mult, PI, calculateArea } from './mathUtils.js';
// 修正:从对应路径引入
import UserSessionClass from './UserSession.js';

// 2. 命名空间整体导入 (Namespace Import)
import * as MathPackage from './mathUtils.js';

// 实例化测试
const session = new UserSessionClass('Alice', 'administrator');
console.log('[User Profile]', session.getProfile());

// 计算测试
const sumResult = add(15, 27);
const productResult = mult(6, 7);
const circleArea = calculateArea(5);

console.log('[Calculations]');
console.log('15 + 27 =', sumResult);
console.log('6 * 7 =', productResult);
console.log('Circle Area (r=5) =', circleArea);

// 命名空间调用
console.log('MathPackage.PI =', MathPackage.PI);
console.log('MathPackage.add(10, 20) =', MathPackage.add(10, 20));

4. 模块门面与聚合导出:index.js

在大型应用或库开发中,为了避免外部深层次引用文件,常常使用 index.js 集中聚合导出子模块(Facade 模式)。

src/index.js
// 聚合导出:将 mathUtils 的所有导出直接对外暴露
export * from './mathUtils.js';

// 重命名并转发 UserSession 的默认导出
export { default as UserSession } from './UserSession.js';

三、运行环境配置与实战操作步骤

编写完成的 ES Module 源码无法直接通过本地双击打开(即 file:/// 协议),否则浏览器出于安全机制会拦截 CORS 跨域请求并报错。必须通过本地 HTTP 服务或配置 Node.js 运行环境。

JavaScript模块化编程实例:ES Module导入导出源代码
图 3:ES Module 项目配置与运行操作步骤

步骤 1:在 HTML 中载入模块(浏览器环境)

在浏览器中,必须在 <script> 标签添加 type="module" 属性。浏览器识别到该属性后,会自动异步下载该脚本及其所有递归依赖,并在文档解析完成后按正确依赖顺序执行。

index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>ES Module 浏览器测试</title>
</head>
<body>
  <h1>ES Module 运行测试</h1>
  <p>请按 F12 打开开发者工具控制台查看输出结果。</p>

  <!-- 关键属性:type="module" -->
  <script type="module" src="./src/main.js"></script>
</body>
</html>
特别注意:如果使用文件系统直接打开(路径形如 file:///Users/.../index.html),浏览器控制台会报错:Access to script at '...' from origin 'null' has been blocked by CORS policy。解决方式是在项目根目录下启动一个简易静态服务:
# 使用 Node.js 自带的 npx 快速开启服务
npx serve .

步骤 2:在 Node.js 环境中运行

Node.js 原生历史采用 CommonJS 规范(即 require 和 module.exports)。若要在 Node.js 中原生执行 ES Module,有两种官方支持的方式:

  1. 推荐方式:在项目根目录的 package.json 中声明 "type": "module"。配置后该目录下的所有 .js 文件均被视为 ES Module。
  2. 单文件方式:将文件扩展名改为 .mjs,Node.js 会自动将其识别为 ESM。
package.json
{
  "name": "esm-demo-project",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node src/main.js"
  }
}

执行运行命令:

node src/main.js
终端示例输出(示例输出非真实执行)
[User Profile] { user: 'Alice', role: 'administrator', activeSince: '2026-10-25T06:27:35.000Z' } [Calculations] 15 + 27 = 42 6 * 7 = 42 Circle Area (r=5) = 78.53981633974483 MathPackage.PI = 3.141592653589793 MathPackage.add(10, 20) = 30

四、进阶特性:动态导入(Dynamic Import)

静态 import 声明必须置于模块的顶层作用域,不能嵌套在 if 语句、循环结构或函数内部。当需要根据用户交互、设备条件或权限动态加载模块,或者需要对大型类库进行路由懒加载时,应使用函数形式的动态导入 import()。

src/dynamicLoader.js
// 动态导入函数返回一个 Promise,可在任何代码块中调用
async function loadChartService(needsAdvancedFeature) {
  if (needsAdvancedFeature) {
    try {
      // 仅在满足特定条件时按需下载加载
      const { mult, calculateArea } = await import('./mathUtils.js');
      console.log('动态加载模块成功,计算结果:', mult(9, 9));
    } catch (error) {
      console.error('模块动态加载失败:', error);
    }
  } else {
    console.log('采用轻量基础模式,无需引入高级模块。');
  }
}

// 模拟触发动态导入
loadChartService(true);

五、ES Module 核心特性与避坑总结

JavaScript模块化编程实例:ES Module导入导出源代码
图 4:ES Module 关键特性、机制对比与开发最佳实践总结

归纳而言,在日常项目中使用 ES Module 时,建议遵循以下实用原则:

  • 优先使用命名导出:除组件或核心服务外,尽量采用命名导出。显式的符号名不仅提供清晰的代码自解释能力,还能获得 IDE 更精确的自动导入与重构支持。
  • 保持扩展名显式完整:在脱离构建工具的原生浏览器和 Node.js 环境下,import './utils.js' 中的 .js 不可省略。
  • 隔离动态行为:页面初次加载所需的主干逻辑使用顶层 import 静态加载,体积较大且非首屏必需的模块统一通过 import() 按需载入,以实现加载性能与代码架构的平衡。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章