¿Cómo puedo construir código JavaScript escalable?

Siento tu dolor. No hay tantos libros y recursos sobre la arquitectura de aplicaciones JS a gran escala, a pesar de que esto se está volviendo cada vez más común. Y el consejo que existe generalmente está orientado a marcos específicos.

Primero, le sugiero que busque una herramienta de automatización como grunt (The JavaScript Task Runner), que le permitirá mantener su código fuente en varios archivos pero compilarlos en un solo archivo para su implementación.

En general, para crear una aplicación escalable, querrá modularizar su código y usar un acoplamiento flexible, de modo que pueda intercambiar módulos cuando sea necesario. La buena noticia es que es bastante fácil escribir módulos en Javascript. Aquí hay una versión del patrón del módulo:

  ; var MY_NAMESPACE = MY_NAMESPACE ||  {};

 MY_NAMESPACE.View = (function () {

  / * métodos privados * /

  función drawRectangle (color, ancho, alto) {/ * ... * /}

  función drawCircle (color, diámetro) {/ * ... * /}

 / * interfaz pública * /
  regreso {
    renderUI: function () {/ * usa drawRectangle y drawCircle * /}
  }

 } (); 

Algunas notas sobre el código anterior: ¿ves el punto y coma al principio? Esto le permite mantener el módulo en su propio archivo sin preocuparse de que algo malo suceda cuando se concatene con el código de otros archivos. En otras palabras, imagine si el Archivo A termina así, con el escritor olvidando agregar un punto y coma al final:

someFileAStatement;
someFileAStatement;
someFileAStatement

Si el archivo B comienza así …

someFileBStatement;

… podrían concatenarse así:

someFileAStatement;
someFileAStatement;
someFileAStatementsomeFileBStatement;

Pero si inicia el archivo B de esta manera …

; someFileBStatement;

… eliminas ese problema. Si el escritor del Archivo A incluyó un punto y coma final, terminará con someFileAStatement ;; someFileBStatement; pero eso está bien.

Segundo, tenga en cuenta que estoy comenzando el módulo creando y / o haciendo referencia a un espacio de nombres. Si inicia todos sus archivos de esa manera, el primero que se ejecute creará el espacio de nombres y los demás lo usarán, agregándolo.

Digamos que el archivo A se ejecuta primero. Llega a; var MY_NAME_SPACE = MY_NAMESPACE || {}; y descubre que MY_NAMESPACE no existe. Entonces, la primera cláusula en la condición MY_NAMESPACE || {} fallará, lo que hará que se evoque la segunda cláusula ({}). MY_NAMESPACE ahora apuntará a {}, un objeto vacío.

El archivo B evaluará la misma condición: MY_NAMESPACE || {}; Pero esta vez, dado que MY_NAMESPACE existe, se utilizará y la cláusula {} nunca se ejecutará. Así que esta es una manera simple de decir: “Haz un objeto si no existe, pero si existe, adelante y úsalo”.

En tercer lugar, observe que estoy configurando MY_NAMESPACE.view igual al resultado de una función:

  MY_NAMESPACE.View = (function () {
  regreso { /* ... */ }
 }) (); 

Ver el () al final? Eso realmente hace que la función sin nombre se ejecute y establece MY_NAMESPACE.View igual a lo que devuelve esa función. MY_NAMESPACE.View no será igual a un objeto que incluye drawRectangle y drawCircle. Pero debido a los cierres, el método MY_NAMESPACE.View, renderUI, podrá usar esas funciones.

Si escribe un montón de módulos como este, puede escribir un punto de entrada que los configure a todos:

window.onload = init ();

  función init () {
  MY_NAMESPACE.Model.init ();
  MY_NAMESPACE.View.renderUI ();
  MY_NAMESPACE.Controller.init (MY_NAMESPACE.Model, MY_NAMESPACE.View);
 } 

Su definición de controlador podría verse así:

  ; var MY_NAMESPACE = MY_NAMESPACE ||  {};

 MY_NAMESPACE.Controller = (function () {

 / * interfaz pública * /
  regreso {
    init: function (modelo, vista) {/ * ... * /}
  }
 } (); 

O podría renunciar a pasar el modelo y la vista al controlador y simplemente dejar que lo tome por sí mismo:

  ; var MY_NAMESPACE = MY_NAMESPACE ||  {};

 MY_NAMESPACE.Controller = (function () {

 / * interfaz pública * /
  regreso {
    init: function () {/ * var view = MY_NAMESPACE.View ... * /}
  }
 } (); 

Por cierto, estoy simplificando aquí. Para una aplicación grande, no solo tendrá un solo módulo para cada parte MVC. Usa tantos como necesites. Y siempre puedes configurar estructuras como esta:

; var MY_NAMESPACE = MY_NAMESPACE || {};
MY_NAMESPACE.View = MY_NAMESPACE.View || {};

Hay muchas versiones útiles del patrón de módulo, y vale la pena aprenderlas todas. Aquí hay un manual: Patrón del módulo de JavaScript: en profundidad

También querrás alguna forma para que tus módulos se comuniquen entre sí. Puede hacer esto con devoluciones de llamada, un patrón de observación o una combinación de ambos.

Dado que casi definitivamente estará lidiando con eventos DOM, vale la pena extraer código de navegador cruzado de alguna biblioteca, a menos que sepa con certeza que está apuntando a navegadores particulares que usan el mismo patrón de observador.

Aquí hay un código súper ligero de John Resig, el inventor de jquery:

  función addEvent (obj, type, fn) {
   if (obj.attachEvent) {
     obj ['e' + tipo + fn] = fn;
     obj [type + fn] = function () {obj ['e' + type + fn] (window.event);}
     obj.attachEvent ('on' + type, obj [type + fn]);
   } más
     obj.addEventListener (tipo, fn, falso);
 }
 función removeEvent (obj, type, fn) {
   if (obj.detachEvent) {
     obj.detachEvent ('on' + type, obj [type + fn]);
     obj [tipo + fn] = nulo;
   } más
     obj.removeEventListener (tipo, fn, falso);
 } 

Ver eventos flexibles de Javascript.

Esto te permitirá escribir código como este:

addEvent (MY_NAMESPACE.View.someButton, “click”, MY_NAMESPACE.Controller.reactToClick);

Aquí hay un patrón de observador de propósito general creado desde aquí: dos ejemplos del patrón de observador en JavaScript

  editor var = {
     suscriptores: {
         ninguna: []
     },
     on: función (tipo, fn, contexto) {
         tipo = tipo ||  'ninguna';
         fn = typeof fn === "función"?  fn: contexto [fn];

         if (typeof this.subscribers [type] === "undefined") {
             this.subscribers [type] = [];
         }
         this.subscribers [type] .push ({fn: fn, context: context || this});
     },
     eliminar: función (tipo, fn, contexto) {
         this.visitSubscribers ('cancelar suscripción', tipo, fn, contexto);
     },
     fuego: función (tipo, publicación) {
         this.visitSubscribers ('publicar', tipo, publicación);
     },
     visitSubscribers: function (action, type, arg, context) {
         var pubtype = type ||  'ninguna',
             suscriptores = this.subscribers [pubtype],
             yo,
             max = suscriptores?  suscriptores.length: 0;

         para (i = 0; i <max; i + = 1) {
             if (acción === 'publicar') {
                 suscriptores [i] .fn.call (suscriptores [i] .context, arg);
             } más {
                 if (suscriptores [i] .fn === arg && suscriptores [i] .context === contexto) {
                     subscribers.splice (i, 1);
                 }
             }
         }
     }
 }; 

Con esto, puede, por ejemplo, agregar esto a su Modelo:

  regreso {
  / * ... otras partes de la interfaz * /
  addRecord: function (record) { 
    allRecords.push (registro);  // Supongo que allRecords existe
    publisher.fire ('recordAdded', record);
  } 
 } 

Y luego en su controlador, puede agregar esto:

var respondToRecordAdded = function (record) {/ *… * /}

  regreso {
  init: function () {
   on ('recordAdded', respondToRecordAdded, esto);
  }
 } 

Aquí hay algunos recursos que vale la pena revisar:

– Patrones de JavaScript: Stoyan Stefanov: 9780596806750: Amazon.com: Libros

– Aprendizaje de patrones de diseño de JavaScript

– Amazon.com: JavaScript profesional para desarrolladores web (9781118026694): Nicholas C. Zakas: Libros

Personalmente considero que un enfoque basado en eventos basado en componentes es una buena opción para las aplicaciones a gran escala. Puede modularizar su código en componentes y manejar la comunicación entre ellos a través de eventos. Cada componente se mantiene independiente de los demás. Twitter lanzó un marco agradable llamado Flight que sigue convenciones similares. Incluso si necesita una solución “raw-JavaScript”, sería valioso analizar su implementación para algunas ideas.

Otra idea que ayuda a mantener una base de código grande es elegir algún tipo de cargador de módulos. Personalmente considero que RequireJS es perfecto para ello. Lleva tiempo acostumbrarse, pero vale la pena a largo plazo. El código está mejor organizado y puede determinar rápidamente las interdependencias entre archivos y módulos, lo que ayuda cuando la base del código y la cantidad de programadores crecen a tiempo. Cuando esté listo para el lanzamiento, puede usar r.js para concat y minificar su código.