¿Cómo puedo compartir código entre Node.js y el navegador?

242

Estoy creando una pequeña aplicación con un cliente JavaScript (se ejecuta en el navegador) y un servidor Node.js, que se comunica mediante WebSocket.

Me gustaría compartir el código entre el cliente y el servidor. Acabo de comenzar con Node.js y mi conocimiento del JavaScript moderno es un poco oxidado, por decir lo menos. Así que todavía estoy entendiendo la función require () de CommonJS. Si estoy creando mis paquetes usando el objeto 'exportar', entonces no puedo ver cómo podría usar los mismos archivos JavaScript en el navegador.

Quiero crear un conjunto de métodos y clases que se utilizan en ambos extremos para facilitar la codificación y decodificación de mensajes y otras tareas duplicadas. Sin embargo, los sistemas de empaquetado Node.js / CommonJS parecen impedirme crear archivos JavaScript que puedan usarse en ambos lados.

También intenté usar JS.Class para obtener un modelo OO más estricto, pero me di por vencido porque no pude descubrir cómo hacer que los archivos JavaScript proporcionados funcionen require (). ¿Hay algo que me estoy perdiendo aquí?

Simon Cave
fuente
44
Gracias a todos por publicar respuestas adicionales a esta pregunta. Este es claramente un tema que cambiará y evolucionará rápidamente.
Simon Cave

Respuestas:

168

Si desea escribir un módulo que pueda usarse tanto del lado del cliente como del lado del servidor, tengo una breve publicación de blog sobre un método rápido y fácil: Escribir para Node.js y el navegador , esencialmente lo siguiente (donde thises lo mismo que window) :

(function(exports){

    // Your code goes here

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

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

Alternativamente, hay algunos proyectos que apuntan a implementar la API Node.js en el lado del cliente, como el gemini de Marak .

También puede estar interesado en DNode , que le permite exponer una función de JavaScript para que se pueda llamar desde otra máquina utilizando un protocolo de red simple basado en JSON.

Caolan
fuente
Excelente. Gracias por la información, Caolan.
Simon Cave
2
Muy buen artículo Caolan. Lo entendí, funcionó, ahora estoy rodando de nuevo. ¡Fantástico!
Michael Dausmann el
2
Estoy usando RequireJs en mi propio proyecto, lo que me permitirá compartir mis módulos en el cliente y el servidor. Veremos cómo funciona.
kamranicus
55
@Caolan ese enlace está muerto
Kamal Reddy
55
El enlace de Géminis está muerto.
borisdiakur
42

Epeli tiene una buena solución aquí http://epeli.github.com/piler/ que incluso funciona sin la biblioteca, solo ponga esto en un archivo llamado share.js

(function(exports){

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

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

En el lado del servidor simplemente use:

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

share.test();

Y en el lado del cliente, simplemente cargue el archivo js y luego use

share.test();
Broesch
fuente
10
Me gusta esta respuesta mejor que la aceptada porque se explica mejor para los novatos como yo.
Howie
En mi carpeta Express además de la carpeta estática (pública), también tengo una carpeta llamada 'compartida' que también es accesible desde el cliente como la carpeta 'pública' así: app.use (express.static ('public')) ; app.use (express.static ('compartido')); Y su publicación amplía mi idea de compartir archivos con el cliente y el servidor. Esto es exactamente lo que necesitaba. ¡Gracias!
Combine el
Esta solución + git subtree == impresionante. ¡Gracias!
kevinmicke
@broesch ¿Cómo funcionaría esto en ES6? He hecho esto como una nueva pregunta , con algunos problemas específicos de ES6, ¡pero me encantaría ver una edición aquí!
Tedskovsky
15

Verifique el código fuente de jQuery que hace que esto funcione en el patrón del módulo Node.js, el patrón del módulo AMD y global en el navegador:

(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)
Wlingke
fuente
Este es el mejor método (para lo que necesitaba). Aquí hay un ejemplo de trabajo que creé: gist.github.com/drmikecrowe/4bf0938ea73bf704790f
Mike Crowe
13

No olvide que la representación de cadena de una función de JavaScript representa el código fuente de esa función. Simplemente puede escribir sus funciones y constructores de forma encapsulada para que puedan ser toString () 'd y enviados al cliente.

Otra forma de hacerlo es usar un sistema de compilación, colocar el código común en archivos separados y luego incluirlos en los scripts del servidor y del cliente. Estoy usando ese enfoque para un juego simple de cliente / servidor a través de WebSockets donde el servidor y el cliente ejecutan esencialmente el mismo bucle de juego y el cliente se sincroniza con el servidor cada tic para asegurarse de que nadie haga trampa.

Mi sistema de compilación para el juego es un simple script Bash que ejecuta los archivos a través del preprocesador C y luego a través de sed para limpiar algunas hojas basura de cpp, para que pueda usar todo el preprocesador normal como #include, #define, #ifdef etc.

Dagg Nabbit
fuente
2
Serializar las funciones de JavaScript como cadenas nunca se me ocurrió. Gracias por el consejo.
Simon Cave
13

Yo recomendaría mirar en el adaptador de RequireJS para Node.js . El problema es que el patrón de módulo CommonJS que Node.js usa por defecto no es asíncrono, lo que bloquea la carga en el navegador web. RequireJS usa el patrón AMD, que es asíncrono y compatible con el servidor y el cliente, siempre que use el r.jsadaptador.

Fornido
fuente
hay una biblioteca asíncrona
Jacek Pietal
11

Tal vez esto no esté totalmente en línea con la pregunta, pero pensé en compartir esto.

Quería hacer un par de funciones de utilidad de cadena simples, declaradas en String.prototype, disponibles tanto para el nodo como para el navegador. Simplemente mantengo estas funciones en un archivo llamado utilities.js (en una subcarpeta) y puedo hacer referencia a ellas fácilmente desde una etiqueta de script en el código de mi navegador y al usar require (omitiendo la extensión .js) en mi script Node.js :

my_node_script.js

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

my_browser_code.html

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

Espero que esta información sea útil para alguien que no sea yo.

Markus Amalthea Magnuson
fuente
1
Me gusta este enfoque, pero creo que mis archivos estáticos se mueven bastante. Una solución que he encontrado es volver a exportar el módulo. Por ejemplo, cree utilites.jscon una sola línea module.exports = require('./static/js/utilities');. De esta manera, solo necesita actualizar una ruta si baraja las cosas.
Tom Makin
Me gusta esta idea. Solo una nota sobre el camino que me llevó un tiempo descubrir. Mi utilities.jsestá en la sharedcarpeta debajo del proyecto. Usar require('/shared/utilities')me dio el error Cannot find module '/shared/utilities'. Tengo que usar algo como esto require('./../../shared/utilities')para que funcione. Por lo tanto, siempre va desde la carpeta actual y viaja hacia la raíz y luego hacia abajo.
newman
Ahora veo dónde colocar el módulo compartido, en la carpeta estática. Gracias por la info!
Combinar
9

Si usa paquetes de módulos como webpack para agrupar archivos JavaScript para su uso en un navegador, simplemente puede reutilizar su módulo Node.js para la interfaz que se ejecuta en un navegador. En otras palabras, su módulo Node.js se puede compartir entre Node.js y el navegador.

Por ejemplo, tiene el siguiente código sum.js:

Módulo Node.js normal: sum.js

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

module.exports = sum

Use el módulo en 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

Reutilízalo en la interfaz

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

El servidor simplemente puede enviar archivos fuente de JavaScript al cliente (navegador), pero el truco es que el cliente tendrá que proporcionar un entorno de mini "exportaciones" antes de que pueda exec el código y almacenarlo como un módulo.

Una manera simple de crear un entorno así es usar un cierre. Por ejemplo, supongamos que su servidor proporciona archivos fuente a través de HTTP como http://example.com/js/foo.js. El navegador puede cargar los archivos requeridos a través de XMLHttpRequest y cargar el código de esta manera:

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 clave es que el cliente puede envolver el código externo en una función anónima que se ejecutará de inmediato (un cierre) que crea el objeto "exportaciones" y lo devuelve para que pueda asignarlo donde desee, en lugar de contaminar el espacio de nombres global. En este ejemplo, se asigna al atributo de ventana fooModuleque contendrá el código exportado por el archivo foo.js.

maerics
fuente
2
cada vez que se utiliza eval se mata a un gnomo
Jacek Pietal
1
Que haría uso window.fooModule = {}; (new Function('exports', xhr.responseText))(window.fooModule).
GingerPlusPlus
2

Ninguna de las soluciones anteriores trae el sistema de módulos CommonJS al navegador.

Como se mencionó en las otras respuestas, existen soluciones de administrador / empaquetador de activos como Browserify o Piler y hay soluciones RPC como dnode o nowjs .

Pero no pude encontrar una implementación de CommonJS para el navegador (incluida una require()función y exports/ module.exportsobjetos, etc.). Entonces escribí el mío, solo para descubrir después que alguien más lo había escrito mejor que yo: https://github.com/weepy/brequire . Se llama Brequire (abreviatura de navegador requiere).

A juzgar por la popularidad, los administradores de activos se ajustan a las necesidades de la mayoría de los desarrolladores. Sin embargo, si necesita una implementación de navegador de CommonJS, Brequire probablemente se ajuste a la factura.

Actualización de 2015: ya no uso Brequire (no se ha actualizado en algunos años). Si solo estoy escribiendo un pequeño módulo de código abierto y quiero que cualquiera pueda usarlo fácilmente, entonces seguiré un patrón similar a la respuesta de Caolan (arriba): escribí una publicación de blog al respecto un par de años hace.

Sin embargo, si estoy escribiendo módulos para uso privado o para una comunidad que está estandarizada en CommonJS (como la comunidad Ampersand ), los escribiré en formato CommonJS y usaré Browserify .

Peter Rust
fuente
1

now.js también vale la pena echarle un vistazo. Le permite llamar al lado del servidor desde el lado del cliente, y las funciones del lado del cliente desde el lado del servidor

Balupton
fuente
1
El proyecto ha sido descontinuado. ¿Conoces algún buen reemplazo para él? groups.google.com/forum/#!msg/nowjs/FZXWZr22vn8/UzTMPD0tdVQJ
Anderson Green el
El único otro que conozco fue el puente y fue por la misma gente, por lo que también fue abandonado. La versión 0.9 de socket.io también admite devoluciones de llamada para eventos; sin embargo, nada como el código para compartir de now.js, pero funciona lo suficientemente bien.
Balupton 01 de
También hay sharejs, que parece mantenerse activamente. sharejs.org
Anderson Green
1

Si desea escribir su navegador en un estilo similar a Node.js, puede intentar dualify .

No hay compilación de código del navegador, por lo que puede escribir su aplicación sin limitaciones.

farincz
fuente
1

Escriba su código como módulos RequireJS y sus pruebas como pruebas de Jasmine .

De esta forma, el código se puede cargar en todas partes con RequireJS y las pruebas se pueden ejecutar en el navegador con jasmine-html y con jasmine-node en Node.js sin la necesidad de modificar el código o las pruebas.

Aquí hay un ejemplo de trabajo para esto.

Blacksonic
fuente
1

Caso de uso: comparta la configuración de su aplicación entre Node.js y el navegador (esto es solo una ilustración, probablemente no sea el mejor enfoque dependiendo de su aplicación).

Problema: no puede usar window(no existe en Node.js) niglobal (no existe en el navegador).

Solución:

  • Archivo config.js:

    var config = {
      foo: 'bar'
    };
    if (typeof module === 'object') module.exports = config;
  • En el navegador (index.html):

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

    Ahora puede abrir las herramientas de desarrollo y acceder a la variable global config

  • En Node.js (app.js):

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

    import config from './config';
    console.log(config.foo); // Prints 'bar'
tanguy_k
fuente
1
Gracias por esto.
Microsis
Seguimiento: Digamos que tengo dos archivos que se comparten entre server.js y client.js: shared.jsy helpers.js- shared.jsutiliza funciones de helpers.js, por lo que necesita const { helperFunc } = require('./helpers')en la parte superior, para que funcione en el lado del servidor. El problema está en el cliente, se queja de requireno ser una función, pero si envuelvo la línea requerida if (typeof module === 'object') { ... }, el servidor dice que helperFunc () no está definido (fuera de la instrucción if). ¿Alguna idea para que funcione en ambos?
Microsis
Actualización: Parece que lo puse en funcionamiento colocando esto en la parte superior de shared.js: helperFunc = (typeof exports === 'undefined') ? helperFunc : require('./helpers').helperFunc;- ¿Necesitará una línea para cada función exportada desafortunadamente pero espero que sea una buena solución?
Microsis
1

Escribí un módulo simple , que puede importarse (ya sea mediante require in Node, o etiquetas de script en el navegador), que puede usar para cargar módulos tanto desde el cliente como desde el servidor.

Ejemplo de uso

1. Definiendo el módulo

Coloque lo siguiente en un archivo log2.js, dentro de su carpeta de archivos web estáticos:

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;

¡Simple como eso!

2. Usando el módulo

Como es un cargador de módulos bilateral , podemos cargarlo desde ambos lados (cliente y servidor). Por lo tanto, puede hacer lo siguiente, pero no necesita hacer ambas cosas a la vez (y mucho menos en un orden particular):

  • En el nodo

En Node, es simple:

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

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

Esto debería volver 2.

Si su archivo no está en el directorio actual de Node, asegúrese de llamar loader.setRootcon la ruta a su carpeta de archivos web estáticos (o donde sea que esté su módulo).

  • En el navegador:

Primero, defina la página 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>

Asegúrese de no abrir el archivo directamente en su navegador; dado que usa AJAX, le sugiero que eche un vistazo al http.servermódulo de Python 3 (o cualquiera que sea su solución de implementación de servidor web de carpetas, línea de comandos y súper rápida).

Si todo va bien, esto aparecerá:

ingrese la descripción de la imagen aquí

Gustavo6046
fuente
0

Escribí esto, es fácil de usar si desea establecer todas las variables en el ámbito global:

(function(vars, global) {
    for (var i in vars) global[i] = vars[i];
})({
    abc: function() {
        ...
    },
    xyz: function() {
        ...
    }
}, typeof exports === "undefined" ? this : exports);
súper
fuente