Cargue bibliotecas de JavaScript "Vanilla" en Node.js

108

Hay algunas bibliotecas de Javascript de terceros que tienen alguna funcionalidad que me gustaría usar en un servidor Node.js. (Específicamente, quiero usar una biblioteca de JavaScript QuadTree que encontré). Pero estas bibliotecas son simplemente .jsarchivos sencillos y no "bibliotecas de Node.js".

Como tal, estas bibliotecas no siguen la exports.var_namesintaxis que Node.js espera para sus módulos. Por lo que tengo entendido, eso significa que cuando lo haga module = require('module_name');o module = require('./path/to/file.js');terminará con un módulo sin funciones de acceso público, etc.

Entonces, mi pregunta es "¿Cómo cargo un archivo javascript arbitrario en Node.js de modo que pueda utilizar su funcionalidad sin tener que reescribirlo para que funcione exports?"

Soy muy nuevo en Node.js, así que avíseme si hay algún vacío evidente en mi comprensión de cómo funciona.


EDITAR : Investigando más cosas y ahora veo que el patrón de carga del módulo que usa Node.js es en realidad parte de un estándar desarrollado recientemente para cargar bibliotecas de Javascript llamado CommonJS . Dice esto directamente en la página del documento del módulo para Node.js , pero me lo perdí hasta ahora.

Puede terminar siendo que la respuesta a mi pregunta sea "espere hasta que los autores de su biblioteca comiencen a escribir una interfaz CommonJS o hágalo usted mismo".

Chris W.
fuente
pregunta relacionada: stackoverflow.com/questions/22898080/…
Josmar

Respuestas:

75

Hay un método mucho mejor que usar eval: el vmmódulo.

Por ejemplo, aquí está mi execfilemódulo, que evalúa la secuencia de comandos en pathen cualquiera contexto en el 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;
}

Y se puede usar así:

> 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

Donde example.jscontiene:

function getSomeGlobal() {
    return someGlobal;
}

La gran ventaja de este método es que tiene un control completo sobre las variables globales en el script ejecutado: puede pasar globales personalizados (vía context), y se agregarán todos los globales creados por el script context. La depuración también es más fácil porque los errores de sintaxis y similares se informarán con el nombre de archivo correcto.

David Wolever
fuente
¿ runInNewContextUtiliza el contexto global si context(de lo contrario sandbox, en los documentos) no está definido? (este punto no quedó claro en ningún documento que encontré)
Steven Lu
Parece que, con el propósito de jugar con una biblioteca de terceros que ignora Node o el patrón CommonJS, el método eval de Christopher < stackoverflow.com/a/9823294/1450294 > funciona bien. ¿Qué beneficios puede vmofrecer el módulo en este caso?
Michael Scheper
2
Consulte mis actualizaciones para obtener una descripción de por qué este método es mejor que eval.
David Wolever
1
esto es totalmente genial: me permitió reutilizar instantáneamente mi código que no es del módulo basado en la web para una implementación del lado del servidor que envía los resultados por correo electrónico [según un horario] en lugar de mostrarlos en una página web. Todo el código web utilizó el patrón de módulo de aumento suelto y la inyección de script, ¡así que esto funciona muy bien!
Al Joslin
¿Cómo podemos usar esto en Node.js si example.js depende de la biblioteca example1.js?
sytolk
80

Esta es la que creo que es la respuesta 'más correcta' para esta situación.

Supongamos que tiene un archivo de script llamado quadtree.js.

Debería crear una personalizada node_moduleque tenga este tipo de estructura de directorios ...

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

Todo en su ./node_modules/quadtree/quadtree-lib/directorio son archivos de su biblioteca de terceros.

Luego, su ./node_modules/quadtree/index.jsarchivo simplemente cargará esa biblioteca desde el sistema de archivos y hará el trabajo de exportar las cosas correctamente.

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

Ahora puede usar su quadtreemódulo como cualquier otro módulo de nodo ...

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

Me gusta este método porque no es necesario cambiar el código fuente de su biblioteca de terceros, por lo que es más fácil de mantener. Todo lo que necesita hacer en la actualización es mirar su código fuente y asegurarse de que todavía está exportando los objetos adecuados.

Chris W.
fuente
3
Acabo de encontrar su respuesta (hacer un juego multijugador y necesitaba incluir JigLibJS, nuestro motor de física, tanto en el servidor como en el cliente) me ahorró MUCHO tiempo y molestias. ¡Gracias!
stevendesu
8
Si sigue esto exactamente, tenga en cuenta que es bastante fácil borrar accidentalmente su carpeta node_modules usando NPM, especialmente si no lo registra en SCM. Definitivamente, considere colocar su biblioteca QuadTree en un repositorio separado y luego ingerirla npm linken su aplicación. Luego se maneja como si fuera un paquete nativo de Node.js.
btown
@btown, ¿podrías expandir un poco para los novatos como yo qué hacen exactamente SCM y npm para evitar el problema potencial que mencionas?
Flion
¿Es esto realmente necesario si solo quiero incluir un guión?
quantumpotato
1
@flion respondiendo al comentario anterior para la referencia de otros, ya que estoy seguro de que ya sabrá que responde. SCM - Gestión de control de código fuente (por ejemplo, GIT) y un enlace a una demostración rápida pero buena del enlace
npm
30

La forma más sencilla es: eval(require('fs').readFileSync('./path/to/file.js', 'utf8')); Esto funciona muy bien para realizar pruebas en el shell interactivo.

Christopher Weiss
fuente
1
¡Salud! Ayudó mucho
Schoening
Esta es también la forma más rápida y, a veces, rápida y sucia es lo que necesita. Entre esto y la respuesta de David, esta página SO es un recurso excelente.
Michael Scheper
5

AFAIK, así es como se deben cargar los módulos. Sin embargo, en lugar de agregar todas las funciones exportadas al exportsobjeto, también puede agregarlas this(lo que de otro modo sería el objeto global).

Entonces, si desea mantener las otras bibliotecas compatibles, puede hacer esto:

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

o, cuando la biblioteca externa ya tiene su propio espacio de nombres, por ejemplo jQuery(no es que pueda usar eso en un entorno del lado del servidor):

this.jQuery = jQuery;

En un entorno sin nodo, thisse resolvería en el objeto global, convirtiéndolo en una variable global ... que ya lo era. Entonces no debería romper nada.

Editar : James Herdman tiene un buen artículo sobre node.js para principiantes, que también menciona esto.

Martijn
fuente
El truco 'esto' parece una buena manera de hacer las cosas más portátiles para que las bibliotecas de Node.js se puedan usar fuera de Node.js, pero aún significa que necesito cambiar manualmente mis bibliotecas de JavaScript para admitir la sintaxis requerida de Node.js .
Chris W.
@ChrisW .: sí, tendrá que cambiar manualmente sus bibliotecas. Personalmente, también me hubiera gustado un segundo mecanismo para incluir archivos externos, uno que convirtiera automáticamente el espacio de nombres global del archivo incluido en el espacio de nombres importado. ¿Quizás podría presentar una RFE a los desarrolladores de Node?
Martijn
3

No estoy seguro de si realmente terminaré usando esto porque es una solución bastante hacky, pero una forma de evitar esto es construir un pequeño importador de mini-módulos como este ...

En el archivo ./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;
}

Luego, cuando desee utilizar la funcionalidad de su biblioteca, deberá elegir manualmente qué nombres exportar.

Entonces, para una biblioteca como el archivo ./lib/mylibrary.js...

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

Cuando desee utilizar su funcionalidad en su 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)

Sin embargo, no sé qué tan bien funcionaría todo esto en la práctica.

Chris W.
fuente
Oye, guau: ¡una respuesta votada en contra (no por mí) y en favor del mismo usuario para la misma pregunta! ¡Debería haber una insignia para eso! ;-)
Michael Scheper
2

Pude hacer que funcionara actualizando su script, muy fácilmente, simplemente agregando module.exports =cuando fuera apropiado ...

Por ejemplo, tomé su archivo y lo copié en './libs/apprise.js'. Entonces, donde comienza con

function apprise(string, args, callback){

Asigné la función module.exports =así:

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

Por lo tanto, puedo importar la biblioteca a mi código de esta manera:

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

Y estaba listo para irme. YMMV, esto fue con webpack .

John Mee
fuente
0

Una include(filename)función simple con mejores mensajes de error (pila, nombre de archivo, etc.) para eval, en caso de errores:

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

Pero incluso se vuelve más sucio con nodejs: debe especificar esto:

export NODE_MODULE_CONTEXTS=1
nodejs tmp.js

De lo contrario, no podrá utilizar variables globales en archivos incluidos con include(...).

lama12345
fuente