JavaScript:CommonJS与ES6模块化的对比

156 阅读7分钟

在开发中,你是否遇到过这样的困惑——为什么有些项目用 require 导入,有些却用 import

模块化,这个看似简单的概念其实贯穿了整个 JavaScript 生态。

接下来,我会从 CommonJS 的同步加载机制ES6 模块化的异步特性,带你一步步拆解它们的底层逻辑。


一、CommonJS 模块化与 ES6 模块化

1.CommonJS 和 ES6 模块化是什么?

在 JavaScript 的世界里,模块化是组织代码的核心方式。简单来说,模块化就是将代码拆分成独立的“模块”,每个模块负责特定功能,并通过清晰的接口与其他模块交互。

CommonJS 是 Node.js 早期的默认模块化方案,它通过 requiremodule.exports 实现模块的导入与导出,适合服务器端开发。

ES6 模块化则是 JavaScript 语言标准的一部分,使用 importexport 语法。


2. CommonJS 模块化

CommonJS 是一种 Node.js 的默认模块化规范,它适用于服务器端开发,它的核心思想是通过 requiremodule.exports 实现模块的导入与导出,支持同步加载模块,适合在服务器端的文件系统中直接操作。

const http = require('http'); // 引入 http 模块,用于创建 HTTP 服务器

const fs = require('fs');     // 导入文件系统模块,用于读取或写入文件
const path = require('path'); // 导入路径处理模块,用于处理文件路径拼接

// 创建 HTTP 服务器
const server = http.createServer((req, res) => {  /* 处理请求逻辑 */ });

server.listen(8080); // 绑定端口并启动服务,监听本地 8080 端口


module.exports = server;

  • require:用于导入模块,例如:

    • require('http'),用于导入内置 HTTP 模块,提供创建服务器的核心功能,如: createServer
    • require('fs'),用于读取或写入文件(例如返回静态资源)。
    • require('path'),用于处理文件路径拼接,避免跨平台路径问题。
  • module.exports:用于导出模块,例如在模块文件中定义函数或变量后通过 module.exports = xxx 暴露接口,使其可以被其他模块调用。

  • 同步加载:CommonJS 的模块加载是同步的,适合在服务器端直接读取本地文件。

ConmonJS 的特点

  • 同步性:由于服务器端文件系统可以直接访问,CommonJS 采用同步加载方式。
  • 全局作用域:模块中的变量和函数默认不会暴露到全局作用域,需通过 module.exports 显式导出。
  • 适用场景:适合中小型 Node.js 项目,尤其是需要直接操作文件系统的场景。

3. ES6 模块化

ES6 模块化(ESM)是 JavaScript 标准的模块化方案,它使用 importexport 语法。相比 CommonJS,它支持 异步加载动态导入静态分析,更适合浏览器端和需要高效打包优化的场景。

如:


import http from 'http'; // 导入内置模块

const server = http.createServer((req, res) => {
  res.end('我是es6模块化');
});

server.listen(1234);

export default { formatDate }; // 默认导出

  • import:用于导入模块,例如 import http from 'http',功能如上面的CommonJS中一样,用于提供创建服务器的核心功能createServer
  • export:用于导出模块,以被其他模块调用,例如 export default xxxexport const xxx
  • 异步加载:ESM 默认支持异步加载,适合浏览器端和需要动态加载的场景。

特点

  • 异步性:ESM 的模块加载是异步的,通过 import() 动态导入实现按需加载。

  • 静态分析:ESM 在编译时就能确定依赖关系,便于优化和打包。

  • 适用场景:适合现代前端项目(如 React/Vue 应用),以及需要动态加载模块的场景。

  • ESM 限制:想要在 Node.js 中运行此代码,则需将文件后缀改为 .mjs 或在 package.json 中设置 "type": "module"。如原本的 XXX.js 文件需要改为 XXX.mjs 文件。

CommonJS 与 ES6 模块化的对比

特性CommonJSES6 模块化
语法require / module.exportsimport / export
加载方式同步异步
适用场景服务器端浏览器端
默认支持Node.js 默认支持Node.js 需启用 .mjs 或配置
动态加载不支持支持 import()
模块绑定值的拷贝引用绑定

二、CommonJS 与 ES6 模块化的实际应用场景

1. 后端开发中的 CommonJS

在 Node.js 后端开发中,CommonJS 是默认的模块化规范,广泛用于服务器端代码组织和功能封装。

典型场景

  • 服务器逻辑模块化
    将 HTTP 服务、数据库连接、路由处理等功能拆分为独立模块,通过 requiremodule.exports 导入导出。

  • 中间件与插件开发
    Express 等框架的中间件通常基于 CommonJS 模块化开发。

  • 工具函数封装
    将常用函数(如文件操作、数据格式化)封装为模块,便于复用。

为什么选择 CommonJS?

  • Node.js 原生支持:无需额外配置即可直接使用。
  • 同步加载适合服务器端:服务器端文件系统可直接访问,同步加载效率更高。
  • 兼容性好:大多数 Node.js 第三方库(如 Express、Mongoose)均基于 CommonJS。

2. 前端开发中的 ES6 模块化

在浏览器端或现代前端框架(React/Vue)中,ES6 模块化是主流选择,其静态分析和异步加载特性更适合复杂项目。

典型场景

  • 组件化开发
    在 React/Vue 项目中,每个组件是一个独立模块,通过 import/export 组合功能。

  • 按需加载与动态导入
    使用 import() 动态加载模块,实现代码分割和懒加载。

  • 工具库封装与共享
    将通用功能(如 API 请求、状态管理)封装为模块,供多个页面复用。

为什么选择 ES6 模块化?

  • 原生浏览器支持:现代浏览器(Chrome/Firefox/Edge)均支持 ES6 模块。
  • 静态分析优化:打包工具(Webpack/Vite)可利用静态依赖关系进行 Tree Shaking 和代码压缩。
  • 异步加载能力:通过 import() 实现按需加载,提升首屏性能。

3. 混合项目中的模块化实践

在同时涉及前后端的项目中(如 SSR 服务端渲染),需要根据运行环境选择合适的模块化规范。

场景 1:Node.js 项目中混合使用 CommonJS 和 ES6

  • 启用 ES6 模块
    在 Node.js 中使用 .mjs 文件或在 package.json 中设置 "type": "module"

    // package.json
    {
      "type": "module"
    }
    
    // server.mjs(ES6)
    import http from 'http';
    const server = http.createServer((req, res) => {
      res.end('Hello from ES6 Module');
    });
    server.listen(8080);
    
  • 兼容 CommonJS
    通过 createRequireimport() 调用 CommonJS 模块。

    // es6-module.js
    const require = createRequire(import.meta.url);
    const fs = require('fs'); // 调用 CommonJS 模块
    

场景 2:前端项目中调用 Node.js API

  • 模块隔离
    前端代码(ES6 模块)通过 HTTP 请求调用后端 API(CommonJS 实现)。
    // 前端代码(ES6)
    fetch('/api/data')
      .then(res => res.json())
      .then(data => console.log(data));
    
    // 后端代码(CommonJS)
    const http = require('http');
    http.createServer((req, res) => {
      if (req.url === '/api/data') {
        res.end(JSON.stringify({ message: 'Hello from Node.js' }));
      }
    }).listen(8080);
    

4. 打包工具与模块化

现代前端项目通常通过 打包工具(Webpack/Vite/Rollup)将模块化代码转换为浏览器兼容的格式。

Webpack/Rollup 的作用

  • 模块解析:自动处理 import/require 语法,将模块打包为单个文件。
  • 代码优化:通过 Tree Shaking 删除未使用代码,缩小包体积。
  • 环境适配:将 ES6 模块转换为 CommonJS 或 UMD 格式,适配不同运行环境。

Vite 的模块化优势

  • 原生 ES 模块支持:开发环境下直接使用浏览器原生 ES6 模块,无需打包。
  • 热更新(HMR):模块修改后实时更新,提升开发效率。
  • 兼容 CommonJS:通过插件支持加载 CommonJS 模块(如 Node.js 依赖)。

5. 模块化开发的最佳实践

  • 按功能划分模块:每个模块职责单一,避免过度耦合。
  • 避免循环依赖:确保模块依赖关系清晰,减少运行时错误。
  • 统一导出接口:使用 export default 或命名导出保持一致性。
  • 静态资源分离:将 CSS/图片等资源与模块解耦,通过 HTTP 服务提供。

总结

场景推荐模块化方案核心优势
Node.js 后端开发CommonJS原生支持、同步加载、兼容性强
浏览器端/前端框架开发ES6 模块化静态分析优化、异步加载、原生支持
混合项目(SSR)ES6 + CommonJS通过 .mjscreateRequire 兼容
打包工具(Webpack/Vite)ES6 模块化支持 Tree Shaking、代码分割、热更新