从运行中的node.js应用程序确定项目根目录

488

除了process.cwd()之外,是否有其他方法可以获取当前项目根目录的路径? Node是否实现了像Ruby中的属性Rails.root一样的东西。我正在寻找一些常数和可靠的方法。


1
你有没有可能取消接受错误的答案? - Dave Newton
19
尝试使用process.env.PWD…请看下面我的回答。 - Alexander Mills
尝试使用path.dirname(require.main.filename) - YATIN GUPTA
35个回答

865

有许多方法可以处理这个问题,每种方法都有其优缺点:

require.main.filename

引用自http://nodejs.org/api/modules.html

当一个文件被直接在Node中运行时,require.main会被设置为它的module。这意味着你可以通过测试require.main === module来确定一个文件是否被直接运行。

因为module提供了一个filename属性(通常等同于__filename),所以可以通过检查require.main.filename来获取当前应用程序的入口点。

所以如果您想要您的应用程序的基本目录,可以这样做:

const { dirname } = require('path');
const appDir = dirname(require.main.filename);

优缺点

大部分情况下,这个方法都能很好地工作,但如果你正在使用像 pm2 这样的启动器或运行 mocha 测试时,这个方法会失败。同时,在使用 Node.js ES 模块时,require.main 不可用,这也不起作用。

module.paths

Node 将所有模块搜索路径都发布到 module.paths 中。我们可以遍历这些路径并选择第一个解析成功的路径。

async function getAppPath() {
  const { dirname } = require('path');
  const { constants, promises: { access } } = require('fs');
  
  for (let path of module.paths) {
    try {
      await access(path, constants.F_OK);
      return dirname(path);
    } catch (e) {
      // Just move on to next path
    }
  }
}

优缺点

这种方法有时会起作用,但在打包时不可靠,因为它可能返回安装软件包的目录,而不是安装应用程序的目录。

使用全局变量

Node拥有一个全局命名空间对象称为global——任何你附加到此对象的内容都将在你的整个应用程序中可用。因此,在你的index.js(或app.js或任何你的主应用文件命名为)中,你可以定义一个全局变量:

// index.js
var path = require('path');
global.appRoot = path.resolve(__dirname);

// lib/moduleA/component1.js
require(appRoot + '/lib/moduleB/component2.js');

优缺点

虽然功能一直稳定,但是需要依赖全局变量,这意味着您不能轻松地重用组件等。

process.cwd()

此函数返回当前工作目录。由于完全取决于进程启动的目录,因此不可靠:

$ cd /home/demo/
$ mkdir subdir
$ echo "console.log(process.cwd());" > subdir/demo.js
$ node subdir/demo.js
/home/demo
$ cd subdir
$ node demo.js
/home/demo/subdir

应用程序根路径

为了解决这个问题,我创建了一个名为app-root-path的节点模块。使用方法很简单:

const appRoot = require('app-root-path');
const myModule = require(`${ appRoot }/lib/my-module.js`);

App-root-path模块使用多种技术来确定应用程序的根路径,考虑到全局安装的模块(例如,如果您的应用程序在/var/www/中运行,但该模块安装在~/.nvm/v0.x.x/lib/node/中)。它不会始终100%地工作,但在大多数常见情况下,它将起作用。

优缺点

在大多数情况下无需配置即可正常工作。还提供了一些方便的附加方法(请参见项目页面)。最大的缺点是,如果:

  • 您正在使用像pm2这样的启动器
  • 并且该模块未安装在您的应用程序的node_modules目录内(例如,如果您将其全局安装)

您可以通过设置APP_ROOT_PATH环境变量或调用模块上的.setPath()来解决此问题,但在这种情况下,最好使用global方法。

NODE_PATH环境变量

如果您正在寻找一种确定当前应用程序根路径的方法,则上述解决方案中的一种可能最适合您。另一方面,如果您要解决可靠地加载应用程序模块的问题,我强烈建议查看NODE_PATH环境变量。

Node的模块系统在各种位置查找模块。其中一个位置是process.env.NODE_PATH指向的任何位置。如果设置了此环境变量,则可以使用标准模块加载器require加载模块而无需进行其他更改。

例如,如果将NODE_PATH设置为/var/www/lib,则以下内容将正常工作:

require('module2/component.js');
// ^ looks for /var/www/lib/module2/component.js

一个很好的方法是使用 npm

{
  "scripts": {
    "start": "NODE_PATH=. node app.js"
  }
}

现在,您可以使用npm start启动应用程序,并且一切都很顺利。我将此与我的enforce-node-path模块结合使用,该模块可防止在没有设置NODE_PATH的情况下意外加载应用程序。要更精细地控制强制执行环境变量,请参见checkenv

一个小问题: 必须在node应用程序之外设置NODE_PATH。您不能像process.env.NODE_PATH = path.resolve(__dirname)这样做,因为模块加载器在应用程序运行之前缓存它将搜索的目录列表。

[添加于4/6/16] 另一个非常有前途的模块,试图解决这个问题是wavy


3
相关但不完全相关:这是一种非常巧妙的方式来组织你的Node项目,使你不必过多地担心这个问题:http://allanhortle.com/2015/02/04/store-your-client-code-in-node_modules.html - inxilpro
1
我不知道是pm2发生了变化还是Node.js发生了变化,但是require.main.filename似乎可以与pm2一起使用。不确定mocha是否也可以。 - Justin Warkentin
9
path.parse(process.mainModule.filename).dir - Cory Robinson
1
为什么在这里使用 path.resolve(__dirname);,它与 __dirname 有什么不同? - faressoft
1
"start": "NODE_PATH=. node app.js" results in the literal . being returned inside the app for me. I had to use:NODE_PATH=$PWD to get the actual path in process.env.NODE_PATH - Jon Wyatt
显示剩余17条评论

90

__dirname不是全局变量,它只是本地模块的变量,所以每个文件都有自己不同的值。

如果您想要运行进程的根目录,那么您可能需要使用process.cwd()

如果您想要可预测性和可靠性,则可能需要将其作为应用程序的要求设置某个环境变量。您的应用程序查找MY_APP_HOME(或其他)是否存在,如果存在,并且该目录包含您的应用程序,则一切正常。如果未定义或目录不包含您的应用程序,则应该退出并提示用户创建变量。它可以作为安装过程的一部分进行设置。

您可以使用类似process.env.MY_ENV_VARIABLE的方法在node中读取环境变量。


5
如果小心使用,这样做可能效果很好。但是在执行 bin/server.jscd bin && server.js 时会得到不同的结果。(假设这些 js 文件已标记为可执行文件) - Myrne Stol
2
使用 process.cwd() 对我非常有效,即使在运行 mocha 测试时也是如此。谢谢! - Diogo Eichert

58

1- 在项目根目录下创建一个名为settings.js的文件。

2- 在这个文件中添加以下代码。

module.exports = {
    POST_MAX_SIZE : 40 , //MB
    UPLOAD_MAX_FILE_SIZE: 40, //MB
    PROJECT_DIR : __dirname
};

在node_modules中创建一个名为“settings”的新模块,并在模块index.js中编写以下代码:

module.exports = require("../../settings");

4- 任何时候您想要访问您的项目目录,只需使用

var settings = require("settings");
settings.PROJECT_DIR; 

以这种方式,您将拥有相对于此文件的所有项目目录 ;)


2
感谢您花时间进行审查和编辑,已点赞。它仍然感觉脆弱,但这可能只是因为没有更好的方法来实现这一点。 - goliatone
13
使用这种方法,用户需要记住的是node_modules通常被排除在版本控制之外。因此,如果你与团队合作或者需要克隆你的代码库,你将需要另一种解决方案来使设置文件保持同步。 - Travesty3
2
使用@goliatone的解决方案,您可以在不知道文件路径的情况下从任何地方获取文件,您只需要知道“设置”。如果没有它,您将必须明确知道要退出多少个文件夹,直到达到项目目录。这是因为节点自动搜索node_modules并始终知道其位置。 - user7917402
1
如果您将settings.js文件从应用程序根目录移动到另一个位置,比如<app_root_path>/settings/settings.py,那么您的代码就会出现问题。更好的做法是实际上“硬编码”(虽然这本身不好,但仍然比您所做的要好)实际路径,例如:PROJECT_DIR:“。” 如果有人选择移动该文件,则可以稍后更改此设置。 - Abhishek Sharma
非常古老,但很聪明。然而,有时更聪明的解决方案更容易出问题,这就是其中之一。如果在某个时候这个“面条”断了(例如,您错误地使用了../而不是../..,您将文件移动到其他位置,您错误地忘记从gitignore中排除它等等),您将不得不重新加载所有这些智慧才能调试和修复它。但我喜欢它 D: - aderchox
显示剩余4条评论

53

简单:

require('path').resolve('./')

5
如果在处理了所有给定的路径段之后,仍未生成绝对路径,则使用当前工作目录。这只是执行 process.cwd() 的一种迂回方式。 - dosentmatter
4
当在根目录之外运行节点应用程序时,这将不起作用! - gignu

45

获取全局根目录的最简单方法(假设您使用NPM运行您的Node.js应用程序'npm start'等

var appRoot = process.env.PWD;

如果你想进行上述内容的交叉验证

假设你想将process.env.PWD与你的node.js应用程序设置进行交叉检查。如果你想要一些运行时测试来检查process.env.PWD的有效性,你可以使用我编写的这段代码进行交叉验证(似乎工作正常)。例如,你可以将appRoot中最后一个文件夹的名称与package.json文件中的npm_package_name进行交叉验证:

    var path = require('path');

    var globalRoot = __dirname; //(you may have to do some substring processing if the first script you run is not in the project root, since __dirname refers to the directory that the file is in for which __dirname is called in.)

    //compare the last directory in the globalRoot path to the name of the project in your package.json file
    var folders = globalRoot.split(path.sep);
    var packageName = folders[folders.length-1];
    var pwd = process.env.PWD;
    var npmPackageName = process.env.npm_package_name;
    if(packageName !== npmPackageName){
        throw new Error('Failed check for runtime string equality between globalRoot-bottommost directory and npm_package_name.');
    }
    if(globalRoot !== pwd){
        throw new Error('Failed check for runtime string equality between globalRoot and process.env.PWD.');
    }

您也可以使用此NPM模块:require('app-root-path'),它非常适合这个目的。


10
这个在大多数Unix系统上都很好用。但是如果您想让您的npm模块/应用程序在Windows上运行,PWD将未定义并且会失败。 - Jeremy Wiebe
5
process.cwd() - Muhammad Umer
1
@MuhammadUmer 为什么 process.cwd() 总是与项目根目录相同? - Alexander Mills
1
如果您在根文件中调用它,那么它将是这样的。 - Muhammad Umer

25

只需在你的根模块中添加这行代码,通常是app.jsapp.ts

global.__basedir = __dirname;

那么,_basedir 将能够被所有你的模块访问。

注意: 对于 TypeScript 实现,请按照上述步骤操作,然后您将能够使用 global.__basedir 来获取根目录路径。


1
简单、易学、高效 - Zorox
有没有办法在TypeScript中定义这个类型?否则我需要在每个引用__basedir的地方都加上ts-ignore。这有点像向window作用域添加额外的字段... - dcsan

20

尝试从__dirname向上遍历,直到找到一个package.json文件,并确定该文件所在的目录是你当前文件所属的应用程序主根目录。

根据Node文档

package.json文件通常位于Node.js项目的根目录。

const fs = require('fs')
const path = require('path')

function getAppRootDir () {
  let currentDir = __dirname
  while(!fs.existsSync(path.join(currentDir, 'package.json'))) {
    currentDir = path.join(currentDir, '..')
  }
  return currentDir
}

这确实是最好的解决方案!每个 NPM 项目默认只有一个唯一的 package.json,始终位于项目根目录。 - João Pimentel Ferreira
不幸的是,这并不起作用,因为某些应用程序部署到了.next文件夹中,该文件夹恰好有一个package.json文件。但是这个逻辑是正确的,所以我只需要找到一个唯一的文件来代表我们的应用程序。 - windmaomao

20

我发现这对我来说始终有效,即使应用程序是从子文件夹调用的,就像某些测试框架(如Mocha)那样:

process.mainModule.paths[0].split('node_modules')[0].slice(0, -1);

为什么它可行:

运行时,Node会创建一个所有已加载文件完整路径的注册表。模块首先被加载,因此在这个注册表的顶部。通过选择注册表的第一个元素并返回“node_modules”目录之前的路径,我们能够确定应用程序的根目录。

这只是一行代码,但出于简单(以及我自己的方便),我将其封装成了一个NPM模块:

https://www.npmjs.com/package/node-root.pddivine

享受吧!

编辑:

process.mainModule从v14.0.0开始已弃用

改用require.main代替:

require.main.paths[0].split('node_modules')[0].slice(0, -1);


2
process.mainModule 自 v14.0.0 起已弃用 - 请使用 require.main.paths[0].split('node_modules')[0].slice(0, -1); 替代。 - RobC
1
如果你有多个node_modules文件夹,就像Electron应用程序一样,会发生什么情况? - Charles Robertson
node_modules/目录始终位于项目的根目录。这就是为什么它能工作的原因。 - João Pimentel Ferreira
显示已弃用 - NiRUS

18

前言

这是一个非常老的问题,但它似乎在2020年和2012年一样引起了关注。 我已经检查了所有其他答案,但没有找到下面提到的技术(它有自己的限制,但其他方法也不适用于每种情况):

Git + 子进程

如果您使用Git作为版本控制系统,那么确定项目根目录的问题可以简化为以下内容(我认为这是项目的适当根目录 - 毕竟,您希望您的VCS具有最广泛的可见性范围):

检索存储库根路径

由于必须运行CLI命令才能执行此操作,因此我们需要生成子进程。此外,由于项目根目录极不可能在运行时更改,因此我们可以在启动时使用child_process模块的同步版本。

我发现spawnSync()最适合此工作。至于要运行的实际命令,git worktree(带有--porcelain选项以便于解析)就足以检索根的绝对路径。

在答案结尾的示例中,我选择返回路径数组,因为可能会有多个工作树(尽管它们很可能具有共同的路径),以确保安全起见,由于我们使用CLI命令,shell选项应设置为true(安全性不应成为问题,因为没有不受信任的输入)。

方法比较和备用方案

理解可能出现VCS无法访问的情况,我在分析文档和其他答案后包括了几个备用方案。提出的解决方案归结为以下内容(不包括第三方模块和软件包):

解决方案 优点 主要问题
__filename 指向模块文件 相对于模块
__dirname 指向模块目录 __filename相同
node_modules树遍历 几乎保证根目录 如果嵌套,则需要复杂的树遍历
path.resolve(".") 如果CWD是根目录,则为根目录 process.cwd()相同
process.argv\[1\] __filename相同 __filename相同
process.env.INIT_CWD 指向npm run目录 需要npm&& CLI启动
process.env.PWD 指向当前目录 相对于(是)启动目录
process.cwd() env.PWD相同 运行时需要process.chdir(path)
require.main.filename 如果=== module,则为根目录 require的模块上失败

从上面的比较表中,以下方法最通用:

  • require.main.filename作为一种简单的方法,如果满足require.main === module,则可以获取根目录
  • node_modules树遍历建议最近使用另一个假设:
如果模块的目录中有node_modules目录,则很可能是根目录。对于主应用程序,它将获取应用程序根目录,而对于模块,则为其项目根目录。 备选方案1.树遍历 我的实现使用了一种更宽松的方法,即一旦找到目标目录,就停止搜索,因为对于给定的模块,其根目录是项目根目录。您可以链接调用或扩展它以使搜索深度可配置:
/**
 * @summary gets root by walking up node_modules
 * @param {import("fs")} fs
 * @param {import("path")} pt
 */
const getRootFromNodeModules = (fs, pt) =>

    /**
     * @param {string} [startPath]
     * @returns {string[]}
     */
    (startPath = __dirname) => {

        //avoid loop if reached root path
        if (startPath === pt.parse(startPath).root) {
            return [startPath];
        }

        const isRoot = fs.existsSync(pt.join(startPath, "node_modules"));

        if (isRoot) {
            return [startPath];
        }

        return getRootFromNodeModules(fs, pt)(pt.dirname(startPath));
    };

Fallback 2. 主模块

第二个实现非常简单:

/**
 * @summary gets app entry point if run directly
 * @param {import("path")} pt
 */
const getAppEntryPoint = (pt) =>

    /**
     * @returns {string[]}
     */
    () => {

        const { main } = require;

        const { filename } = main;

        return main === module ?
            [pt.parse(filename).dir] :
            [];
    };

实现

我建议使用树遍历器作为首选回退,因为它更加灵活:

const { spawnSync } = require("child_process");
const pt = require('path');
const fs = require("fs");

/**
 * @summary returns worktree root path(s)
 * @param {function : string[] } [fallback]
 * @returns {string[]}
 */
const getProjectRoot = (fallback) => {

    const { error, stdout } = spawnSync(
        `git worktree list --porcelain`,
        {
            encoding: "utf8",
            shell: true
        }
    );

    if (!stdout) {
        console.warn(`Could not use GIT to find root:\n\n${error}`);
        return fallback ? fallback() : [];
    }

    return stdout
        .split("\n")
        .map(line => {
            const [key, value] = line.split(/\s+/) || [];
            return key === "worktree" ? value : "";
        })
        .filter(Boolean);
};

缺点

最明显的一个是需要安装和初始化Git,这可能是不可取/不切实际的(顺便说一下:在生产服务器上安装Git并不罕见,也不不安全)。可以通过上述描述的备选方案来解决。


16

所有这些“根目录”大多需要将一些虚拟路径解析为真实的文件路径,所以你可能应该查看 path.resolve

var path= require('path');
var filePath = path.resolve('our/virtual/path.ext');

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接