Comment partager du code entre Node.js et le navigateur?


242

Je crée une petite application avec un client JavaScript (exécuté dans le navigateur) et un serveur Node.js, communiquant via WebSocket.

Je souhaite partager du code entre le client et le serveur. Je viens juste de commencer avec Node.js et ma connaissance du JavaScript moderne est un peu rouillée, c'est le moins qu'on puisse dire. Je continue donc à me familiariser avec la fonction CommonJS require (). Si je crée mes packages à l'aide de l'objet 'export', je ne vois pas comment utiliser les mêmes fichiers JavaScript dans le navigateur.

Je veux créer un ensemble de méthodes et de classes qui sont utilisées aux deux extrémités pour faciliter l'encodage et le décodage des messages et d'autres tâches en miroir. Cependant, les systèmes de packaging Node.js / CommonJS semblent m'empêcher de créer des fichiers JavaScript pouvant être utilisés des deux côtés.

J'ai également essayé d'utiliser JS.Class pour obtenir un modèle OO plus serré, mais j'ai abandonné parce que je ne pouvais pas comprendre comment faire fonctionner les fichiers JavaScript fournis avec require (). Y a-t-il quelque chose qui me manque ici?


4
Merci à tous d'avoir posté des réponses supplémentaires à cette question. Il s'agit clairement d'un sujet qui changera et évoluera rapidement.
Simon Cave

Réponses:


168

Si vous voulez écrire un module qui peut être utilisé à la fois côté client et côté serveur, j'ai un court article de blog sur une méthode rapide et facile: écrire pour Node.js et le navigateur , essentiellement ce qui suit (où thisest le même que window) :

(function(exports){

    // Your code goes here

   exports.test = function(){
        return 'hello world'
    };

})(typeof exports === 'undefined'? this['mymodule']={}: exports);

Il existe également des projets visant à implémenter l'API Node.js du côté client, comme le gemini de Marak .

Vous pourriez également être intéressé par DNode , qui vous permet d'exposer une fonction JavaScript afin qu'elle puisse être appelée à partir d'une autre machine à l'aide d'un simple protocole réseau basé sur JSON.


Excellent. Merci pour l'info, Caolan.
Simon Cave

2
Vraiment super article Caolan. Je l'ai compris, ça a marché, maintenant je roule à nouveau. Fantastique!
Michael Dausmann

2
J'utilise RequireJs dans mon propre projet, ce qui me permettra de partager mes modules sur le client et le serveur. Nous verrons comment cela fonctionne.
kamranicus

5
@Caolan ce lien est mort
Kamal Reddy

5
Le lien Gemini est mort.
borisdiakur

42

Epeli a une belle solution ici http://epeli.github.com/piler/ qui fonctionne même sans la bibliothèque, il suffit de la mettre dans un fichier appelé share.js

(function(exports){

  exports.test = function(){
       return 'This is a function from shared module';
  };

}(typeof exports === 'undefined' ? this.share = {} : exports));

Côté serveur, utilisez simplement:

var share = require('./share.js');

share.test();

Et du côté client, il suffit de charger le fichier js, puis d'utiliser

share.test();

10
J'aime mieux cette réponse que celle acceptée car elle est mieux expliquée pour les débutants comme moi.
Howie

Dans mon dossier Express en plus du dossier statique (public), j'ai également un dossier nommé 'shared' qui est également accessible depuis le client comme le dossier 'public' comme ça: app.use (express.static ('public')) ; app.use (express.static («partagé»)); Et votre message prolonge mon idée de partager des fichiers avec le client et le serveur. C'est exactement ce dont j'avais besoin. Je vous remercie!
Combinez le

Cette solution + git subtree == génial. Merci!
kevinmicke

@broesch Comment cela fonctionnerait-il dans ES6? J'ai posé cette question comme une nouvelle question , avec quelques problèmes spécifiques à ES6, mais je serais tout aussi heureux de voir une modification ici!
Tedskovsky

15

Extraire le code source jQuery qui fait que cela fonctionne dans le modèle de module Node.js, le modèle de module AMD et global dans le navigateur:

(function(window){
    var jQuery = 'blah';

    if (typeof module === "object" && module && typeof module.exports === "object") {

        // Expose jQuery as module.exports in loaders that implement the Node
        // module pattern (including browserify). Do not create the global, since
        // the user will be storing it themselves locally, and globals are frowned
        // upon in the Node module world.
        module.exports = jQuery;
    }
    else {
        // Otherwise expose jQuery to the global object as usual
        window.jQuery = window.$ = jQuery;

        // Register as a named AMD module, since jQuery can be concatenated with other
        // files that may use define, but not via a proper concatenation script that
        // understands anonymous AMD modules. A named AMD is safest and most robust
        // way to register. Lowercase jquery is used because AMD module names are
        // derived from file names, and jQuery is normally delivered in a lowercase
        // file name. Do this after creating the global so that if an AMD module wants
        // to call noConflict to hide this version of jQuery, it will work.
        if (typeof define === "function" && define.amd) {
            define("jquery", [], function () { return jQuery; });
        }
    }
})(this)

C'est la meilleure méthode (pour ce dont j'avais besoin). Voici un exemple de travail que j'ai créé: gist.github.com/drmikecrowe/4bf0938ea73bf704790f
Mike Crowe

13

N'oubliez pas que la représentation sous forme de chaîne d'une fonction JavaScript représente le code source de cette fonction. Vous pouvez simplement écrire vos fonctions et constructeurs de manière encapsulée afin qu'ils puissent être toString () 'd et envoyés au client.

Une autre façon de le faire est d'utiliser un système de génération, de placer le code commun dans des fichiers séparés, puis de les inclure dans les scripts serveur et client. J'utilise cette approche pour un simple jeu client / serveur via WebSockets où le serveur et le client exécutent tous deux essentiellement la même boucle de jeu et le client se synchronise avec le serveur à chaque tick pour s'assurer que personne ne triche.

Mon système de construction pour le jeu est un simple script Bash qui exécute les fichiers via le préprocesseur C puis via sed pour nettoyer les feuilles de cpp indésirables, donc je peux utiliser toutes les choses normales du préprocesseur comme #include, #define, #ifdef , etc.


2
La sérialisation des fonctions javascript comme des chaînes ne m'est jamais venue à l'esprit. Merci pour le conseil.
Simon Cave

13

Je vous conseille de regarder dans l' adaptateur RequireJS pour Node.js . Le problème est que le modèle de module CommonJS que Node.js utilise par défaut n'est pas asynchrone, ce qui bloque le chargement dans le navigateur Web. RequireJS utilise le modèle AMD, qui est à la fois asynchrone et compatible avec le serveur et le client, tant que vous utilisez l' r.jsadaptateur.


il y a une bibliothèque asynchrone
Jacek Pietal

11

Peut-être que ce n'est pas tout à fait conforme à la question, mais j'ai pensé partager cela.

Je voulais rendre quelques fonctions utilitaires de chaîne simples, déclarées sur String.prototype, disponibles à la fois pour le nœud et le navigateur. Je garde simplement ces fonctions dans un fichier appelé utilities.js (dans un sous-dossier) et je peux facilement y faire référence à la fois à partir d'une balise de script dans mon code de navigateur et en utilisant require (en omettant l'extension .js) dans mon script Node.js :

my_node_script.js

var utilities = require('./static/js/utilities')

my_browser_code.html

<script src="/static/js/utilities.js"></script>

J'espère que ces informations sont utiles à quelqu'un d'autre que moi.


1
J'aime cette approche mais je trouve que mes fichiers statiques se déplacent beaucoup. Une solution que j'ai trouvée est de réexporter le module. Par exemple, créez utilites.jsavec une seule ligne module.exports = require('./static/js/utilities');. De cette façon, vous n'avez besoin de mettre à jour un chemin que si vous mélangez des éléments.
Tom Makin

J'aime cette idée. Juste une note sur le chemin qui m'a pris du temps à comprendre. Mon utilities.jsest dans le shareddossier dans le cadre du projet. L'utilisation require('/shared/utilities')m'a donné l'erreur Cannot find module '/shared/utilities'. Je dois utiliser quelque chose comme ça require('./../../shared/utilities')pour le faire fonctionner. Ainsi, il va toujours du dossier actuel et se déplace vers le haut puis vers le bas.
newman

Maintenant, je vois où placer le module partagé - dans le dossier statique. Merci pour l'info!
Combinez

9

Si vous utilisez des regroupeurs de modules tels que webpack pour regrouper des fichiers JavaScript à utiliser dans un navigateur, vous pouvez simplement réutiliser votre module Node.js pour le frontend exécuté dans un navigateur. En d'autres termes, votre module Node.js peut être partagé entre Node.js et le navigateur.

Par exemple, vous disposez du code sum.js suivant:

Module Node.js normal: sum.js

const sum = (a, b) => {
    return a + b
}

module.exports = sum

Utilisez le module dans Node.js

const sum = require('path-to-sum.js')
console.log('Sum of 2 and 5: ', sum(2, 5)) // Sum of 2 and 5:  7

Réutilisez-le dans le frontend

import sum from 'path-to-sum.js'
console.log('Sum of 2 and 5: ', sum(2, 5)) // Sum of 2 and 5:  7

4

Le serveur peut simplement envoyer des fichiers source JavaScript au client (navigateur) mais l'astuce est que le client devra fournir un mini environnement "exports" avant de pouvoir execle code et le stocker en tant que module.

Un moyen simple de créer un tel environnement est d'utiliser une fermeture. Par exemple, supposons que votre serveur fournit des fichiers source via HTTP comme http://example.com/js/foo.js. Le navigateur peut charger les fichiers requis via un XMLHttpRequest et charger le code comme ceci:

ajaxRequest({
  method: 'GET',
  url: 'http://example.com/js/foo.js',
  onSuccess: function(xhr) {
    var pre = '(function(){var exports={};'
      , post = ';return exports;})()';
    window.fooModule = eval(pre + xhr.responseText + post);
  }
});

La clé est que le client peut encapsuler le code étranger dans une fonction anonyme à exécuter immédiatement (une fermeture) qui crée l'objet "exports" et le renvoie afin que vous puissiez l'attribuer où vous le souhaitez, plutôt que de polluer l'espace de noms global. Dans cet exemple, il est attribué à l'attribut window fooModulequi contiendra le code exporté par le fichier foo.js.


2
chaque fois que vous utilisez eval, vous tuez un gnome
Jacek Pietal

1
J'utiliserais window.fooModule = {}; (new Function('exports', xhr.responseText))(window.fooModule).
GingerPlusPlus

2

Aucune des solutions précédentes n'apporte le système de modules CommonJS au navigateur.

Comme mentionné dans les autres réponses, il existe des solutions de gestionnaire / packager d'actifs comme Browserify ou Piler et il existe des solutions RPC comme dnode ou nowjs .

Mais je n'ai pas pu trouver une implémentation de CommonJS pour le navigateur (y compris une require()fonction et exports/ module.exportsobjets, etc.). J'ai donc écrit le mien, pour découvrir ensuite que quelqu'un d'autre l'avait écrit mieux que moi: https://github.com/weepy/brequire . Il s'appelle Brequire (abréviation de Browser require).

À en juger par la popularité, les gestionnaires d'actifs répondent aux besoins de la plupart des développeurs. Cependant, si vous avez besoin d'une implémentation de navigateur de CommonJS, Brequire fera probablement l' affaire .

Mise à jour 2015: je n'utilise plus Brequire (il n'a pas été mis à jour depuis quelques années). Si je suis en train d'écrire un petit module open-source et que je veux que tout le monde puisse l'utiliser facilement, je suivrai un modèle similaire à la réponse de Caolan (ci-dessus) - j'ai écrit un blog à ce sujet quelques années depuis.

Cependant, si j'écris des modules pour un usage privé ou pour une communauté normalisée sur CommonJS (comme la communauté Ampersand ), je vais simplement les écrire au format CommonJS et utiliser Browserify .


1

now.js vaut également le détour. Il vous permet d'appeler côté serveur depuis le côté client et les fonctions côté client depuis le côté serveur


1
Le projet a été interrompu - connaissez-vous de bons remplaçants? groups.google.com/forum/#!msg/nowjs/FZXWZr22vn8/UzTMPD0tdVQJ
Anderson Green

le seul autre que je connaisse était le pont et il était par les mêmes personnes, donc aussi abandonné. La version 0.9 de socket.io prend également en charge les rappels pour les événements - cependant rien de tel que le code de partage de now.js, mais cela fonctionne assez bien.
balupton

Il y a aussi sharejs, qui semble être activement maintenu. sharejs.org
Anderson Green

1

Si vous souhaitez écrire votre navigateur dans un style semblable à Node.js, vous pouvez essayer la duplication .

Il n'y a pas de compilation de code de navigateur, vous pouvez donc écrire votre application sans limitations.


1

Écrivez votre code en tant que modules RequireJS et vos tests en tant que tests Jasmine .

De cette façon, le code peut être chargé partout avec RequireJS et les tests doivent être exécutés dans le navigateur avec jasmine-html et avec jasmine-node dans Node.js sans avoir besoin de modifier le code ou les tests.

Voici un exemple de travail pour cela.


1

Cas d'utilisation: partagez la configuration de votre application entre Node.js et le navigateur (ce n'est qu'une illustration, probablement pas la meilleure approche en fonction de votre application).

Problème: vous ne pouvez pas utiliser window(n'existe pas dans Node.js) ni global(n'existe pas dans le navigateur).

Solution:

  • Fichier config.js:

    var config = {
      foo: 'bar'
    };
    if (typeof module === 'object') module.exports = config;
  • Dans le navigateur (index.html):

    <script src="config.js"></script>
    <script src="myApp.js"></script>

    Vous pouvez maintenant ouvrir les outils de développement et accéder à la variable globale config

  • Dans Node.js (app.js):

    const config = require('./config');
    console.log(config.foo); // Prints 'bar'
  • Avec Babel ou TypeScript:

    import config from './config';
    console.log(config.foo); // Prints 'bar'

1
Merci pour ça.
Microsis

Suivi: Disons que j'ai deux fichiers qui sont partagés entre server.js et client.js: shared.jset helpers.js- shared.jsutilise des fonctions de helpers.js, donc il a besoin const { helperFunc } = require('./helpers')en haut, pour qu'il fonctionne côté serveur. Le problème est sur le client, il se plaint de requirene pas être une fonction, mais si j'encapsule la ligne require if (typeof module === 'object') { ... }, le serveur dit que helperFunc () n'est pas défini (en dehors de l'instruction if). Des idées pour le faire fonctionner sur les deux?
Microsis

Mise à jour: Il semble que je l'ai fait fonctionner en plaçant ceci en haut de shared.js: helperFunc = (typeof exports === 'undefined') ? helperFunc : require('./helpers').helperFunc;- Aurez-vous besoin d'une ligne pour chaque fonction exportée malheureusement, mais j'espère que c'est une bonne solution?
Microsis

1

J'ai écrit un module simple , qui peut être importé (en utilisant require dans Node, ou des balises de script dans le navigateur), que vous pouvez utiliser pour charger des modules à la fois depuis le client et depuis le serveur.

Exemple d'utilisation

1. Définition du module

Placez ce qui suit dans un fichier log2.js, à l'intérieur de votre dossier de fichiers Web statiques:

let exports = {};

exports.log2 = function(x) {
    if ( (typeof stdlib) !== 'undefined' )
        return stdlib.math.log(x) / stdlib.math.log(2);

    return Math.log(x) / Math.log(2);
};

return exports;

Aussi simple que cela!

2. Utilisation du module

Puisqu'il s'agit d'un chargeur de module bilatéral , nous pouvons le charger des deux côtés (client et serveur). Par conséquent, vous pouvez effectuer les opérations suivantes, mais vous n'avez pas besoin de faire les deux à la fois (et encore moins dans un ordre particulier):

  • Dans le nœud

Dans Node, c'est simple:

var loader = require('./mloader.js');
loader.setRoot('./web');

var logModule = loader.importModuleSync('log2.js');
console.log(logModule.log2(4));

Cela devrait revenir 2.

Si votre fichier ne se trouve pas dans le répertoire actuel de Node, assurez-vous d'appeler loader.setRootavec le chemin d'accès à votre dossier de fichiers Web statiques (ou où que se trouve votre module).

  • Dans le navigateur:

Définissez d'abord la page Web:

<html>
    <header>
        <meta charset="utf-8" />
        <title>Module Loader Availability Test</title>

        <script src="mloader.js"></script>
    </header>

    <body>
        <h1>Result</h1>
        <p id="result"><span style="color: #000088">Testing...</span></p>

        <script>
            let mod = loader.importModuleSync('./log2.js', 'log2');

            if ( mod.log2(8) === 3 && loader.importModuleSync('./log2.js', 'log2') === mod )
                document.getElementById('result').innerHTML = "Your browser supports bilateral modules!";

            else
                document.getElementById('result').innerHTML = "Your browser doesn't support bilateral modules.";
        </script>
    </body>
</html>

Assurez-vous de ne pas ouvrir le fichier directement dans votre navigateur; comme il utilise AJAX, je vous suggère de jeter un coup d'œil au http.servermodule de Python 3 (ou quelle que soit votre solution de déploiement de serveur Web ultra-rapide, en ligne de commande, de dossier) à la place.

Si tout se passe bien, cela apparaîtra:

entrez la description de l'image ici


0

J'ai écrit ceci, il est simple à utiliser si vous souhaitez définir toutes les variables sur la portée globale:

(function(vars, global) {
    for (var i in vars) global[i] = vars[i];
})({
    abc: function() {
        ...
    },
    xyz: function() {
        ...
    }
}, typeof exports === "undefined" ? this : exports);
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.