使用地理定位

如果您想在 Chrome 扩展程序中获取地理定位信息,请使用所有网站通常使用的 navigator.geolocation Web 平台 API。本文之所以介绍,是因为 Chrome 扩展程序处理敏感数据的权限处理方式与网站不同。地理定位是非常敏感的数据,因此浏览器可确保用户完全了解并控制其确切位置信息的分享时间和位置。

在 MV3 扩展中使用地理定位

在网络上,浏览器会通过显示提示,要求用户授权特定起点访问其位置,从而保护用户的地理位置数据。该权限模型并不总是适合扩展程序。

网站请求访问 Geolocation API 时看到的权限提示的屏幕截图
地理定位权限提示

权限并不是唯一的区别。如上所述,navigator.geolocation 是一个 DOM API,即构成网站的 API 的一部分。因此,它无法在 worker 上下文中访问,例如作为 Manifest V3 扩展程序的支柱的扩展 Service Worker。不过,您绝对仍然可以使用 geolocation。只是使用方式和位置存在细微差别。

在 Service Worker 中使用地理定位

Service Worker 内没有 navigator 对象。它只能在有权访问页面的 document 对象的上下文中使用。如需在 Service Worker 内部获取访问权限,请使用 Offscreen Document,它可以提供对可与扩展程序捆绑的 HTML 文件的访问权限。

首先,将 "offscreen" 添加到清单的 "permissions" 部分。

manifest.json:

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

添加 "offscreen" 权限后,向扩展程序中添加包含屏幕外文档的 HTML 文件。该案例没有使用网页上的任何内容,因此可能是一个近乎空白的文件。它只需要一个可在您的脚本中加载的小型 HTML 文件即可。

offscreen.html:

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

将此文件保存在项目的根目录中,并将其保存为 offscreen.html

如上所述,您需要一个名为 offscreen.js 的脚本。您还需要将其与扩展程序捆绑在一起。它将作为 Service Worker 的地理位置信息来源。您可以在它和 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)
    );
  });
}

这样,您现在就可以在 Service Worker 中访问屏幕外文档了。

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

请注意,访问屏幕外文档时,您需要添加 reasongeolocation 原因最初不可用,因此请指定 DOM_SCRAPING 的回退,并在 justification 部分说明代码实际执行的操作。Chrome 应用商店的审核流程会使用这些信息来确保屏幕外文档得到使用是有正当用途的。

获得对屏幕外文档的引用后,您可以向其发送消息,要求它为您提供最新的地理定位信息。

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();
}

因此,现在每当您想要从 Service Worker 获取地理定位时,只需调用:

const location = await getGeolocation()

在弹出式窗口或侧边栏中使用地理定位

弹出式窗口侧边栏中使用地理定位非常简单。弹出式窗口和侧边栏只是网络文档,因此可以访问普通的 DOM API。您可以直接访问navigator.geolocation。与标准网站的唯一区别是,您需要使用 manifest.json "permission" 字段来请求 "geolocation" 权限。如果您未添加该权限,则仍可以访问 navigator.geolocation不过,任何尝试使用此 API 都会导致立即出现错误,就像用户拒绝了请求一样。您可以在弹出式内容示例中查看相关信息。

在内容脚本中使用地理定位

与弹出式窗口一样,内容脚本拥有对 DOM API 的完整访问权限;不过,用户需要完成正常的用户权限流程。也就是说,将 "geolocation" 添加到您的 "permissions" 不会自动授予您访问用户地理定位信息的权限。如需了解相关信息,请参阅内容脚本示例