2011-09-30 14 views
10

Por lo tanto, estoy escribiendo una aplicación web. Casi todo se hace en el lado del cliente, el servidor no es más que una interfaz REST. Estoy utilizando jQuery como mi marco de trabajo de elección e implementando mi código en un Revealing Module Pattern.Implementando de forma programada devoluciones de llamada con JS/jQuery

La estructura metálica de mi código, básicamente, tiene el siguiente aspecto:

(function($){ 
    $.fn.myplugin = function(method) 
    { 
     if (mp[method]) 
     { 
      return mp[method].apply(this, Array.prototype.slice.call(arguments, 1)); 
     } 
     else if (typeof method === 'object' || ! method) 
     { 
      return mp.init.apply(this, arguments); 
     } 
     else 
     { 
      $.error('Method ' + method + ' does not exist on $.myplugin'); 
     } 
    }; 

    var mp = 
    { 
     init : function(options) 
     { 
      return this.each(function() 
      { 
       // stuff 
      } 
     }, 
     callbacks : {}, 
     addCallback : function(hook_name, cb_func, priority) 
     { 
      // some sanity checking, then push cb_func onto a stack in mp.callbacks[hook_name] 
     }, 
     doCallbacks : function(hook_name) 
     { 
      if (!hook_name) { hook_name = arguments.callee.caller.name; } 
      // check if any callbacks have been registered for hook_name, if so, execute one after the other 
     } 
    }; 
})(jQuery); 

Bastante sencillo, ¿verdad?

Ahora, podemos registrar (múltiples, jerárquicas) devoluciones de llamada desde dentro y fuera del alcance de la aplicación.

Lo que me está molestando: Para hacer que todo sea tan extensible como sea posible, tendría que recurrir a algo como lo siguiente:

foo : function() { 
    mp.doCallbacks('foo_before'); 
    // do actual stuff, maybe some hookpoints in between 
    mp.doCallbacks('foo_after');   
} 

cada función dentro de mi aplicación tendría que empezar y terminar como eso. Esto simplemente no parece correcto.

Entonces, JS wizards of SO - what do?

+0

Casi cada arquitectura de plugin que he visto ejecutando ha recurrido a antes y después de ganchos. La mayor parte del tiempo, solo implementas lo que las personas piden mientras lo piden, hasta donde yo sé. Ninguna solución mágica que yo sepa. –

+0

Esto parece un caso de exceso de ingeniería. ¿Realmente necesita todas las funciones _internal_ en su biblioteca para admitir devoluciones de llamada antes y después? Rails es como un sándwich de devolución de llamada gigante, y aun así, solo el 1% de los métodos en Rails admiten callbacks. Y, cuando admiten callbacks, lo hacen exactamente como lo ha descrito en su ejemplo _foo_ function. –

Respuesta

10

Puede escribir una función que toma otra función como argumento y devuelve una nueva función que llama a sus ganchos alrededor de ese argumento. Por ejemplo:

function withCallbacks(name, func) 
{ 
    return function() { 
     mp.doCallbacks(name + "_before"); 
     func(); 
     mp.doCallbacks(name + "_after"); 
    }; 
} 

entonces usted puede escribir algo como:

foo: withCallbacks("foo", function() { 
    // Do actual stuff, maybe some hookpoints in between. 
}) 
+0

+1 ¡Casi digo algo antes de que edites tu publicación, buena solución! – Chad

+0

@Chad, gracias, me tomó un tiempo aceptar que no pude deducir los nombres de los ganchos de la función :) –

+0

En mi implementación 'doCallbacks()', si no se da 'hook_name', uso' arguments. callee.caller.name' para deducir el nombre del gancho de la función ... Que obviamente estaría bastante roto si se llama a través de 'withCallbacks()';) – vzwick

2

se están aplicando esencialmente una versión reducida de la jQueryUI Widget factory. recomendaría usar esa funcionalidad para evitar tener que tirar esto usted mismo.

la fábrica widget se auto-mágicamente una cadenas a las llamadas de método, de manera que:

$("#foo").myPlugin("myMethod", "someParam") 

llamarán myMethod en la instancia del complemento con 'someParam' como argumento. Además, si activa un evento personalizado, los usuarios pueden agregar devoluciones de llamadas agregando una propiedad a las opciones que coinciden con el nombre del evento.

Por ejemplo, el widget tabs tiene un evento select que se puede aprovechar mediante la adición de una propiedad select a las opciones durante la inicialización:

$("#container").tabs({ 
    select: function() { 
     // This gets called when the `select` event fires 
    } 
}); 

por supuesto, tendrá que añadir el antes y después ganchos como eventos para poder tomar prestada esta funcionalidad, pero que a menudo conduce a un mantenimiento más fácil de todos modos.

espero que ayude. ¡aclamaciones!

+0

Gracias por el puntero, era consciente de eso, pero de todos modos;) – vzwick

4

Puede que no he entendido bien la pregunta, porque yo no veo por qué no se agrega el código para llamar a las devoluciones de llamada directamente en el código miplugin:

$.fn.myplugin = function(method) 
{ 
    if (mp[method]) 
    { 
     var params = Array.prototype.slice.call(arguments, 1), ret; 
     // you might want the callbacks to receive all the parameters 
     mp['doCallbacks'].apply(this, method + '_before', params); 
     ret = mp[method].apply(this, params); 
     mp['doCallbacks'].apply(this, method + '_after', params); 
     return ret; 
    } 
    // ... 
} 

EDIT:

Ok, después de leer tu comentario creo que otra solución sería (por supuesto) otra indirección. Es decir, tiene una función de invocación que se usa desde el constructor, así como los otros métodos públicos para las llamadas entre ellos.Me gustaría señalar que solo funcionará para los métodos públicos, ya que adjuntar a los métodos privados rompe la encapsulación de todos modos.

La versión simple sería algo como esto:

function invoke(method) { 
    var params = Array.prototype.slice.call(arguments, 1), ret; 
    // you might want the callbacks to receive all the parameters 
    mp['doCallbacks'].apply(this, method + '_before', params); 
    ret = mp[method].apply(this, params); 
    mp['doCallbacks'].apply(this, method + '_after', params); 
} 

$.fn.myplugin = function() { 
    // ... 
    invoke('init'); 
    // ... 
}; 

Pero, de hecho me he escrito un poco más código, que reduciría la duplicación entre los plugins también. Esta es la forma en la creación de un plugin se vería en el extremo

(function() { 

function getResource() { 
    return {lang: "JS"}; 
} 

var mp = NS.plugin.interface({ 
    foo: function() { 
     getResource(); // calls "private" method 
    }, 
    bar: function() { 
     this.invoke('foo'); // calls "fellow" method 
    }, 
    init: function() { 
     // construct 
    } 
}); 

$.fn.myplugin = NS.plugin.create(mp); 

})(); 

y es así como la aplicación parcial parece:

NS = {}; 
NS.plugin = {}; 

NS.plugin.create = function(ctx) { 
    return function(method) { 
     if (typeof method == "string") { 
      arguments = Array.prototype.slice.call(arguments, 1); 
     } else { 
      method = 'init'; // also gives hooks for init 
     } 

     return ctx.invoke.apply(ctx, method, arguments); 
    }; 
}; 

// interface is a reserved keyword in strict, but it's descriptive for the use case 
NS.plugin.interface = function(o) { 
    return merge({ 
     invoke:  NS.plugin.invoke, 
     callbacks: {}, 
     addCallback: function(hook_name, fn, priority) {}, 
     doCallbacks: function() {} 
    }, o); 
}; 

NS.plugin.invoke = function(method_name) { 
    if (method_name == 'invoke') { 
     return; 
    } 

    // bonus (if this helps you somehow) 
    if (! this[method]) { 
     if (! this['method_missing') { 
      throw "Method " + method + " does not exist."; 
     } else { 
      method = 'method_missing'; 
     } 
    } 

    arguments = Array.prototype.slice.call(arguments, 1); 

    if (method_name in ["addCallbacks", "doCallbacks"]) { 
     return this[method_name].apply(this, arguments); 
    } 

    this.doCallbacks.apply(this, method_name + '_before', arguments); 
    var ret = this[method_name].apply(this, arguments); 
    this.doCallbacks.apply(this, method_name + '_after', arguments); 

    return ret; 
}; 

Por supuesto, esto es, no está comprobado :)

+0

Eso sería viable para los métodos externamente llamados/"públicos". Necesito ser capaz de hacer 'this.someFunction()' desde "inside" 'mp' con devoluciones de llamada aún ejecutándose. – vzwick

+0

He dirigido su comentario en mi edición. Puede ser demasiado complicado para su caso de uso, pero fue divertido escribirlo. – deviousdodo

+0

¡Elegante pedazo de código! – vzwick

1

Básicamente Prefiero evitar devoluciones de llamada y usar eventos en su lugar. La razón es simle. Puedo agregar más de una función para escuchar un evento dado, no tengo que meterme con los parámetros de devolución de llamada y no tengo que verificar si se define una devolución de llamada. En la medida en que se invoquen todos sus métodos a través del $.fn.myplugin, es fácil desencadenar eventos antes y después de la llamada al método.

Aquí es un ejemplo de código:

(function($){ 
    $.fn.myplugin = function(method) 
    { 
     if (mp[method]) 
     { 
      $(this).trigger("before_"+method); 
      var res = mp[method].apply(this, Array.prototype.slice.call(arguments, 1)); 
      $(this).trigger("after_"+method); 
      return res; 
     } 
     else if (typeof method === 'object' || ! method) 
     { 
      return mp.init.apply(this, arguments); 
     } 
     else 
     { 
      $.error('Method ' + method + ' does not exist on $.myplugin'); 
     } 
    }; 

    var mp = 
    { 
     init : function(options) 
     { 
      $(this).bind(options.bind); 
      return this.each(function() 
      { 
       // stuff 
      }); 
     }, 

     foo: function() { 
      console.log("foo called"); 
     } 
    }; 
})(jQuery); 


$("#foo").myplugin({ 
    bind: { 
     before_foo: function() { 
      console.log("before foo"); 
     }, 

     after_foo: function() { 
      console.log("after foo"); 
     } 
    } 
}); 
$("#foo").myplugin("foo"); 
Cuestiones relacionadas