将"Vanilla" Javascript库加载到Node.js中

114

有一些第三方的JavaScript库拥有我想在Node.js服务器中使用的一些功能。 (具体来说,我想使用找到的QuadTree JavaScript库。)但是这些库只是简单的 .js 文件,而不是"Node.js库"。

因此,这些库不遵循Node.js期望其模块的exports.var_name语法。 换句话说,据我所知,这意味着当你执行module = require('module_name');module = require('./path/to/file.js');时,你会得到一个没有公开可访问函数等的模块。

我的问题是:“如何将任意JavaScript文件加载到Node.js中,以便我可以利用它的功能,而无需重写它以执行exports?”

我对Node.js非常陌生,因此如果我的工作方式理解上存在一些明显漏洞,请告诉我。


编辑: 我进一步研究后发现,Node.js使用的模块加载模式实际上是最近开发的一种用于加载JavaScript库的标准CommonJS的一部分。这在Node.js的模块文档页面中有说明,但我直到现在才发现。

也许我的问题的答案是"等待您的库的作者编写一个CommonJS接口或自己动手做"。


相关问题:https://dev59.com/1X7aa4cB1Zd3GeqPqG4- - Josmar
7个回答

81

这是我认为对于这种情况的“最正确”的答案。

假设你有一个名为quadtree.js的脚本文件。

你应该构建一个自定义的node_module,它应该具有这样的目录结构...

./node_modules/quadtree/quadtree-lib/
./node_modules/quadtree/quadtree-lib/quadtree.js
./node_modules/quadtree/quadtree-lib/README
./node_modules/quadtree/quadtree-lib/some-other-crap.js
./node_modules/quadtree/index.js

你的 ./node_modules/quadtree/quadtree-lib/ 目录中的所有内容都是你的第三方库文件。

然后你的 ./node_modules/quadtree/index.js 文件将从文件系统中加载该库,并正确地导出需要的内容。

var fs = require('fs');

// Read and eval library
filedata = fs.readFileSync('./node_modules/quadtree/quadtree-lib/quadtree.js','utf8');
eval(filedata);

/* The quadtree.js file defines a class 'QuadTree' which is all we want to export */

exports.QuadTree = QuadTree

现在您可以像使用任何其他节点模块一样使用您的 quadtree 模块...

var qt = require('quadtree');
qt.QuadTree();
我喜欢这种方法,因为不需要更改第三方库的任何源代码——所以更容易维护。升级时,你只需要查看他们的源代码,并确保你仍然在正确地导出对象即可。

3
刚刚看到你的回答(正在制作多人游戏,需要在服务器和客户端都包含JigLibJS物理引擎),你为我节省了大量时间和麻烦。谢谢! - stevendesu
8
如果你按照这个步骤进行操作,请记住使用NPM时非常容易意外地删除您的node_modules文件夹,特别是如果您没有将其检入SCM。建议将您的QuadTree库放在一个单独的代码库中,然后使用npm link将其链接到您的应用程序中。这样它就像一个本机的Node.js包一样处理。 - btown
@btown,你能否为像我这样的新手详细解释一下SCM和npm link到底是如何防止你提到的潜在问题的? - Flion
如果我只想包含一个脚本,这真的有必要吗? - quantumpotato
1
@flion 回复旧评论,供其他人参考,我相信你现在已经知道了答案。SCM - 源代码管理(例如GIT),以及一个快速但不错的npm link演示链接。 - delp

78

除了使用 eval,还有一种更好的方法:使用vm模块。

例如,这是我的execfile模块,它将path中的脚本在context或全局上下文中进行评估:

var vm = require("vm");
var fs = require("fs");
module.exports = function(path, context) {
  context = context || {};
  var data = fs.readFileSync(path);
  vm.runInNewContext(data, context, path);
  return context;
}

而它可以这样使用:

> var execfile = require("execfile");
> // `someGlobal` will be a global variable while the script runs
> var context = execfile("example.js", { someGlobal: 42 });
> // And `getSomeGlobal` defined in the script is available on `context`:
> context.getSomeGlobal()
42
> context.someGlobal = 16
> context.getSomeGlobal()
16

example.js 包含:

function getSomeGlobal() {
    return someGlobal;
}

使用这种方法的最大优点是,您可以完全控制执行脚本中的全局变量:您可以通过context传递自定义全局变量,并且脚本创建的所有全局变量都将添加到context中。调试也更容易,因为语法错误等问题将显示正确的文件名。


如果context(在文档中称为sandbox)未定义,runInNewContext是否使用全局上下文?(我没有找到任何文档明确说明这一点) - Steven Lu
看起来,为了使用一个不了解Node或CommonJS模式的第三方库,Christopher的eval方法https://dev59.com/BG435IYBdhLWcg3w20F8#9823294效果很好。在这种情况下,vm模块能提供哪些好处? - Michael Scheper
2
请查看我的更新,了解为什么这种方法比eval更好。 - David Wolever
1
这绝对太棒了——它让我能够立即重用我的基于Web的非模块代码,实现服务器端的输出邮件发送(按计划执行),而不是在网页上显示。所有的Web代码都使用了松散增强模式和脚本注入——所以这个工具运行得非常好!! - Al Joslin
如果example.js依赖于example1.js库,我们如何在Node.js中使用它? - sytolk
npm模块rewire非常适合这种情况。它基本上是相同的想法。不是:var dep = require('someDep')而是var context = rewire('someDep'); context.get('someDep')。我们在这里使用它,主要作为模拟对象的一种方式(您可以通过上下文覆盖函数),但是相同的想法。 - Robert Christ

31
最简单的方法是: eval(require('fs').readFileSync('./path/to/file.js', 'utf8'));。这对于在交互式shell中进行测试非常有效。

1
谢谢你的帮助,太感谢了! - Schoening
1
这也是最快的方法,有时候你需要快速而粗略的解决方案。在这个和David的回答之间,这个SO页面是一个很好的资源。 - Michael Scheper

5
据我所知,这确实是模块必须加载的方式。 然而,你可以将所有导出函数附加到exports对象上,也可以将它们附加到this上(否则将成为全局对象)。
因此,如果你想保持其他库的兼容性,可以这样做:
this.quadTree = function () {
  // the function's code
};

或者,当外部库已经有了自己的命名空间,例如 jQuery(并不是您可以在服务器端环境中使用):
this.jQuery = jQuery;

在非Node环境中,this会解析为全局对象,因此使它成为一个全局变量...这本来就是。所以它不应该破坏任何东西。 编辑: James Herdman撰写了一篇适合初学者的有关node.js的很好的文章,其中也提到了这一点。

“this” 技巧听起来像是一种很好的方法,可以使得 Node.js 库在 Node.js 之外被使用更加便携,但这仍然意味着我需要手动更改我的 JavaScript 库以支持 Node.js 的 require 语法。 - Chris W.
@ChrisW.:是的,您将不得不手动更改您的库。就我个人而言,我也希望有第二种机制来包含外部文件,一种可以自动将所包含文件的全局命名空间转换为导入命名空间的机制。也许您可以向Node开发人员提交一个RFE? - Martijn

4

我不确定我是否会真正使用这个方法,因为它是一种相当hacky的解决方案,但一个解决方法是构建一个小型的模块导入器,就像这样...

在文件./node_modules/vanilla.js中:

var fs = require('fs');

exports.require = function(path,names_to_export) {
    filedata = fs.readFileSync(path,'utf8');
    eval(filedata);
    exported_obj = {};
    for (i in names_to_export) {
        to_eval = 'exported_obj[names_to_export[i]] = ' 
            + names_to_export[i] + ';'
        eval(to_eval); 
    }
    return exported_obj;
}

当你想要使用库的功能时,你需要手动选择要导出哪些名称。

因此,对于像文件./lib/mylibrary.js这样的库...

function Foo() { //Do something... }
biz = "Blah blah";
var bar = {'baz':'filler'};

当您想在Node.js代码中使用其功能时...
var vanilla = require('vanilla');
var mylibrary = vanilla.require('./lib/mylibrary.js',['biz','Foo'])
mylibrary.Foo // <-- this is Foo()
mylibrary.biz // <-- this is "Blah blah"
mylibrary.bar // <-- this is undefined (because we didn't export it)

不过不确定这些在实践中的效果如何。


哇,真是太神奇了:同一个用户对同一个问题的回答既被踩又被赞!这应该有个徽章来表彰一下!;-) - Michael Scheper

2
我通过更新他们的脚本使其正常运转,非常容易,只需在适当的位置添加 module.exports = 即可...
例如,我将 他们的 文件复制到 './libs/apprise.js'。然后,在它开始的地方添加
function apprise(string, args, callback){

我把这个函数赋值给了module.exports =,代码如下:
module.exports = function(string, args, callback){

因此,我可以像这样将库导入到我的代码中:my
window.apprise = require('./libs/apprise.js');

然后我就可以开始了。个人经验可能会有所不同,这是使用webpack的情况。


0
一个简单的include(filename)函数,具有更好的错误消息(堆栈、文件名等),用于eval在出现错误时:
var fs = require('fs');
// circumvent nodejs/v8 "bug":
// https://github.com/PythonJS/PythonJS/issues/111
// http://perfectionkills.com/global-eval-what-are-the-options/
// e.g. a "function test() {}" will be undefined, but "test = function() {}" will exist
var globalEval = (function() {
    var isIndirectEvalGlobal = (function(original, Object) {
        try {
            // Does `Object` resolve to a local variable, or to a global, built-in `Object`,
            // reference to which we passed as a first argument?
            return (1, eval)('Object') === original;
        } catch (err) {
            // if indirect eval errors out (as allowed per ES3), then just bail out with `false`
            return false;
        }
    })(Object, 123);
    if (isIndirectEvalGlobal) {
        // if indirect eval executes code globally, use it
        return function(expression) {
            return (1, eval)(expression);
        };
    } else if (typeof window.execScript !== 'undefined') {
        // if `window.execScript exists`, use it
        return function(expression) {
            return window.execScript(expression);
        };
    }
    // otherwise, globalEval is `undefined` since nothing is returned
})();

function include(filename) {
    file_contents = fs.readFileSync(filename, "utf8");
    try {
        //console.log(file_contents);
        globalEval(file_contents);
    } catch (e) {
        e.fileName = filename;
        keys = ["columnNumber", "fileName", "lineNumber", "message", "name", "stack"]
        for (key in keys) {
            k = keys[key];
            console.log(k, " = ", e[k])
        }
        fo = e;
        //throw new Error("include failed");
    }
}

但使用nodejs会更加麻烦:你需要指定这个:

export NODE_MODULE_CONTEXTS=1
nodejs tmp.js

否则,您无法在使用include(...)包含的文件中使用全局变量。

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