Charger les bibliothèques Javascript «Vanilla» dans Node.js

108

Il existe des bibliothèques Javascript tierces qui ont des fonctionnalités que je voudrais utiliser dans un serveur Node.js. (Plus précisément, je veux utiliser une bibliothèque javascript QuadTree que j'ai trouvée.) Mais ces bibliothèques ne sont que des .jsfichiers simples et non des "bibliothèques Node.js".

En tant que telles, ces bibliothèques ne suivent pas la exports.var_namesyntaxe attendue par Node.js pour ses modules. Pour autant que je sache, cela signifie quand vous faites module = require('module_name');ou module = require('./path/to/file.js');vous vous retrouverez avec un module sans fonctions accessibles au public, etc.

Ma question est alors "Comment charger un fichier javascript arbitraire dans Node.js de manière à pouvoir utiliser ses fonctionnalités sans avoir à le réécrire pour qu'il le fasse exports?"

Je suis très nouveau sur Node.js, alors faites-moi savoir s'il y a un trou flagrant dans ma compréhension de son fonctionnement.


EDIT : En recherchant davantage et je vois maintenant que le modèle de chargement de module utilisé par Node.js fait en fait partie d'un standard récemment développé pour le chargement de bibliothèques Javascript appelé CommonJS . Il dit cela directement sur la page de documentation du module pour Node.js , mais je l'ai manqué jusqu'à présent.

Il se peut que la réponse à ma question soit "attendez que les auteurs de votre bibliothèque se mettent à écrire une interface CommonJS ou faites-le vous-même".

Chris W.
la source
question connexe: stackoverflow.com/questions/22898080/…
Josmar

Réponses:

75

Il existe une bien meilleure méthode que d'utiliser eval: le vmmodule.

Par exemple, voici mon execfilemodule, qui évalue le script pathdans l'un contextou l'autre contexte 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;
}

Et il peut être utilisé comme ceci:

> 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.jscontient:

function getSomeGlobal() {
    return someGlobal;
}

Le gros avantage de cette méthode est que vous avez un contrôle complet sur les variables globales dans le script exécuté: vous pouvez passer des globaux personnalisés (via context), et tous les globaux créés par le script seront ajoutés context. Le débogage est également plus facile car les erreurs de syntaxe et autres seront signalées avec le nom de fichier correct.

David Wolever
la source
Utilise-t-il runInNewContextle contexte global si context(autrement appelé sandbox, dans la documentation) n'est pas défini? (ce point n'a été clarifié par aucun document que j'ai trouvé)
Steven Lu
Il semble que, dans le but de jouer avec une bibliothèque tierce ignorant Node ou le modèle CommonJS, la méthode eval de Christopher < stackoverflow.com/a/9823294/1450294 > fonctionne bien. Quels avantages le vmmodule peut-il offrir dans ce cas?
Michael Scheper
2
Voir mes mises à jour pour une description des raisons pour lesquelles cette méthode est meilleure que eval.
David Wolever
1
c'est totalement génial - cela m'a permis de réutiliser instantanément mon code non-module basé sur le Web pour une implémentation côté serveur qui envoie les sorties par e-mail [selon un calendrier] au lieu de les afficher sur une page Web. Tout le code Web a utilisé le modèle de module à augmentation lâche et l'injection de script - donc cela fonctionne très bien !!
Al Joslin
Comment pouvons-nous utiliser cela dans Node.js si example.js dépend de la bibliothèque example1.js?
sytolk
80

Voici ce que je pense être la réponse «la plus juste» à cette situation.

Supposons que vous ayez un fichier de script appelé quadtree.js.

Vous devez créer une personnalisation node_modulequi a ce type de structure de répertoires ...

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

Tout ce qui se trouve dans votre ./node_modules/quadtree/quadtree-lib/répertoire est constitué de fichiers de votre bibliothèque tierce.

Ensuite, votre ./node_modules/quadtree/index.jsfichier chargera simplement cette bibliothèque à partir du système de fichiers et effectuera le travail d'exportation des choses correctement.

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

Vous pouvez maintenant utiliser votre quadtreemodule comme n'importe quel autre module de nœud ...

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

J'aime cette méthode car il n'est pas nécessaire de changer le code source de votre bibliothèque tierce - c'est donc plus facile à maintenir. Tout ce que vous avez à faire lors de la mise à niveau est de consulter leur code source et de vous assurer que vous exportez toujours les objets appropriés.

Chris W.
la source
3
Je viens de trouver votre réponse (créer un jeu multijoueur et avoir besoin d'inclure JigLibJS, notre moteur physique, sur le serveur ainsi que sur le client), vous m'avez économisé BEAUCOUP de temps et de tracas. Je vous remercie!
stevendesu
8
Si vous suivez exactement cela, gardez à l'esprit qu'il est assez facile d'effacer accidentellement votre dossier node_modules à l'aide de NPM, surtout si vous ne l'enregistrez pas dans SCM. Pensez à mettre votre bibliothèque QuadTree dans un référentiel séparé, puis à l'intégrer npm linkdans votre application. Ensuite, il est traité comme s'il s'agissait d'un package Node.js natif.
btown
@btown, pourriez-vous développer un peu pour les débutants comme moi ce que font exactement SCM et npm link pour éviter le problème potentiel que vous mentionnez?
Flion
Est-ce vraiment nécessaire si je veux simplement inclure un script?
quantumpotato
1
@flion répondant à l'ancien commentaire pour les autres ref car je suis sûr que vous saurez que vous répondez maintenant. SCM - Source Control Management (par exemple GIT) et un lien vers une démo rapide mais bonne de npm link
delp
30

Le moyen le plus simple est: eval(require('fs').readFileSync('./path/to/file.js', 'utf8')); Cela fonctionne très bien pour les tests dans le shell interactif.

Christopher Weiss
la source
1
Cheers Mate! A beaucoup aidé
Schoening
C'est aussi le moyen le plus rapide, et parfois rapide et sale est ce dont vous avez besoin. Entre ceci et la réponse de David, cette page SO est une excellente ressource.
Michael Scheper
5

AFAIK, c'est bien ainsi que les modules doivent être chargés. Cependant, au lieu de coller toutes les fonctions exportées sur l' exportsobjet, vous pouvez également les coller sur this(ce qui serait autrement l'objet global).

Donc, si vous souhaitez garder les autres bibliothèques compatibles, vous pouvez le faire:

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

ou, lorsque la bibliothèque externe a déjà son propre espace de noms, par exemple jQuery(pas que vous pouvez utiliser que dans un environnement côté serveur):

this.jQuery = jQuery;

Dans un environnement non-Node, thisse résoudrait à l'objet global, en faisant ainsi une variable globale ... ce qu'il était déjà. Donc ça ne devrait rien casser.

Edit : James Herdman a un bel article sur node.js pour les débutants, qui le mentionne également.

Martijn
la source
L'astuce 'this' semble être un bon moyen de rendre les choses plus portables afin que les bibliothèques Node.js puissent être utilisées en dehors de Node.js, mais cela signifie toujours que je dois modifier manuellement mes bibliothèques javascript pour prendre en charge la syntaxe Node.js .
Chris W.
@ChrisW .: oui, vous devrez modifier manuellement vos bibliothèques. Personnellement, j'aurais aussi aimé un deuxième mécanisme pour inclure les fichiers externes, un qui convertit automatiquement l'espace de noms global du fichier inclus en l'espace de noms importé. Peut-être pourriez-vous déposer une RFE auprès des développeurs Node?
Martijn
3

Je ne sais pas si je finirai par l'utiliser car c'est une solution plutôt pirate, mais une solution consiste à créer un petit importateur de mini-modules comme celui-ci ...

Dans le fichier ./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;
}

Ensuite, lorsque vous souhaitez utiliser les fonctionnalités de votre bibliothèque, vous devez choisir manuellement les noms à exporter.

Donc pour une bibliothèque comme le fichier ./lib/mylibrary.js...

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

Lorsque vous souhaitez utiliser ses fonctionnalités dans votre code 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)

Je ne sais pas si tout cela fonctionnerait dans la pratique.

Chris W.
la source
Hey, wow: une réponse à la baisse (pas par moi) et à la hausse par le même utilisateur pour la même question! Il devrait y avoir un badge pour ça! ;-)
Michael Scheper
2

J'ai pu le faire fonctionner en mettant à jour leur script, très facilement, en ajoutant simplement le module.exports =cas échéant ...

Par exemple, j'ai pris leur fichier et j'ai copié dans './libs/apprise.js'. Alors où ça commence par

function apprise(string, args, callback){

J'ai assigné la fonction à module.exports =ainsi:

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

Ainsi, je suis capable d'importer la bibliothèque dans mon code comme ceci:

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

Et j'étais prêt à partir. YMMV, c'était avec webpack .

John Mee
la source
0

Une include(filename)fonction simple avec une meilleure messagerie d'erreur (pile, nom de fichier etc.) pour eval, en cas d'erreurs:

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

Mais cela devient encore plus sale avec nodejs: vous devez spécifier ceci:

export NODE_MODULE_CONTEXTS=1
nodejs tmp.js

Sinon, vous ne pouvez pas utiliser de variables globales dans les fichiers inclus avec include(...).

lama12345
la source