La mejor forma de organizar el código jQuery / JavaScript (2013) [cerrado]

104

El problema

Esta respuesta ha sido respondida antes, pero es antigua y no está actualizada. Tengo más de 2000 líneas de código en un solo archivo y, como todos sabemos, esto es una mala práctica, especialmente cuando estoy revisando el código o agregando nuevas funciones. Quiero organizar mejor mi código, por ahora y para el futuro.

Debo mencionar que estoy construyendo una herramienta (no un sitio web simple) con muchos botones, elementos de la interfaz de usuario, arrastrar, soltar, oyentes / controladores de acción y función en el ámbito global donde varios oyentes pueden usar la misma función.

Código de ejemplo

$('#button1').on('click', function(e){
    // Determined action.
    update_html();
});

... // Around 75 more of this

function update_html(){ .... }

...

Más código de ejemplo

Conclusión

Realmente necesito organizar este código para un mejor uso y no repetirme y poder agregar nuevas funciones y actualizar las antiguas. Trabajaré en esto por mi cuenta. Algunos selectores pueden tener 100 líneas de código, otros son 1. He mirado un poco require.jsy lo he encontrado un poco repetitivo, y de hecho escribo más código del necesario. Estoy abierto a cualquier posible solución que se ajuste a este criterio y los enlaces a recursos / ejemplos siempre son una ventaja.

Gracias.

Kivylius
fuente
Si desea agregar backbone.js y require.js, será mucho trabajo.
jantimon
1
¿Qué tareas te encuentras haciendo una y otra vez al escribir esto?
Mike Samuel
4
¿Ha visitado codereview.stackexchange.com ?
Antony
4
¡Aprende Angular! Es el futuro.
Onur Yıldırım
2
Su código no debe estar en un enlace externo, debe estar aquí. Además, @codereview es un lugar mejor para este tipo de preguntas.
George Stocker

Respuestas:

98

Voy a repasar algunas cosas simples que pueden ayudarlo o no. Algunos pueden ser obvios, algunos pueden ser extremadamente arcanos.

Paso 1: Compartimente su código

Separar su código en múltiples unidades modulares es un muy buen primer paso. Reúna lo que funciona "en conjunto" y colóquelo en su propia unidad pequeña. no se preocupe por el formato por ahora, manténgalo en línea. La estructura es un punto posterior.

Entonces, suponga que tiene una página como esta:

ingrese la descripción de la imagen aquí

Tendría sentido compartimentar de modo que todos los controladores / carpetas de eventos relacionados con el encabezado estén allí, para facilitar el mantenimiento (y no tener que examinar 1000 líneas).

Luego puede usar una herramienta como Grunt para reconstruir su JS en una sola unidad.

Paso 1a: gestión de la dependencia

Utilice una biblioteca como RequireJS o CommonJS para implementar algo llamado AMD . La carga asincrónica del módulo le permite indicar explícitamente de qué depende su código, lo que luego le permite descargar la llamada de biblioteca al código. Puedes decir literalmente "Esto necesita jQuery" y AMD lo cargará y ejecutará tu código cuando jQuery esté disponible .

Esto también tiene una joya oculta: la carga de la biblioteca se realizará en el momento en que el DOM esté listo, no antes. ¡Esto ya no detiene la carga de su página!

Paso 2: modularizar

¿Ves la estructura alámbrica? Tengo dos bloques de anuncios. Lo más probable es que tengan oyentes de eventos compartidos.

Su tarea en este paso es identificar los puntos de repetición en su código e intentar sintetizar todo esto en módulos . Los módulos, ahora mismo, abarcarán todo. Dividiremos las cosas a medida que avanzamos.

La idea general de este paso es ir desde el paso 1 y eliminar todas las pastas de copia, para reemplazarlas con unidades que estén poco acopladas. Entonces, en lugar de tener:

ad_unit1.js

 $("#au1").click(function() { ... });

ad_unit2.js

 $("#au2").click(function() { ... });

Tendré:

ad_unit.js:

 var AdUnit = function(elem) {
     this.element = elem || new jQuery();
 }
 AdUnit.prototype.bindEvents = function() {
     ... Events go here
 }

page.js:

 var AUs = new AdUnit($("#au1,#au2"));
 AUs.bindEvents();

Lo que le permite compartimentar entre sus eventos y su marcado, además de deshacerse de la repetición. Este es un paso bastante decente y lo ampliaremos más adelante.

Paso 3: ¡Elija un marco!

Si desea modularizar y reducir las repeticiones aún más, hay un montón de marcos impresionantes que implementan enfoques MVC (Modelo - Vista - Controlador). Mi favorito es Backbone / Spine, sin embargo, también hay Angular, Yii, ... La lista continúa.

Un modelo representa sus datos.

Una vista representa su marca y todos los eventos asociados a ella

Un controlador representa la lógica de su negocio; en otras palabras, el controlador le dice a la página qué vistas cargar y qué modelos usar.

Este será un paso de aprendizaje significativo, pero el premio lo vale: favorece el código modular y limpio sobre los espaguetis.

Hay muchas otras cosas que puede hacer, esas son solo pautas e ideas.

Cambios específicos del código

A continuación, se muestran algunas mejoras específicas para su código:

 $('.new_layer').click(function(){

    dialog("Create new layer","Enter your layer name","_input", {

            'OK' : function(){

                    var reply = $('.dialog_input').val();

                    if( reply != null && reply != "" ){

                            var name = "ln_"+reply.split(' ').join('_');
                            var parent = "";

                            if(selected_folder != "" ){
                            parent = selected_folder+" .content";
                            }

                            $R.find(".layer").clone()
                            .addClass(name).html(reply)
                            .appendTo("#layer_groups "+parent);

                            $R.find(".layers_group").clone()
                            .addClass(name).appendTo('#canvas '+selected_folder);

            }

        }

    });
 });

Esto se escribe mejor como:

$("body").on("click",".new_layer", function() {
    dialog("Create new layer", "Enter your layer name", "_input", {
         OK: function() {
             // There must be a way to get the input from here using this, if it is a standard library. If you wrote your own, make the value retrievable using something other than a class selector (horrible performance + scoping +multiple instance issues)

             // This is where the view comes into play. Instead of cloning, bind the rendering into a JS prototype, and instantiate it. It means that you only have to modify stuff in one place, you don't risk cloning events with it, and you can test your Layer stand-alone
             var newLayer = new Layer();
             newLayer
               .setName(name)
               .bindToGroup(parent);
          }
     });
});

Anteriormente en su código:

window.Layer = function() {
    this.instance = $("<div>");
    // Markup generated here
};
window.Layer.prototype = {
   setName: function(newName) {
   },
   bindToGroup: function(parentNode) {
   }
}

De repente, tiene una forma de crear una capa estándar desde cualquier lugar de su código sin copiar y pegar. Estás haciendo esto en cinco lugares diferentes. Le acabo de guardar cinco copias y pegadas.

Uno mas:

// Contenedor de conjunto de reglas para acciones

var PageElements = function(ruleSet) {
ruleSet = ruleSet || [];
this.rules = [];
for (var i = 0; i < ruleSet.length; i++) {
    if (ruleSet[i].target && ruleSet[i].action) {
        this.rules.push(ruleSet[i]);
    }
}
}
PageElements.prototype.run = function(elem) {
for (var i = 0; i < this.rules.length; i++) {
    this.rules[i].action.apply(elem.find(this.rules.target));
}
}

var GlobalRules = new PageElements([
{
    "target": ".draggable",
    "action": function() { this.draggable({
        cancel: "div#scrolling, .content",
        containment: "document"
        });
    }
},
{
    "target" :".resizable",
    "action": function() {
        this.resizable({
            handles: "all",
            zIndex: 0,
            containment: "document"
        });
    }
}

]);

GlobalRules.run($("body"));

// If you need to add elements later on, you can just call GlobalRules.run(yourNewElement);

Esta es una forma muy potente de registrar reglas si tiene eventos que no son estándar o eventos de creación. Esto también es realmente sorprendente cuando se combina con un sistema de notificación de publicación / suscripción y cuando está vinculado a un evento, dispara cada vez que crea elementos. ¡Fire'n'forget enlace de eventos modular!

Sébastien Renauld
fuente
2
@Jessica: ¿por qué una herramienta en línea debería ser diferente? El enfoque sigue siendo el mismo: compartimentar / modularizar, promover el acoplamiento flexible entre componentes utilizando un marco (todos vienen con delegación de eventos en estos días), dividir el código. ¿Qué hay que no se aplique a su herramienta? ¿El hecho de que tengas muchos botones?
Sébastien Renauld
2
@Jessica: actualizado. He simplificado y optimizado la creación de capas utilizando un concepto similar a View. Entonces. ¿Cómo no se aplica esto a su código?
Sébastien Renauld
10
@Jessica: Dividir en archivos sin optimizar es como comprar más cajones para almacenar basura. Un día, tienes que limpiar, y es más fácil limpiar antes de que se llenen los cajones. ¿Por qué no hacer ambas cosas? En este momento, parece que van a querer una layers.js, sidebar.js, global_events.js, resources.js, files.js, dialog.jssi sólo vamos a dividir el código de arriba. Úselo gruntpara reconstruirlos en uno y Google Closure Compilerpara compilarlos y minimizarlos.
Sébastien Renauld
3
Al usar require.js, también debes mirar en el optimizador de r.js, esto es realmente lo que hace que requiera.js valga la pena usarlo. Combinará y optimizará todos sus archivos: requirejs.org/docs/optimization.html
Willem D'Haeseleer
2
@ SébastienRenauld su respuesta y comentarios aún son muy apreciados por otros usuarios. Si eso puede hacerte sentir mejor;)
Adrien Be
13

Aquí hay una forma sencilla de dividir su base de código actual en varios archivos, utilizando require.js. Le mostraré cómo dividir su código en dos archivos. Agregar más archivos será sencillo después de eso.

Paso 1) En la parte superior de su código, cree un objeto de aplicación (o el nombre que prefiera, como MyGame):

var App = {}

Paso 2) Convierta todas sus variables y funciones de nivel superior para que pertenezcan al objeto Aplicación.

En vez de:

var selected_layer = "";

Usted quiere:

App.selected_layer = "";

En vez de:

function getModified(){
...
}

Usted quiere:

App.getModified = function() {

}

Tenga en cuenta que en este punto su código no funcionará hasta que termine el siguiente paso.

Paso 3) Convierta todas las referencias de funciones y variables globales para pasar por la aplicación.

Cambiar cosas como:

selected_layer = "."+classes[1];

a:

App.selected_layer = "."+classes[1];

y:

getModified()

a:

App.GetModified()

Paso 4) Pruebe su código en esta etapa; todo debería funcionar. Probablemente obtendrá algunos errores al principio porque se perdió algo, así que corríjalos antes de continuar.

Paso 5) Configure requirejs. Supongo que tiene una página web, servida desde un servidor web, cuyo código está en:

www/page.html

y jquery en

www/js/jquery.js

Si estas rutas no son exactamente así, lo siguiente no funcionará y tendrá que modificar las rutas.

Descargue requirejs y coloque require.js en su www/jsdirectorio.

en su page.html, elimine todas las etiquetas de secuencia de comandos e inserte una etiqueta de secuencia de comandos como:

<script data-main="js/main" src="js/require.js"></script>

crear www/js/main.jscon contenido:

require.config({
 "shim": {
   'jquery': { exports: '$' }
 }
})

require(['jquery', 'app']);

luego coloque todo el código que acaba de arreglar en los Pasos 1-3 (cuya única variable global debería ser Aplicación) en:

www/js/app.js

En la parte superior de ese archivo, ponga:

require(['jquery'], function($) {

En la parte inferior ponga:

})

Luego cargue page.html en su navegador. ¡Tu aplicación debería funcionar!

Paso 6) Crea otro archivo

Aquí es donde su trabajo da sus frutos, puede hacerlo una y otra vez.

Extraiga un código de las www/js/app.jsreferencias a $ y App.

p.ej

$('a').click(function() { App.foo() }

Ponlo adentro www/js/foo.js

En la parte superior de ese archivo, ponga:

require(['jquery', 'app'], function($, App) {

En la parte inferior ponga:

})

Luego cambie la última línea de www / js / main.js a:

require(['jquery', 'app', 'foo']);

¡Eso es! ¡Haga esto cada vez que quiera poner código en su propio archivo!

Lyn Headley
fuente
Esto tiene múltiples problemas: el obvio es que está fragmentando todos sus archivos al final y forzando 400 bytes de datos desperdiciados a cada usuario por script por carga de página al no usar el r.jspreprocesador. Además, en realidad no ha abordado el problema del OP, simplemente proporcionó un require.jscómo genérico .
Sébastien Renauld
7
¿Eh? Mi respuesta es específica a esta pregunta. Y r.js es obviamente el siguiente paso, pero el problema aquí es la organización, no la optimización.
Lyn Headley
Me gusta esta respuesta, nunca he usado require.js, así que tendré que ver si puedo usarlo y obtener algún beneficio de él. Utilizo mucho el patrón del módulo, pero quizás esto me permita abstraer algunas cosas y luego requerirlas.
Tony
1
@ SébastienRenauld: esta respuesta no se trata simplemente de require.js. Habla principalmente de tener un espacio de nombres para el código que está creando. Creo que deberías apreciar las partes buenas y hacer una edición si encuentras algún problema. :)
mithunsatheesh
10

Para su pregunta y comentarios, asumiré que no está dispuesto a transferir su código a un marco como Backbone, o usar una biblioteca de carga como Require. Solo desea una mejor manera de organizar el código que ya tiene, de la manera más simple posible.

Entiendo que es molesto desplazarse por más de 2000 líneas de código para encontrar la sección en la que desea trabajar. La solución es dividir su código en diferentes archivos, uno para cada funcionalidad. Por ejemplo sidebar.js, canvas.jsetc. Luego, puede unirlos para la producción usando Grunt, junto con Usemin puede tener algo como esto:

En tu html:

<!-- build:js scripts/app.js -->
<script src="scripts/sidebar.js"></script>
<script src="scripts/canvas.js"></script>
<!-- endbuild -->

En tu Gruntfile:

useminPrepare: {
  html: 'app/index.html',
  options: {
    dest: 'dist'
  }
},
usemin: {
  html: ['dist/{,*/}*.html'],
  css: ['dist/styles/{,*/}*.css'],
  options: {
    dirs: ['dist']
  }
}

Si desea utilizar Yeoman, le dará un código repetitivo para todo esto.

Luego, para cada archivo en sí, debe asegurarse de seguir las mejores prácticas y de que todo el código y las variables estén todos en ese archivo y no dependan de otros archivos. Esto no significa que no pueda llamar a funciones de un archivo desde otro, el punto es tener variables y funciones encapsuladas. Algo similar al espacio de nombres. Asumiré que no desea portar todo su código para que esté orientado a objetos, pero si no le importa refactorizar un poco, le recomendaría agregar algo equivalente con lo que se llama un patrón de módulo. Se parece a esto:

sidebar.js

var Sidebar = (function(){
// functions and vars here are private
var init = function(){
  $("#sidebar #sortable").sortable({
            forceHelperSize: true,
            forcePlaceholderSize: true,
            revert: true,
            revert: 150,
            placeholder: "highlight panel",
            axis: "y",
            tolerance: "pointer",
            cancel: ".content"
       }).disableSelection();
  } 
  return {
   // here your can put your "public" functions
   init : init
  }
})();

Entonces puedes cargar este fragmento de código como este:

$(document).ready(function(){
   Sidebar.init();
   ...

Esto le permitirá tener un código mucho más fácil de mantener sin tener que reescribirlo demasiado.

Jesús Carrera
fuente
1
Es posible que desee reconsiderar seriamente ese anteúltimo fragmento, que no es mejor que tener código escrito en línea: su módulo requiere #sidebar #sortable. También puede ahorrar memoria insertando el código y guardando los dos IETF.
Sébastien Renauld
El punto es que puede usar cualquier código que necesite. Solo estoy usando un ejemplo del código original
Jesús Carrera
Estoy de acuerdo con Jesús, esto es solo un ejemplo, el OP puede agregar fácilmente un "objeto" de opciones que les permitiría especificar el selector del elemento en lugar de codificarlo, pero esto fue solo un ejemplo rápido. Me gustaría decir que me encanta el patrón del módulo, es el patrón principal que uso, pero incluso con eso dicho, todavía estoy tratando de organizar mejor mi código. Utilizo C # normalmente, por lo que la creación y el nombre de la función se sienten genéricos. Intento mantener un "patrón" como los guiones bajos son locales y privados, las variables son solo "objetos" y luego hago referencia a la función en mi devolución, que es pública.
Tony
Sin embargo, todavía encuentro desafíos con este enfoque y deseo tener una mejor manera de hacerlo. Pero funciona mucho mejor que simplemente declarar mis variables y funciones en el espacio global para causar conflictos con otros js ... jajaja
Tony
6

Utilice javascript MVC Framework para organizar el código javascript de forma estándar.

Los mejores marcos de JavaScript MVC disponibles son:

Seleccionar un marco MVC de JavaScript requirió muchos factores a considerar. Lea el siguiente artículo comparativo que le ayudará a seleccionar el mejor marco basado en los factores importantes para su proyecto: http://sporto.github.io/blog/2013/04/12/comparison-angular-backbone-can-ember/

También puede usar RequireJS con el marco para admitir la carga de archivos y módulos js asincrónicos.
Mire a continuación para comenzar con la carga del módulo JS:
http://www.sitepoint.com/understanding-requirejs-for-effective-javascript-module-loading/

Sastre Rohit
fuente
4

Categoriza tu código. Este método me está ayudando mucho y funciona con cualquier marco js:

(function(){//HEADER: menu
    //your code for your header
})();
(function(){//HEADER: location bar
    //your code for your location
})();
(function(){//FOOTER
    //your code for your footer
})();
(function(){//PANEL: interactive links. e.g:
    var crr = null;
    $('::section.panel a').addEvent('click', function(E){
        if ( crr) {
            crr.hide();
        }
        crr = this.show();
    });
})();

En su editor preferido (el mejor es Komodo Edit) puede plegar colapsando todas las entradas y verá solo los títulos:

(function(){//HEADER: menu_____________________________________
(function(){//HEADER: location bar_____________________________
(function(){//FOOTER___________________________________________
(function(){//PANEL: interactive links. e.g:___________________

fuente
2
+1 para una solución JS estándar que no depende de bibliotecas.
hobberwickey
-1 por múltiples razones. Su código equivalente es exactamente el mismo que el de los OP ... + un IETF por "sección". Además, está utilizando selectores demasiado amplios sin permitir que los desarrolladores de módulos anulen la creación / eliminación de esos o extiendan el comportamiento. Finalmente, los IETF no son gratuitos.
Sébastien Renauld
@hobberwickey: No sé ustedes, pero prefiero confiar en algo impulsado por la comunidad y donde se encontrarán errores rápidamente si puedo. Especialmente si hacer lo contrario me condena a reinventar la rueda.
Sébastien Renauld
2
Todo esto está haciendo es organizar el código en secciones discretas. La última vez que verifiqué que era A: buena práctica y B: no es algo para lo que realmente necesita una biblioteca respaldada por la comunidad. No todos los proyectos encajan en Backbone, Angular, etc. y modularizar el código envolviéndolo en funciones es una buena solución general.
hobberwickey
En cualquier momento que desee, es posible confiar en cualquier biblioteca favorita para utilizar este enfoque. Pero la solución anterior funciona con javascript puro, bibliotecas personalizadas o cualquier marco js famoso
3

Yo sugeriría:

  1. patrón de editor / suscriptor para la gestión de eventos.
  2. orientación a objetos
  3. espacio de nombres

En tu caso Jessica, divide la interfaz en páginas o pantallas. Las páginas o pantallas pueden ser objetos y ampliarse desde algunas clases principales. Gestione las interacciones entre páginas con una clase PageManager.

Fazle Rabbi
fuente
¿Puede ampliar esto con ejemplos / recursos?
Kivylius
1
¿Qué quiere decir con "orientación a objetos"? Casi todo en JS es un objeto. Y no hay clases en JS.
Bergi
2

Sugiero que uses algo como Backbone. Backbone es una biblioteca de JavaScript compatible con RESTFUL. Ik hace que su código sea más limpio y legible y es poderoso cuando se usa junto con requirejs.

http://backbonejs.org/

http://requirejs.org/

Backbone no es una biblioteca real. Está destinado a darle estructura a su código javascript. Es capaz de incluir otras bibliotecas como jquery, jquery-ui, google-maps, etc. En mi opinión, Backbone es el enfoque javascript más cercano a las estructuras del controlador de vista de modelo y orientado a objetos.

También con respecto a su flujo de trabajo ... Si crea sus aplicaciones en PHP, use la biblioteca Laravel. Funcionará perfectamente con Backbone cuando se use con el principio RESTfull. Debajo del enlace a Laravel Framework y un tutorial sobre la construcción de API RESTfull:

http://maxoffsky.com/code-blog/building-restful-api-in-laravel-start-here/

http://laravel.com/

A continuación se muestra un tutorial de nettuts. Nettuts tiene muchos tutoriales de alta calidad:

http://net.tutsplus.com/tutorials/javascript-ajax/understanding-backbone-js-and-the-server/

Chris Visser
fuente
0

Tal vez sea hora de que empieces a implementar todo un flujo de trabajo de desarrollo utilizando herramientas como yeoman http://yeoman.io/ . Esto ayudará a controlar todas sus dependencias, el proceso de compilación y, si lo desea, las pruebas automatizadas. Es mucho trabajo para empezar, pero una vez implementado hará que los cambios futuros sean mucho más fáciles.

drobson
fuente