JavaScript 模块导入导出方式详解:CommonJS、ESModule 及动态导入

407 阅读3分钟

一、CommonJS 模块系统

CommonJS 是 Node.js 默认采用的模块系统,主要用于服务器端 JavaScript。

1. 导出方式

(1) 导出单个值

// 方式1:直接赋值给 module.exports
module.exports = function() {
  console.log('Hello from CommonJS');
};

// 方式2:先定义后赋值
const myFunction = () => {
  console.log('Hello from CommonJS');
};
module.exports = myFunction;

(2) 导出多个值

// 方式1:导出对象
module.exports = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b
};

// 方式2:逐个添加属性
exports.add = (a, b) => a + b;
exports.subtract = (a, b) => a - b;

// 注意:不能直接给 exports 赋值,这会断开与 module.exports 的链接
// 错误示例:exports = { add, subtract };

2. 导入方式

(1) 导入整个模块

const math = require('./math.js');
console.log(math.add(2, 3)); // 5

(2) 解构导入

const { add, subtract } = require('./math.js');
console.log(add(2, 3)); // 5

(3) 导入核心模块

const fs = require('fs');
const path = require('path');

(4) 导入 JSON 文件

const config = require('./config.json');

(5) 导入目录(自动查找 index.js)

const myModule = require('./my-module-dir');

二、ES Module (ESM) 模块系统

ES Module 是 ECMAScript 标准模块系统,现代浏览器和 Node.js 都支持。

1. 导出方式

(1) 命名导出 (Named Exports)

// 方式1:声明时直接导出
export const name = 'Alice';
export function greet() {
  console.log('Hello from ESM');
}

// 方式2:先定义后统一导出
const age = 30;
const sayHello = () => console.log('Hi');
export { age, sayHello };

// 方式3:导出时重命名
export { age as userAge, sayHello as greet };

(2) 默认导出 (Default Export)

// 每个模块只能有一个默认导出
// 方式1:直接默认导出
export default function() {
  console.log('Default export');
}

// 方式2:先定义后默认导出
const myComponent = () => console.log('My Component');
export default myComponent;

// 方式3:默认导出对象
export default {
  version: '1.0',
  author: 'Alice'
};

(3) 混合导出

export const version = '1.0';
export default function main() {
  console.log('Main function');
}

2. 导入方式

(1) 导入命名导出

import { name, greet } from './module.js';
import { name as userName, greet as sayHello } from './module.js';

(2) 导入默认导出

import mainFunction from './module.js';
import myDefault from './module.js';

(3) 混合导入

import mainFunction, { version } from './module.js';

(4) 导入所有导出

import * as module from './module.js';
console.log(module.version);
module.default();

(5) 仅执行模块(不导入任何内容)

import './module.js'; // 适用于有副作用的模块

三、动态导入 (Dynamic Imports)

动态导入允许在运行时按需加载模块,返回 Promise。

1. CommonJS 动态导入

CommonJS 的 require 本身就是动态的,可以在任何地方使用:

// 条件加载
if (condition) {
  const module = require('./moduleA');
} else {
  const module = require('./moduleB');
}

// 动态路径
const path = someCondition ? './moduleA' : './moduleB';
const module = require(path);

2. ES Module 动态导入

ESM 使用 import() 函数实现动态导入:

(1) 基本用法

import('./module.js')
  .then(module => {
    module.default();
    console.log(module.version);
  })
  .catch(err => {
    console.error('模块加载失败', err);
  });

(2) 结合 async/await

async function loadModule() {
  try {
    const module = await import('./module.js');
    module.greet();
  } catch (error) {
    console.error('加载失败', error);
  }
}

(3) 动态路径

const lang = getUserLanguage();
import(`./locales/${lang}.js`)
  .then(module => {
    // 使用本地化模块
  });

(4) 多个模块并行加载

Promise.all([
  import('./moduleA.js'),
  import('./moduleB.js')
]).then(([moduleA, moduleB]) => {
  // 使用两个模块
});

3. 动态导入的应用场景

  1. 代码分割:减少初始加载体积
  2. 按需加载:只在需要时加载模块
  3. 条件加载:根据不同环境/条件加载不同模块
  4. 路由级加载:SPA 中的路由懒加载
  5. Polyfill 加载:根据浏览器特性动态加载

四、CommonJS 与 ES Module 互操作

1. 在 CommonJS 中加载 ESM

Node.js 中必须使用动态 import()

// commonjs 文件
(async () => {
  const esModule = await import('./es-module.mjs');
  esModule.someFunction();
})();

2. 在 ESM 中加载 CommonJS

可以直接使用 import 语法:

// esm 文件
import cjsModule from './commonjs-module.cjs';
console.log(cjsModule.someValue);

// CommonJS 的 module.exports 会作为默认导出
import { namedExport } from './commonjs-module.cjs'; // 错误!不能这样解构

// 正确方式:先导入默认,再解构
import cjsModule from './commonjs-module.cjs';
const { namedExport } = cjsModule;

五、最佳实践建议

  1. 新项目:优先使用 ES Module
  2. Node.js 项目
    • 新项目设置 "type": "module"
    • 旧项目继续使用 CommonJS 或逐步迁移
  3. 浏览器项目
    • 使用打包工具处理模块
    • 或直接使用 <script type="module">(现代浏览器)
  4. 动态导入
    • 性能敏感场景使用代码分割
    • 注意错误处理
  5. 模块设计
    • 保持模块功能单一
    • 合理使用默认导出和命名导出
    • 避免复杂的循环依赖

六、关键区别总结

特性CommonJSES Module
加载方式同步异步
语法require/module.exportsimport/export
动态导入原生支持使用 import() 函数
加载时机运行时编译时静态分析
值传递值拷贝值引用(实时绑定)
顶层 this指向 module.exports指向 undefined
主要环境Node.js浏览器和现代 Node.js