Node.js新手必看:解决__dirname报错的3种实用方法(附代码示例)

【nodejs】“__dirname is not defined”错误修复 在这个示例中,我们首先导入了 Node.js 中的 fileURLToPath 和 dirname 函数,然后使用 import.meta.url 获取当前模块的 URL。接着,通过 fileURLToPath 将 URL 转换为文件路径,并最终通过 dirname 获取该文件路径的目录部分,从而模拟实现了类似 __dirname 的功能。这样,您就可以在 ESM 模式下获取当前模块的目录路径,类似于 CommonJS 中的 __dirname 变量。不过,您可以通过一些方法来模拟实现类似的功能。 阅读详情

Node.js模块化演进:从__dirname报错看现代JavaScript开发实战

最近在技术社区里,经常看到刚接触Node.js的朋友们被一个看似简单的问题困扰——运行代码时突然抛出“__dirname is not defined”的错误。这个错误信息直白得让人有点懵,特别是当你从网上找到的示例代码明明运行得好好的,复制到自己的项目里却突然罢工。实际上,这个看似简单的报错背后,反映的是JavaScript生态正在经历的一场深刻变革:从传统的CommonJS模块系统向现代ESM(ECMAScript Modules)的全面迁移。

如果你是Node.js的新手,遇到这个问题不必慌张。这恰恰是一个很好的切入点,让你理解Node.js模块系统的工作原理。今天我们就从这个问题出发,深入探讨三种不同场景下的解决方案,并借此机会梳理一下Node.js模块化的发展脉络。无论你是在构建一个全新的项目,还是维护一个遗留代码库,这些知识都会让你在开发中更加得心应手。

1. 理解问题的根源:CommonJS与ESM的模块化之争

要彻底解决__dirname报错问题,我们首先得弄清楚Node.js中两种模块系统的根本区别。这不仅仅是技术细节的差异,更代表了JavaScript语言发展的不同阶段和设计哲学。

1.1 CommonJS:Node.js的“原生”模块系统

CommonJS是Node.js诞生之初采用的模块系统,它深深植根于服务器端JavaScript的开发需求。在这个系统中,每个文件都被视为一个独立的模块,拥有自己的作用域。__dirname和__filename就是CommonJS环境下的两个特殊变量:

  • __dirname:当前执行脚本所在的目录的绝对路径
  • __filename:当前执行脚本的绝对路径

这两个变量在CommonJS中是由Node.js运行时自动注入的,开发者可以直接使用,无需任何导入操作。这种设计简单直观,特别适合服务器端文件操作的需求。

// CommonJS示例 - 直接使用__dirname
const fs = require('fs');
const path = require('path');

// 读取当前目录下的config.json文件
const configPath = path.join(__dirname, 'config.json');
const configData = fs.readFileSync(configPath, 'utf8');
console.log('配置文件内容:', configData);

CommonJS采用同步加载机制,模块在首次被require()时加载并缓存,后续的require()调用直接返回缓存结果。这种机制在服务器端环境中表现良好,但在浏览器端会导致性能问题。

1.2 ESM:JavaScript的“标准”模块系统

ESM(ECMAScript Modules)是ECMAScript标准的一部分,从ES6(ES2015)开始引入。与CommonJS不同,ESM是语言层面的标准,旨在为JavaScript提供统一的模块系统,无论是在浏览器端还是服务器端。

ESM的几个关键特性:

  • 静态分析:模块的导入导出关系在代码执行前就可以确定
  • 异步加载:支持按需加载,更适合浏览器环境
  • 严格模式:ESM模块默认处于严格模式
  • 没有全局变量:不自动注入__dirname、__filename等变量
// ESM示例 - 需要手动实现__dirname功能
import { readFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';

// 获取当前文件的目录路径
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// 读取配置文件
const configPath = join(__dirname, 'config.json');
const configData = readFileSync(configPath, 'utf8');
console.log('ESM模式下的配置文件内容:', configData);

注意:ESM的设计哲学是“显式优于隐式”。所有依赖必须明确声明,这虽然增加了少许代码量,但大大提高了代码的可维护性和可预测性。

1.3 两种模块系统的对比

为了更清晰地理解两者的差异,我们通过一个表格来对比它们的主要特性:

特性维度CommonJSESM(ECMAScript Modules)
引入方式require()import
导出方式module.exports 或 exportsexport
加载时机运行时动态加载编译时静态分析
加载机制同步加载异步加载(支持动态导入)
作用域每个文件独立作用域每个模块独立作用域
特殊变量提供__dirname、__filename、require等不提供这些变量,需通过import.meta.url获取
严格模式默认非严格模式默认严格模式
循环依赖支持但可能有问题静态分析,处理更清晰
浏览器支持需要打包工具转换现代浏览器原生支持

这个对比表格清晰地展示了为什么在ESM模式下直接使用__dirname会报错——ESM根本没有这个变量!Node.js在运行ESM模块时,不会像CommonJS那样自动注入这些全局变量。

2. 解决方案一:回归CommonJS的兼容性策略

对于许多现有项目,特别是那些依赖大量第三方库的遗留系统,最简单直接的解决方案可能就是暂时回到CommonJS模式。这听起来像是技术上的倒退,但在实际开发中,稳定性往往比追求最新技术更重要。

2.1 项目级配置:package.json的type字段

Node.js从v13.2.0开始,允许通过package.json中的type字段来指定项目的默认模块系统。这个字段有两个可选值:

  • "commonjs":默认值,文件使用CommonJS模块系统
  • "module":使用ESM模块系统

如果你的项目意外地设置了type: "module",那么所有.js文件都会被当作ESM模块处理,这时使用__dirname就会报错。解决方法很简单:

{
  "name": "your-project",
  "version": "1.0.0",
  "description": "一个Node.js项目",
  "type": "commonjs",  // 明确指定使用CommonJS
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "express": "^4.18.0"
  }
}

或者,如果你不确定是否需要这个字段,直接删除type字段也可以,因为Node.js默认就是CommonJS模式。

提示:在团队协作项目中,建议明确设置type字段,避免因不同开发者的Node.js版本或配置差异导致的不一致问题。

2.2 文件扩展名的微妙差异

Node.js判断一个文件使用哪种模块系统,有一套完整的优先级规则。了解这些规则可以帮助你避免很多莫名其妙的错误:

  1. .mjs文件:总是作为ESM模块处理
  2. .cjs文件:总是作为CommonJS模块处理
  3. .js文件:取决于最近的package.json中的type字段
    • 如果type: "module",则作为ESM处理
    • 如果type: "commonjs"或未设置,则作为CommonJS处理

这里有一个实际开发中容易踩的坑:即使你的package.json设置了type: "commonjs",如果你不小心把文件保存为.mjs扩展名,Node.js还是会把它当作ESM模块处理。

# 错误示例:文件扩展名导致的问题
# 假设有一个文件叫 utils.mjs
node utils.mjs  # 会被当作ESM,使用__dirname会报错

# 解决方案:重命名文件
mv utils.mjs utils.js
node utils.js   # 现在会被当作CommonJS,__dirname可用

2.3 混合模块项目的处理策略

现实中的项目往往不是纯粹的一种模块系统。你可能需要同时使用CommonJS和ESM模块,特别是在渐进式迁移或使用第三方库时。Node.js提供了几种处理混合模块的策略:

策略一:使用完整的文件扩展名

  • CommonJS模块使用.cjs扩展名
  • ESM模块使用.mjs扩展名
  • 这样无论package.json如何配置,Node.js都能正确识别

策略二:动态导入(适用于ESM中调用CommonJS)

// 在ESM模块中导入CommonJS模块
async function loadCommonJSModule() {
  const commonJSModule = await import('./legacy-module.cjs');
  console.log(commonJSModule.default); // CommonJS的module.exports会变成default导出
}

策略三:创建适配层 对于大型项目,可以创建一个薄薄的适配层,统一处理模块系统的差异:

// adapter.js - 模块系统适配器
import { createRequire } from 'module';

// 在ESM中创建require函数
const require = createRequire(import.meta.url);

// 现在可以像在CommonJS中一样使用require
const path = require('path');
const fs = require('fs');

export { require, path, fs };

这种方法的优点是可以在ESM环境中继续使用那些尚未支持ESM的第三方库,为迁移争取时间。

3. 解决方案二:在ESM中实现__dirname功能

如果你决定拥抱ESM,或者项目要求必须使用ESM模块系统,那么就需要学会在ESM中获取当前文件目录的方法。这不仅仅是解决__dirname报错的问题,更是理解ESM设计哲学的好机会。

3.1 理解import.meta.url

ESM引入了一个新的元属性:import.meta.url。这个属性返回当前模块文件的URL字符串,格式通常是file://协议。这是ESM中获取模块自身信息的标准方式。

// 查看import.meta.url的内容
console.log(import.meta.url);
// 输出类似:file:///Users/username/project/src/index.js

import.meta.url有几个重要特点:

  • 它是只读的,不能在运行时修改
  • 它总是返回当前模块的绝对URL
  • 在浏览器和Node.js中行为一致(浏览器中可能是http/https协议)

3.2 完整的__dirname实现方案

要从import.meta.url获取类似CommonJS中__dirname的功能,需要经过几个步骤:

import { fileURLToPath } from 'url';
import { dirname, join } from 'path';

// 步骤1:将URL转换为文件路径
const __filename = fileURLToPath(import.meta.url);

// 步骤2:获取目录路径
const __dirname = dirname(__filename);

// 步骤3:使用路径(示例)
const configPath = join(__dirname, 'config', 'app.json');
console.log('配置文件路径:', configPath);

让我们分解一下这个过程:

  1. fileURLToPath():这个函数来自Node.js的url模块,专门用于将file://协议的URL转换为本地文件系统路径。它会自动处理不同操作系统的路径分隔符差异。

  2. dirname():来自path模块,用于获取路径的目录部分。如果路径是文件,返回其所在目录;如果已经是目录,返回父目录。

  3. join():同样是path模块的方法,用于安全地拼接路径片段。相比字符串拼接,join()能正确处理路径分隔符,避免出现//或缺少分隔符的问题。

3.3 创建可复用的工具函数

在实际项目中,你可能会在很多地方需要获取__dirname。为了避免重复代码,可以创建一个工具函数或工具模块:

// utils/path-utils.js
import { fileURLToPath } from 'url';
import { dirname } from 'path';

/**
 * 获取当前文件的目录路径(ESM版本的__dirname)
 * @param {string} metaUrl - import.meta.url
 * @returns {string} 目录路径
 */
export function getDirname(metaUrl) {
  return dirname(fileURLToPath(metaUrl));
}

/**
 * 获取当前文件的路径(ESM版本的__filename)
 * @param {string} metaUrl - import.meta.url
 * @returns {string} 文件路径
 */
export function getFilename(metaUrl) {
  return fileURLToPath(metaUrl);
}

/**
 * 一次性获取__dirname和__filename
 * @param {string} metaUrl - import.meta.url
 * @returns {Object} 包含dirname和filename的对象
 */
export function getPathInfo(metaUrl) {
  const filename = fileURLToPath(metaUrl);
  return {
    __filename: filename,
    __dirname: dirname(filename)
  };
}

使用这个工具模块:

// 在其他文件中使用
import { getPathInfo } from './utils/path-utils.js';

const { __dirname, __filename } = getPathInfo(import.meta.url);
console.log('当前文件:', __filename);
console.log('当前目录:', __dirname);

这种封装不仅减少了代码重复,还提高了代码的可测试性。你可以为这些工具函数编写单元测试,确保它们在不同环境下都能正常工作。

3.4 处理嵌套调用的特殊情况

有时候你可能会遇到这样的情况:在一个工具函数中需要获取调用者的文件路径,而不是工具函数自身的路径。这在日志记录、错误跟踪等场景中很常见。

// logger.js - 日志工具
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { createRequire } from 'module';
import { captureStackTrace } from 'console';

const require = createRequire(import.meta.url);

/**
 * 增强版路径获取,可以获取调用者信息
 * @param {number} depth - 调用栈深度(0表示当前函数,1表示调用者,以此类推)
 * @returns {Object} 路径信息
 */
export function getCallerPathInfo(depth = 1) {
  // 使用Error栈追踪获取调用信息
  const stack = new Error().stack.split('\n');
  
  // 解析调用栈获取文件信息(简化示例,实际实现会更复杂)
  let callerInfo = stack[depth + 2] || '';
  
  // 这里只是示意,实际项目中可能需要使用专门的栈解析库
  console.log('调用栈信息:', stack.slice(0, 5));
  
  // 回退方案:返回当前模块信息
  const currentFilename = fileURLToPath(import.meta.url);
  return {
    filename: currentFilename,
    dirname: dirname(currentFilename),
    callerInfo: callerInfo.trim()
  };
}

// 使用示例
export function logWithContext(message) {
  const pathInfo = getCallerPathInfo(1);
  console.log(`[${new Date().toISOString()}] ${pathInfo.filename}: ${message}`);
}

这种高级用法虽然复杂,但在构建框架或库时非常有用。它允许你的工具代码感知调用上下文,提供更精准的调试信息。

4. 解决方案三:构建时预处理与现代化工具链

对于大型项目或团队协作项目,手动处理模块系统差异可能会变得繁琐且容易出错。这时,现代化的构建工具和开发工具链可以大大简化工作流程。

4.1 使用TypeScript的路径处理

如果你使用TypeScript开发Node.js应用,TypeScript编译器可以帮你处理很多路径相关的问题。TypeScript对ESM和CommonJS都有很好的支持,并且可以在编译时进行转换。

// tsconfig.json配置示例
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext", // 或"ESNext"、"CommonJS"
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@utils/*": ["src/utils/*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

TypeScript的module配置项决定了输出的模块系统:

  • "CommonJS":输出为CommonJS模块,可以使用__dirname
  • "ESNext"或"NodeNext":输出为ESM模块,需要使用import.meta.url

对于需要兼容两种模块系统的库项目,可以配置多目标输出:

{
  "compilerOptions": {
    // 主配置...
  },
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist/cjs",
    "module": "CommonJS"
  }
},
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist/esm",
    "module": "ESNext"
  }
}

4.2 Babel转译方案

Babel是一个广泛使用的JavaScript编译器,可以将新版本的JavaScript代码转译成向后兼容的版本。对于模块系统的处理,Babel提供了灵活的配置选项。

// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', {
      targets: {
        node: 'current' // 根据当前Node.js版本决定转译特性
      }
    }]
  ],
  plugins: [
    // 处理ESM模块语法
    '@babel/plugin-transform-modules-commonjs',
    
    // 或者保持ESM语法
    // '@babel/plugin-syntax-import-meta'
  ]
};

Babel的一个强大之处在于可以根据需要选择性地转译某些语法特性。例如,你可以保持ESM的import/export语法,但通过插件处理import.meta.url:

// 自定义Babel插件示例(简化版)
// 这个插件会将import.meta.url转换为Node.js兼容的形式
const importMetaPlugin = () => {
  return {
    visitor: {
      MetaProperty(path) {
        if (path.node.meta.name === 'import' && 
            path.node.property.name === 'meta') {
          // 替换import.meta为兼容的实现
          // 实际实现会更复杂,这里只是示意
        }
      }
    }
  };
};

4.3 构建工具集成:Webpack、Rollup、Vite

现代前端构建工具都对模块系统有很好的支持,并且提供了处理路径问题的各种配置选项。

Webpack配置示例:

// webpack.config.js
const path = require('path');
const NodePolyfillPlugin = require('node-polyfill-webpack-plugin');

module.exports = {
  target: 'node', // 重要:指定构建目标为Node.js
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
    libraryTarget: 'commonjs2'
  },
  resolve: {
    extensions: ['.js', '.mjs', '.cjs', '.json'],
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  },
  plugins: [
    new NodePolyfillPlugin() // 提供Node.js核心模块的polyfill
  ],
  // 处理__dirname和__filename
  node: {
    __dirname: true, // 保留__dirname的真实值
    __filename: true // 保留__filename的真实值
  }
};

Vite配置示例(针对Node.js):

// vite.config.js
import { defineConfig } from 'vite';
import { resolve } from 'path';

export default defineConfig({
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.js'),
      formats: ['es', 'cjs'] // 同时输出ESM和CommonJS版本
    },
    rollupOptions: {
      // 确保Node.js内置模块不被打包
      external: ['fs', 'path', 'url', 'util']
    }
  },
  // 开发服务器配置
  server: {
    port: 3000
  }
});

这些构建工具不仅可以处理模块系统差异,还能提供代码分割、tree shaking、热重载等高级功能,大大提升开发体验和代码质量。

4.4 开发环境工具:nodemon、ts-node-dev

在开发过程中,实时重新加载和TypeScript支持非常重要。以下是一些常用开发工具的最佳实践配置:

nodemon配置(支持ESM):

{
  "watch": ["src"],
  "ext": "js,mjs,json",
  "ignore": ["src/**/*.test.js"],
  "exec": "node --loader ts-node/esm src/index.js", // 支持TypeScript和ESM
  "env": {
    "NODE_ENV": "development"
  }
}

ts-node-dev配置:

{
  "scripts": {
    "dev": "ts-node-dev --respawn --transpile-only --ignore-watch node_modules src/index.ts",
    "dev:esm": "ts-node-dev --esm --respawn --transpile-only src/index.ts"
  }
}

这些工具的正确配置可以让你在开发过程中几乎感受不到CommonJS和ESM的差异,专注于业务逻辑的实现。

5. 实战案例:从零构建一个双模块兼容的Node.js库

理论讲了很多,现在让我们通过一个完整的实战案例,看看如何在实际项目中优雅地处理模块系统问题。我们将构建一个简单的配置文件加载库,这个库需要同时支持CommonJS和ESM。

5.1 项目结构与初始化

首先创建项目的基本结构:

# 创建项目目录
mkdir config-loader && cd config-loader

# 初始化package.json
npm init -y

# 创建目录结构
mkdir -p src/{cjs,esm} tests dist

编辑package.json,设置双入口点:

{
  "name": "config-loader",
  "version": "1.0.0",
  "description": "一个支持CommonJS和ESM的配置文件加载器",
  "type": "commonjs", // 默认使用CommonJS
  "main": "./dist/cjs/index.js", // CommonJS入口
  "module": "./dist/esm/index.js", // ESM入口
  "exports": {
    ".": {
      "import": "./dist/esm/index.js", // ESM导入时使用
      "require": "./dist/cjs/index.js" // CommonJS导入时使用
    },
    "./package.json": "./package.json"
  },
  "scripts": {
    "build:cjs": "tsc --project tsconfig.cjs.json",
    "build:esm": "tsc --project tsconfig.esm.json",
    "build": "npm run build:cjs && npm run build:esm",
    "test": "jest",
    "test:cjs": "jest --config jest.cjs.config.js",
    "test:esm": "jest --config jest.esm.config.js"
  },
  "devDependencies": {
    "@types/jest": "^29.5.0",
    "@types/node": "^20.0.0",
    "jest": "^29.5.0",
    "ts-jest": "^29.1.0",
    "typescript": "^5.0.0"
  },
  "keywords": ["config", "loader", "commonjs", "esm"],
  "author": "Your Name",
  "license": "MIT"
}

5.2 核心代码实现

创建TypeScript源代码:

// src/types/config.ts
export interface ConfigOptions {
  path?: string;
  watch?: boolean;
  defaults?: Record<string, any>;
}

// src/utils/path-utils.ts
/**
 * 路径工具函数 - 处理不同模块系统的路径差异
 */

// 这些函数会在编译时被替换为适当的实现
export function getDirname(metaUrl?: string): string {
  // 这个实现会在编译时根据目标模块系统被替换
  throw new Error('This function should be replaced during build');
}

export function getFilename(metaUrl?: string): string {
  throw new Error('This function should be replaced during build');
}

// src/core/loader.ts
import { readFileSync, watch } from 'fs';
import { join } from 'path';
import { ConfigOptions } from '../types/config';
import { getDirname } from '../utils/path-utils';

export class ConfigLoader {
  private configPath: string;
  private config: Record<string, any> = {};
  private watcher: any = null;

  constructor(options: ConfigOptions = {}) {
    // 使用getDirname获取当前目录
    const dirname = getDirname();
    this.configPath = options.path || join(dirname, 'config.json');
    
    if (options.defaults) {
      this.config = { ...options.defaults };
    }
    
    this.loadConfig();
    
    if (options.watch) {
      this.startWatching();
    }
  }

  private loadConfig(): void {
    try {
      const content = readFileSync(this.configPath, 'utf8');
      const fileConfig = JSON.parse(content);
      this.config = { ...this.config, ...fileConfig };
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
        console.warn(`Failed to load config from ${this.configPath}:`, error);
      }
    }
  }

  private startWatching(): void {
    if (this.watcher) {
      this.watcher.close();
    }

    this.watcher = watch(this.configPath, (eventType) => {
      if (eventType === 'change') {
        console.log(`Config file changed, reloading...`);
        this.loadConfig();
      }
    });
  }

  get<T = any>(key: string, defaultValue?: T): T {
    const keys = key.split('.');
    let value: any = this.config;

    for (const k of keys) {
      if (value && typeof value === 'object' && k in value) {
        value = value[k];
      } else {
        return defaultValue as T;
      }
    }

    return value as T;
  }

  set(key: string, value: any): void {
    const keys = key.split('.');
    let config = this.config;

    for (let i = 0; i < keys.length - 1; i++) {
      const k = keys[i];
      if (!(k in config) || typeof config[k] !== 'object') {
        config[k] = {};
      }
      config = config[k];
    }

    config[keys[keys.length - 1]] = value;
  }

  stopWatching(): void {
    if (this.watcher) {
      this.watcher.close();
      this.watcher = null;
    }
  }
}

// src/index.ts
export { ConfigLoader } from './core/loader';
export type { ConfigOptions } from './types/config';

5.3 构建配置

创建不同的TypeScript配置文件:

// tsconfig.base.json - 基础配置
{
  "compilerOptions": {
    "target": "ES2022",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}

// tsconfig.cjs.json - CommonJS配置
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "module": "CommonJS",
    "outDir": "./dist/cjs",
    "rootDir": "./src"
  }
}

// tsconfig.esm.json - ESM配置
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "module": "ESNext",
    "outDir": "./dist/esm",
    "rootDir": "./src"
  }
}

创建构建脚本,处理路径工具函数的不同实现:

// scripts/build-utils.js
const fs = require('fs');
const path = require('path');

// CommonJS版本的路径工具实现
const cjsPathUtils = `
const path = require('path');

function getDirname() {
  return __dirname;
}

function getFilename() {
  return __filename;
}

module.exports = { getDirname, getFilename };
`;

// ESM版本的路径工具实现
const esmPathUtils = `
import { fileURLToPath } from 'url';
import { dirname } from 'path';

export function getDirname(metaUrl = import.meta.url) {
  return dirname(fileURLToPath(metaUrl));
}

export function getFilename(metaUrl = import.meta.url) {
  return fileURLToPath(metaUrl);
}
`;

// 写入文件
fs.writeFileSync(
  path.join(__dirname, '../dist/cjs/utils/path-utils.js'),
  cjsPathUtils
);

fs.writeFileSync(
  path.join(__dirname, '../dist/esm/utils/path-utils.js'),
  esmPathUtils
);

console.log('Path utilities built successfully!');

5.4 测试配置

创建针对两种模块系统的测试配置:

// jest.cjs.config.js - CommonJS测试配置
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  testMatch: ['**/*.test.ts'],
  moduleFileExtensions: ['ts', 'js', 'json'],
  transform: {
    '^.+\\.ts$': ['ts-jest', {
      tsconfig: 'tsconfig.cjs.json'
    }]
  },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1'
  }
};

// jest.esm.config.js - ESM测试配置
export default {
  preset: 'ts-jest/presets/default-esm',
  testEnvironment: 'node',
  testMatch: ['**/*.test.ts'],
  moduleFileExtensions: ['ts', 'js', 'json'],
  extensionsToTreatAsEsm: ['.ts'],
  moduleNameMapper: {
    '^(\\.{1,2}/.*)\\.js$': '$1',
    '^@/(.*)$': '<rootDir>/src/$1'
  },
  transform: {
    '^.+\\.ts$': ['ts-jest', {
      tsconfig: 'tsconfig.esm.json',
      useESM: true
    }]
  }
};

创建测试文件:

// tests/config-loader.test.ts
import { ConfigLoader } from '../src/core/loader';
import { writeFileSync, unlinkSync } from 'fs';
import { join } from 'path';
import { getDirname } from '../src/utils/path-utils';

describe('ConfigLoader', () => {
  const testDir = getDirname();
  const testConfigPath = join(testDir, 'test-config.json');
  
  beforeEach(() => {
    // 创建测试配置文件
    writeFileSync(testConfigPath, JSON.stringify({
      database: {
        host: 'localhost',
        port: 5432
      },
      server: {
        port: 3000
      }
    }));
  });

  afterEach(() => {
    // 清理测试文件
    try {
      unlinkSync(testConfigPath);
    } catch (error) {
      // 文件可能已被删除
    }
  });

  test('should load config from file', () => {
    const loader = new ConfigLoader({ path: testConfigPath });
    expect(loader.get('database.host')).toBe('localhost');
    expect(loader.get('server.port')).toBe(3000);
  });

  test('should use defaults when file not found', () => {
    const loader = new ConfigLoader({
      path: '/non-existent/path.json',
      defaults: { app: { name: 'TestApp' } }
    });
    expect(loader.get('app.name')).toBe('TestApp');
  });

  test('should allow setting values', () => {
    const loader = new ConfigLoader({ path: testConfigPath });
    loader.set('new.key', 'value');
    expect(loader.get('new.key')).toBe('value');
  });
});

5.5 使用示例

最后,提供使用示例:

// examples/cjs-usage.js - CommonJS使用示例
const { ConfigLoader } = require('config-loader');

const loader = new ConfigLoader({
  path: './config.json',
  watch: true,
  defaults: {
    app: {
      name: 'MyApp',
      version: '1.0.0'
    }
  }
});

console.log('Database host:', loader.get('database.host', 'localhost'));
console.log('App name:', loader.get('app.name'));

// 修改配置
loader.set('app.debug', true);

// 示例结束,停止监听
setTimeout(() => {
  loader.stopWatching();
  console.log('Stopped watching config file');
}, 5000);
// examples/esm-usage.mjs - ESM使用示例
import { ConfigLoader } from 'config-loader';

const loader = new ConfigLoader({
  path: './config.json',
  watch: true,
  defaults: {
    app: {
      name: 'MyApp',
      version: '1.0.0'
    }
  }
});

console.log('Database host:', loader.get('database.host', 'localhost'));
console.log('App name:', loader.get('app.name'));

// 修改配置
loader.set('app.debug', true);

// 示例结束,停止监听
setTimeout(() => {
  loader.stopWatching();
  console.log('Stopped watching config file');
}, 5000);

通过这个完整的实战案例,我们可以看到,构建一个同时支持CommonJS和ESM的库需要一些额外的工作,但这些工作带来的好处是明显的:你的库可以同时服务于使用不同模块系统的项目,大大提高了兼容性和可用性。

在实际开发中,我遇到过不少因为模块系统不兼容而导致的问题。有一次,一个团队在迁移到ESM时,发现他们依赖的一个关键库只支持CommonJS,导致整个迁移计划受阻。如果他们提前考虑了双模块支持,这个问题完全可以避免。这也是为什么我现在在新项目开始时,就会考虑模块系统的兼容性问题——这看起来是额外的工作,但实际上能节省大量的后续维护成本。

Open UI5 源代码解析之1352:PropertyEditor.js PropertyEditor.js 摘要 该文件是SAP OpenUI5框架中sap.ui.integration模块的设计时编辑器核心组件,作为属性编辑器的通用包装容器。其主要功能包括: 桥接架构:连接上层BaseEditor、中间配置对象与底层BasePropertyEditor子类,实现可插拔的编辑器机制 动态编辑器创建:通过PropertyEditorFactory根据配置类型动态创建具体编辑器实例 双重配置来源:支持通过propertyName从BaseEditor获取配置或直接传入config对 阅读详情

相关推荐

Spring AI实现RAG(检索增强生成)详解与实践

支持多种文件格式的文档加载实现高效的向量存储和检索构建智能的问答系统提供可追溯的知识服务RAG技术为AI应用提供了强大的知识增强能力,使得模型能够基于实际文档生成准确、可靠的回答。

beiduofen2011的专栏 1238

关于 __dirname和__filename介绍以及使用场景

解释__dirname和__filename代表什么,以及__dirname的使用场景

weixin_47505105的博客 1万+

ros2 humble cartographer 2D激光雷达建图

Cartographer是一个支持多平台和传感器配置的SLAM的系统。 本文主要讲述在ros2 humble系统上cartographer的安装方法,以及如何配置参数实现自己的ros2 bag包的离线建图。

weixin_43087302的博客 6939

nodejs中的__dirname 和 __filename是什么意思?

console.log("相对路径.:",path.resolve(".", "index.js"));

qq_14820093的博客 3811

工业质检新利器:手把手教你用AD-DINOv3实现零样本缺陷检测(附MVTec AD实战)

本文详细介绍了AD-DINOv3框架在工业质检中的应用,通过零样本异常检测技术解决传统方法的标注成本高和泛化能力弱问题。文章包含技术原理解析、MVTec AD数据集实战部署、模型训练流程及工业部署优化技巧,帮助读者快速掌握这一创新技术。

seed的博客 763

热测试芯片(TTC)在ASIC热设计中的关键作用与应用

热测试芯片(TTC)是半导体热管理领域的重要工具,通过物理仿真技术精确测量芯片温度分布。其核心原理是利用金属膜电阻和二极管传感器阵列,实现微米级空间分辨率和毫秒级动态响应。在工程实践中,TTC能有效预防热岛效应、优化散热设计,特别适用于AI加速芯片、3D堆叠封装等先进半导体器件。相比传统热仿真软件,TTC提供更真实的温度数据,帮助工程师在流片前发现潜在热问题。随着1mm微型单元技术的突破,TTC已能检测0.3mm²微小区域的热变化,满足GaN/SiC等宽禁带半导体器件的测试需求。

weixin_33688840的博客 508

两个全局变量__dirname和__filename 、fs模块常用功能进一步介绍

读取文件成功err返回null,data返回文件内容,因为用了toString()转换为了字符串,我们能看到测试文本这几个字,如果不用则打印的是二进制数据。注意我们在写入文件时,如果已经该写入路径下已经存在该文件,那么我们新写的会覆盖掉原来的,如果没有就创建该文件。callback(err)回调函数,err为追加失败返回的错误信息,追加成功err为null。callback(err)回调函数,err删除文件时的错误信息,成功时err为null。callback(err),操作完成回调函数;...

m0_59345890的博客 3856

【GameFramework框架内置模块】9、有限状态机(FSM)

这篇文章分享FSM有限状态机,了解过StarForce源码的同学相比都比较了解,GameFramework框架给我们抽象了一个Procedure流程的概念。这个Procedure流程可以方便的处理我们游戏从启动到游戏的各个流程阶段的处理逻辑,实现的核心就是FSM状态机,内部的Procedure流程就是不同的FsmState状态。接下来,就是FSM有限状态机的教程了。有限状态机(Finite-state machine,FSM)

优快云博客专家,Unity3D高级软件工程师,《Unity 3D从入门到实战》作者。 2533

node中的__dirname

先说结论:__dirname指的是当前文件所在文件夹的绝对路径。 测试路径如下: 即 根目录/dir0.js 根目录/path1/dir1.js 根目录/paht1/path2/dir2.js 每个dir*.js文件里的代码都只有一行(*号代表数字,下同): console.log('dir*: '+__dirname); 一....

weixin_30810583的博客 1万+

链路状态路由协议 OSPF (一)

本章将会学习链路状态路由协议OSPF,理解OSPF的基本概念,工作原理,掌握OSPF配置基本命令。本章重点,OSPF的基本概念,OSPF配置。

yj11290301的博客 3475

UE5 推荐的资产命名规范

在开发阶段中,随着 虚幻引擎(UE)(Unreal Engine (UE)) 项目日益复杂,内容浏览器(Content Browser) 中的 资产(Assets) 列表会不断扩充。这会导致资产出现各种变体版本,也会导致资产名称因过于相似而造成混淆。比如,你有一个文件夹名为"Soldier",这个文件夹中有蓝图、纹理和模型,它们的名称中都带"Soldier"一词,但在简单列表中,很难区别它们。

一碗清深的博客 3374

【Node.js】从CommonJS到ESM:__dirname的兼容性解决方案全解析

本文深入解析了Node.js开发中常见的`__dirname is not defined`错误,该问题源于从CommonJS向ESM模块系统迁移时的兼容性差异。文章提供了三种核心解决方案:通过修改package.json或文件后缀回归CommonJS;在ESM中使用`import.meta.url`自行实现`__dirname`;以及为库开发者设计的双模块兼容方案,帮助开发者根据项目需求平滑过渡。

m2n3b4v5c6的博客 611

滤波电路Multisim电路仿真实验汇总——硬件工程师笔记

本文系统介绍了滤波电路的基础知识及其应用。首先按频率选择性和实现方式对滤波电路进行分类,详细阐述了低通、高通、带通、带阻滤波器以及无源、有源滤波器的特点。接着重点分析了桥式整流电容滤波、电感滤波、LC滤波和CRπ型滤波四种典型电路的工作原理、应用场景及设计要点。通过理论公式和实际案例,指导如何选择滤波元件参数,考虑寄生效应,并通过仿真验证设计效果。最后提供了硬件工程师笔试面试相关的技术资源链接,包括器件知识、电路仿真及企业真题解析等内容。本文可作为电子电路设计领域工程技术人员的学习参考资料。

XU157303764的博客 2166

从CommonJS到ESM:Node.js模块系统迁移中的__dirname替代方案详解

本文详细解析了Node.js从CommonJS迁移到ESM模块系统时,如何解决`__dirname is not defined`错误。核心方案是利用`import.meta.url`结合`fileURLToPath`和`path.dirname`来替代原有的`__dirname`功能,并提供了封装工具函数、路径处理最佳实践以及渐进式迁移策略,帮助开发者彻底理解模块系统差异并优雅完成迁移。

linux的博客 725

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

【Node.js】从CommonJS到ESM:__dirname的替代方案与实践

本文深入探讨了Node.js从CommonJS向ESM模块系统迁移时,经典全局变量__dirname失效的问题。文章分析了错误根源,并提供了两种核心解决方案:临时切换回CommonJS模式,或使用`import.meta.url`结合`fileURLToPath`与`dirname`方法在ESM中重构路径获取逻辑。同时,文中还分享了文件扩展名、动态导入等实践技巧与渐进式项目迁移策略,帮助开发者平滑过渡。

ol78901234的博客 224

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

node的内置常量__dirname和__filename

基本介绍 __dirname和__filename是node内置的一个常量 内置的意思就是不需要额外去定义它 它的作用就是表示当前所在的地址(__dirname和__filename 表示的地址不同) __dirname当前文件(你用node运行的文件)所在的文件夹地址 __filename当前文件的完整地址(精确到你所运行的那个js文件) 1.__dirname  &nb...

北芒的博客 4535

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

Node常用的内置模块

文章目录一、Path 路径模块1.1 案例引入 之 path.resolve 一、Path 路径模块 path模块用于对路径和文件进行处理,提供了很多好用的方法。 需求引出: 我们知道在 Mac OS、 Linux和 window上的路径是不一样的 window上会使用\或者\\来作为文件路径的分隔符,当然目前也支持/ 在 Mac OS、 Linux的Uniⅸ操作系统上使用/来作为文件路径的分隔符 那么如果我们在 window上使用\来作为分隔符开发了一个应用程序,要部署到 Linux上面应该怎么办

欢迎访问我的主页:https://www.bookbook.cc 981

__dirname在项目当中是干什么的

__dirname(两个下划线) 它是node 环境下的全局内置变量 当前文件所在文件夹的绝对路径 __dirname值 (自己电脑上的):C:\Windows\System32\cmd.exe // hack: `true; @import "${path.join(__dirname + "/src/styles/cover.less" )}";`, ...

weixin_59519449的博客 1294

Node.js 中的 __dirname 与 __filename:理解文件路径的神秘面纱

_dirname是一个字符串,表示当前文件所在目录的绝对路径。这个变量非常有用,当你需要引用同级或上级目录中的文件时,可以避免硬编码路径,从而提高代码的可移植性。无论你的脚本在哪里被执行,__dirname总是指向包含该脚本文件的目录。1.__dirname 是指当前执行脚本所在的目录的路径。2.它总是返回文件所在的目录路径,不包括文件名。3.在模块中使用时,它是动态的,会根据当前模块的位置而变化。

随笔 1550

__dirname和__filename

Node中的__dirname和__filename的基本使用

webchang的博客 3056

nodeJS中的__dirname

__dirname是node.js中的一个全局变量,它可以获得当前文件所在目录从盘符开始的全路径, console.log(__dirname) H:\Code\03high\03socketIO\groupChat0

qq_38796823的博客 2436

fs模块 路径问题 __dirname(代表当前文件所在路径)

使用__dirname(表示当前文件所在的目录)进行改进

weixin_47295886的博客 999

node 项目中 __dirname / __filename 是什么,为什么有时候不能用?

是一个全局变量,可在任何地方使用,用于获取当前模块文件所在的目录路径。它不是由用户定义的,而是由 Node.js 运行时提供的。是一个全局变量,而不是一个模块的属性。因此,在 CommonJS 模块中,你可以直接使用。是 Node.js 中的一个特殊变量,表示当前执行脚本所在的目录的绝对路径。可以方便地构建文件的绝对路径,而不受当前工作目录的影响。来获取当前模块的 URL 信息。不再被支持,你需要使用。在 Node.js 中,,但在 ES6 模块中,

有一棵树的博客 1422

Node.js 中 __dirname 和 ./ 的区别

stackoverflow 地址 概要 Node.js 中,__dirname 总是指向被执行 js 文件的绝对路径,所以当你在 /d1/d2/myscript.js 文件中写了 __dirname, 它的值就是 /d1/d2 。 相反,./ 会返回你执行 node 命令的路径,例如你的工作路径。 有一个特殊情况是在 require() 中使用 ./ 时,这时的路径就会是含有 requi

zsensei的博客 12万+

nodejs中的__filename和__dirname

__filename:返回当前模块文件被解析过后的绝对路径, 使用__filename变量获取当前模块文件的带有完整绝对路径的文件名 __dirname:返回当前模块文件解析过后所在的文件夹(目录)的绝对路径 使用__dirname变量获得当前文件所在目录的完整目录名 __dirname等同于path.dirname(__filename) 验证示例

YQRQR的博客 3240

process.cwd() 与 __dirname 的区别

Node.js 中的 __dirname 和 process.cwd() 都是用于获取文件系统路径的全局变量和方法,但它们有不同的含义和用途。

天问博客-专注于大前端技术 5054
上一篇: 飞书机器人消息推送避坑指南:Java版SDK这些细节要注意
下一篇: 避坑指南:权重计算中90%人会犯的3个错误(附SPSSAU操作修正)
info6
博客等级 码龄3年 68粉丝 1629原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值