Daphyre-photoBienvenidos a este pequeño curso intensivo (espero, igualmente completo), para aprender a hacer extensiones para Google Chrome. Crearemos una demostración completa de una extensión básica aprovechando el nuevo API creado en Foros del Web.

Para comprender mejor este texto, es necesario tener los conocimientos básicos de HTML, CSS y Javascript; aunque si te estás aventurando a crear tus propias extensiones para Google Chrome, seguramente ya tienes dichos conocimientos.

Quiero suponer que ya has realizado el curso básico “Hello World” que viene en la página de Chrome extensions. De igual forma, en este curso repasaremos lo básico.

1. Información básica de la extensión

Lo primero, es crear una carpeta que contendrá nuestra extensión y dentro de ella, un archivo llamado manifest.json; aquí viene toda la información básica de nuestra extensión:

{
  "name": "Credencial FdW",
  "version": "1.0",
  "description": "Ejemplo de extensión de Google Chrome que muestra tus datos en Foros del Web.",
  "icons": {
     "16": "icon16.png",
     "48": "icon48.png",
     "128": "icon128.png"
            },
  "browser_action": {
    "default_title": "Credencial FdW",
    "default_icon": "icon.png"
  },
  "permissions": [
    "http://www.forosdelweb.com/"
  ]
}

Necesitarás tener también dentro de la carpeta las imágenes correspondientes. icon.png es la imagen de 19x19px que se mostrará en la esquina superior de tu navegador, mientras que los íconos icon16.png, icon48.png e icon128.png (cada uno del tamaño que mencionan) son opcionales para funciones extras de la extensión, aunque es buena práctica tenerlos en la medida de lo posible.

Instalemos la extensión en tu computadora para comprobar que lo haz hecho bien. Escribe en la barra de navegación chrome://extensions y si está inactivo, activa el modo programador del lado derecho de la página. Aparecerán inmediatamente abajo tres botones, haz clic en el primero Cargar extensión sin empaquetar y navega hasta seleccionar la carpeta donde tienes la extensión que estás desarrollando.

Si lo has hecho correctamente, aparecerá en la parte superior de tu navegador el ícono de tu extensión, el cual en estos momentos no hace función alguna.

2. Pop-up

Agreguemos algo de función a tu extensión. Modifica de tu manifest.json los browser action, agregando un default_popup :

  "browser_action": {
    "default_title": "Credencial FdW",
    "default_icon": "icon.png",
    "default_popup": "popup.html"
  },

Esto mostrará la página popup.html al presionar el ícono. Por supuesto, habrá que crear dicha página antes. Comencemos por algo básico, mostrar un enlace que abra la página de Foros del Web. Copia esto en el archivo popup.html:


<!DOCTYPE html>
<html>
<head>
<style type="text/css">
body{
  min-width:360px;
}
a{
  color:#f90;
  text-decoration:none;
}
a:hover{
  text-decoration:underline;
}
p{
  font:0.8em sans-serif;
}
h1{
  font:1em sans-serif;
  color:#fff;
  background:#000;
  padding:5px
}
</style>
</head>

<body>
<h1>Credencial FdW</h1>
<p><a href="http://www.forosdelweb.com">Foros del Web</a></p>
</body>
</html>

Ve de nuevo a chrome://extensions y de tu extensión, selecciona la opción Cargar de nuevo. Si lo has hecho correctamente, al presionar el ícono, aparecerá una caja popup mostrando nuestro enlace hacia Foros del Web. Al presionar nuestro enlace… ¡Espera un segundo! ¡El enlace no abre!

Seguramente tras pensarlo un poco, podrás darte cuenta que la página popup.html no puede abrir un enlace sobre si mismo, por lo que tenemos que indicarle que lo abra en una nueva ventana/pestaña de Google Chrome y eso se hace con el comando window.open de javascript. Para conservar nuestros enlaces y sus propiedades, podemos hacerlo de esta forma:

<a href="http://www.forosdelweb.com" onclick="window.open(this.href)">Foros del Web</a>

Ahora si, al cargar de nuevo nuestra extensión, el enlace se abrirá sin problemas.

3. Obtener datos de una página

Demos un poco de funcionalidad a nuestra extensión, aprovechando los datos proporcionados por el API de foros del web, crearemos una credencial con los datos del usuario conectado. Antes de cerrar la etiqueta head, crearemos una etiqueta script con las siguientes líneas:


<script type="text/javascript">
var xhr=new XMLHttpRequest();
xhr.open('GET', 'http://www.forosdelweb.com/usercp.php?fdwapi=1', true);
xhr.onreadystatechange=function(){
  if(xhr.readyState==4){
    var xmlDoc=new DOMParser().parseFromString(xhr.responseText,'text/xml');

    var userinfo=xmlDoc.getElementsByTagName('userinfo')[0];
    if(!userinfo){
      document.getElementsByTagName('body')[0].innerHTML='<h1>Credencial FdW</h1><p>Desconectado.</p>';
      return;
    }
    else{
      var data='<h1>Credencial FdW</h1>';
      data+='<img src='+userinfo.getAttribute('avatar')+' class="avatar">';
      data+='<p><b>ID:</b> '+userinfo.getAttribute('userid');
      data+='&emsp;<b>Username:</b> '+userinfo.getAttribute('username')+'</p>';
      data+='<p><b>Karma:</b> '+userinfo.getAttribute('karma');
      data+='&emsp;<b>Posts:</b> '+userinfo.getAttribute('posts')+'</p>';
      data+='<p><b>Friend Requests:</b> '+userinfo.getAttribute('friendrequest');
      data+='&emsp;<b>Private Messages:</b> '+userinfo.getAttribute('privatemessage');
      data+='&emsp;<b>Visitor Messages:</b> '+userinfo.getAttribute('visitormessage')+'</p>';
      data+='<p><a href="http://www.forosdelweb.com" onclick="window.open(this.href)">Foros del Web</a>';
      data+='&emsp;<a href="http://www.forosdelweb.com/usercp.php" onclick="window.open(this.href)">Panel de Control</a></p>';
      document.getElementsByTagName('body')[0].innerHTML=data;
    }
  }
  else return;
}
xhr.send(null);
</script>

Lo explicaré de la forma más breve posible. Al comienzo creamos un nuevo XHR (XMLHttpRequest) mediante el cual abrimos el XML del API de Foros del Web. En cuanto la página haya sido cargada, asignaremos los datos a la variable xmlDoc. Convertimos el responseText en un DOM con DOMParser, ya que si intentamos asignar directamente el responseXML, por alguna extraña razón no logra leerlo correctamente y regresa un error.

var xhr=new XMLHttpRequest();
xhr.open('GET', 'http://www.forosdelweb.com/usercp.php?fdwapi=1', true);
xhr.onreadystatechange=function(){
  if(xhr.readyState==4){
    var xmlDoc=new DOMParser().parseFromString(xhr.responseText,'text/xml');
[/sorucecode]
Una vez obtenido el xmlDoc, extraemos de él la etiqueta userinfo, que contiene todos los datos básicos de nuestro usuario. Comparamos con un if si la etiqueta ha sido hallada, si no es así, significa que el usuario no está conectado y asignaremos a nuestro body dicha información.

1

    var userinfo=xmlDoc.getElementsByTagName('userinfo')[0];
    if(!userinfo){
      document.getElementsByTagName('body')[0].innerHTML='<h1>Credencial FdW</h1><p>Desconectado.</p>';
      return;
    }

De lo contrario, si la etiqueta ha sido hallada, agregaremos todos los datos del usuario a una variable y posteriormente los agregaremos al body. Nótese que se ponen dos enlaces (A la página principal y al panel de control) y que a ambos se les asigna el javascript que vimos anteriormente para que pueda abrirlos.

else{
      var data='<h1>Credencial FdW</h1>';
      data+='<img src='+userinfo.getAttribute('avatar')+' class="avatar">';
      data+='<p><b>ID:</b> '+userinfo.getAttribute('userid');
      data+='&emsp;<b>Username:</b> '+userinfo.getAttribute('username')+'</p>';
      data+='<p><b>Karma:</b> '+userinfo.getAttribute('karma');
      data+='&emsp;<b>Posts:</b> '+userinfo.getAttribute('posts')+'</p>';
      data+='<p><b>Friend Requests:</b> '+userinfo.getAttribute('friendrequest');
      data+='&emsp;<b>Private Messages:</b> '+userinfo.getAttribute('privatemessage');
      data+='&emsp;<b>Visitor Messages:</b> '+userinfo.getAttribute('visitormessage')+'</p>';
      data+='<p><a href="http://www.forosdelweb.com" onclick="window.open(this.href)">Foros del Web</a>';
      data+='&emsp;<a href="http://www.forosdelweb.com/usercp.php" onclick="window.open(this.href)">Panel de Control</a></p>';
      document.getElementsByTagName('body')[0].innerHTML=data;
    }

No olvides al final cerrar el xhr enviándole una variable null. Por último, modificaremos nuestro body original, para que muestre que se está conectando a la página mientras obtiene los datos:


<body>
<h1>Credencial FdW</h1>
<p>Conectando...</p>
</body>

4. Background

Ahora nuestra extensión es funcional, pero tener que revisar nuestra credencial cada 5 minutos para ver si tenemos nuevas notificaciones, no es lo más ideal. Además de los popup, las extensiones de Chrome ofrecen otra opción importante que son las páginas de fondo, que a continuación mostraremos uno de sus usos prácticos.

Primero, regresaremos a editar nuestro manifest.json y agregaremos nuestra página de fondo justo después de la descripción:

"background_page": "background.html",

Después crearemos la nueva página background.html y le agregaremos estos datos:


<!DOCTYPE html>
<html>
<head>
<script type="text/javascript">
var counter=0;
var timerVar=null;
var timerDelay=300000;

// Badge colors
var BADGE_ACTIVE = {color:[204,0,51,255]};
var BADGE_LOADING = {color:[204,204,51,255]};
var BADGE_INACTIVE = {color:[153,153,153,255]};

function loadData(){
  var xhr=new XMLHttpRequest();
  xhr.open('GET','http://www.forosdelweb.com/usercp.php?fdwapi=1',true);
  xhr.onreadystatechange=function(){
    if(xhr.readyState == 4){
      var xmlDoc=new DOMParser().parseFromString(xhr.responseText,'text/xml');

      chrome.browserAction.setBadgeBackgroundColor(BADGE_INACTIVE);
      var userinfo=xmlDoc.getElementsByTagName('userinfo')[0];
      if(!userinfo){
        chrome.browserAction.setIcon({path:'icon-gray.png'});
        chrome.browserAction.setTitle({title: 'Credencial FdW\n--Desconectado--'});
        chrome.browserAction.setBadgeText({text: '?'});
        return;
      }
      else{
        counter=0;
        var notifications=xmlDoc.getElementsByTagName('notification');
        for (var i=0;i<notifications.length;i++){
          if(notifications[i].getAttribute('highlight')=='true'){
            counter++;
          }
        }
        chrome.browserAction.setIcon({path:'icon.png'});
        chrome.browserAction.setTitle({title: 'Credencial FdW\n--Conectado--'});
        if(counter>0){
          chrome.browserAction.setBadgeText({text: counter+''});
          chrome.browserAction.setBadgeBackgroundColor(BADGE_ACTIVE);
        }
        else
          chrome.browserAction.setBadgeText({text: ''});
      }
    }
    else return;
  }
  xhr.send(null);
  window.clearTimeout(timerVar);
  timerVar=window.setTimeout(loadData,timerDelay);
}

function init() {
  chrome.browserAction.setIcon({path:'icon-gray.png'});
  chrome.browserAction.setBadgeText({text: '...'});
  chrome.browserAction.setBadgeBackgroundColor(BADGE_LOADING);
  loadData();
}
</script>
</head>

<body onload="init()">
</body>
</html>

Como esta página no será mostrada, nuestro body no contiene nada, sin embargo, es importante, ya que al terminar de ser cargada la página, llamará a nuestra función init(). La función init se encarga de establecer variables predeterminadas (Que en nuestro caso no tenemos) e indica que nuestra extensión se está conectado a los datos. Al final, llamará a loadData().


function init() {
  chrome.browserAction.setIcon({path:'icon-gray.png'});
  chrome.browserAction.setBadgeText({text: '...'});
  chrome.browserAction.setBadgeBackgroundColor(BADGE_LOADING);
  loadData();
}

Nótese que llamamos a una nueva imagen llamada icon-gray.png, que no es mas que nuestra imagen icon.png pero en gris, la cual será mostrada cuando nuestra extensión se encuentre desconectada. No olvides crearla y agregarla a la carpeta, de lo contrario, la extensión se mostrará vacía.

La función loadData comienza similar que nuestro script en el popup, usando userinfo para verificar si el usuario está conectado o no. Si no lo está, el ícono se quedará de color gris, el “title” mostrará que estamos desconectados, y aparecerá un “?” en la pequeña caja que anteriormente dijimos sería de color gris (BADGE_INACTIVE).


      chrome.browserAction.setBadgeBackgroundColor(BADGE_INACTIVE);
      if(!userinfo){
        chrome.browserAction.setIcon({path:'icon-gray.png'});
        chrome.browserAction.setTitle({title: 'Credencial FdW\n--Desconectado--'});
        chrome.browserAction.setBadgeText({text: '?'});
        return;
      }

Por el contrario, si el usuario está conectado, comenzaremos el contador desde cero, y buscaremos cada notificación cuyo “highlight” sea verdadero (O sea, las nuevas notificaciones), la cual agregaremos al contador. Finalmente, cambiaremos el ícono al original con color, el title mostrará que estamos conectados, si hay mensajes, mostraremos en la pequeña cajita cuántos son y pondremos el fondo de la cajita roja (BADGE_ACTIVE). Si no estamos conectados, asignaremos a la cajita un texto vacío, que hará que esta no se muestre.


      else{
        counter=0;
        var notifications=xmlDoc.getElementsByTagName('notification');
        for (var i=0;i<notifications.length;i++){
          if(notifications[i].getAttribute('highlight')=='true'){
            counter++;
          }
        }
        chrome.browserAction.setIcon({path:'icon.png'});
        chrome.browserAction.setTitle({title: 'Credencial FdW\n--Conectado--'});
        if(counter>0){
          chrome.browserAction.setBadgeText({text: counter+''});
          chrome.browserAction.setBadgeBackgroundColor(BADGE_ACTIVE);
        }
        else
          chrome.browserAction.setBadgeText({text: ''});
      }

Finalmente, enviaremos nulo a nuestro xhr y asignaremos un Timeout de 5 segundos (300000 milisegundos) antes que volvamos a recargar los datos para verificar de nuevo si hay nuevas notificaciones. Nótese que antes de esto, limpiamos el Timeout, para evitar crear procesos extras en caso de ser llamada antes de tiempo.


  xhr.send(null);
  window.clearTimeout(timerVar);
  timerVar=window.setTimeout(loadData,timerDelay);

Cuando actualicemos nuestra extensión, veremos estos cambios. Si tenemos notificaciones se mostrarán en la caja roja, si no las tenemos se mostrará como era originalmente y si estamos desconectados, se mostrará una caja gris.

Mucha gente prefiere crear extensiones “más prácticas”, que en lugar de mostrar un popup al abrir la extensión, desean que esta abra directo la página correspondiente. Para lograr esto, primero se debe eliminar la línea del popup de nuestro manifes.json y luego se debe agregar un escucha de clic a la extensión en nuestro background, en el caso de nuestro código, al final antes de cerrar el script:


chrome.browserAction.onClicked.addListener(function(tab){
  window.open('http://www.forosdelweb.com/');
  loadData();
});

Como podremos ver, aprovechamos la oportunidad para, además de abrir la página, recargar los datos para tenerlos más frescos. Si la extensión que estás haciendo ahora utilizará el popup o abrirá la página al hacer clic sobre ella, será tu decisión.

5. Options

Para finalizar, veremos esta última opción importante que nos ofrecen las extensiones para Chrome, que son precisamente, las opciones. Para ejemplificar su uso, mostraré como realizar dos opciones populares: Permitir que el usuario decida cada cuantos minutos se actualice la información de la extensión y si desea o no reproducir un sonido cuando haya nuevos mensajes (tendrás que conseguir un sonido para ello).

Comenzaremos por agregar nuestra página de opciones al manifest.json, justo después de la página background:

"options_page": "options.html",

Luego, por supuesto, crearemos la página options.html y le agregaremos el código correspondiente:


<!DOCTYPE html>
<html>
<head>
<style type="text/css">
body{
  font-family:sans-serif;
}
h1{
  color:#fff;
  background:#000;
  padding:5px
}
</style>
<script type="text/javascript">
var saveButton;
var playSoundCheckbox;
var refreshIntervalTextbox;

function save(){
  localStorage.refreshInterval = (refreshIntervalTextbox.value && parseInt(refreshIntervalTextbox.value)>=1) ? parseInt(refreshIntervalTextbox.value)*60000 : 300000;
  localStorage.playSound = playSoundCheckbox.checked ? 'yes' : 'no';

  init();
  chrome.extension.getBackgroundPage().init();
}

function init(){
  playSoundCheckbox = document.getElementById("play-sound");
  refreshIntervalTextbox = document.getElementById("refresh-interval");
  saveButton = document.getElementById("save-button");

  playSoundCheckbox.checked = (localStorage.playSound) ? (localStorage.playSound == 'yes') : true;
  refreshIntervalTextbox.value = (parseInt(localStorage.refreshInterval)) ? parseInt(localStorage.refreshInterval)/60000 + '' : "5";

  disableSave();
}

function enableSave(){saveButton.disabled = false;}
function disableSave(){saveButton.disabled = true;}
</script>
</head>

<body onload="init();">
<h1><img src="icon48.png" alt="[icon]" /> Credencial FdW</h1>
<h2>Opciones</h2>
<p><label><input type="checkbox" id="play-sound" onclick="enableSave()" /> Al haber nuevos mensajes, reproducir un sonido.</label> [por defecto: si]</p>
<p>Intervalo de actualizacion (minutos):<input type="text" id="refresh-interval" oninput="enableSave()" /> [por defecto: 5]</p>
<p><input type="button" value="Guardar" id="save-button" onclick="save()" /></p>
</body>
</html>

Como vemos en nuestro body, estamos poniendo un checkbox para tomar una decisión si/no y un textbox para tomar una decisión libre (En este caso, un número). Al cargar llamamos a nuestro init(), el cual asigna nuestros elementos del body a variables para manejarlos más fácil, busca si ya tenemos valores guardados en nuestro localstorage y si no, muestra los predeterminados. Al final, deshabilitamos el botón de guardar.


function init(){
  playSoundCheckbox = document.getElementById("play-sound");
  refreshIntervalTextbox = document.getElementById("refresh-interval");
  saveButton = document.getElementById("save-button");

  playSoundCheckbox.checked = (localStorage.playSound) ? (localStorage.playSound == 'yes') : true;
  refreshIntervalTextbox.value = (parseInt(localStorage.refreshInterval)) ? parseInt(localStorage.refreshInterval)/60000 + '' : "5";

  disableSave();
}

Dos funciones se encargan de deshabilitar o habilitar nuestro botón de guardar, las cuales son llamadas en el momento en que se hace un cambio a nuestros datos (onclick para el checkbox y oninput para el textbox). Por último, tenemos la función save(), que guarda los datos y posteriormente llama a la función init(), tanto local, como la de background, para que esta comience a funcionar con los nuevos datos.

En el caso del checkbox, para guardar el dato, tenemos que verificar si está activado para guardar “yes” o “no” en caso opuesto:

localStorage.playSound = playSoundCheckbox.checked ? 'yes' : 'no';

Guardar un string es más sencillo, ya que solo hay que guardar el dato de forma directa:

localStorage.someString = someStringTextbox.value;

Pero en caso de tener que guardar un número, primero debemos de verificar que no esté vacío el texto y luego que este sea un número en verdad, o de lo contrario, guardar el texto predeterminado:

localStorage.refreshInterval = (refreshIntervalTextbox.value && parseInt(refreshIntervalTextbox.value)) ? parseInt(refreshIntervalTextbox.value) : 5;

Además, en nuestro caso particular debemos de verificar que el número sea mayor de 1, o la extensión se estaría actualizando una y otra vez sin parar causando un error. También debemos multiplicar el valor final por 60000, que es la cantidad de milisegundos en un minuto, unidad en que solicitamos nuestro valor.

Ahora que ya tenemos nuestro options.html, debemos regresar a nuestro background.html para agregar los nuevos cambios. Primero, agregar las nuevas variables para nuestro sonido, justo al comienzo de nuestro script, después de timerDelay:


var playSound=true;
var audio=new Audio('sound.aac');

Después, inicializar las variables en nuestra función init():


function init(){
timerDelay=parseInt(localStorage.refreshInterval || '300000');
playSound=(localStorage.playSound)?(localStorage.playSound=='yes'):true;

Con respecto a timerDelay no habrá que hacer más cambios, pero para playSound, tendremos que agregar una variable llamada “lastCounter” a la que se le asignará el valor de Counter antes que este regrese a cero y pase por el contador de notificaciones y al final comparar si hay nuevas notificaciones para reproducir el sonido:

if(playSound && counter>lastCounter)
audio.play();

6. Publicar en la tienda web

Ahora que ya tenemos las bases para crear una extensión para Google Chrome, seguro que te gustaría publicarla para que todo el mundo pueda utilizarla. La forma más sencilla y posiblemente de mayor alcance, sería utilizar la tienda web de Chrome.

Chrome-storePrimero, selecciona todos tus archivos y comprímelos en una carpeta zip. Después, ingresa al Dashboard de Google Chrome y sigue los pasos para publicarla; si no te has registrado antes, te lo pedirá en este momento. Te pedirá además algo de información y unos screenshots de tu extensión. Una vez que termines de seguir los pasos, tu extensión sera pública para todo el mundo. ¡Felicidades!

Revisa la documentación completa de extensiones, los códigos fuente de otras extensiones o cualquier extensión que instales (Cuida de no violar copyright y dar los créditos apropiados si usas códigos de terceros). Si en algún momento quedaste inseguro con este texto, puedes revisar los archivos finales.

¡Felices códigos! 😉

Si tienes dudas al respecto puedes encontrar a daPhyre en Foros del web o contactarlo directamente a su correo.