在开发中,你是否遇到过这样的困惑——为什么有些项目用
require导入,有些却用import?模块化,这个看似简单的概念其实贯穿了整个 JavaScript 生态。
接下来,我会从 CommonJS 的同步加载机制 到 ES6 模块化的异步特性,带你一步步拆解它们的底层逻辑。
一、CommonJS 模块化与 ES6 模块化
1.CommonJS 和 ES6 模块化是什么?
在 JavaScript 的世界里,模块化是组织代码的核心方式。简单来说,模块化就是将代码拆分成独立的“模块”,每个模块负责特定功能,并通过清晰的接口与其他模块交互。
CommonJS 是 Node.js 早期的默认模块化方案,它通过 require 和 module.exports 实现模块的导入与导出,适合服务器端开发。
ES6 模块化则是 JavaScript 语言标准的一部分,使用 import 和 export 语法。
2. CommonJS 模块化
CommonJS 是一种 Node.js 的默认模块化规范,它适用于服务器端开发,它的核心思想是通过 require 和 module.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 标准的模块化方案,它使用 import 和 export 语法。相比 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 xxx或export const xxx。- 异步加载:ESM 默认支持异步加载,适合浏览器端和需要动态加载的场景。
特点
-
异步性:ESM 的模块加载是异步的,通过
import()动态导入实现按需加载。 -
静态分析:ESM 在编译时就能确定依赖关系,便于优化和打包。
-
适用场景:适合现代前端项目(如 React/Vue 应用),以及需要动态加载模块的场景。
-
ESM 限制:想要在 Node.js 中运行此代码,则需将文件后缀改为
.mjs或在package.json中设置"type": "module"。如原本的XXX.js文件需要改为XXX.mjs文件。
CommonJS 与 ES6 模块化的对比
| 特性 | CommonJS | ES6 模块化 |
|---|---|---|
| 语法 | require / module.exports | import / export |
| 加载方式 | 同步 | 异步 |
| 适用场景 | 服务器端 | 浏览器端 |
| 默认支持 | Node.js 默认支持 | Node.js 需启用 .mjs 或配置 |
| 动态加载 | 不支持 | 支持 import() |
| 模块绑定 | 值的拷贝 | 引用绑定 |
二、CommonJS 与 ES6 模块化的实际应用场景
1. 后端开发中的 CommonJS
在 Node.js 后端开发中,CommonJS 是默认的模块化规范,广泛用于服务器端代码组织和功能封装。
典型场景
-
服务器逻辑模块化:
将 HTTP 服务、数据库连接、路由处理等功能拆分为独立模块,通过require和module.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:
通过createRequire或import()调用 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 | 通过 .mjs 或 createRequire 兼容 |
| 打包工具(Webpack/Vite) | ES6 模块化 | 支持 Tree Shaking、代码分割、热更新 |