Nunca se para de aprender, y de John Resig menos. DocumentFragments son contenedores ligeros que pueden almacenar objetos DOM de Javascript. Yo ni había oído hablar de ellos y la verdad es que su uso es sencillo y mucho más potente que trabajar con el DOM normalmente (de 2 a 3 veces más rápido).
El ejemplo que muestra John lo dice todo y muy fácil. Primero creamos unos elementos para luego insertarlos (de forma normal y con DocumentFragments):
var div = document.getElementsByTagName("div");
for ( var i = 0; i < div.length; i++ ) {
for ( var e = 0; e < elems.length; e++ ) {
div[i].appendChild( elems[e].cloneNode(true) );
}
}
Y por último el nuevo método:
var div = document.getElementsByTagName("div");
var fragment = document.createDocumentFragment();
for ( var e = 0; e < elems.length; e++ ) {
fragment.appendChild( elems[e] );
}
for ( var i = 0; i < div.length; i++ ) {
div[i].appendChild( fragment.cloneNode(true) );
}
Resumiendo, mucho más eficiente y sencillo de usar. Seguro que para la próxima versión de jQuery nos ofrece esta implementación.
Hoy toca realizar conexiones con el servidor usando llamadas autenticadas. Para eso usaremos JWT (JSON Web Token), que viene a ser el envío de un token en cada llamada que necesita autenticación. El token está formado por tres cadenas codificadas en base64 y separadas por un punto (header.payload.signature). En el payload se envía toda la info que queramos, pero sin pasarse porque el token no debería ser muy largo y sobre todo sin enviar información sensible, porque el token no está encriptado, es por ello que la comunicación navegador/servidor debe ser mediante HTTPS. Si quieres una explicación más detallada, aquí te lo explican mejor.
En el payload vamos a guardar varios claims, entre ellos el username y un uuid que usaremos para validar que el usuario es correcto. JWT asegura que el token es válido, pero no viene mal añadir cierta seguridad. Cuando el usuario se loguea un nuevo uuid es generado, por lo que se comprobará si coinciden para el username enviado.
Ya tenemos la explicación teórica, ahora vamos con la parte de programación, primero la parte servidor y luego el frontend.
Lo primero que tenemos que hacer es añadir un nuevo plugin a hapi.js que se encargue de la autenticación, registrando un nuevo strategy a server.auth que use JWT. El plugin hapi-auth-jwt2 se encargará de toda la autenticación JWT y añadiremos una capa extra de validación comprobando que el username y el uuid coinciden.
Y por último añadimos una nueva ruta para autenticar (login) el usuario, comprobamos que el usuario y la contraseña coinciden, y si es así, creamos un uuid que guardamos en la bd y generamos el JWT y lo enviamos en la cabecera Authorization:
Vale, ya tenemos la parte del servidor, ahora la parte del frontend. Primero es añadir las rutas para /login, /account y /logout. He añadido un meta que indica si la ruta tiene que ser obligatoriamente autenticada, obligatoriamente no autenticada o como sea. Para ello, para cada ruta comprobará si el JWT token está almacenado o no y según el meta redirigirá a home o continuará:
Para gestionar el almacenamiento en el navegador en vez de cookies vamos a usar sessionStorage y localStorage para guardar el token JWT. Como el formulario de login permite recordar la sesión, vamos a usar ambos storages. Si no se recuerda usaremos sessionStorage, que se borrará cuando se cierra el navegador, en caso contrario usaremos localStorage.
import Config from'@/js/config';
/**
* API methods for sesion/local storage.
* Depending on `this.session` it saves only on `sessionStorage` or also in `localStorage`
*
* @since v0.8.0
*/classstorage{
/**
* Constructor
*
* @param {boolean} session If stored only in session
*/constructor( session = false ) {
this.session = session;
}
/**
* It saves the token in the session (and local storage is this.session === false),
*
* @param {strig} token JWT token
*/
setJWTToken( token ) {
sessionStorage.setItem( Config.jwt.storageKey, token );
if ( ! this.session ) {
localStorage.setItem( Config.jwt.storageKey, token );
}
}
/**
* It gets a value from session storage or in local if session = false
*
* @returns {string}
*/
getJWTToken() {
const sessionValue = sessionStorage.getItem( Config.jwt.storageKey );
if ( sessionValue ) {
return sessionValue;
}
if ( ! this.session ) {
const storedValue = localStorage.getItem( Config.jwt.storageKey );
return storedValue;
}
returnnull;
}
/**
* Removes JWT token from session and local storage
*/
removeJWTToken() {
sessionStorage.removeItem( Config.jwt.storageKey );
localStorage.removeItem( Config.jwt.storageKey );
}
}
exportdefault storage;
También hemos creado una librería para tratar las llamadas a la API. Hay dos métodos, uno para autenticar el usuario (login) que mirará la cabecera Authorization, y otro método que obtiene los datos del usuario actual realizando una llamada autenticada enviando el JWT token en la cabecera Authorization:
import Config from'@/js/config';
import Storage from'@/js/utils/storage';
/**
* API backend methods
*
* @since 0.8.0
*/classapiFetch{
/**
* Authenticate an user, if ok, JWT token is sent by the server in Authorization header
*
* @param {string} username User name
* @param {string} password Password
*
* @returns {Promise}
*/
auth( username, password ) {
return fetch( Config.api.user.auth, {
method: 'POST',
body: JSON.stringify(
{ username, password }
),
mode: 'cors',
} ).then( response => {
const auth = response.headers.get( 'Authorization' );
if ( auth ) {
return {
response: true,
token: auth,
};
}
return {
response: false,
message: 'Username or password not valid',
};
} );
}
getUser( username ) {
return fetch( `${ Config.api.user.get }${ username }`, {
method: 'GET',
headers: {
Authorization: new Storage().getJWTToken(),
},
} ).then( response => response.json() );
}
}
exportdefault apiFetch;
Ahora solo faltan los controladores para login, account y logout. Login realizar la llamada al servidor y si se obtiene el JWT se guarda:
<template><sectionclass="section"><divclass="container"><h1class="title">
Login
</h1><divv-if="error"class="columns is-centered has-margin-bottom-2"
><b-notificationclass="column is-7-tablet is-6-desktop is-5-widescreen"type="is-danger"has-iconaria-close-label="Close notification"role="alert"size="is-small "
>
{{ error }}
</b-notification></div></div><divclass="container"><divclass="columns is-centered"><divclass="column is-5-tablet is-4-desktop is-3-widescreen has-background-light login-form"><b-fieldlabel="Username"><b-inputv-model="username"value=""maxlength="30"icon="account-circle-outline"
/></b-field><b-fieldlabel="Password"><b-inputv-model="password"value=""type="password"icon="lock-outline"
/></b-field><divclass="field"><b-checkboxv-model="remember">
Remember me
</b-checkbox></div><divclass="has-text-right"><b-buttontype="is-primary"
@click="submit"
>
Log in
</b-button></div></div></div></div></section></template><script>import ApiFectch from'@/js/utils/api';
import Storage from'@/js/utils/storage';
exportdefault {
name: 'Login',
// Form data and error messages if login fails
data() {
return {
username: '',
password: '',
remember: false,
error: '',
};
},
methods: {
// It logs in, using the backend API for authenticate the user data.// If user logs in, it saves the JWT token in the browser. If not, shows error message.
submit: function() {
const api = new ApiFectch();
api.auth( this.username, this.password )
.then( response => {
const storage = new Storage( ! this.remember );
if ( !! response.response && !! response.token ) {
storage.setJWTToken( response.token );
this.error = false;
// `go` refreshes the page, so user data is updatedthis.$router.go( '/' );
} else {
storage.removeJWTToken();
this.error = response.message;
}
} );
},
},
};
</script><stylelang="scss"scoped>.login-form {
border-radius: 4px;
}
</style>
Logout borra los datos JWT del navegador y redirige a home:
<template><sectionclass="hero is-medium is-primary is-bold"><divclass="hero-body"><divclass="container has-text-centered"><h1class="title">
{{ message }}
</h1><h2class="subtitle">
Miss you 💛
</h2></div></div></section></template><script>import User from'@/js/utils/user';
exportdefault {
name: 'Logout',
// Dummy data
data() {
return {
message: 'Bye',
};
},
// After being created it logs out and go to home
created() {
new User().logout();
// `go` instead of `push` because refreshing the page updates the user data// Maybe using vuex is a better way to do it, or not...this.$router.go( '/' );
},
};
</script>
Y Account recupera los datos del usuario una vez que ha creado el controlador:
<template><divid="account"><sectionclass="hero is-primary is-bold"><divclass="hero-body"><divclass="has-text-centered"><h1class="title">
{{ message }}
</h1><h2class="subtitle">
Your data
</h2></div></div></section><sectionclass="section"><divclass="container"><divclass="tile is-ancestor"><divclass="tile is-parent"><pclass="tile is-child notification">
Some content
</p></div><divclass="tile is-8 is-parent"><divclass="tile is-child notification is-info"><ulid="data"><liv-for="( value, key ) in user":key="key"
>
{{ key }} : {{ value }}
</li></ul></div></div></div></div></section></div></template><script>// Dummy componentimport ApiFectch from'@/js/utils/api';
import User from'@/js/utils/user';
exportdefault {
name: 'Account',
data() {
return {
message: 'Account',
user: {},
};
},
created() {
const user = new User().getCurrentUser();
new ApiFectch().getUser( user.username )
.then( response =>this.user = response );
},
};
</script>
Interesante script que carga el contenido en una capa según se va utilizando el scroll y se va llegando al final, algo similar a lo que ocurre en Google Reader.
El autor nos explica paso a paso, incluida la parte PHP, cómo realizar esta funcionalidad. Load Data while Scrolling Page Down with jQuery and PHP
Vía / DZone
Aunque Microsoft dijo que se iba a centrar en corregir los errores en CSS y no en Javascript, no está mal decir que cosas se ha corregido y cuales estarían bien para futuras versiones.
Los errores que se han corregido son: los memory leaks cuando se hacían referencias circulares entre elementos, un fallo que cerraba la aplicación cuando se usaba la función normalize() y soporte para XMLHttpRequest de forma nativa en vez de mediante ActiveX.
Los errores que no estaría mal que corrigieran en futuras versiones son: getElementById debería ignorar el atributo name, debería cambiar el window.location cuando se navega entre referencias en el mismo documento mediante el historial, cuando se mueven checkbox mediante métodos del DOM recuperan el estado original y no conservan el que tenían, setAttribute no funciona con el atributo style o con eventos, varios atributos necesitan ser escritos usando las máyusculas en las iniciales de las plabras (camelCase), radio buttons clonados no crean su propio grupo.
Buen truco, o mejor dicho, implementación, para crear un objeto en Javascript en el que las funciones son referenciadas mediante un string. Puede ser muy útil cuando tenemos en un string el nombre de la función que queremos ejecutar, pero no queremos usar eval.
Para ello lo que creamos es un array de funciones, y el array, implementado como una tabla hash (los indices son strings), hace referencia a una función.
var obj = {
funciones : new Array(),
creaMetodo : function(nombre, fn) {
this.funciones[nombre] = fn;
},
ini : function() {
this.creaMetodo("prueba", function(){alert(1);});
}
}
Ahora podremos referenciar a la función ejecutando:
Algo que me gusta bastante de algunos editores tipo Netbeans es que cuando hay algún error en una línea aparece un enlace en el lateral en la posición relativa de la línea respecto al alto del editor.
Algo parecido se me ha ocurrido hacer con jQuery, obtener los elementos cabecera (H1..H6) y crear un índice, y mediante estilos y jQuery repartirlos por el lateral de la ventana.
El código es sencillito, recupero los elementos h1..h6, calculo su posición X y luego ordeno el array por esta posición, creo una lista ordenada y anidada con OLs y LIs y con CSS y Javascript coloco los elementos donde corresponde.
$(document).ready(function() {
var ids = 0;
var haches = new Array();
for (var i=1; i<7; i++) {
$('#content h'+i).each(function () {
if (!this.id) this.id = 'haches_'+ids++;
if ($(this).css('display') != 'none') {
haches.push(new Array($(this).offset().top, $(this).text(), this.id, i));
}
});
}
haches.sort(function(a,b){return a[0]>b[0];});
var ant = 1;
var html = '<ol class="indice">\n'
for(var i=0; i<haches.length; i++) {
if (haches[i][3] > ant) {
html += '\n<ol>';
} else if (haches[i][3] < ant) {
html += '\n</ol>';
}
html += '<li id="haches'+i+'" class="indice'+haches[i][3]+'"><span><a href="#'+haches[i][2]+'">'+haches[i][1]+'</a></span></li>\n';
ant = haches[i][3];
}
html += '</ol>\n';
$(document.body).prepend(html);
$('#indice').css('position: absolute; top: 0px');
var alto = $(window).height();
var max = $(document).height();
for(var i=0; i<haches.length; i++) {
console.log(parseInt(alto*haches[i][0]/max));
$('#haches'+i).css('top', parseInt(alto*haches[i][0]/max)+"px");
$('#haches'+i).css('left', ($(window).width()-50)+"px");
}
});
Ni que decir tiene que le faltan cosas por hacer, como por ejemplo moverlo según se mueve el scroll, pero para hacerlo en un rato no está tampoco muy mal. Los distintos tipos de enlaces a cabecera tienen sus estilos propios (feos pero propios) y cuando te pones sobre uno de ellos aparece el título enlazando al elemento en cuestión (esto también habría que refinarlo).
Normalmente lo que intentamos es comprimir nuestro código Javascript o bien para que ocupe menos, o bien para ofuscarlo. En este caso se trata de un script que nos permite embellecer scripts comprimidos.
AsÃ, por ejemplo, el código de Google Reader, el cual, como todos los de Google son casi imposibles de seguir, lo verÃamos asÃ:
...
function nd(a, b) {
a = String(a);
return (new Array(Math.max(0, b - a.length + 1))).join(mc) + a
}
var l = "";
function tj() {
return Array.prototype.join.call(arguments, l)
}
var R = " ",
nc = ", ",
Jc = ":";
function zQ(a) {
...