Cómo compartir lógica entre cliente y en servidor con node.js

Supongamos la situación en la que tenemos una aplicación Web (o una WebApp) en la que ejecutamos JavaScript tanto en el cliente (un navegador) como en el servidor (corriendo node.js). Queremos implementar lógica de negocio y aunque en la capa de presentación solo queríamos en un principio mostrar la información, nos es muy útil traer al cliente ciertos aspectos de la lógica de negocio: desde validación de datos, manejo de objetos, etc…

Pues es completamente posible, y de hecho hasta positivo, reutilizar nuestra lógica de objetos de negocio a ambos lados de la aplicación.

Paso 1: Encapsular nuestros objetos de negocio

El primer paso es crear un módulo javascript en un fichero al que llamaremos negocio.js. en él pondremos los objetos de negocio, con sus miembros y métodos, y también constantes.

(function(exports) {
	/**
	 * Constants
	 */
	exports.OK = 1;
	exports.ERROR = 1;

	/**
	 * Business object constructor
	 * 
	 * @param {array} datarow
	 * @param {boolean} init
	 */
	exports.business =
			function(datarow, init) {
				// Extend the object with the common members
				this._object_name = 'business';

				/**
				 * Formats a given value into euro. Returns 0,00 € in case of
				 * error or null
				 */
				this.toEuro = function(n) {
					if (!n || isNaN(n) || isNaN(parseFloat(n))) {
						n = 0.0;
					}
					return parseFloat(n).toLocaleString('es-ES', {
						style : 'currency',
						currency : 'EUR',
						minimumFractionDigits : 2
					});
	};
}(typeof exports === 'undefined' ? this.negocio = {} : exports));

Como se ve todo lo definido en el módulo es crea dentro del espacio “exports“, aunque cuando lo referenciemos desde fuera será el espacio “negocio” el que utilizaremos.

Podemos crear tantos “módulos” como queramos y definir en su interiors todos los objetos que queramos.

Paso 2: Invocar desde navegador y desde node.js

Ahora podemos invocar al fichero .js tanto desde el navegador:

<head>
  <script src="ruta/al/js/negocio.js" type="application/javascript"></script>
</head>

como desde nuestra implementación en node.js:

var negocio = require('ruta/al/js/negocio.js');
if (negocio.business === null) {
	throw 'Business objects not loaded!';
}

Una vez incluido, podemos trabajar con estos objetos o constantes con completa libertad desde el javascript del navegador o desde el código node.js, utilizando el prefijo que hemos designado “negocio”:

var miNegocio = new negocio.business(null, true);
var dinero = miNegocio.toEuro(12.34)
var resultado = negocio.error.OK;

Paso 3: Enriquecer los objetos JSON

Es posible que entre el cliente y el servidor se intercambien nuestros objetos que estarán serializados en formato JSON. Al igual que cuando serializamos objetos con otros lenguajes en formato XML, solamente se transmiten los datos. Pero al contrario que un deserializador de .Net o Java, al deserializar solo tenemos una estructura JSON que contiene sus miembros y no sus métodos (por algún lado se tiene que pagar la flexibilidad de trabajar con prototipos y no clases). En estos casos en los que tenemos un objetos JSON con información, y el prototipo de la clase que lo construye, tenemos que extender el objeto recibido con un objeto nuevo creado con el constructor que contenga sus métodos (o miembros no existentes).

En este caso, la implementación puede tener que ser diferente en el cliente y en el servidor:

En el navegador, podemos utilizar el método extend de jQuery para extender el objeto:

// En el objeto retrieved_data tenemos un objeto devuelto por el servidor,
// sin método 'toEuro' definido.
var miNegocio = new negocio.business(null, true);
$.extend(miNegocio, retrieved_data);
var dinero = miNegocio.toEuro(12.34);

En la parte node.js, podemos utilizar el método “_extend” del paquete “util”:

// En el objeto retrieved_data tenemos un objeto enviado por el cliente,
// sin método 'toEuro' definido.
var extend = require('util')._extend;
var nuevo_negocio = new negocio.business(null, true);
extend(retrieved_data, nuevo_negocio);
var dinero = retrieved_data.toEuro(12.34);

Se puede observar como las funciones ‘extend’ inyectan en el primer parámetro las propiedades del segundo, sean miembros o funciones. Esto significa que en el segundo ejemplo con node.js, si el constructor inicializa un miembro “id” con un valor, aunque sea nulo, esté machacará la propiedad llamada así del que estamos extendiendo.

También podemos implementar nuestra propia función para extender objetos si no queremos depender de librerias externas, y asegurarnos de que el resultado es exactamente el mismo en amos lados (Aunque esto es “inventar la sopa de ajo”):

	function myExtend(obj) {
		for ( var prop in obj) {
			if (obj.hasOwnProperty(prop)) {
				this[prop] = obj[prop];
			}
		}
	}

También hemos de tener en cuenta que ‘extend’ solo extiende el objeto pasado, no los miembros que su vez sean objetos nuestros o miembros de objetos. En el ejemplo, si ‘business’ tuviese un array de objetos ‘cliente’, habría que implementar una función en business para extender sus subobjetos.

Consideraciones

Cuando escribamos código para ser utilizado en ambos lados, tenemos que procurar:

  • Utilizar solamente elementos estándar de ECMAScript que sepamos están implementados en uno u otro lado. No podremos apoyarnos en utilidades de jQuery o módulos de node porque cliente y servidor son dos mundos diferentes.
  • Ser extremadamente cuidadosos con el código, respetando las reglas de JSLint y comentando adecuadamente las funciones, como si las fuese a utilizar otra personal. Ahora corre en más de un sitio a la vez.
  • Ser conscientes de aspectos de seguridad:
    • Hacer siempre comprobaciones de parámetros en el servidor y no fiarnos que ya se han hecho en el cliente. Todo lo que esté en el navegador puede ser modificado.
    • No almacenar rutas o contraseñas en negocio.js. Todo podrá ser visto desde el navegador.
    • Si por un error de instalación negocio.js cuya ruta conoce el cliente se pudiese modificar, ejecutarían código en nuestro servidor. Es preferible desplegar dos copias (cliente y servidor) del mismo fichero.

¡Felicidades! Ya habeis implementado la funcionalidad una vez para ser utilizada en sitios diferentes.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.