Usar ubicación geográfica

Si deseas obtener información de la ubicación geográfica en la extensión de Chrome, usa la misma API de plataforma web de navigator.geolocation que usaría cualquier sitio web. Este artículo existe porque las extensiones de Chrome manejan el permiso para acceder a datos sensibles de manera diferente a los sitios web. La ubicación geográfica es un dato muy sensible, por lo que los navegadores garantizan que los usuarios sean plenamente conscientes y tengan el control de cuándo y dónde se comparte su ubicación exacta.

Usar la ubicación geográfica en las extensiones de MV3

En la Web, para proteger los datos de ubicación geográfica de los usuarios, los navegadores muestran un mensaje en el que se les solicita que otorguen acceso a su ubicación a ese origen específico. El mismo modelo de permisos no siempre es apropiado para las extensiones.

Captura de pantalla de la solicitud de permiso que ves cuando un sitio web solicita acceso a la API de ubicación geográfica
Mensaje del permiso de ubicación geográfica

Los permisos no son la única diferencia. Como se mencionó anteriormente, navigator.geolocation es una API de DOM, es decir, algo que forma parte de las APIs que conforman sitios web. Como resultado, no se puede acceder a él dentro de los contextos de trabajadores, como el service worker de extensión, que es la columna vertebral de las extensiones de Manifest v3. Sin embargo, puedes usar geolocation. Solo hay matices en cómo y dónde se usa.

Usa la ubicación geográfica en service workers

No hay un objeto navigator dentro de los service workers. Solo está disponible dentro de contextos que tienen acceso al objeto document de una página. Para obtener acceso al service worker, usa un elemento Offscreen Document, que proporciona acceso a un archivo HTML que puedes empaquetar con tu extensión.

Para comenzar, agrega "offscreen" a la sección "permissions" de tu manifiesto.

manifest.json:

{
  "name": "My extension",
    ...
  "permissions": [
    ...
   "offscreen"
  ],
  ...
}

Después de agregar el permiso "offscreen", agrega un archivo HTML a la extensión que incluya el documento fuera de pantalla. Este caso no utiliza nada del contenido de la página, por lo que podría ser un archivo casi en blanco. Solo necesita ser un pequeño archivo HTML que se cargue en tu secuencia de comandos.

offscreen.html:

<!doctype html>
<title>offscreenDocument</title>
<script src="offscreen.js"></script>

Guarda este archivo en la raíz de tu proyecto como offscreen.html.

Como se mencionó, necesitas una secuencia de comandos llamada offscreen.js. También deberá empaquetarlo con su extensión. Será la fuente de la información de ubicación geográfica del service worker. Puedes pasar mensajes entre esta y tu service worker.

offscreen.js:

chrome.runtime.onMessage.addListener(handleMessages);
function handleMessages(message, sender, sendResponse) {
  // Return early if this message isn't meant for the offscreen document.
  if (message.target !== 'offscreen') {
    return;
  }

  if (message.type !== 'get-geolocation') {
    console.warn(`Unexpected message type received: '${message.type}'.`);
    return;
  }

  // You can directly respond to the message from the service worker with the
  // provided `sendResponse()` callback. But in order to be able to send an async
  // response, you need to explicitly return `true` in the onMessage handler
  // As a result, you can't use async/await here. You'd implicitly return a Promise.
  getLocation().then((loc) => sendResponse(loc));

  return true;
}

// getCurrentPosition() returns a prototype-based object, so the properties
// end up being stripped off when sent to the service worker. To get
// around this, create a deep clone.
function clone(obj) {
  const copy = {};
  // Return the value of any non true object (typeof(null) is "object") directly.
  // null will throw an error if you try to for/in it. Just return
  // the value early.
  if (obj === null || !(obj instanceof Object)) {
    return obj;
  } else {
    for (const p in obj) {
      copy[p] = clone(obj[p]);
    }
  }
  return copy;
}

async function getLocation() {
  // Use a raw Promise here so you can pass `resolve` and `reject` into the
  // callbacks for getCurrentPosition().
  return new Promise((resolve, reject) => {
    navigator.geolocation.getCurrentPosition(
      (loc) => resolve(clone(loc)),
      // in case the user doesnt have/is blocking `geolocation`
      (err) => reject(err)
    );
  });
}

Con eso en su lugar, ahora estás listo para acceder al documento fuera de pantalla en el service worker.

chrome.offscreen.createDocument({
  url: 'offscreen.html',
  reasons: [chrome.offscreen.Reason.GEOLOCATION || chrome.offscreen.Reason.DOM_SCRAPING],
  justification: 'geolocation access',
});

Ten en cuenta que, cuando accedes a un documento fuera de pantalla, debes incluir un reason. El motivo de geolocation no estaba disponible originalmente, por lo que debes especificar un resguardo de DOM_SCRAPING y explicar en la sección justification lo que realmente hace el código. El proceso de revisión de Chrome Web Store usa esta información para garantizar que los documentos fuera de pantalla se usen con un fin válido.

Una vez que tengas una referencia al documento fuera de pantalla, puedes enviarle un mensaje para pedirle que te proporcione información de ubicación geográfica actualizada.

.

service_worker.js:

const OFFSCREEN_DOCUMENT_PATH = '/offscreen.html';
let creating; // A global promise to avoid concurrency issues

chrome.runtime.onMessage.addListener(handleMessages);

async function getGeolocation() {
  await setupOffscreenDocument(OFFSCREEN_DOCUMENT_PATH);
  const geolocation = await chrome.runtime.sendMessage({
    type: 'get-geolocation',
    target: 'offscreen'
  });
  await closeOffscreenDocument();
  return geolocation;
}

async function hasDocument() {
  // Check all windows controlled by the service worker to see if one
  // of them is the offscreen document with the given path
  const offscreenUrl = chrome.runtime.getURL(OFFSCREEN_DOCUMENT_PATH);
  const matchedClients = await clients.matchAll();

  return matchedClients.some(c => c.url === offscreenUrl)
}

async function setupOffscreenDocument(path) {
  //if we do not have a document, we are already setup and can skip
  if (!(await hasDocument())) {
    // create offscreen document
    if (creating) {
      await creating;
    } else {
      creating = chrome.offscreen.createDocument({
        url: path,
        reasons: [chrome.offscreen.Reason.GEOLOCATION || chrome.offscreen.Reason.DOM_SCRAPING],
        justification: 'add justification for geolocation use here',
      });

      await creating;
      creating = null;
    }
  }
}

async function closeOffscreenDocument() {
  if (!(await hasDocument())) {
    return;
  }
  await chrome.offscreen.closeDocument();
}

Entonces, ahora, cuando quieras obtener la ubicación geográfica de tu service worker, solo debes llamar a lo siguiente:

const location = await getGeolocation()

Usa la ubicación geográfica en una ventana emergente o en un panel lateral

Usar la ubicación geográfica en una ventana emergente o en un panel lateral es muy sencillo. Las ventanas emergentes y los paneles laterales son solo documentos web y, por lo tanto, tienen acceso a las APIs de DOM normales. Puedes acceder a navigator.geolocation directamente. La única diferencia con los sitios web estándares es que debes usar el campo manifest.json "permission" para solicitar el permiso "geolocation". Si no incluyes el permiso, seguirás teniendo acceso a navigator.geolocation. Sin embargo, cualquier intento de usarla causará un error inmediato, al igual que si el usuario rechazara la solicitud. Puedes ver esto en el ejemplo de ventana emergente.

Usa la ubicación geográfica en una secuencia de comandos de contenido

Al igual que una ventana emergente, una secuencia de comandos de contenido tiene acceso completo a la API de DOM. Sin embargo, los usuarios pasarán por el flujo normal de permisos de usuario. Esto significa que agregar "geolocation" a tu "permissions" no te dará acceso automáticamente a la información de ubicación geográfica de los usuarios. Puedes ver esto en la muestra de secuencia de comandos de contenido.