Escribir una extensión para cualquier aplicación que lo soporte suele ser tan complicado como detallada sea la documentación que ofrece. En el caso que nos ocupa, Google Chrome, la documentación es bastante completa aunque haya que navegar por distintos negociados de la web de desarrolladores de Chrome para encontrar todas las piezas. Y una de las partes difíciles de encontrar es cómo depurar nuestra extensión. Usando como base la que se encuentra aquí vamos a entender un poco mejor como funcionan las extensiones para Google Chrome.
El mejor comienzo es este enlace donde ya empezamos con la primera duda: ¿qué tipo de funcionalidad se quiere implementar? Una extensión puede incluir una browser action, es decir, una acción que afecta a todas las páginas, o una page action, cuando queremos que la extensión sólo se active en determinadas páginas. Además, nuestra extensión puede tener su propio interface, en forma de popup. Para completar la extensión, la mayoría requieren de un script independiente que se ejecuta en el contexto del contenido de la página que estamos visitando. Todas estas decisiones de implementación se comunican al navegador a través de un archivo de manifiesto como el siguiente:
{ "manifest_version": 2, "name": "FilterOut", "version": "0.1", "description": "Filters out unwanted content.", "default_locale": "en", "permissions": ["storage", "tabs"], "background": { "scripts": ["lib/dom.js", "lib/arrays.js", "background.js"] }, "content_scripts": [{ "matches": ["<all_urls>"], "css": ["filterout.css"], "js": ["lib/dom.js", "lib/arrays.js", "content_script.js"] }], "browser_action": { "default_icon": {"128": "frontend/icon.png"}, "default_title": "__MSG_extName__", "default_popup": "frontend/popup.html" } }
En este manifiesto en concreto se está definiendo una extensión que necesita acceso tanto al almacenamiento local como a las pestañas del navegador y que tiene, además del proceso en background que es el código principal de la extensión, una browser action con un popup y varios scripts de contenido.
Esta organización ya nos indica que vamos a trabajar con tres juegos de código: el de la extensión, background.js, el del interface, popup.html y el código que examina y manipula el contenido de la página, content_script.js. La manera como se organicen estos archivos, los nombres que tengan, etc. quedan a elección del desarrollador. Lo mismo se aplica a la forma en que se estructure el código, sabiendo que: la extensión se ejecuta en su propio proceso, el popup se crea nuevo cada vez que se abre y la comunicación entre el script de contenido y el resto de la aplicación es mediante mensajes, no directamente. Es decir, tenemos tres componentes que se ejecutan cada uno en su propio entorno y que tienen una comunicación más o menos limitada en función del tipo de componente.
A la hora de crear el código de la extensión podemos definir una serie de funciones independientes o un objeto con distintos métodos. A la hora de llamar a estas funciones desde el interface no hay gran diferencia, ya que toda la comunicación en este caso es a través del método chrome.extension.getBackgroundPage(), que devuelve un objeto conteniendo todas las variables y funciones definidas en background.js. Desde el punto de vista del script de contenido tampoco hay ninguna diferencia, porque éste código no puede acceder a la extensión más que respondiendo a un mensaje emitido desde la misma. Se trata pues de querer o estar acostumbrado a aplicar determinados patterns a nuestro código. Y de querer aislar completamente los detalles de la extensión del código de interface, único que tiene acceso a la misma.
La extensión se carga al iniciar Chrome y se ejecuta en función de los handlers que se hayan definido en el código de la misma. El navegador puede enviar un gran número de eventos a nuestra extensión: cuando se refresca la ventana, cuando se activa o desactiva una pestaña… En nuestra extensión queremos ser informados cuando la página se ha cargado por completo para poder procesar su contenido y, por cuestiones estéticas y de sincronización con el popup, cuando el usuario cambia de pestaña.
El código de la extensión no sólo envía mensajes al que se encarga de procesar el contenido, si no que también gestiona el almacenamiento de las configuraciones, usando el objeto localStorage. Para cada dominio configurado se almacena un objeto con la siguiente estructura:
{ target: ‘[SELECTOR O SELECTORES PARA LOS TARGETS]’, container: ‘[SELECTOR O SELECTORES PARA LOS CONTAINERS]’, filtered: [] }
Como la extensión utiliza el método querySelectorAll() para buscar los selectores podemos pasar una cadena con tantos como necesitemos, separados por comas, como en ‘.md-day-pinture-item .byline, .byline a’.
La comunicación entre la extensión y el código que se ejecuta en el contenido se realiza enviando mensajes a la pestaña que nos interesa, usando el método interno de chrome chrome.tabs.sendMessage().
doProcessPage: function (tabId) { var self = this; chrome .tabs .sendMessage(tabId, { op: 'init', config: this.objConfig[this.strCurDomain] }, function(response) { if(response) { self.arrResults[self.strCurDomain] = response.results || []; self.setBadge(tabId); } }); }
Este método envía un mensaje con el payload que indiquemos al script de contenido y éste, a su vez, llama al callback que se pasa como parámetro. De esta manera tenemos una forma simple y segura de comunicar nuestra extensión con el código que procesa el contenido.
chrome.runtime.onMessage.addListener(function(req, sender, sendResponse) { switch(req.op) { case 'init': sendResponse(processPage(req.config)); break; case 'filter': sendResponse(filterOut(req.config)); break; } });
El interface
El interface es multilingüe. Esto quiere decir que, en función del idioma configurado para el navegador (típicamente el mismo que el del sistema) mostrará sus textos en uno u otro idioma. Para configurar la extensión como multilingüe hay que:
- indicar un idioma por defecto en el manifiesto y
- crear un directorio _locales con una directorio para cada idioma soportado dentro del cual hay un archivo messages.
El popup se crea nuevo cada vez que se abre. Y es una página web completamente independiente del resto de la extensión y de la página que tengamos activa en el navegador. La forma de comunicar el interface con la extensión es pidiendo explícitamente a chrome que nos de acceso al background usando el método chrome.extension.getBackgroundPage(). A partir de objeto que nos devuelve este método tenemos acceso a todas las variables definidas en nuestra extensión. Como el código de la extensión está encapsulado en un objeto, la forma de acceder a dicho objeto será:
var objExtension = chrome.extension.getBackgroundPage().objExtension;
El archivo html que hemos indicado en el manifiesto será el que se cargue al abrir el popup. Y dentro de este archivo podemos hacer cualquier cosa que podamos hacer en una página web. Cargar hojas de estilos y librerías, añadir eventos, animaciones, etc. A todos los efectos es una página web independiente. La librería que se carga en este caso es una que intenta imitar los métodos de jQuery, pero que sólo incluye los métodos básicos y sólo para navegadores de última generación. Básicamente la historia es: si tienes una entrevista de trabajo en la que te van a preguntar sobre tu conocimiento del DOM la mejor manera de prepararte es escribiendo código. Y que todos tenemos derecho a tener nuestra propia librería.
Como se ve en el código, cada vez que la página se carga hay que rellenar los literales y rellenar la lista de targets con los resultados que haya obtenido previamente la extensión. El resto son gestores de eventos para proporcionar un poco de interactividad al popup: mostrar el panel de configuración, responder a eventos en la lista, etc. Cada vez que queramos que la extensión haga algo como respuesta a una acción del usuario directamente llamamos al método correspondiente del objeto objExtension que hemos leido al cargar la página.
Depuración
La parte más complicada puede ser la depuración porque, a todos los efectos, estamos depurando tres páginas diferentes. Y una de ellas solo existe mientras el popup está abierto. Aunque es lioso, se puede solucionar con un poco de organización y, sobre todo, sabiendo desde donde se accede al código de cada parte.
Es importante instalar la herramienta de Chrome para desarrollar aplicaciones y extensiones, disponible en este enlace. A través de ella podemos instalar, desinstalar o recargar rápidamente la extensión, acceder al código de la misma y tener una especie de consola donde ver los errores que están sucediendo en el código de la extensión, pero no en las otras dos partes. Chrome internamente genera una página donde se carga y ejecuta el código de nuestra extensión.
Para depurar el popup hay que abrir primero éste y después abrir la herramienta de desarrollo de Chrome seleccionando la opción “Inspeccionar pop-up” en el icono de nuestra extensión. Es un poco latoso porque cada vez que se cierre el popup desaparecerá el inspector.
Y para depurar el código que manipula el contenido hay que ir a la página en la que se ejecuta la extensión y seleccionar “Content scripts”. Ahí se accede al código de todas las extensiones instaladas que se ejecuta en el contenido de la página, no al código de la propia extensión.
El código completo de la extensión está disponible en github.