DESARROLLA EXTENSIONES CHROME MODERNAS CON MANIFEST V3
Introducción al desarrollo de extensiones Chrome con Manifest V3
El desarrollo de extensiones para Google Chrome representa una oportunidad significativa para personalizar la experiencia de navegación y ofrecer herramientas útiles directamente en el navegador. Con la evolución de la plataforma de extensiones web, la versión Manifest V3 se ha consolidado como el estándar actual, introduciendo cambios fundamentales en seguridad, rendimiento y arquitectura. Este tutorial presenta un enfoque práctico y detallado para construir una extensión funcional que permite marcar timestamps en videos de YouTube, utilizando exclusivamente tecnologías modernas y respetando las directrices actuales de Google.
A través de este proceso, se explorarán los componentes esenciales de una extensión Chrome: el archivo de manifiesto, scripts de contenido, service workers como scripts de fondo, y la interfaz de usuario emergente. Cada etapa se acompaña de ejemplos de código claros y explicaciones técnicas que facilitan la comprensión y replicación del proyecto.
Estructura inicial del proyecto
Para comenzar, es necesario organizar los archivos que conforman la extensión. La estructura recomendada incluye los siguientes elementos:
extension-youtube-bookmarker/
├── manifest.json
├── popup.html
├── popup.js
├── content-script.js
├── background.js
├── utils.js
├── assets/
│ ├── bookmark.png
│ ├── play.png
│ └── delete.png
└── styles.css
Esta organización permite una separación clara de responsabilidades: el manifiesto define la configuración general, los scripts de contenido interactúan con las páginas web, el service worker gestiona eventos en segundo plano, y los archivos de popup conforman la interfaz visible al usuario.
Configuración del archivo manifest.json
El archivo manifest.json constituye el núcleo de cualquier extensión Chrome. En él se especifica la versión del manifiesto, permisos requeridos, scripts ejecutables y elementos de interfaz.
{
"name": "Marcador de Videos YouTube",
"version": "1.0.0",
"description": "Permite guardar timestamps en videos de YouTube",
"manifest_version": 3,
"permissions": ["storage", "tabs"],
"host_permissions": ["*://*.youtube.com/watch*"],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["*://*.youtube.com/watch*"],
"js": ["content-script.js"]
}
],
"action": {
"default_popup": "popup.html"
},
"icons": {
"16": "assets/bookmark.png",
"48": "assets/bookmark.png",
"128": "assets/bookmark.png"
}
}
La clave manifest_version: 3 indica el uso de la versión actual. Los permisos storage y tabs son esenciales para persistir datos y detectar la pestaña activa. El patrón *://*.youtube.com/watch* limita la ejecución del script de contenido a páginas de video de YouTube.
Implementación del script de fondo con service worker
El service worker reemplaza a los scripts de fondo persistentes de Manifest V2, ejecutándose solo cuando es necesario y liberando recursos. Su función principal es detectar navegación a videos de YouTube y notificar al script de contenido.
// background.js
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === "complete" && tab.url) {
if (tab.url.includes("youtube.com/watch")) {
const urlParams = new URL(tab.url).searchParams;
const videoId = urlParams.get("v");
if (videoId) {
chrome.tabs.sendMessage(tabId, {
type: "NEW_VIDEO_LOADED",
videoId: videoId,
});
}
}
}
});
Este código escucha actualizaciones de pestañas y, al detectar una URL de video de YouTube, extrae el identificador único mediante URL.searchParams.get('v'). Luego envía un mensaje al script de contenido con el tipo NEW_VIDEO_LOADED.
Desarrollo del script de contenido
El script de contenido se ejecuta en el contexto de la página web, permitiendo manipulación directa del DOM. Su responsabilidad incluye insertar un botón de marcador en el reproductor de YouTube y gestionar la creación de bookmarks.
// content-script.js
let currentVideo = "";
let youtubeLeftControls, youtubePlayer;
let currentVideoBookmarks = [];
const fetchBookmarks = () => {
return new Promise((resolve) => {
chrome.storage.sync.get([currentVideo], (obj) => {
resolve(obj[currentVideo] ? JSON.parse(obj[currentVideo]) : []);
});
});
};
const addNewBookmarkEventHandler = async () => {
const currentTime = youtubePlayer.currentTime;
const newBookmark = {
time: currentTime,
desc: `Bookmark at ${getTime(currentTime)}`,
};
currentVideoBookmarks = await fetchBookmarks();
currentVideoBookmarks.push(newBookmark);
currentVideoBookmarks.sort((a, b) => a.time - b.time);
chrome.storage.sync.set({
[currentVideo]: JSON.stringify(currentVideoBookmarks),
});
};
const newVideoLoaded = async () => {
const bookmarkBtnExists =
document.getElementsByClassName("bookmark-btn")[0];
if (!bookmarkBtnExists) {
const bookmarkBtn = document.createElement("img");
bookmarkBtn.src = chrome.runtime.getURL("assets/bookmark.png");
bookmarkBtn.className = "ytp-button bookmark-btn";
bookmarkBtn.title = "Click to bookmark current timestamp";
youtubeLeftControls =
document.getElementsByClassName("ytp-left-controls")[0];
youtubePlayer = document.getElementsByClassName("video-stream")[0];
youtubeLeftControls.appendChild(bookmarkBtn);
bookmarkBtn.addEventListener("click", addNewBookmarkEventHandler);
}
currentVideoBookmarks = await fetchBookmarks();
};
// Forzar ejecución al cargar la página
if (window.location.href.includes("youtube.com/watch")) {
newVideoLoaded();
}
chrome.runtime.onMessage.addListener((obj, sender, response) => {
const { type, videoId } = obj;
if (type === "NEW_VIDEO_LOADED") {
currentVideo = videoId;
newVideoLoaded();
} else if (type === "PLAY") {
youtubePlayer.currentTime = obj.value;
} else if (type === "DELETE") {
currentVideoBookmarks = currentVideoBookmarks.filter(
(b) => b.time != obj.value
);
chrome.storage.sync.set({
[currentVideo]: JSON.stringify(currentVideoBookmarks),
});
response(currentVideoBookmarks);
}
});
La función getTime convierte segundos en formato HH:MM:SS:
const getTime = (t) => {
const date = new Date(0);
date.setSeconds(t);
return date.toISOString().substr(11, 8);
};
El script verifica si el botón ya existe para evitar duplicados, inserta el icono en los controles izquierdos del reproductor, y establece un listener para capturar clics. Al recibir mensajes del service worker o popup, actualiza el estado según corresponda.
Construcción de la interfaz emergente
La interfaz popup se activa al hacer clic en el icono de la extensión. Debe mostrar bookmarks existentes, permitir reproducción y eliminación, y manejar páginas no compatibles.
HTML base
<!-- popup.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="container">
<h2>Your bookmarks for this video</h2>
<div id="bookmarks"></div>
</div>
<script src="popup.js"></script>
</body>
</html>
Lógica del popup
// popup.js
import { getActiveTabURL } from "./utils.js";
const addNewBookmark = (bookmarksElement, bookmark) => {
const bookmarkTitleElement = document.createElement("div");
const newBookmarkElement = document.createElement("div");
const controlsElement = document.createElement("div");
bookmarkTitleElement.textContent = bookmark.desc;
bookmarkTitleElement.className = "bookmark-title";
newBookmarkElement.id = "bookmark-" + bookmark.time;
newBookmarkElement.className = "bookmark";
newBookmarkElement.setAttribute("timestamp", bookmark.time);
controlsElement.className = "bookmark-controls";
setBookmarkAttributes("play", onPlay, controlsElement);
setBookmarkAttributes("delete", onDelete, controlsElement);
newBookmarkElement.appendChild(bookmarkTitleElement);
newBookmarkElement.appendChild(controlsElement);
bookmarksElement.appendChild(newBookmarkElement);
};
const viewBookmarks = (currentBookmarks = []) => {
const bookmarksElement = document.getElementById("bookmarks");
bookmarksElement.innerHTML = "";
if (currentBookmarks.length > 0) {
for (let i = 0; i < currentBookmarks.length; i++) {
const bookmark = currentBookmarks[i];
addNewBookmark(bookmarksElement, bookmark);
}
} else {
bookmarksElement.innerHTML =
'<i class="no-bookmarks">No bookmarks to show</i>';
}
};
const onPlay = async (e) => {
const bookmarkTime =
e.target.parentElement.parentElement.getAttribute("timestamp");
const activeTab = await getActiveTabURL();
chrome.tabs.sendMessage(activeTab.id, {
type: "PLAY",
value: bookmarkTime,
});
};
const onDelete = async (e) => {
const bookmarkTime =
e.target.parentElement.parentElement.getAttribute("timestamp");
const activeTab = await getActiveTabURL();
const bookmarkElementToDelete = document.getElementById(
"bookmark-" + bookmarkTime
);
bookmarkElementToDelete.parentNode.removeChild(bookmarkElementToDelete);
chrome.tabs.sendMessage(
activeTab.id,
{
type: "DELETE",
value: bookmarkTime,
},
viewBookmarks
);
};
const setBookmarkAttributes = (src, eventListener, controlParentElement) => {
const controlElement = document.createElement("img");
controlElement.src = "assets/" + src + ".png";
controlElement.title = src;
controlElement.addEventListener("click", eventListener);
controlParentElement.appendChild(controlElement);
};
document.addEventListener("DOMContentLoaded", async () => {
const activeTab = await getActiveTabURL();
const urlParams = new URL(activeTab.url).searchParams;
const currentVideo = urlParams.get("v");
if (activeTab.url.includes("youtube.com/watch") && currentVideo) {
chrome.storage.sync.get([currentVideo], (obj) => {
const currentVideoBookmarks = obj[currentVideo]
? JSON.parse(obj[currentVideo])
: [];
viewBookmarks(currentVideoBookmarks);
});
} else {
const container = document.getElementsByClassName("container")[0];
container.innerHTML =
'<div class="title">This is not a YouTube video page.</div>';
}
});
Utilidad para obtener pestaña activa
// utils.js
export const getActiveTabURL = () => {
return new Promise((resolve) => {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
resolve(tabs[0]);
});
});
};
La función getActiveTabURL encapsula la consulta a la API de pestañas, retornando una promesa que resuelve con la pestaña activa actual.
Estilos visuales
Los estilos aseguran una presentación limpia y consistente:
/* styles.css */
body {
width: 300px;
padding: 16px;
font-family: Arial, sans-serif;
}
.container {
display: flex;
flex-direction: column;
gap: 12px;
}
.bookmark {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.bookmark-title {
font-size: 14px;
color: #333;
}
.bookmark-controls img {
width: 16px;
height: 16px;
cursor: pointer;
margin-left: 8px;
}
.no-bookmarks {
color: #666;
font-style: italic;
}
.title {
font-weight: bold;
color: #d93025;
}
Pruebas y carga de la extensión
Para probar la extensión:
- Abre Chrome y navega a
chrome://extensions/ - Activa el modo desarrollador
- Selecciona “Cargar descomprimida”
- Elige la carpeta del proyecto
La extensión aparecerá en la barra de herramientas. Al visitar un video de YouTube, se mostrará el botón de marcador en el reproductor. Los bookmarks se guardan por video y persisten entre sesiones.
Depuración avanzada
Utiliza las herramientas de desarrollador:
- Inspecciona el popup: clic derecho en el icono → “Inspeccionar”
- Scripts de contenido: DevTools en la página de YouTube → pestaña “Sources”
- Service worker:
chrome://extensions/→ “Inspeccionar vistas” en el service worker
Mejoras posibles
Una vez dominada la base, considera implementar:
- Soporte para listas de reproducción
- Exportación e importación de bookmarks
- Sincronización entre dispositivos
- Temas oscuro/claro
- Atajos de teclado personalizados
Conclusiones
El desarrollo de extensiones Chrome con Manifest V3 representa un paradigma moderno que prioriza seguridad, eficiencia y mantenibilidad. A través de este proyecto práctico, se ha demostrado cómo integrar service workers, scripts de contenido y almacenamiento persistente para crear una herramienta funcional y escalable. La separación clara de responsabilidades entre componentes facilita el mantenimiento y la extensión de funcionalidades futuras.
La plataforma Chrome continúa evolucionando, y dominar sus APIs actuales posiciona al desarrollador para crear soluciones innovadoras que mejoren la productividad y experiencia de usuario. Este conocimiento trasciende el ejemplo específico, aplicándose a cualquier proyecto que requiera interacción profunda con el navegador.