如何检查脚本是否在 Node.js 下运行?

185

我有一个脚本,我正在从Node.js脚本中引用它,我希望保持JavaScript引擎的独立性。

例如,我希望只在运行在Node.js下时执行exports.x = y;。如何进行此测试?


发表这个问题时,我不知道Node.js模块功能是基于CommonJS的。

对于我提供的具体示例,一个更准确的问题应该是:

脚本如何确定它是否被作为CommonJS模块引用?


3
我不知道你为什么要这样做,但通常情况下,你应该使用特性检测而不是引擎检测。http://www.quirksmode.org/js/support.html - Quentin
4
这实际上是有关如何实现特性检测的请求,但问题描述不够清晰。 - monokrome
我发布了一个供自己使用的库,希望这能有所帮助 https://www.npmjs.com/package/detect-is-node - abhirathore2006
这个问题以及大部分回答的一个问题是假设只有两种可能:浏览器或Node.js。实际上,还有一种可能,即像Oracle Java Nashorn这样既不是浏览器也不是Node.js。如果安装了JDK,则可以使用jjs命令运行脚本。但是Nashorn和Node.js之间存在许多差异,因此您不能做出任何假设。而且谁知道未来会带来什么选项?需要进行功能检测。 - user2895783
显示剩余3条评论
24个回答

124

由于每个网站都可以轻松地声明相同的变量,因此没有可靠的方法来检测在Node.js中运行。然而,在Node.js中默认没有window对象,因此您可以采用另一种方式,并检查是否正在浏览器内部运行。

这是我为应该在浏览器和Node.js下工作的库使用的方法:

if (typeof window === 'undefined') {
    exports.foo = {};

} else {
    window.foo = {};
}

如果在Node.js中定义了window,它仍然可能爆炸,但没有充分的理由让某人这样做,因为您必须明确地省略var或在global对象上设置该属性。

编辑

要检测您的脚本是否作为CommonJS模块被引入,这是不容易的。唯一的共同点是:A:通过调用函数require来包含模块;B:模块通过exports对象的属性导出东西。现在如何实现取决于底层系统。Node.js将模块的内容包装在一个匿名函数中:

function (exports, require, module, __filename, __dirname) { 

参考:https://github.com/ry/node/blob/master/src/node.js#L325

不要尝试通过一些疯狂的arguments.callee.toString()代码来检测,而是使用上面我提供的示例代码来检查浏览器。Node.js环境更加清洁,所以在那里声明window的可能性很小。


2
关于“Node.js是一个更干净的环境,因此不太可能在那里声明窗口。”:我只是来这里寻找一种方法,以确定我的脚本是在由node.js + JSDOM模拟的浏览器中运行还是在普通浏览器中运行...原因是我使用setTimeout进行无限循环以检查URL位置,在浏览器中很好,但会使node.js脚本永远运行...所以在node.js脚本中可能会有一个窗口 :) - Eric Bréchemier
2
@Eric 我非常怀疑它会在全局范围内存在,所以除非你在模块的第一行导入了window,否则你不应该有任何问题。你也可以运行一个匿名函数并检查其中的this[[Class]](仅适用于非严格模式)。请参见“Class”:http://bonsaiden.github.com/JavaScript-Garden/#typeof - Ivo Wetzel
1
我的问题与 OP 的略有不同:我不需要脚本,它是由 JSDOM 以模拟窗口为全局上下文加载的... 它仍然由 node.js + V8 运行,只是在不同于通常模块的上下文中运行。 - Eric Bréchemier
1
可能我选择了另一种方法:1)检测是否支持onhashchange("onhashchange" in window),以避免创建无限循环;2)通过在主Node.js脚本中设置模拟窗口的onhashchange属性来模拟支持。 - Eric Bréchemier
1
“typeof self === 'object'”可能更安全,因为“typeof window === 'undefined'”无法在Web Workers范围内使用。 - Lewis
显示剩余5条评论

82

通过寻找 CommonJS 支持,这就是 Underscore.js 库的实现方法:

编辑:对于您更新后的问题:

(function () {

    // Establish the root object, `window` in the browser, or `global` on the server.
    var root = this; 

    // Create a reference to this
    var _ = new Object();

    var isNode = false;

    // Export the Underscore object for **CommonJS**, with backwards-compatibility
    // for the old `require()` API. If we're not in CommonJS, add `_` to the
    // global object.
    if (typeof module !== 'undefined' && module.exports) {
            module.exports = _;
            root._ = _;
            isNode = true;
    } else {
            root._ = _;
    }
})();

这里的示例保留了模块模式。


49
这个检测模块可以检测浏览器是否支持CommonJS。 - mikemaccana
8
这里有一个问题,而且Nailer“解决”了它。我正在尝试在浏览器中使用CommonJS,并且我正在使用的模块加载器定义了module.exports,因此这个解决方案会错误地告诉我我在node环境下。 - Mark Melville
1
@MarkMelville 可以说,这正是原帖所问的,因此并不是一个“问题”。 - Ross
13
我的措辞可能不太恰当。我的意思是这个解决方案存在问题。提问者可能已经接受了它,但我并没有。 - Mark Melville
7
这绝对不是最佳答案。 - user3751385
显示剩余5条评论

59
我最近遇到了一个错误的Node检测,它在Electron中没有意识到Node环境,这是由于一个误导性的特征检测引起的。以下解决方案明确地识别了进程环境。

仅识别Node.js

(typeof process !== 'undefined') && (process.release.name === 'node')

这将发现您是否在Node进程中运行,因为process.release包含与当前[Node-]发布相关的元数据。
在io.js(很久以前已经停止)产生之后,process.release.name的值也可能变为io.jsBun旨在实现完全的Node.js兼容性,因此模仿了Node的行为。
为了正确检测Node-ready环境,我猜您应该按照以下方式进行检查:

识别Node(>= 3.0.0)、Bun或io.js

(typeof process !== 'undefined') &&
(process.release.name.search(/node|io.js/) !== -1)

这个声明在初始发布时已经测试过,使用的是Node 5.5.0,Electron 0.36.9(带有Node 5.1.1)和Chrome 48.0.2564.116。
识别Node(>= 0.10.0),Bun或io.js。
(typeof process !== 'undefined') &&
(typeof process.versions.node !== 'undefined')

@daluege的评论激发了我对更一般的证明进行思考。这应该适用于Node.js版本>= 0.10。我没有找到先前版本的唯一标识符。

明确标识Bun(>= 1.0.3)

(typeof process !== 'undefined') && process.versions.bun)

最近,Bun为process对象添加了支持,这种检测方法来自Bun的检测bun指南。
明确地识别Bun(< 1.0.3)
(typeof process !== 'undefined') && process.isBun)

P.s.: 我在这里发布这个答案,因为问题引导我来到这里,尽管提问者正在寻找另一个问题的答案。

2
这似乎是目前最可靠的方法,谢谢。尽管仅适用于版本>= 3.0.0。 - filip
4
我发现在使用React Webpack时,processprocess.version存在于捆绑包中,因此我添加了一个额外的检查来检查process.version,其中客户端上的process.release.node未定义,但服务器端具有节点版本作为值。 - Aaron
@Aaron:感谢你的提示。我找不到process.version变量的任何定义(在React、Webpack或React-Webpack中)。我会很感激如果你能提供任何关于版本变量定义的提示,以便我可以将其添加到答案中。这取决于发布节点对Node >= 3.x.x的限制。 - Florian Neumann
2
一行代码更安全:function isNodejs() { return typeof "process" !== "undefined" && process && process.versions && process.versions.node; } - brillout
1
Bun现在实施process.release.name https://github.com/oven-sh/bun/issues/1404。或者,`typeof Bun !== "undefined"是一个非常简洁的测试,如果你使用"types": ["bun-types"]`,它也会让TypeScript感到满意。 - undefined
显示剩余8条评论

25

尝试确定代码运行环境的问题在于任何对象都可以被修改和声明,这使得几乎不可能弄清楚哪些对象是原生环境中的,哪些是程序修改过的。

然而,我们可以使用一些技巧来确定当前运行的环境。

让我们从 underscore 库中通常采用的解决方案开始:

typeof module !== 'undefined' && module.exports

这种技巧在服务器端实际上非常好用,因为当调用 require 函数时,它会将 this 对象重置为空对象,然后为您重新定义 module,这意味着您不必担心外部修改。只要使用 require 加载代码,就是安全的。

然而,在浏览器端,它会失效,因为任何人都可以轻松地定义 module 以使其看起来像您寻找的对象。这可能是您想要的行为,但它也决定了库用户可以在全局作用域中使用的变量。也许有人想使用名称为 module 的变量,其中包含其他用途的 exports。虽然这很不可能,但我们无法判断别人可以使用哪些变量,只因为另一个环境使用了这个变量名。

然而,技巧在于,如果我们假设您的脚本正在全局作用域中加载(如果通过script标签加载,它确实是如此),则变量不能被保留在外部闭包中,因为浏览器不允许那样。请记住,在 Node 中,this 对象是一个空对象,但 module 变量仍然可用,因为它是在外部闭包中声明的。所以,我们可以通过添加额外的检查来修复 underscore 的检查:

this.module !== module

通过这样做,如果有人在浏览器的全局范围内声明module,它将被放置在this对象中,这将导致测试失败,因为this.module将是与module相同的对象。在Node中,this.module不存在,而module存在于外部闭包中,因此测试将成功,因为它们不等价。

因此,最终的测试为:

typeof module !== 'undefined' && this.module !== module

注意:虽然现在允许在全局范围内自由使用module变量,但在浏览器上仍然可能绕过这一点,方法是创建一个新的闭包并在其中声明module,然后在该闭包中加载脚本。此时,用户完全复制了Node环境,并希望知道自己在做什么并尝试进行Node风格的require。如果代码在脚本标签中调用,则仍将安全。


3
谢谢你清楚地解释你一行代码中每个部分的原理。 - Jon Coombs
在Mocha测试中,例如由于未定义而出现“无法读取未定义的属性'module'”错误。 - srghma

23
以下内容适用于浏览器,除非有意地、明确地进行破坏:
if(typeof process === 'object' && process + '' === '[object process]'){
    // is node
}
else{
    // not node
}

砰。


4
var process = { toString: function () { return '[object process]'; } }; 变量进程= { toString: function () { return '[对象 进程]'; } }; - Nick Desaulniers
1
你使用 process+'' 而不是 process.toString() 有什么特别的原因吗? - harmic
3
几乎一样,使用这个替代:Object.prototype.toString.call(process) - sospedra
2
这是这个问题的最佳答案。 - loretoparisi
4
如果使用var process = null;,第二种情况将失败。在JavaScript和Java中,表达式'' + x产生的结果与x.toString()相同,除非 x是有问题的,前者会产生"null""undefined"的字符串,而后者会抛出一个错误。 - joeytwiddle
当考虑到 var process = { toString: function () { return '[object process]'; } }; 时,使用 Object.prototype.toString.call(process) 而不是 process + '' 是最佳解决方案。 - Michał Pietraszko

17

以下也是一种相当不错的方法:

const isBrowser = this.window === this;

这是因为在浏览器中,全局的'this'变量有一个自我引用叫做'window'。但是在Node中,这个自我引用是不存在的。

  • 在浏览器中,'this'是指向全局对象的引用,即'window'。
  • 在Node中,'this'是指向module.exports对象的引用。
    • 'this'不是指向Node全局对象的引用,即'global'。
    • 'this'也不是指向模块变量声明空间的引用。

要打破上述建议的浏览器检查,您需要执行以下操作:

this.window = this;

在执行检查之前。


为什么不直接使用 const isBrowser = this.window !== undefined 呢?理论上,在 Node 中,我可以执行 this.window = this 来欺骗这个解决方案。 - Tyler Liu
1
很好的答案,但有一个警告:这在“使用严格模式”下会出错。 - Kithraya

13

另一种 环境检测

(意思是:这里的大部分答案都可以。)

function isNode() {
    return typeof global === 'object'
        && String(global) === '[object global]'
        && typeof process === 'object'
        && String(process) === '[object process]'
        && global === global.GLOBAL // circular ref
        // process.release.name cannot be altered, unlike process.title
        && /node|io\.js/.test(process.release.name)
        && typeof setImmediate === 'function'
        && setImmediate.length === 4
        && typeof __dirname === 'string'
        && Should I go on ?..
}

有点偏执,是吗?您可以检查更多的全局对象来使其更加详细。

但是千万别这么做!

以上所有内容都可以被伪造/模拟。

例如,要伪造global对象:

global = {
    toString: function () {
        return '[object global]';
    },
    GLOBAL: global,
    setImmediate: function (a, b, c, d) {}
 };
 setImmediate = function (a, b, c, d) {};
 ...

这不会被附加到Node的原始全局对象上,但它将被附加到浏览器中的window对象上。因此,这将意味着你在浏览器内部的Node环境中。

生命苦短!

如果我们的环境被伪造了,我们会在意吗?当一些愚蠢的开发者在全局作用域中声明一个名为global的全局变量时,就会发生这种情况。或者一些邪恶的开发者以某种方式注入代码到我们的环境中。

我们可以在捕获到这种情况时防止我们的代码执行,但是我们应用程序的许多其他依赖关系可能会陷入这种情况。因此,最终代码将会出错。如果你的代码足够好,你不应该为别人可能犯的每一个愚蠢错误而担忧。

那又怎样?

如果针对两个环境:浏览器和Node;
"use strict"; 并检查 windowglobal;并在文档中明确说明你的代码仅支持这些环境。就这样!

var isBrowser = typeof window !== 'undefined'
    && ({}).toString.call(window) === '[object Window]';

var isNode = typeof global !== "undefined" 
    && ({}).toString.call(global) === '[object global]';

如果可能的话,建议在try/catch块内进行同步特性检测,而不是进行环境检测(这些操作将花费几毫秒的时间)。例如:
function isPromiseSupported() {
    var supported = false;
    try {
        var p = new Promise(function (res, rej) {});
        supported = true;
    } catch (e) {}
    return supported;
}

10

一行代码可以在现代JavaScript运行时中使用。

const runtime = globalThis.process?.release?.name || 'not node'

runtime的值将是nodenot node

这取决于一些较新的JavaScript功能。 globalThis在ECMAScript 2020规范中得以确定。 可选链接/空值合并(?部分)在V8引擎的8.x+版本中受到支持(该版本已在Chrome 80和Node 14中发布,于2020年4月21日发布)。


4
这应该是被接受的答案!而且每个人都应该使用 Node 14。 - Sceat

8

大多数提出的解决方案实际上是可以伪造的。一种健壮的方式是使用Object.prototype.toString检查全局对象的内部Class属性。在JavaScript中,内部类无法伪造:

var isNode = 
    typeof global !== "undefined" && 
    {}.toString.call(global) == '[object global]';

2
这将在Browserify下返回true。 - alt
1
你测试过了吗?我不明白 browserify 怎么可能改变对象的内部类。这需要在 JavaScript VM 中更改代码或覆盖 Object.prototype.toString,这是非常糟糕的做法。 - Fabian Jakobs
我测试过了。这是Browserify的作用:var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}; - Vanuan
你看,在Chrome中,({}.toString.call(window))等于"[object global]" - Vanuan
2
很奇怪,因为 window.toString() 会产生 "[object Window]" - Vanuan
在OSX上,Chrome版本56.0.2924.87(64位)可用。猜测他们修复了它。 - Peter Tseng

4

使用进程对象并检查execPath是否为node怎么样?

process.execPath

这是启动进程的可执行文件的绝对路径名。

例如:

/usr/local/bin/node


3
window.process = {execPath: "/usr/local/bin/node"};的意思是什么? - Константин Ван

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