Carregue as bibliotecas Javascript “Vanilla” em Node.js

108

Existem algumas bibliotecas Javascript de terceiros que têm algumas funcionalidades que eu gostaria de usar em um servidor Node.js. (Especificamente, quero usar uma biblioteca javascript QuadTree que encontrei.) Mas essas bibliotecas são apenas .jsarquivos diretos e não "bibliotecas Node.js".

Como tal, essas bibliotecas não seguem a exports.var_namesintaxe que o Node.js espera para seus módulos. Pelo que entendi, isso significa quando você o faz module = require('module_name');ou module = require('./path/to/file.js');acabará com um módulo sem funções acessíveis ao público, etc.

Minha pergunta é: "Como carrego um arquivo javascript arbitrário no Node.js de modo que possa utilizar sua funcionalidade sem ter que reescrevê-lo para que funcione exports?"

Eu sou muito novo no Node.js, então, por favor, deixe-me saber se há alguma falha gritante no meu entendimento de como ele funciona.


EDIT : Pesquisando mais sobre as coisas, vejo agora que o padrão de carregamento do módulo que o Node.js usa é na verdade parte de um padrão desenvolvido recentemente para carregar bibliotecas Javascript chamado CommonJS . Diz isso diretamente na página de doc do módulo para Node.js , mas eu perdi isso até agora.

Pode acabar sendo que a resposta à minha pergunta é "espere até que os autores da sua biblioteca consigam escrever uma interface CommonJS ou faça você mesmo".

Chris W.
fonte
pergunta relacionada: stackoverflow.com/questions/22898080/…
Josmar

Respostas:

75

Existe um método muito melhor do que usar eval: o vmmódulo.

Por exemplo, aqui está meu execfilemódulo, que avalia o script pathem um contextou no contexto global:

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;
}

E pode ser usado assim:

> 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

Onde example.jscontém:

function getSomeGlobal() {
    return someGlobal;
}

A grande vantagem desse método é que você tem controle total sobre as variáveis ​​globais no script executado: você pode passar globais personalizados (via context) e todos os globais criados pelo script serão adicionados context. A depuração também é mais fácil porque os erros de sintaxe e similares serão relatados com o nome de arquivo correto.

David Wolever
fonte
Usa runInNewContexto contexto global se context(também conhecido como sandbox, nos documentos) for indefinido? (este ponto não foi esclarecido por nenhum documento que encontrei)
Steven Lu
Parece que, com o propósito de brincar com uma biblioteca de terceiros que não conhece o Node ou o padrão CommonJS, o método eval de Christopher < stackoverflow.com/a/9823294/1450294 > funciona bem. Quais benefícios o vmmódulo pode oferecer neste caso?
Michael Scheper
2
Veja minhas atualizações para uma descrição de por que esse método é melhor do que eval.
David Wolever
1
esta totalmente rochas - que me permitiu imediatamente re-utilizar o meu código não-módulo baseado na Web para uma implementação do lado do servidor que e-mails as saídas [em uma programação] em vez de exibi-los em uma página web. Todo o código da web usou o padrão de módulo de aumento livre e injeção de script - então isso funciona tão bem !!
Al Joslin
Como podemos usar isso em Node.js se example.js depende da biblioteca example1.js?
sytolk de
80

Aqui está o que eu acho que é a resposta 'mais certa' para esta situação.

Digamos que você tenha um arquivo de script chamado quadtree.js.

Você deve construir um custom node_moduleque tenha este tipo de estrutura de diretório ...

./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

Tudo em seu ./node_modules/quadtree/quadtree-lib/diretório são arquivos de sua biblioteca de terceiros.

Em seguida, seu ./node_modules/quadtree/index.jsarquivo irá apenas carregar aquela biblioteca do sistema de arquivos e fazer o trabalho de exportar as coisas corretamente.

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

Agora você pode usar seu quadtreemódulo como qualquer outro módulo de nó ...

var qt = require('quadtree');
qt.QuadTree();

Eu gosto desse método porque não há necessidade de alterar nenhum código-fonte de sua biblioteca de terceiros - por isso é mais fácil de manter. Tudo o que você precisa fazer na atualização é olhar o código-fonte e garantir que ainda está exportando os objetos apropriados.

Chris W.
fonte
3
Acabei de encontrar sua resposta (fazer um jogo multiplayer e precisava incluir JigLibJS, nosso mecanismo de física, no servidor e também no cliente), você me economizou MUITO tempo e aborrecimento. Obrigado!
stevendesu
8
Se você seguir exatamente isso, lembre-se de que é muito fácil obliterar acidentalmente sua pasta node_modules usando o NPM, especialmente se você não fizer check-in no SCM. Definitivamente, considere colocar sua biblioteca QuadTree em um repositório separado e, em seguida, inseri npm link-la em seu aplicativo. Em seguida, é tratado como se fosse um pacote Node.js nativo.
btown
@btown, você poderia expandir um pouco para novatos como eu o que o SCM e o link npm fazem exatamente para evitar o problema potencial que você mencionou?
Flion
Isso é realmente necessário se eu só quiser incluir um script?
quantumpotato
1
@flion respondendo ao comentário antigo para outros ref, pois tenho certeza que você já sabe que responderá. SCM - Gerenciamento de controle de fonte (por exemplo, GIT) e um link para uma demonstração rápida, mas boa do link
npm
30

A maneira mais simples é: eval(require('fs').readFileSync('./path/to/file.js', 'utf8')); Isso funciona muito bem para testar no shell interativo.

Christopher Weiss
fonte
1
Cheers Mate! Ajudou muito
Schoening
Essa também é a maneira mais rápida e, às vezes, rápido e sujo é o que você precisa. Entre isso e a resposta de David, esta página SO é um excelente recurso.
Michael Scheper
5

AFAIK, é assim que os módulos devem ser carregados. No entanto, em vez de anexar todas as funções exportadas ao exportsobjeto, você também pode anexá-las this(o que de outra forma seria o objeto global).

Portanto, se você quiser manter as outras bibliotecas compatíveis, pode fazer o seguinte:

this.quadTree = function () {
  // the function's code
};

ou, quando a biblioteca externa já tem seu próprio namespace, por exemplo jQuery(não que você possa usar isso em um ambiente do lado do servidor):

this.jQuery = jQuery;

Em um ambiente não-Node, thisresolveria para o objeto global, tornando-o uma variável global ... o que já era. Portanto, não deve quebrar nada.

Edit : James Herdman tem um bom artigo sobre node.js para iniciantes, que também menciona isso.

Martijn
fonte
O truque "isso" parece uma boa maneira de tornar as coisas mais portáteis para que as bibliotecas Node.js possam ser usadas fora do Node.js, mas ainda significa que preciso alterar manualmente minhas bibliotecas javascript para suportar a sintaxe necessária do Node.js .
Chris W.
@ChrisW .: sim, você terá que alterar manualmente suas bibliotecas. Pessoalmente, eu também gostaria de um segundo mecanismo para incluir arquivos externos, um que convertesse automaticamente o namespace global do arquivo incluído para o namespace importado. Talvez você pudesse enviar um RFE para os desenvolvedores do Node?
Martijn,
3

Não tenho certeza se vou realmente acabar usando isso porque é uma solução bastante hacky, mas uma maneira de contornar isso é construir um pequeno importador de mini-módulo como este ...

No arquivo ./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;
}

Então, quando quiser usar a funcionalidade de sua biblioteca, você precisará escolher manualmente quais nomes exportar.

Então, para uma biblioteca como o arquivo ./lib/mylibrary.js...

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

Quando você deseja usar sua funcionalidade em seu código 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)

Não sei se isso funcionaria bem na prática.

Chris W.
fonte
Ei, uau: uma resposta votada contra (não por mim) e votada positivamente pelo mesmo usuário para a mesma pergunta! Deve haver um emblema para isso! ;-)
Michael Scheper
2

Consegui fazer funcionar atualizando o script deles, com muita facilidade, simplesmente adicionando module.exports =onde apropriado ...

Por exemplo, peguei o arquivo deles e copiei para './libs/apprise.js'. Então, onde começa com

function apprise(string, args, callback){

Atribuí a função para module.exports =:

module.exports = function(string, args, callback){

Assim, posso importar a biblioteca para o meu código desta forma:

window.apprise = require('./libs/apprise.js');

E eu estava pronto para ir. YMMV, isso foi com webpack .

John Mee
fonte
0

Uma include(filename)função simples com melhor mensagem de erro (pilha, nome do arquivo etc.) para eval, em caso de erros:

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");
    }
}

Mas fica ainda mais sujo com nodejs: você precisa especificar isto:

export NODE_MODULE_CONTEXTS=1
nodejs tmp.js

Caso contrário, você não pode usar variáveis ​​globais em arquivos incluídos com include(...).

lama12345
fonte