CommonJS 模块系统 Node.js 模块导入

CommonJS 模块系统 Node.js 模块导入
ESM 支持两种导出方式:具名导出 (Named Exports) 和 默认导出 (Default Export)。

实例
// user.mjs

// 具名导出
export const name = 'Bob';
export const age = 25;

// 默认导出
const sayHello = () => {
  console.log(`Hello, my name is ${name}.`);
};
export default sayHello;

导入方式:

实例
// main.mjs

// 导入具名导出
import { name, age } from './user.mjs';
console.log(name); // 输出: Bob

// 导入默认导出
import sayHello from './user.mjs';
sayHello(); // 输出: Hello, my name is Bob.

// 同时导入具名和默认导出
import sayHello, { name } from './user.mjs';
console.log(name);
sayHello();

// 导入所有具名导出,并将其作为对象的属性
import * as user from './user.mjs';
console.log(user.name);
user.default(); // 默认导出会作为 default 属性

ESM 特点:

异步加载:默认是异步加载,不会阻塞主线程,更适合浏览器环境,但在 Node.js 中通常表现为同步加载。
静态分析:import 和 export 语句在代码执行前就可以确定模块的依赖关系,这使得工具(如 Webpack、Vite)可以进行更好的优化(如 Tree Shaking)。
严格模式:ESM 模块默认在严格模式下运行。
CommonJS 和 ESM 的区别
项目    CommonJS    ESM
语法    require / module.exports    import / export
加载机制    运行时同步加载    编译时静态加载
默认支持    Node.js 默认支持    需 .mjs 或 "type": "module"
适合场景    后端脚本、老项目    前后端现代项目、Tree-shaking
是否可混用    不能直接混用(需额外配置)    不能直接混用(需额外配置)
混合使用
Node.js 在较新版本中支持两种模块系统是共存。你可以在同一个项目中同时使用 CommonJS 和 ESM,但需要注意以下几点:

ESM 中不能直接使用 require() 和 module.exports。
CommonJS 中不能直接使用 import 和 export。
如果想在 ESM 中导入 CommonJS 模块,可以直接使用 import 语句,ESM 会将其视为默认导出。

// commonjs_module.jsNode.js 模块导入
module.exports = { data: 'hello' };

// esm_module.mjs
import commonModule from './commonjs_module.js';
console.log(commonModule.data); // 输出: hello
如果想在 CommonJS 中导入 ESM 模块,你需要使用动态 import() 函数。

// esm_module.mjs
export const name = 'Bob';

// commonjs_module.js
async function loadESM() {
  const { name } = await import('./esm_module.mjs');
  console.log(name);
}
loadESM();
最佳实践建议
对于新的项目,强烈推荐使用 ES Modules (ESM)。它不仅是 JavaScript 的官方标准,而且与现代前端工具链(如 Vite、Next.js)兼容性更好,能够充分利用 Tree Shaking 等优化技术,减少打包后的代码体积。

如果你正在维护一个老旧的 CommonJS 项目,并且不需要 ESM 的特性,可以继续使用 CommonJS。但如果需要引入新的依赖或利用 ESM 的新特性,可以考虑逐步迁移,或者使用混合模式来过渡。

从文件模块缓存中加载
尽管原生模块与文件模块的优先级不同,但是都会优先从文件模块的缓存中加载已经存在的模块。

从原生模块加载
原生模块的优先级仅次于文件模块缓存的优先级。require 方法在解析文件名之后,优先检查模块是否在原生模块列表中。以http模块为例,尽管在目录下存在一个 http/http.js/http.node/http.json 文件,require("http") 都不会从这些文件中加载,而是从原生模块中加载。

原生模块也有一个缓存区,同样也是优先从缓存区加载。如果缓存区没有被加载过,则调用原生模块的加载方式进行加载和执行。

从文件加载
当文件模块缓存中不存在,而且不是原生模块的时候,Node.js 会解析 require 方法传入的参数,并从文件系统中加载实际的文件,加载过程中的包装和编译细节在前一节中已经介绍过,这里我们将详细描述查找文件模块的过程,其中,也有一些细节值得知晓。

require方法接受以下几种参数的传递:

http、fs、path等,原生模块。
./mod或../mod,相对路径的文件模块。
/pathtomodule/mod,绝对路径的文件模块。
mod,非原生模块的文件模块。
在路径 Y 下执行 require(X) 语句执行顺序:

1. 如果 X 是内置模块
   a. 返回内置模块
   b. 停止执行
2. 如果 X 以 '/' 开头
   a. 设置 Y 为文件根路径
3. 如果 X 以 './' 或 '/' or '../' 开头
   a. LOAD_AS_FILE(Y + X)
   b. LOAD_AS_DIRECTORY(Y + X)
4. LOAD_NODE_MODULES(X, dirname(Y))
5. 抛出异常 "not found"

LOAD_AS_FILE(X)
1. 如果 X 是一个文件, 将 X 作为 JavaScript 文本载入并停止执行。
2. 如果 X.js 是一个文件, 将 X.js 作为 JavaScript 文本载入并停止执行。
3. 如果 X.json 是一个文件, 解析 X.json 为 JavaScript 对象并停止执行。
4. 如果 X.node 是一个文件, 将 X.node 作为二进制插件载入并停止执行。

LOAD_INDEX(X)
1. 如果 X/index.js 是一个文件,  将 X/index.js 作为 JavaScript 文本载入并停止执行。
2. 如果 X/index.json 是一个文件, 解析 X/index.json 为 JavaScript 对象并停止执行。
3. 如果 X/index.node 是一个文件,  将 X/index.node 作为二进制插件载入并停止执行。

LOAD_AS_DIRECTORY(X)
1. 如果 X/package.json 是一个文件,
   a. 解析 X/package.json, 并查找 "main" 字段。
   b. let M = X + (json main 字段)
   c. LOAD_AS_FILE(M)
   d. LOAD_INDEX(M)
2. LOAD_INDEX(X)

LOAD_NODE_MODULES(X, START)
1. let DIRS=NODE_MODULES_PATHS(START)
2. for each DIR in DIRS:
   a. LOAD_AS_FILE(DIR/X)
   b. LOAD_AS_DIRECTORY(DIR/X)

NODE_MODULES_PATHS(START)
1. let PARTS = path split(START)
2. let I = count of PARTS - 1
3. let DIRS = []
4. while I >= 0,
   a. if PARTS[I] = "node_modules" CONTINUE
   b. DIR = path join(PARTS[0 .. I] + "node_modules")
   c. DIRS = DIRS + DIR
   d. let I = I - 1
5. return DIRS
exports 和 module.exports 的使用

如果要对外暴露属性或方法,就用 exports 就行,要暴露对象(类似class,包含了很多属性和方法),就用 module.exports。

模块缓存
模块缓存机制:Node.js 会将已加载的模块缓存起来,以提高性能。再次 require() 同一模块时,直接返回缓存中的模块,而不是重新加载。

刷新缓存:要重新加载模块,可以删除缓存:

delete require.cache[require.resolve('./myModule')];
循环依赖
当两个或多个模块相互导入时,称为循环依赖。Node.js 能处理简单的循环依赖,但可能导致部分模块导出的对象未完全初始化。

实例
// a.js
const b = require('./b');
console.log('a.js:', b.message);
module.exports = { message: 'Hello from a' };

// b.js
const a = require('./a');
console.log('b.js:', a.message);
module.exports = { message: 'Hello from b' };

// main.js
require('./a');

输出说明 b.js 导入 a.js 时,由于 a.js 尚未完全执行,a.message 为 undefined。

以上代码中,我们把 say 函数作为 execute 函数的第一个变量进行了传递。这里传递的不是 say 的返回值,而是 say 本身!

这样一来, say 就变成了execute 中的本的地变量 someFunction ,execute 可以通过调用 someFunction() (带括号的形式)来使用 say 函数。

当然,因为 say 有一个变量, execute 在调用 someFunction 时可以传递这样一个变量。

匿名函数:我们可以把一个函数作为变量传递。但是我们不一定要绕这个"先定义,再传递"的圈子,我们可以直接在另一个函数的括号中定义和传递这个函数:

实例
function execute(someFunction, value) {
  someFunction(value);
}

execute(function(word){ console.log(word) }, "Hello");

我们在 execute 接受第一个参数的地方直接定义了我们准备传递给 execute 的函数。

用这种方式,我们甚至不用给这个函数起名字,这也是为什么它被叫做匿名函数 。

函数传递是如何让 HTTP 服务器工作的
带着这些知识,我们再来看看我们简约而不简单的HTTP服务器:

实例
var http = require("http");

http.createServer(function(request, response) {
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.write("Hello World");
  response.end();
}).listen(8888);

现在它看上去应该清晰了很多:我们向 createServer 函数传递了一个匿名函数。

用这样的代码也可以达到同样的目的:

实例
var http = require("http");

function onRequest(request, response) {
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.write("Hello World");
  response.end();
}

http.createServer(onRequest).listen(8888);

函数最佳实践
函数设计原则
单一职责:一个函数只做一件事
合理命名:使用动词+名词形式(如 getUserInfo)
控制长度:建议不超过20行代码
避免副作用:纯函数更易于测试和维护
性能优化技巧
实例
// 使用闭包缓存结果
function memoize(fn) {
  const cache = new Map();
  return (...args) => {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

实战练习
练习1:创建温度转换函数
实例
/**
 * 实现摄氏度和华氏度互相转换
 * @param {number} temp - 温度值
 * @param {string} unit - 原始单位 ('C' 或 'F')
 * @returns {number} 转换后的温度
 */
function convertTemperature(temp, unit) {
  // 你的代码...
}

练习2:实现简单的事件发射器
实例
class EventEmitter {
  constructor() {
    this.events = {};
  }
 
  // 实现 on/emit/off 方法
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值