SQL Server: cuáles son las diferencias entre @@identity y Scope_identity

14/08/2018
Artículo original


Foto de Kyle Glenn en Unsplash

Cuando insertamos un registro en una tabla de una base de datos, lo más habitual es que se genere automáticamente un nuevo identificador único para el nuevo registro, generalmente un autonumérico. Esto significa que empezamos en el 1 y para cada nuevo registro que añadimos se va aumentando de manera automática a 2, 3, 4... y así sucesivamente. Esto que parece una tontería, tiene muchas implicaciones ya que este identificador es el que vamos a utilizar luego para relacionar el nuevo registro con cualquier otra tabla de la base de datos que necesite utilizarlo. Por ejemplo, una factura y sus líneas de factura están relacionadas a partir del identificador de la cabecera. Cosas por el estilo...

El caso es que la generación de este autonumérico, que parece una tontería, es mucho más compleja de lo que parece ya que se debe tener en cuenta una cosa muy importante: la simultaneidad. Cuando tú haces pruebas en tu equipo de desarrollo, añadir nuevos registros es muy fácil: solo estás tú. Pero en un sistema real en el que pueden estar entrando miles de usuarios a la vez y añadiendo registros en las mismas tablas, el sistema debe asegurar que la secuencia se cumple sin dejar huecos y que no se producen bloqueos importantes.

Una manera ingenua de obtener el autonumérico de un registro que acabas de insertar sería simplemente obtener el máximo de esos números justo a continuación de hacer el INSERT correspondiente. Pero en un sistema en producción no te funcionará con garantías salvo que provoques un bloqueo, ya que mucha gente puede estar introduciendo datos a la vez y entre que insertas el tuyo y lees el máximo puede haberse insertado otro. Es un problema peliagudo.

Cada sistema gestor de base de datos relacionales (SGBDR) que hay en el mercado aborda el problema con su propia filosofía. Ya os hemos explicado aquí cómo obtener el último autonumérico insertado en los más utilizados: Oracle, MySQL y SQL Server.

En el caso de SQL Server explicábamos que la forma de hacerlo era utilizando la instrucción SCOPE_IDENTITY(). Puedes repasarlo en el enlace anterior. Sin embargo SQL Server ofrece al menos 3 formas de obtener el último ID insertado. Aunque puedan parecer iguales, no son equivalentes y conocer sus diferencias es muy importante:

  • @@Identity: devuelve el último ID en la misma conexión.
  • Scope_identity(): la que recomendamos, devuelve el último ID creado en la misma conexión y el mismo contexto (de ahí su nombre). En este caso el contexto se refiere a la consulta o procedimiento almacenado actual.
  • ident_current(nombre): devuelve el último ID de la tabla que le indiquemos (el que haya en ese momento)

En la mayor parte de los casos @@identity y scope_identity() se comportan igual, pero no siempre es así.

Por ejemplo, si tenemos un disparador (trigger) en nuestra tabla y lanzamos una consulta de inserción que genera un nuevo registro y por lo tanto un nuevo ID, si el trigger a su vez genera otro registro en otra tabla (y probablemente otro ID en ésta), en esta situación @@identity nos devolverá el ID del registro que ha insertado el disparador, no el ID de la tabla que hemos actualizado (lo cual no es lo que queremos casi nunca), ya que te devuelve el último que se haya generado en esa conexión, tal y comentábamos en la lista anterior. Sin embargo, en este ejemplo, scope_identity() nos devuelve el identificador que esperábamos (el de la inserción en nuestra tabla) ya que siempre devuelve el del contexto actual, que en este caso es nuestra consulta de inserción.

Este es el motivo de que normalmente se recomiende el uso de scope_identity().

¡Espero que te resulte útil!

Odio las excepciones verificadas

10/08/2018
Artículo original

De verdad que deberían haber eliminado por completo las excepciones verificadas para Java 8. Estorban. No es una coincidencia que casi todos los demás lenguajes de la JVM las eliminaron.

BEGIN RANT

Tengo una clase donde implemento un método abstracto más o menos así:

@Override
protected Runnable crearTarea(Request req) {
  Response resp = new Respuesta(Errores.CANT_CONNECT);
  if (req.getTipo() == Request.UnaCosa) {
    return () -> {
      try {
        unaCosa(req, resp);
      } catch (IOException ex) {
        log.error("Haciendo una cosa con {}", req, ex);
      } finally {
        procesaRespuesta(resp);
      }
    };
  } else if (req.getTipo() == Request.OtraCosa) {
    return () -> {
      try {
        otraCosa(req, resp);
      } catch (IOException ex) {
        log.error("Haciendo otra cosa con {}", req, ex);
      } finally {
        procesaRespuesta(resp);
      }
    };
  } else if (req.getTipo() == Request.OoootraCosa) {
    return () -> {
      try {
        oootraCosa(req, resp);
      } catch (IOException ex) {
        log.error("Haciendo todavía otra cosa con {}", req, ex);
      } finally {

leer más

Cómo postear a Instagram desde el ordenador

09/08/2018
Artículo original

Una de las redes sociales que más me gustan en la actualidad es Instagram. La llevo usando desde hace años porque me gusta la fotografía y modestamente hago mis "pinitos" en este ámbito, pero últimamente la uso mucho más y veo que tiene cada vez una tracción mayor, con más gente, temas interesantes de todo tipo y con una gran ventaja: la inmediatez que te da una imagen o un vídeo corto. Tiene sus pegas, claro está (que es de Facebook, por ejemplo), y no vale para todo ni para expresar ideas muy complejas, pero está bastante bien.

Dado que la mayor parte de las fotografías las tomas hoy en día con el móvil tiene bastante lógica que sea una aplicación nativa para este medio. Pero a veces puede resultar útil poder enviar fotografías que ya tienes en el ordenador. Bien porque las has pasado allí desde una cámara profesional, bien porque las has creado con Photoshop o similar, etc... Aunque Instagram tiene una versión web que puedes usar desde un navegador moderno cualquiera, solo te permite ver los posts, comentar y poco más. No permite enviar fotografías desde el ordenador.

Esto es un fastidio porque te obliga a pasar al móvil tus fotos (usando un disco cloud, Telegram o incluso un cable, como los Neandertales) y hace que todo el proceso sea mucho más ineficiente de lo que debiera.

Por suerte siempre hay alguna forma de saltarse las normas absurdas, y en este caso no es una excepción.

Para conseguirlo lo único que tenemos que hacer es simular que nos conectamos desde un navegador móvil, como si accediésemos a la web de Instagram usando un iPhone o un dispositivo con Android. La versión móvil de la aplicación Web sí que permite enviar fotos.

Tiene algunas limitaciones:

  • No puedes subir vídeos
  • No puedes crear stories (aunque sí verlas)
  • No tienes disponibles todos los filtros
  • No puedes usar las herramientas de edición de imágenes (como Tilt&Shift)

De hecho los dos últimos puntos es muy probable que no te importen en absoluto, ya que en el ordenador puedes hacer procesos a la imagen mucho más potentes que los que ofrece Instagram, y usar programas especializados. Seguramente irán solucionando todas estas cosas en el futuro, pero a día de hoy (agosto de 2018) es lo que hay.

Pero aún así merece mucho la pena usarla desde el ordenador.

A continuación te he dejado un vídeo explicativo en el que te cuento paso a paso qué debes hacer para conseguirlo. Verás que es muy sencillo y funciona en todos los sistemas operativos y en cualquier navegador moderno. Yo he utilizado Chrome en Windows 10, pero no te será complicado hacer lo mismo con otros navegadores como Firefox u Opera, y el vídeo sería exactamente igual si lo hubiese grabado en Mac o Linux.

Allá va:

[youtube:FzhjB6untmQ]

¡Espero que te resulte útil!

¡Ah! y no te olvides de seguirme en Instagram :-)

Funciones reguladoras en JavaScript: cómo limitar el número de veces que se puede llamar a una función cada segundo (throttling y debouncing)

05/08/2018
Artículo original

En muchas ocasiones tenemos la necesidad de ejecutar en una aplicación la misma función JavaScript muchas veces seguidas, incluso sin pretenderlo.

El ejemplo habitual que se suele poner es detectar el redimensionado de una página. Si debes redistribuir y redimensionar mediante código muchos elementos o redibujar un canvas a medida que cambia y que tiene que repintar muchas entidades que el usuario ha definido (en un programa de dibujo, por ejemplo), el proceso que se lleva a cabo en cada cambio de tamaño puede ser largo. Y este evento de redimensionado se llama muchas veces a medida que el usuario modifica el tamaño de la ventana. Pero existen muchas otras situaciones en las que puede ocurrir: encadenar muchas llamadas AJAX en paralelo a un servicio para traer información (p.ej. una búsqueda) a medida que un usuario teclea algo en un cuadro de texto... Cosas por el estilo.

Los motores modernos de JavaScript son capaces de conseguir rendimientos para el código cercanos al del código nativo y, en contra de lo que mucha gente suele pensar, es un lenguaje muy rápido incluso ejecutándolo en un navegador. A pesar de eso en ciertos casos esa llamada indiscriminada a un evento o a una función, si es costosa, puede llegar a bloquear la interfaz de usuario o al menos hacer que ésta no vaya todo lo ágil que debiera. Si se lo permitimos, el navegador hará la llamada todas las veces que sea necesario, acaparando el hilo de ejecución.

En otras ocasiones, aunque no se note ese bloqueo por ser un proceso sencillo y rápido, es una tontería estar llamando 300 veces por segundo a una función si no es necesario. Si con llamarla 5 o 10 veces por segundo es suficiente y los usuarios no van a notar la diferencia, ¿para qué hacer otra cosa?.

Para evitar estos problemas, existen un par de técnicas avanzadas de control denominadas regulación (throttling) y anti-rebote (debouncing, por cierto un término derivado de la electrónica, como muchos otros en programación). Ambas limitan el número de veces que se puede ejecutar una función en un intervalo de tiempo determinado, pero el primero la ejecuta y bloquea la ejecución hasta que pase el intervalo señalado, y la segunda es al revés: no permite que se ejecute hasta que pase como mínimo dicho intervalo (o sea, retrasa la ejecución también) y además mientras se repita no la ejecuta tampoco. En esta página de demo podemos ver la diferencia. La primera es la que más interesante me parece y a la que más casos prácticos le veo, mientras que la segunda la encuentro interesante para casos más restringidos, como por ejemplo no ejecutar un evento keypress hasta que el usuario deje de teclear en un cuadro de texto, y luego ejecutarlo al cabo de x ms.

Nota: quiero agradecer a Pedro J. Molina sus comentarios al respecto de estos matices entre ambos métodos, que había dejado fuera en la versión inicial del artículo y acabo de añadir en el párrafo anterior para mayor claridad a raíz de un comentario suyo en Twitter. ¡Gracias Pedro!

Vamos a ver cómo se suele abordar el problema y luego entraremos en detalle de una solución propia que he creado para la ocasión.

La función clásica de regulación (throttling) y anti-rebote (debouncing)

El método más conocido y replicado hasta la saciedad por Internet de hacer la regulación es el que define la archiconocida biblioteca de utilidades JavaScript Underscore (que te recomiendo conocer porque es muy útil). Esta biblioteca tiene un método llamado debounce() que sirve precisamente para hacer esto. Se usa de la siguiente manera:

var funcionRegulada = _.debounce(funcionNormal, 200);

lo que hace es devolver una versión de la función que queremos llamar que solo se podrá llamar una vez cada 200 milisegundos (es decir 5 veces cada segundo como máximo).

El código que utiliza actualmente lo puedes ver aquí, pero es un poquito más complicado que el original, aunque añade dependencias de otros métodos de underscore.

Es más directo y sencillo el que usaba originalmente hace unos pocos años, que no tenía dependencias en otras funciones propias y era así:

function debounce(func, wait, immediate) {
	var timeout;
	return function() {
		var context = this, args = arguments;
		var later = function() {
			timeout = null;
			if (!immediate) func.apply(context, args);
		};
		var callNow = immediate && !timeout;
		clearTimeout(timeout);
		timeout = setTimeout(later, wait);
		if (callNow) func.apply(context, args);
	};
};

No voy a entrar en explicarlo detalladamente porque, como digo, si tienes los fundamentos avanzados de JavaScript claros no es complejo, pero básicamente lo que hace es devolver una función nueva, que sustituye a la original, y que establece un temporizador para ejecutar la verdadera función al cabo del tiempo que se le indique.

Posee un parámetro opcional para forzar la ejecución de la función al principio del intervalo, no al final. Si no lo usamos lo que hace la función es meter un retraso y no permitir la ejecución como mínimo hasta que pase éste, por mucho que sea llamado varias veces antes.

En su versión actual además se pueden cancelar las llamadas reguladas, pero tanto esto como lo anterior no dejan de ser florituras.

En mi opinión complica un poco el uso (yo generalmente lo usaría con true en el tercer parámetro opcional o pierde bastante la gracia).

Mi propia función reguladora (throttling)

Conociendo lo anterior me apetecía intentar mi propia función reguladora que funcionase exactamente cómo yo quería (regulación, no anti-rebote), más fácil de usar (sin tener que pensar en ese tercer parámetro y sus implicaciones) y más obvia sobre lo que está haciendo: bloqueando de verdad la ejecución y limitándola a un determinado número de veces por segundo como máximo, que me parece más natural que indicar el tiempo en milisegundos y decidir si se debe ejecutar antes o después.

Así, he creado la función limitarLLamadas() a la cual se le pasa la función original que queremos limitar y se le indica el número máximo de llamadas que queremos permitir en un segundo.

Ahora explico cómo funciona y te dejo una descarga, pero antes vamos a ver cuál es su efecto.

He creado una página sencilla que tiene mucho texto y que por lo tanto permite hacer scroll arriba y abajo en el contenido (mejor si lo haces estrechando la ventana en caso de que tengas mucha resolución de pantalla). Se detecta y gestiona el evento scroll de la página de modo que cada vez que se mueve el contenido se lleva a cabo una acción como resultado de dicho evento. En este caso simplemente he definido dos funciones: una que cuenta cuántas veces se llama al evento de scroll y la otra que cuenta cuántas veces se llama a una función específica que en este caso no hace nada (solo aumenta un contador) pero que en un caso real sería nuestra función "costosa" que se llama muchas veces. Ambas funciones muestran en una esquinita de la página los contadores, de modo que primero tenemos el número de eventos de scroll que se han lanzado y luego el número de veces que se ha ejecutado la función que nos interesa. Por ejemplo, si pone "100 / 32" significa que el evento de scroll se ha lanzado 100 veces pero que nuestra función (regulada) solo se ha ejecutado 32 veces.

window.addEventListener('scroll', scrollDetectado);
window.addEventListener('scroll', scrollGestionado, 10);

Con esto llamaríamos a las dos funciones cada vez que se haga scroll. La segunda es la que nos interesa limitar. En este caso, al no estar regulada y ser una función normal, se llaman ambas siempre el mismo número de veces. Si lanzamos la página y empezamos a hacer scroll arriba y abajo a toda velocidad con la rueda del ratón veremos esto:

Como podemos apreciar, los dos números del contador siempre son iguales ya que ambas funciones (la que cuenta y la que "gestiona" el evento) se llaman el mismo numero de veces. Y puede llegar a ser muy alto.

Sin embargo si en el código ponemos esto (fíjate en cómo limitamos las llamadas a la función a un máximo de 10 veces por segundo):

window.addEventListener('scroll', scrollDetectado);
window.addEventListener('scroll', limitarLLamadas(scrollGestionado, 10));

...lo que veríamos en este caso es algo similar a lo de este pequeño vídeo:

¿Cómo funciona?

Vamos a echar un vistazo el código de la función que he creado, que como verás no es nada complicado (parece largo pero es por los comentarios explicativos. Sin ellos se queda en nada):

function limitarLLamadas(func, maxXSeg) {
    var bloqueoActivado = false;  //Sirve para indicar que está bloqueada a la función
    
	return function() {
        //Esta función interna simplemente desbloquea las llamadas
		var anularBloqueo = function() {
			bloqueoActivado = false;   //Anulamos el bloqueo
        };

        if (!bloqueoActivado) {
            //Si no hay un bloqueo, llamamos a la función inmediatamente
            func.apply(this, arguments);
            //Bloqueamos
            bloqueoActivado = true;
            //Y desbloqueamos cuando sea necesario para evitar llamadas innecesarias
            setTimeout(anularBloqueo, 1000/maxXSeg);
        }
	};
}

Lo que hace es, en el fondo, parecido a lo que hace underscore (tampoco hay muchas otras formas de plantear la base) en el sentido de que devuelve una nueva función que "envuelve" a la original para dotarla de limitaciones a la hora de llamarla. Pero en este caso utilizo el temporizador solamente para desbloquear después de un tiempo las posibles llamadas a la función original. De este modo se regula muy bien esta limitación y me parece una forma mucho más natural de hacerlo. Aunque puedo estar equivocado, por supuesto.

Se establece una variable booleana para determinar si las llamadas están bloqueadas o no (la primera y la siguiente tras el ultimo bloqueo no lo están nunca, claro), y defino una función anularBloqueo() que, como su propio nombre indica, lo único que hace es establecer la variable anterior de nuevo a false.

La primera vez que se llama a la función se llama al método original y acto seguido se establece un bloqueo (bloqueoActivado = true;). Mientras dure éste no se permiten más llamadas a la función original (debido al if (!bloqueoActivado)). Además se establece un temporizador que llama a la función anularBloqueo() que hemos visto hace un instante para liberar dicho bloqueo al cabo del tiempo apropiado, que se obtiene de dividir el número de milisegundos en un segundo (1000, claro) entre el número máximo de veces que queremos permitir que se llame a la función.

Por cierto, si quisiésemos limitar el máximo de llamadas aún más, por ejemplo a 1 cada dos segundos, podríamos pasarle un decimal. Por ejemplo: limitarLLamadas(scrollGestionado, 0.5) limitaría las llamadas a 1 cada dos segundos.

Parece (y es) más sencillo y creo que funciona muy bien.

Te dejo una descarga (ZIP, 7.24KB) con el ejemplo y un pequeño archivo debounce.js con el código de la función.

¡Espero que te haya resultado interesante y que te sea útil!

Funciones reguladoras en JavaScript: cómo limitar el número de veces que se puede llamar a una función cada segundo (debouncing)

04/08/2018
Artículo original

En muchas ocasiones tenemos la necesidad de ejecutar en una aplicación la misma función JavaScript muchas veces seguidas, incluso sin pretenderlo.

El ejemplo habitual que se suele poner es detectar el redimensionado de una página. Si debes redistribuir y redimensionar mediante código muchos elementos o redibujar un canvas a medida que cambia y que tiene que repintar muchas entidades que el usuario ha definido (en un programa de dibujo, por ejemplo), el proceso que se lleva a cabo en cada cambio de tamaño puede ser largo. Y este evento de redimensionado se llama muchas veces a medida que el usuario modifica el tamaño de la ventana. Pero existen muchas otras situaciones en las que puede ocurrir: encadenar muchas llamadas AJAX en paralelo a un servicio para traer información (p.ej. una búsqueda) a medida que un usuario teclea algo en un cuadro de texto... Cosas por el estilo.

Los motores modernos de JavaScript son capaces de conseguir rendimientos para el código cercanos al del código nativo y, en contra de lo que mucha gente suele pensar, es un lenguaje muy rápido incluso ejecutándolo en un navegador. A pesar de eso en ciertos casos esa llamada indiscriminada a un evento o a una función, si es costosa, puede llegar a bloquear la interfaz de usuario o al menos hacer que ésta no vaya todo lo ágil que debiera. Si se lo permitimos, el navegador hará la llamada todas las veces que sea necesario, acaparando el hilo de ejecución.

En otras ocasiones, aunque no se note ese bloqueo por ser un proceso sencillo y rápido, es una tontería estar llamando 300 veces por segundo a una función si no es necesario. Si con llamarla 5 o 10 veces por segundo es suficiente y los usuarios no van a notar la diferencia, ¿para qué hacer otra cosa?.

Para evitar estos problemas, existe una técnica avanzada de control denominada regulación, muchas veces conocida por su término en inglés: debouncing (por cierto un término derivado de la electrónica, como muchos otros en programación).

Vamos a ver cómo se suele abordar el problema y luego entraremos en detalle de una solución propia que he creado para la ocasión.

La función clásica de regulación o debouncing

El método más conocido y replicado hasta la saciedad por Internet de hacer la regulación es el que define la archiconocida biblioteca de utilidades JavaScript Underscore (que te recomiendo conocer porque es muy útil). Esta biblioteca tiene un método llamado debounce() que sirve precisamente para hacer esto. Se usa de la siguiente manera:

var funcionRegulada = _.debounce(funcionNormal, 200);

lo que hace es devolver una versión de la función que queremos llamar que solo se podrá llamar una vez cada 200 milisegundos (es decir 5 veces cada segundo como máximo).

El código que utiliza actualmente lo puedes ver aquí, pero es un poquito más complicado que el original, aunque añade dependencias de otros métodos de underscore.

Es más directo y sencillo el que usaba originalmente hace unos pocos años, que no tenía dependencias en otras funciones propias y era así:

function debounce(func, wait, immediate) {
	var timeout;
	return function() {
		var context = this, args = arguments;
		var later = function() {
			timeout = null;
			if (!immediate) func.apply(context, args);
		};
		var callNow = immediate && !timeout;
		clearTimeout(timeout);
		timeout = setTimeout(later, wait);
		if (callNow) func.apply(context, args);
	};
};

No voy a entrar en explicarlo detalladamente porque, como digo, si tienes los fundamentos avanzados de JavaScript claros no es complejo, pero básicamente lo que hace es devolver una función nueva, que sustituye a la original, y que establece un temporizador para ejecutar la verdadera función al cabo del tiempo que se le indique.

Posee un parámetro opcional para forzar la ejecución de la función al principio del intervalo, no al final.

En su versión actual además se pueden cancelar las llamadas reguladas, pero tanto esto como lo anterior no dejan de ser florituras.

En mi opinión complica un poco el uso (yo generalmente lo usaría con true en el tercer parámetro opcional o pierde bastante la gracia).

Mi propia función reguladora

Conociendo lo anterior me apetecía intentar mi propia función reguladora que funcionase exactamente cómo yo quería, más fácil de usar (sin tener qe pensar en ese tercer parámetro y sus implicaciones) y más obvia sobre lo que está haciendo: bloqueando de verdad la ejecución y limitándola a un determinado número de veces por segundo como máximo, que me parece más natural que indicar el tiempo en milisegundos.

Así, he creado la función limitarLLamadas() a la cual se le pasa la función original que queremos limitar y se le indica el número máximo de llamadas que queremos permitir en un segundo.

Ahora explico cómo funciona y te dejo una descarga, pero antes vamos a ver cuál es su efecto.

He creado una página sencilla que tiene mucho texto y que por lo tanto permite hacer scroll arriba y abajo en el contenido (mejor si lo haces estrechando la ventana en caso de que tengas mucha resolución de pantalla). Se detecta y gestiona el evento scroll de la página de modo que cada vez que se mueve el contenido se lleva a cabo una acción como resultado de dicho evento. En este caso simplemente he definido dos funciones: una que cuenta cuántas veces se llama al evento de scroll y la otra que cuenta cuántas veces se llama a una función específica que en este caso no hace nada (solo aumenta un contador) pero que en un caso real sería nuestra función "costosa" que se llama muchas veces. Ambas funciones muestran en una esquinita de la página los contadores, de modo que primero tenemos el número de eventos de scroll que se han lanzado y luego el número de veces que se ha ejecutado la función que nos interesa. Por ejemplo, si pone "100 / 32" significa que el evento de scroll se ha lanzado 100 veces pero que nuestra función (regulada) solo se ha ejecutado 32 veces.

window.addEventListener('scroll', scrollDetectado);
window.addEventListener('scroll', scrollGestionado, 10);

Con esto llamaríamos a las dos funciones cada vez que se haga scroll. La segunda es la que nos interesa limitar. En este caso, al no estar regulada y ser una función normal, se llaman ambas siempre el mismo número de veces. Si lanzamos la página y empezamos a hacer scroll arriba y abajo a toda velocidad con la rueda del ratón veremos esto:

Como podemos apreciar, los dos números del contador siempre son iguales ya que ambas funciones (la que cuenta y la que "gestiona" el evento) se llaman el mismo numero de veces. Y puede llegar a ser muy alto.

Sin embargo si en el código ponemos esto (fíjate en cómo limitamos las llamadas a la función a un máximo de 10 veces por segundo):

window.addEventListener('scroll', scrollDetectado);
window.addEventListener('scroll', limitarLLamadas(scrollGestionado, 10));

...lo que veríamos en este caso es algo similar a lo de este pequeño vídeo:

¿Cómo funciona?

Vamos a echar un vistazo el código de la función que he creado, que como verás no es nada complicado (parece largo pero es por los comentarios explicativos. Sin ellos se queda en nada):

function limitarLLamadas(func, maxXSeg) {
    var bloqueoActivado = false;  //Sirve para indicar que está bloqueada a la función
    
	return function() {
        //Esta función interna simplemente desbloquea las llamadas
		var anularBloqueo = function() {
			bloqueoActivado = false;   //Anulamos el bloqueo
        };

        if (!bloqueoActivado) {
            //Si no hay un bloqueo, llamamos a la función inmediatamente
            func.apply(this, arguments);
            //Bloqueamos
            bloqueoActivado = true;
            //Y desbloqueamos cuando sea necesario para evitar llamadas innecesarias
            setTimeout(anularBloqueo, 1000/maxXSeg);
        }
	};
}

Lo que hace es, en el fondo, parecido a lo que hace underscore (tampoco hay muchas otras formas de plantear la base) en el sentido de que devuelve una nueva función que "envuelve" a la original para dotarla de limitaciones a la hora de llamarla. Pero en este caso utilizo el temporizador solamente para desbloquear después de un tiempo las posibles llamadas a la función original. De este modo se regula muy bien esta limitación y me parece una forma mucho más natural de hacerlo. Aunque puedo estar equivocado, por supuesto.

Se establece una variable booleana para determinar si las llamadas están bloqueadas o no (la primera y la siguiente tras el ultimo bloqueo no lo están nunca, claro), y defino una función anularBloqueo() que, como su propio nombre indica, lo único que hace es establecer la variable anterior de nuevo a false.

La primera vez que se llama a la función se llama al método original y acto seguido se establece un bloqueo (bloqueoActivado = true;). Mientras dure éste no se permiten más llamadas a la función original (debido al if (!bloqueoActivado)). Además se establece un temporizador que llama a la función anularBloqueo() que hemos visto hace un instante para liberar dicho bloqueo al cabo del tiempo apropiado, que se obtiene de dividir el número de milisegundos en un segundo (1000, claro) entre el número máximo de veces que queremos permitir que se llame a la función.

Por cierto, si quisiésemos limitar el máximo de llamadas aún más, por ejemplo a 1 cada dos segundos, podríamos pasarle un decimal. Por ejemplo: limitarLLamadas(scrollGestionado, 0.5) limitaría las llamadas a 1 cada dos segundos.

Parece (y es) más sencillo y creo que funciona muy bien.

Te dejo una descarga (ZIP, 7.24KB) con el ejemplo y un pequeño archivo debounce.js con el código de la función.

¡Espero que te haya resultado interesante y que te sea útil!

Por qué Data Transfer Project (DTP) es el futuro de la portabilidad de datos entre servicios online

01/08/2018
Artículo original

Data Transfer Project (DTP)

Data Transfer Project (DTP) es el estándar de portabilidad de datos de código abierto que intenta solucionar uno de los grandes problemas cuando queremos migrar datos. Nos referimos al Data Lock-in, la imposibilidad de obtener todos los datos que tenemos almacenados en servicio para irnos a otro. Recientemente, el proyecto apoyado por Google, Facebook, Microsoft, Twitter presentó la especificación y la arquitectura que permitirá llegar a una API común para la transferencia de datos de usuario entre servicios. La clave es poder definir una comunicación directa y sin intermediarios, y lo más importante: un formato común de datos.

Está la idea se remonta a 2007 cuando surgió, Data Liberation Front, un grupo de ingenieros de Google que han promovido desde entonces la liberación de los datos para evitar este tipo de problemas con los servicios ofrecidos por la empresa. Gracias a ellos disponemos de Google Takeout, una herramienta para los usuarios que permite hacer copias de seguridad de tus archivos en todos lo servicios de Google.

"Users should be able to control the data they store in any of Google's products. Our team's goal is to make it easier to move data in and out."

Ahora el proyecto entre manos es aún más ambicioso y tiene como objetivo crear un estándar común capaz de ser usado para convertir cualquier API propietaria a un conjunto de formatos comunes creando de paso un ecosistema de herramientas en torno a ello.

Data Liberation Front

El futuro de la portabilidad Service-to-Service

El objetivo es que los usuarios deberían ser capaces de transferir sus datos desde un servicio a otro, sin la necesidad de apoyarse en un intermediario (otro SaaS, por ejemplo) ni tener que descargar y volver a enviar todos lo datos de nuevo. Existen multitud de servicio que base su modelo en migrar emails, contactos, etc de un proveedor a otro. Por ejemplo, cuando cambias de cuenta de Yahoo a Gmail.

Para facilitar el complicado mundo de la portabilidad de datos, Data Transfer Project define una serie de adapters para transformar los formatos propietarios a una serie de Data Models comunes. Esto permite que la transferencia de datos pueda hacerse desde el propio proveedor del servicio a otro manteniendo el control sobre la seguridad e integridad de los datos.

Obviamente, estos Data Models están limitados en cuanto a la información que pueden ser compartidos de un servicio a otro, ya que es imposible modelar toda la información pero con los distintos casos de uso ya creados como fotos o contactos se cubre ampliamente la funcionalidades esperada. Aún habrá que seguir usando algún mecanismo para incorporar ciertos metadata más específicos.

Cómo funciona: la arquitectura de Data Transfer Project

Existen 3 componentes fundamentales descritos en el white paper de Data Transfer Project:

  • Data Models que proveen un formato común para transferir los datos.
  • Adapters que facilitan los métodos para convertir cada dato propietario y posibilitar la autenticación dentro del sistema.
  • Task Management que realizan las tareas en background para ejecutar la migración de datos.

Data Transfer Project, Data Model

Por medio de los Data Models podemos definir el formato de datos que queremos intercambiar. Actualmente se puede consultar los existentes en el Github del proyecto: emails, contactos, fotos, calendarios, tareas, etc..

Con los Adapters podemos hacer la traducción entre los datos dados de un proveedor concreto (la API del servicio) a los Data Models de intercambio de DTP. Además de la transformación de datos, existe otro tipo de adapters que se ocupan de la autenticación de los datos. OAuth ha sido el proveedor más extendido, aunque DTP es agnóstico en el tipo de autenticación.

Por último, el conjunto de tareas necesarias para realizar la carga de la portabilidad de datos reside en el otro gran componente de Tasks. Se encargan de realizar las llamadas entre los adapters, realizar la lógica de reintentos, gestionar los rate limits, paginación, notificaciones individuales, etc..

Arquitectura Data Model Data Transfer Project Arquitectura y flujo de trabajo de DTP

¿Cómo podemos probarlo y participar los desarrolladores?

DTP está aún en desarrollo y no está preparado para ser usado en producción. Sin embargo existen distintos casos de uso que podemos probar migrando datos entre algunas plataformas. Disponible en el Github del proyecto.

Todo el código está escrito en Java y puede ser desplegado vía Docker. Algunos de sus módulos desacoplados pueden ser escritos en Python, C#/.NET, etc.. La plataforma en cloud que se está usando para probarlo está disponible tanto para Google Cloud, Microsoft Azure, Amazon AWS.

Dentro del repositorio publicado tenemos la posibilidad de añadir nuevos Providers utilizando el conjunto de interfaces descritas en Github. Es necesario completar cada una de las piezas descritas en la arquitectura DTP: Data Adapter, Auth Adapter y, potencialmente, un nuevo Data Model.

También se puede contribuir al Core Framework, el cual involucra algunas piezas importantes como las plataformas Host, el sistema de almacenamiento.

Todo ello a través de pull requests al proyecto en Github.

Más información | Data Transfer Project

3 trucos para automatizar tus tareas de desarrollo con git hooks

01/08/2018
Artículo original

El sistema de control de versiones Git, como muchos otros, viene con unos cuantos trucos en la manga que lo hacen muy extensible de forma programática. En este artículo aprenderemos uno de ellos, los hooks, que permiten realizar acciones automáticas junto a muchos de los comandos típicos de Git.

Los hooks son programas que se ejecutan antes o después que algún evento importante de Git. Por ejemplo, antes de completar un git commit, tras cambiar de rama o antes de enviar los cambios durante un git push. Se almacenan en el directorio .git/hooks/ de cada proyecto clonado, y es necesario que sean programas o scripts ejecutables (en sistemas tipo Unix han de tener el permiso de ejecución).

Para añadir un hook a tu proyecto, simplemente copia o enlaza a dicho directorio el programa que quieras ejecutar automáticamente con el nombre del "evento" (por ejemplo, pre-commit, todos los posibles nombres están disponibles en la documentación de Git). Si el programa es un script y quieres que el resto de colaboradores puedan usarlo, es conveniente incluirlo en la raíz del proyecto y crear un acceso directo a él en .git/hooks/:

ln -s ../../pre-commit.sh .git/hooks/pre-commit

Vamos a ver algunas aplicaciones practicas de los hooks de Git.

1. Comprobar la calidad del código antes del commit

Los hooks que se ejecutan previos a la confirmación de un commit son útiles puesto que nos permiten realizar comprobaciones sobre el código añadido o eliminado justo antes de reflejar dichos cambios en el árbol de versiones del repositorio. Si el hook falla o devuelve código de error, el commit no se efectuará.

En el siguiente ejemplo (válido para sistemas Unix, como Linux o macOS), utilizamos el paquete jslint de Node.js para comprobar la calidad del código de una aplicación JavaScript antes de completar el commit:

#!/bin/bash
# pre-commit.sh

# Guardar los cambios no confirmados
STASH_NAME="pre-commit-$(date +%s)"
git stash save -q --keep-index $STASH_NAME

# Comprobaciones y tests
jslint my_application.js || exit 1

# Recuperar los cambios guardados
STASHES=$(git stash list)
if [[ $STASHES == "$STASH_NAME" ]]; then
  git stash pop -q
fi

Se podría proceder con la misma estrategia a lanzar una suite de tests sobre la aplicación u otras comprobaciones necesarias, por ejemplo, una búsqueda de claves o tokens secretos para evitar introducirlos al repositorio.

2. Generar la documentación conforme se suben cambios

Si en nuestro proyecto contamos con un generador de documentación a partir del código, podemos ejecutarlo regularmente conforme desarrollamos mediante un hook de tipo pre-push. Simplemente lanzamos el comando necesario para componer toda la documentación en algún directorio (por ejemplo, docs/) y la añadimos a un nuevo commit.

En el siguiente listado te muestro varios ejemplos de los posibles comandos que podrías usar para tal fin:

#!/bin/bash
# pre-push.sh

# Genera la documentación
doxygen Doxyfile

# Otro ejemplo, con Python:
pydoc3 -w docs/

# Otro ejemplo, con R:
Rscript -e 'pkgdown::build_site()'

# Añade y confirma los cambios relativos a la documentación
git add docs/
git commit -m "Update documentation ($(date +%F@%R))"

La ventaja de esto es que, si utilizas GitHub Pages o un servicio similar para servir tu documentación online, siempre estará al día con los cambios de tu código y no se quedará obsoleta.

3. Comprueba dependencias al cambiar de rama

Por último, una aplicación de los hooks muy interesante es actualizar las dependencias instaladas al cambiar de rama en un proyecto. Si utilizas gestores de paquetes y dependencias para tu lenguaje de desarrollo (Pip en Python, npm en Node.js, Nuget en .NET, Bundler en Ruby, Cargo en Rust...), te puede ser muy útil el siguiente ejemplo.

El listado de código a continuación correspondería a un hook post-checkout, y lo que consigue es comprobar si entre la rama anterior y la nueva ha cambiado el archivo de dependencias (en este caso, Gemfile para Ruby), en cuyo caso ejecuta el instalador conveniente (en este caso, bundle).

#!/bin/bash
# post-checkout.sh

if [ $1 = 0000000000000000000000000000000000000000 ]; then
  # Si estamos en un proyecto recién clonado, comparar con el directorio vacío
  old=$(git hash-object -t tree /dev/null)
else
  # El primer argumento es el hash de la anterior HEAD
  old=$1
fi
if [ -f Gemfile ] &&
  git diff --name-only $old $2 | egrep -q '^Gemfile|\.gemspec$'
then
  # Vaciar $GIT_DIR evita problemas si bundle llama a git
  (unset GIT_DIR; exec bundle)
  # Se completa el checkout aunque falle bundler
  true
fi

Puedes adaptar este código para tu propio uso simplemente cambiando Gemfile por el archivo de dependencias que utilices (package.json o requirements.txt, por ejemplo) y bundle por el comando correspondiente (npm install o pip install -r requirements.txt, por ejemplo).

Fuentes y más información:

Las 15 mejores webs donde encontrar imágenes increíbles gratuitas para usar en tu blog

31/07/2018
Artículo original

En Google imágenes seguramente encontrarás todas las que quieras pero cuando estás buscando fotos de alta resolución, de calidad y creativas, te recomendamos que las busques directamente en webs de fotógrafos profesionales. Saben centrarse en detalles que hacen que sus fotos sean únicas. En esta entrada hemos seleccionado 15 webs en las que encontrarás fotos espectaculares. Todas ellas gratuitas y con licencias para uso incluso comercial.

La entrada Las 15 mejores webs donde encontrar imágenes increíbles gratuitas para usar en tu blog se publicó primero en Nelio Software.

Las expectativas fallidas con React Native que te harán plantearte si usarlo o descartarlo para tu app

22/07/2018
Artículo original

React Native Airbnb

Que Airbnb haya dejado de ser un ejemplo de startup utilizando React Native nos hace preguntarnos si las expectativas que depositamos sobre esta tecnología son correctas. Como reflexión podemos revisar la decisión de Airbnb, duramente meditada. Prueba de ello es la serie de blog posts que acompañaron el anuncio, explicando las razones desde lo más técnico a cultural.

Es un excelente aprendizaje de cómo un equipo técnico con unas dimensiones y unas ciertas expectativas asume una nueva tecnologías y, posteriormente, se ve obligado a descartarlas. Con las consecuencias que eso conlleva. Recordemos que React Native no es una simple librería o framework sino que tiene otras implicaciones que pueden modificar la forma de trabajar o hasta la cultura de un equipo de desarrollo.

Airbnb no ha sido la única compañía que ha anunciado el abandono de React Native, a los poco días lo hizo Udacity, publicando un riguroso post con sus razones también. Mencionando muchos de los quebraderos de cabeza con los que nos hemos encontrado algunos de nosotros intentando introducir React Native en una aplicación ya existente. En este caso, era un equipo reducido de 4 desarrolladores el que tomaba la decisión, en contraposición a los casi 100 ingenieros de Airbnb.

Ni Facebook se libró del rumor que incluso ellos estaban abandonando parte del desarrollo React Native a favor del nativo en Android e iOS, al poco tiempo lo desmintieron rotundamente. En su keynote de F8 mostraron como lo usan en diversas partes de la app como blood donations, crisis responses, los atajos de gestión privacidad o wellness checks.

Los factores para adoptar React Native suelen ser algunos de estos:

  • Poder avanzar rápido. Las necesidades de una startup en pleno crecimiento y evolución demandan poder desarrollar rápido. Y más si es en mobile. La carencia de desarrolladores y la “duplicidad” en cierta forma de desarrollos en Android e iOs.
  • Escribir el mismo código sólo una vez, en lugar de replicarlo en cada plataforma casi prácticamente. Aquí hay que distinguir entre empezar de cero una feature/app en React Native o tener que convivir con código de Java/Kotlin y Objective-C/Swift de por medio.
  • Mejorar la experiencia de desarrollo. En el desarrollo mobile los tiempos de compilación, incluso algunos IDE como Xcode no dan una experiencia buena. Por eso, React Native promete mejorar la calidad de vida de los desarrolladores o al menos los tiempo de compilación
  • Experiencia en javascript y desarrollo web. Disponer de un equipo con experiencia en frontend y no disponer de suficientes desarrolladores móviles es un razón de peso, más si ya en la web se está usando React.
  • Atraer desarrolladores interesados en una nueva tecnología. Aunque Android e iOS sigan siendo tecnologías punteras, muchas empresas ven React Native como una forma de atraer gente interesada en nuevas formas de trabajar y con una tecnología, ojo, que viene de Facebook. Probablemente no sea uno de los mejores reclamos ni tampoco fácil para los recruiters, pero no sería la primera vez que veamos adoptar una nueva tecnología guiados por el marketing.
  • Las historias de éxito de algunas compañías que lo están usando: Airbnb era una de ellas, pero tranquilo quedan muchas más.
React Native React fue creada por Jordan Walke, un ingeniero de software en Facebook en 2013.Es usado intensivamente por Facebook e Instagram

Quebraderos de cabeza con React Native

El mayor quebradero de cabeza sufrido por Udacity y Airbnb es que no es tan fácil el atribuido lema de “write once, run everywhere”. Sobre todo porque ellos ya disponían de una gran cantidad de funcionalidades desarrolladas en nativo. Pero lo más importante de eso es que la parte más core de la aplicación debía ser nativa y comunicarse con React Native. Algo nada trivial que no funciona del mismo modo según la plataforma y requiere un esfuerzo extra y nada trivial.

Airbnb dispone de un gran número de ingenieros para poder crear la infraestructura necesaria. Lo que podría parecer a simple vista que React Native simplificaría, no fue así. Tampoco que debido a la inmadurez de React Native durante el tiempo que lo ha usado Airbnb haya tenido que parchear ciertas cosas. Llevandoles a mantener un fork con más de 50 commit por delante. Cada actualización de versión se hacía más compleja, además de amplio esfuerzo en construir librerías para adaptar sus necesidades a React Native.

Por el camino como detallan en el tercer post de la serie van otras piedras en el camino:

  • Los problemas del tipado de JavaScript. Suplidos con TypeScript a duras penas por temas de compatibilidad, aunque posteriormente resueltos. Facebook siempre impulsó Flow.
  • El tamaño extra que añade React Native a las aplicaciones que ya tienen código nativo. Entorno a 8-12 MB algo que para algunos mercados es un gran problema.
  • La imposibilidad de crear builds de Android 64-bit. Un problema para usar ciertas librerías también ya metidas en el proyecto. La issue sigue abierta.
  • Los tiempos de inicialización y renderización de entre 280 ms (iOs) y 440 ms(Android). O los problemas entre transiciones en Android lo que llevó a meter un delay de 50 ms para evitar problemas
  • Tuvieron que evitar usar gestos complejos en pantallas creadas con React Native. Lo que llevó a trabajar en la unificar API de ambas plataformas en react-native-gesture-handler.
  • Actualizar React Native ha sido problemático durante estos dos años. Sobre todo de Native 0.43 a 0.49, ya que usaba React 16 en beta.
  • Airbnb debe cumplir con la suficiente accesibilidad en todas sus funcionalidades lo que les llevó a modificar ciertas cosas de React Native en su propio fork.
  • Tal como comentaba Udacity, incorporar React Native requiere actualizar algunos procesos en CI que lo hacen más complejo y pesado. Incrementando el peso de las build de media un 20%, además de introducir más paso y dependencias.
  • La fragmentación de dispositivos (sobre todo en Android) provoca que haya que hacer modificaciones en ciertos componentes React Native, hasta al punto de tener que crear custom component como relata Udacity, además de un mayor esfuerzo en el testing. Nada nuevo en el ecosistema de Android pero que React Native ni soluciona ni minimiza, en ocasiones todo lo contrario.
  • El mantenimiento del código resolviendo problemas de UI o de compatibilidad llevó a los equipos de Android e iOS a trabajar en branch distintos. Lo que llevó a un divergencia más que la esperada uniformidad de ambas plataformas.
React Native es más que una librería o framework, afecta a la cultura de desarrollo de tu equipo
Airbnb Descarta React Native Sigue habiendo grandes empresas confiando en React Native, pero Airbnb ha tenido que descartarlo después de dos años de intenso desarrollo

Construyendo un equipo cross-platform

Uno de los aprendizajes que más provecho se puede extraer del relato de Airbnb sobre React Native es la parte del equipo. Y es algo que podemos aplicar a cualquier decisión técnica que tengamos que tomar en el seno de un equipo. No es simplemente un lenguaje, una librería o un framework. Hay ciertas decisiones, como en este caso de React Native, que puede afectar a la forma de trabajar al equipo de desarrollo, incluyendo al producto y diseño, por supuesto.

React Native está polarizado, se nota hablando con desarrolladores de Android e iOs. La causa principal es que esa “bala de plata” no funciona de la misma forma en cada plataforma ni los retos ni errores a superar son los mismos. Dependiendo de la experiencia tenida, puedes llevarte una opinión u otra. Claro que no todos construimos las mismas aplicaciones ni son igual de complejas ni tienen los mismos retos tecnológicos.

Una de las primeras realidades que puedes encontrarte construyendo ese ansiado equipo cross-platform es que vas a seguir necesitando expertos en las tres plataformas. Tanto en el hecho de mejorar la experiencia con algunos elementos custom.

No sólo porque el diseño sea distinto por plataforma, sino que React Native tiene algunos comportamiento por defect en algunas situaciones como la renderización de textos, el uso del teclado o las propias Activities que tendrás que cambiar. Y sin hablar de la parte del debugging cuando entras en las profundidades de JavaScript-React y su relación del entorno nativo. No todos los desarrolladores están los suficientemente preparados para eso.

Un hecho curioso que remarca Airbnb fue como les afectó en la contratación de ingenieros. Muchos desarrolladores Android o iOS dudaban de entrar a formar parte de una empresa que apostaba tan fuerte con React Native.

La división de equipos también fue compleja. Cada codebase estaba dividido entre nativo (Android e iOS) y React Native. Compartir lógica de negocio, modelos, estados, etc… era cada vez más complicado y muy poca gente era capaz de entender el flujo completo de todo. Compartir código entre la web y móvil era el objetivo pero dejo de ser el beneficio real, ya que debía ser usado y mantenido de forma independiente.

Todo depende si tu aplicación empieza de cero en React Native o es híbrida (Android/iOS)

No es lo mismo desarrollar una aplicación cross-plataform desde cero a integrar código React Native. Dicho esto y teniendo en la cabeza los problemas que tuvieron Airbnb y Udacity: esos son tus riesgos. Ahora bien, todo depende de ti y el tipo de aplicación quieras construir.

1. Si quieres construir una app desde cero en React Native, dónde la mayor parte del código Javascript

Adelante, quizás esta sea la situación ideal. Muchas startup a día de hoy en sus primeros meses ya no necesitan contar con un programador Android y otro iOS. Con una app 100% React puedes construir un buen MVP que funcione en ambas plataforma Para ello debes conocer todo el ecosistema de React Native, aqui un buen roadmpa para ello. Siempre y cuando tu desarollo no se salga de lo convencional.

Siempre me guardaría bien las espaldas con un equipo de frontend que maneje ampliamente JavaScript. Con eso puede ser suficiente para tener una gran experiencia con React Native, sin ni siquiera tocar XCode o Android Studio.

2. Tienes ya un aplicación relativamente compleja en Swift/Objective-C o Java/Kotlin

Este es un claro campo minado donde una aplicación existente tenga que sufrir numerosos cambios para dar el primer paso con React Native: problemas de compatibilidades con librerías, gestionar repositorios aparte, migrar funcionalidad o intercomunicar la capa de negocio o dominio. Todo el tipo de problemas con los que Airbnb y Udacity se han enfretado.

Tu equipo actual tendrá que aprender una nueva tecnología, o quizás el conocimiento de algunas funcionalidades deban dividirse entre expertos en JavaScript y Java/Kotlin, por ejemplo. Además de tender a separar el código de la app de 3 repositorios como mínimo. Además de tener que abordar problemas similares a los anteriormente relatados para mantener la coherencia de la app con partes 100% nativas y otras React Native. Estas serías navegaciones, layout, comunicación de partes dominio y vista, etc..

En definitiva, React Native depende mucho de tu tipo de aplicación y tus aspiraciones. Con las experiencias de Udacity o Airbnb podemos ver reflejada la realidad no tan perfecta de una tecnologías prometedora pero no perfecta para todos.

Copiado de texto al portapapeles con JavaScript - API Asíncrona

20/07/2018
Artículo original

El mes pasado os hablaba aquí de una manera sencilla y soportada por todos los navegadores para acceder al portapapeles desde código JavaScript en el navegador. Es un método simple pero un poco engorroso de implementar, puesto que hay que seleccionar rangos en la página para poder copiar su contenido. Además, su implementación varía ligeramente de un navegador a otro porque no está estandarizada. 

Otra pega importante que tiene el método "clásico" es que es síncrono. Es decir, mientras se efectúa la operación de copiado o pegado se bloquea el hilo principal de ejecución del navegador. En condiciones normales no importa demasiado ya que es una operación muy rápida, pero si la operación involucrase un contenido muy extenso podría llegar a bloquearse la interfaz de la página, con lo que ello conlleva en cuanto a la usabilidad y experiencia de usuario.

Para solucionar todos estos problemas la W3C ha creado la Async Clipboard API, que e el momento de escribir esto se encuentra todavía como borrador, pero es muy estable. Esta API unifica el modelo de permisos para que todos los navegadores lo implemente igual y además es asíncrona, lo cual hace que no se bloquee la página al utilizarla. Para la parte asíncrona podemos utilizar tanto promesas como la API async/await de ECMAScript 2017.

En el momento de escribir esto, esta API solo está soportada por Google Chrome, pero es de suponer que en breve los demás navegadores la soporten también. Como veremos, es fácil añadir una comprobación y usar el método más antiguo en caso de no estar soportada, de modo que así nuestra aplicación esté preparada para el futuro y soporte también el presente y el pasado.

Vamos a ver cómo funciona.

El objeto clipboard

Este objeto es una nueva propiedad del objeto navigator y podemos, por tanto, acceder a ella simplemente escribiendo:

if (!navigator.clipboard) {
alert('¡Tu navegador no soporta la API Asíncrona del Portapapeles!')
}

De esta manera, por ejemplo, comprobamos si la API está soportada o no por el navegador actual.

Este objeto posee dos métodos para leer y otros dos para escribir en el portapapeles. Veamos cómo se utilizan.

Escribir al portapapeles con la API asíncrona

La manera más sencilla de escribir texto plano al portapapeles es utilizando el método writeText del objeto anterior. Su uso es muy sencillo puesto que lo único que tenemos que hacer es llamarlo y gestionar el resultado de la llamada de manera asíncrona.

Para ello tenemos dos formas básicas de conseguirlo. La primera es el uso de promesas. Explicar las promesas no es el objeto de este artículo, así que te refiero a la MDN (o a nuestro fabuloso curso de JavaScript y ECMAScript avanzado de campusMVP) si necesitas aprender sobre ellas.

Con promesas la escritura consiste en hacer algo como esto:

navigator.clipboard.writeText(contenidoACopiar).then(function() {
exito();
mostrarAlerta();
});

Es decir, llamas a la función y con el método then de la promesa resultante gestionas lo que quieres hacer cuando haya funcionado (en este caso mostrar un mensaje de alerta).

También podrías gestionar un posible fallo con catch(), claro.

Si prefieres utilizar la asincronía, algo que todos los navegadores que soportan esta API de portapapeles deberían soportar también, entonces es todavía más sencillo y directo:

await navigator.clipboard.writeText(contenidoACopiar)
exito();
mostrarAlerta();

Evidentemente la función que contenga a este fragmento de código debería estar marcada con async para poder funcionar. De esta manera se ve más claro todo y la ejecución se detiene hasta que vuelve la llamada a writeText. Para controlar posibles errores usaríamos una gestión estructurada normal y corriente, con try-catch.

Bien, con este método conseguimos de manera muy sencilla copiar texto al portapapeles.

No es necesario permiso especial alguno ya que, aunque existe uno específico según el estándar, Chrome no lo solicita y siempre te da permiso para copiar al portapapeles en la pestaña activa. Ya veremos que para leer desde el portapapeles la cosa cambia.

Además del método writeText que acabamos de ver existe otro más genérico llamado write que permite escribir cualquier cosa de manera genérica al portapapeles (por ejemplo las versiones de texto y HTML del mismo contenido), para lo cual utiliza el mismo objeto DataTransfer que la API de arrastrar y soltar.

No entraré en mucho detalle sobre su uso, pero este ejemplo sirve para ver que tampoco es muy complicado:

var data = new DataTransfer();
data.items.add("Hola <b>amiguetes</b>", "text/html");
data.items.add("Hola amiguetes", "text/plain");
await navigator.clipboard.write(data);

Se trata de crear el objeto de transferencia de datos, rellenarlo con los formatos y llamar al método. Es bastante directo, sobre todo en su versión con async.

Leer desde el portapapeles

El proceso de lectura desde el portapapeles (que sería equivalente a "pegar" desde él), es idéntico al anterior, solo que se utilizan los métodos read() y readText() para leer todos los formatos que haya o solo el posible texto. Ambos métodos funcionan de la misma manera, solo que no toman parámetro alguno y reciben como resultado de la llamada respectivamente el objeto DataTransfer o el texto con lo que haya en el portapapeles.

Por ejemplo, con una promesa haríamos:

navigator.clipboard.readText().then(function(contenido) {
zonaDondePegar.innerText = contenido;
}).catch(function(ex) {
excepcion();
mostrarAlerta();
});

Fíjate como en este caso recibimos el contenido del portapapeles como parámetro de la función de callback para el método then de la promesa. También capturamos los posibles errores con el método catch, ya que es muy fácil que se produzca uno cuando no tenemos permiso (más sobre esto enseguida).

La versión con async sería más sencilla:

try {
var contenido = await navigator.clipboard.readText();
zonaDondePegar.innerText = contenido;
}
catch(ex) {
excepcion();
mostrarAlerta();
}

ya que se gestiona como código lineal normal.

La parte más interesante del "pegado" es que, ahora sí, necesitaremos que el usuario de la páginas nos conceda permisos, o en caso contrario podríamos robarle la información del portapapeles sin que lo supiese, con las terribles implicaciones de privacidad y seguridad que ello tendría.

Por eso, cuando intentamos usar el código anterior nos saldría un mensaje como este:

En este caso como la estaba usando desde disco directamente por eso sale ese URL tan rato (file:///), pero en condiciones normales saldría el dominio actual.

Por cierto, aunque te funcionará desde el sistema de archivos y desde localhost, si la colocas en un servidor en Internet, al igual que la mayoría de las APIs de HTML5, solo te funcionará si estás bajo HTTPS. Tenlo muy en cuenta si aún eres de los que piensa que "mi dominio no necesita HTTPS porque no manejo datos privados ni claves".

Permisos

Cuando aceptamos y se ejecuta la lectura de datos, veremos en la barra del navegador un iconito de una carpeta que indicará que hemos concedido permisos de acceso al portapapeles:

en caso de bloquear el acceso aparecerá uno similar, pero tachado:

Si el usuario lo pulsa siempre puede cambiar el permiso que haya otorgado previamente:

De este modo tenemos un modelo de permisos coherente con el de las demás APIs del navegador, y no algo que cada cual implementa como le parece. Además le cedemos control al usuario para que decida en cada momento si quiere otorgar o no los permisos correspondientes.

Existe una API del navegador (accesible a través del objeto permissions) que nos permite comprobar los diferentes permisos de un usuario antes de, por ejemplo, realizar una acción concreta, como la de lectura de información desde el portapapeles. En el caso concreto del portapapeles los permisos que se pueden comprobar son dos:

  • clipboard-read
  • clipboard-write

El importante ahora mismo (mientras no cambie la cosa) es el primero, que es el que permite leer desde el portapapeles.

Podemos comprobarlo con un código similar a este:

if (navigator.permissions) {
var estadoAct = await navigator.permissions.query({
name: 'clipboard-read'
})

switch(estadoAct.state) {
case "prompt":
alert("Permisos sin establecer todavía")
break;
case "denied":
alert("Permiso denegado")
break;
case "granted":
alert("Permiso concedido")
break;
default:
alert("Estado desconocido: " + estadoAct.state)
}
}

Lo que se hace es leer el estado actual del permiso clipboard-read. En el objeto recibido consultamos la propiedad state que devuelve una cadena con tres posibles valores para el permiso en cuestión:

  • prompt: que quiere decir que no se le ha pedido permiso todavía al usuario, o sea, que está sin definir explícitamente.
  • denied: que se le ha preguntado al usuario y este lo ha denegado explícitamente.
  • granted: que se le ha preguntado al usuario y este lo ha concedido explícitamente.

Así podremos saber si tenemos ya o no un permiso y pedirlo si es necesario, aunque como es el propio navegador quien lo hará la primera vez que lo intentemos no será necesario la mayor parte de las veces. Eso sí, si ya sabemos de antemano que está denegado podemos deshabilitar ya los botones que tengamos para permitir leer desde el portapapeles.

En resumen

Ya hemos visto como será la futura API para manejar los contenidos del portapapeles. Es estándar, asíncrona y con un modelo de permisos coherente, por lo que será la forma preferida de implementar estas funcionalidades, frente a la manera clásica.

Te he dejado un ejemplo completo (ZIP, 2.24KB), equivalente al del post anterior, para que puedas descargarlo y jugar con él:

Fíjate en el código: tienes comentadas las versiones con async para que puedas probarlas. Comenta las versiones con Promise y descomenta las otras para probarlas. Y si lo pones online tiene que ser con HTTPS.

De momento esta API solo la implementa Chrome pero en los próximos meses espero que ya la implementen los demás. Y mientras tanto podemos hacer que coexistan fácilmente las dos.

¡Espero que te resulte útil!

Página Siguiente