使用 ES6 模块时 Node.js 中 __dirname 的替代方案

Posted

技术标签:

【中文标题】使用 ES6 模块时 Node.js 中 __dirname 的替代方案【英文标题】:Alternative for __dirname in Node.js when using ES6 modules 【发布时间】:2018-03-26 11:32:59 【问题描述】:

我在运行我的 Node 应用程序时使用标志 --experimental-modules 以使用 ES6 模块。

但是,当我使用此标志时,元变量 __dirname 不可用。是否有另一种方法可以获取与此模式兼容的__dirname 中存储的相同字符串?

【问题讨论】:

这是一个让 __dirname 在 ES6 中工作的变通方法,看看 【参考方案1】:

从 Node.js 10.12 开始,有一个替代方案不需要创建多个文件并跨平台处理文件名中的特殊字符:

import  dirname  from 'path';
import  fileURLToPath  from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));

【讨论】:

很酷,但是如何设置 WebStrom IDE 来理解这种语法? 这个方案相比path.dirname(process.argv[1])有什么优势? @DanDascalescu __dirname不是进程目录路径,是当前模块的目录路径。 这在 Linux 下对我不起作用。其他人遇到过同样的问题吗? 在 Windows 上(在其他平台上未经测试),此解决方案会导致路径中出现前导 `` 字符,这会破坏很多东西。我可以对其进行修整以解决问题,但我不确定该解决方案是否跨平台兼容。【参考方案2】:

2021年最标准化的方式

import  URL  from 'url'; // in Browser, the URL in native accessible on window

const __filename = new URL('', import.meta.url).pathname;
// Will contain trailing slash
const __dirname = new URL('.', import.meta.url).pathname;

忘记join从当前文件创建路径,只需使用URL

const pathToAdjacentFooFile = new URL('./foo.txt', import.meta.url).pathname;
const pathToUpperBarFile = new URL('../bar.json', import.meta.url).pathname;

【讨论】:

感谢您的回答,不过有一个问题,返回路径中的空格替换为%20,我确定会不会有问题 @AlexG 结果显然是 URL 编码的,为了更好的输出,它必须用 decodeURIComponent 解码。 我切换到节点 16,从现在开始我的项目是 ESM,这是我需要让我的项目工作的明确答案,因为它与 __dirnamepath.join 崩溃了 在 Windows 中不起作用 - 导致类似 "/C:/..." - 更好地使用 fileUrlToPath: nodejs.org/api/url.html#url_url_fileurltopath_url 甚至不需要导入URL,因为它是available as a global。【参考方案3】:

对于节点 10.12 +...

假设您正在使用模块,此解决方案应该可以工作,并且还为您提供 __filename 支持

import path from 'node:path';
import  fileURLToPath  from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

好消息是你也只需要两行代码就可以支持 CommonJS 模块的 require() 了。为此,您将添加:

import  createRequireFromPath  from 'module';
const require = createRequireFromPath(__filename); 

【讨论】:

没有createRequireFromPath - 它是createRequire 并以“import.meta.url”作为参数【参考方案4】:

已经有人提议通过import.meta 公开这些变量,但现在,你需要一个我发现here 的hacky 解决方法:

// expose.js
module.exports = __dirname;

// use.mjs
import expose from './expose.js';
const __dirname = expose;

【讨论】:

大括号步骤不是必需的,而且实际上不推荐使用此方法,因为如果./expose.js 在另一个目录中,它将给出该目录的值__dirname,而不是当前脚本的值。 ..看我的回答 这是我发现让__dirname 在 CJS 和 ESM 模式下工作的唯一方法。 应该是expose.cjs 而不是expose.js?您可以使用import __dirname from './expose.cjs'; 仅在一行中导入?【参考方案5】:

在大多数情况下,使用 Node.js 的原生内容(带有 ES 模块)而不是外部资源,在大多数情况下完全不需要使用 __filename__dirname。大多数(如果不是全部)用于读取(流式传输)的本机方法支持 new URL + import.meta.url正如官方文档本身所建议的那样

No __filename or __dirname No JSON Module Loading No require.resolve

正如您在方法的描述中看到的,path参数显示了支持的格式,其中包括<URL>,示例:

Method path param supports
fs.readFile(path[, options], callback) <string>, <Buffer>, <URL>, <integer>
fs.readFileSync(path[, options]) <string>, <Buffer>, <URL>, <integer>
fs.readdir(path[, options], callback) <string>, <Buffer>, <URL>
fs.readdirSync(path[, options]) <string>, <Buffer>, <URL>, <integer>
fsPromises.readdir(path[, options]) <string>, <Buffer>, <URL>
fsPromises.readFile(path[, options]) <string>, <Buffer>, <URL>, <FileHandle>

因此,new URL('<path or file>', import.meta.url) 可以解决,您无需处理字符串并创建稍后连接的变量。

示例:

了解如何在不需要__filename 或任何解决方法的情况下读取与脚本同一级别 的文件:

import  readFileSync  from 'fs';

const output = readFileSync(new URL('./foo.txt', import.meta.url));

console.log(output.toString());

列出脚本目录下的所有文件:

import  readdirSync  from 'fs';

readdirSync(new URL('./', import.meta.url)).forEach((dirContent) => 
  console.log(dirContent);
);

注意:在示例中我使用同步函数只是为了更容易复制和执行。

如果打算创建一个依赖于第三方的“自己的日志”(或类似的东西),手动完成一些事情是值得的,但在语言和 Node.js 中这不是必需的,@987654372 @ 完全有可能不依赖__filename__dirname,因为带有new URL 的原生资源已经解决了它。


请注意,如果您有兴趣在战略时刻使用 require 之类的东西并且需要主脚本的绝对路径,您可以使用 module.createRequire(filename)(仅限 Node.js v12.2.0 +)与 import.meta.url在当前脚本级别以外的级别加载脚本,因为这已经有助于避免需要__dirname,例如使用import.meta.urlmodule.createRequire

import  createRequire  from 'module';

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

// foo-bar.js is a CommonJS module.
const fooBar = require('./foo-bar');

fooBar();

来源foo-bar.js

module.exports = () => 
    console.log('hello world!');
;

这类似于使用没有“ECMAScript 模块”

const fooBar = require('./foo-bar');

【讨论】:

如果我按预期消化所有这些内容,那么您基本上说的与接受的答案和您链接的节点文档相同:“__filename__dirname 用例可以是通过import.meta.url复制。” @jacobq 这与接受的答案不同,相反,它表示实际上大多数情况下不需要文件名和目录名,因为所有本机 NodeJS API 都可以识别 @987654389 @ 班级。我打算以特定方式指出有关“fileURLToPath”使用的一些问题,但在我看来,答案中的解释足以理解我们不需要文件名和目录名。感谢您的评论。 一个常见的用例是通过相对于脚本文件位置的路径来引用文件,例如fs.readFile(path.resolve(__dirname, ...))。使用 ESM 时,__dirname 可以替换为 path.dirname(url.fileURLToPath(import.meta.url));。如果不使用__dirnameimport.meta.url,你怎么能做到这一点?似乎有必要通过某种方式“知道你 [脚本/模块] 在哪里”,无论是否将其表示为 URL 等路径。你似乎说 URL 正在解决问题,但 @987654396 @只能在使用import.meta.url之类的东西通知它时才能解决。 @jacobq 我在答案的任何时候都没有说没有“import.meta.url”。在fs.readFile(<path>) 中,路径参数支持new URL。所有原生 NodeJS API 都支持原生。我将重复我在回答中所说的话:在大多数情况下使用 __filename 和 __dirname 可能完全没有必要 ...我没有说你不会有时间'不使用它,但是对于绝大多数您并不真正需要“dirname”或“filename”,“new URL + import.meta.url”解决它,这是来自文档本身我没有说它。 .. 这是本帖中对我来说最有价值的答案,谢谢!【参考方案6】:

我用过:

import path from 'path';

const __dirname = path.resolve(path.dirname(decodeURI(new URL(import.meta.url).pathname)));

decodeURI 很重要:在我的测试系统的路径中使用了空格和其他内容。

path.resolve() 处理相对 url。

编辑:

修复以支持 Windows (/C:/... => C:/...):

import path from 'path';

const __dirname = (() => let x = path.dirname(decodeURI(new URL(import.meta.url).pathname)); return path.resolve( (process.platform == "win32") ? x.substr(1) : x ); )();

【讨论】:

BABEL 还是有问题,它不会接受 import.meta 因为那里不接受 import :) 您可以使用 Rollup(使用 rollup-plugin-babel)进行捆绑或使用 esm 进行实时执行,例如测试【参考方案7】:

我创建了这个模块es-dirname,它将返回当前脚本目录名。

import dirname from 'es-dirname'

console.log(dirname())

WindowsLinux 上的 CommonJs 脚本和 ES 模块 中都可以使用。

如果出现错误,请在此处打开一个问题,因为到目前为止脚本在我的项目中一直在运行,但在其他一些情况下它可能会失败。因此,请勿在生产环境中使用它。这是一个临时解决方案,因为我相信 Node.js 团队会在不久的将来发布一个强大的方法来做到这一点。

【讨论】:

你故意出错并检查堆栈......这很聪明。 谈论过度工程 :) TS 用户注意:如果您还没有准备好使用@ts-expect-error,请不要打扰(尽管它确实很聪明)。为什么不只是 throw 一个错误?【参考方案8】:
import path from 'path';
const __dirname = path.join(path.dirname(decodeURI(new URL(import.meta.url).pathname))).replace(/^\\([A-Z]:\\)/, "$1");

此代码也适用于 Windows。 (替换在其他平台上是安全的,因为 path.join 仅在 Windows 上返回反斜杠分隔符)

【讨论】:

试过了,但在 Windows 上对我不起作用:Error: ENOENT: no such file or directory, open 'C:\C:\Projects\...such and such...\SomeFile.ts'。但是,BananaAcid 的 edited code 有效。 我为您的答案找到了一个简短的解决方案,可以在 Windows 上工作(确认工作):[existing code].replace(/^\\([A-Z]:\\)/, "$1"); 添加为编辑建议。【参考方案9】:

我使用此选项,因为路径以file:// 开头,只需删除该部分即可。

const __filename = import.meta.url.slice(7);
const __dirname = import.meta.url.slice(7, import.meta.url.lastIndexOf("/"));

【讨论】:

在 Windows 上,这需要是 slice(8),否则会导致 /C:/...,解析为 C:/C:/...。有关跨平台版本,请参阅 BananaAcid 的 edited code。【参考方案10】:

正如Geoff 指出的,以下代码返回的不是模块的路径,而是工作目录。

import path from 'path';
const __dirname = path.resolve();

适用于--experimental-modules

【讨论】:

不幸的是,这只是返回当前工作目录,而不是模块的路径。 nodejs.org/docs/latest-v10.x/api/… 如果目标是找到您的“公共”网络目录,这将非常有用。 当前工作目录和__dirname有什么区别?【参考方案11】:
import path from 'path';
import  fileURLToPath  from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);


// do not use the following code which is bad for CJK characters
const __filename = new URL('', import.meta.url).pathname;

【讨论】:

【参考方案12】:

在您的项目根目录中创建一个名为 root-dirname.js 的文件,其中包含以下内容:

import  dirname  from 'path'

const dn = dirname(new URL(import.meta.url).pathname)
const __dirname = process.platform === 'win32' ? dn.substr(1) : dn // remove the leading slash on Windows
export const rootDirname = __dirname

然后当你想要项目根文件夹的路径时,只需导入rootDirname

除此之外,Rudolf Gröhling 的回答也是正确的。

【讨论】:

【参考方案13】:

另一种选择

import createRequire from 'module'; // need node v12.2.0

const require = createRequire(import.meta.url);
const __dirname = require.resolve.paths('.')[0];

【讨论】:

【参考方案14】:
process.cwd()

来自文档:

process.cwd() 方法返回当前工作目录 Node.js 进程。

【讨论】:

“工作目录”与__dirname不同(虽然它们可能相等)

以上是关于使用 ES6 模块时 Node.js 中 __dirname 的替代方案的主要内容,如果未能解决你的问题,请参考以下文章

node.js中的ES6变量导入名称?

如何欺骗 Node.js 将 .js 文件加载为 ES6 模块?

Node.js 全球化 es6 模块以像 ImportScripts 一样工作

使用Node.js需要与ES6导入/导出

es6和node.js模块的区别

Node.js 和 ES6 中的 module.exports 与 export default