This is the 9th day of my participation in the August More Text Challenge


Series of articles:

  • Introduction to Browser Plug-ins
  • Manifest V3
  • Essential concept for browser plug-in development – user interaction
  • Essential concepts for browser plug-in development – Common techniques
  • Essential concepts for browser plug-in development – Customizing the browser experience
  • Essential concepts for browser plug-in development – customizing the Web experience
  • Key concepts for browser plug-in development — Experience optimization
  • Browser plug-in development and debugging

Service Workers

In the Manifest V2 version, background pages is a separate page in the extender. It is generally set to listen for events in response to user actions, but it will stay in the background for a long time affecting performance. In the Manifest V3 version, background scripts are migrated to run in Service Workers to provide performance with two features:

  • The Service Workers terminates after executing an event handler and re-runs on a new event trigger
  • It is a JavaScript Worker with no direct access to the DOM.

💡 Service Worker is a script that the browser runs completely independently of the web page. In addition to the above features, we should pay attention to the following matters:

  • It is a programmable network agent that allows you to control how network requests sent by a page are handled.
  • It makes extensive use of Promise for asynchronous operations.

If you need to use service workers, you need to declare registration in the background-service_worker option of the manifest manifest. Property value specifies the path to a JavaScript document that needs to be executed (it must be under the root of the project).

{
  // ...
  "background": {
    "service_worker": "background.js"}},Copy the code

Background scripts run in Service Workers to perform operations based on the listening event-response model, so the logical code should also be written to follow this model to optimize performance:

  • The event listener is registered in the first round of the event cycle, that is, the event listener is written in the top-level scope of the background script, and should not be embedded in other logical code (because the Service Workers code terminates after execution and does not reside for long, it only runs again when there is an event to dispatch, This will not respond to events if listeners are not registered in the first event poll).

    // background.js
    chrome.storage.local.get(["badgeText"].({ badgeText }) = > {
      chrome.action.setBadgeText({ text: badgeText });
    });
    
    // Listener is registered on startup
    chrome.action.onClicked.addListener(handleActionClick);
    
    function handleActionClick() {
        // ...
    }
    Copy the code
  • Because the Service Workers life cycle is short, if you need persistent data (such as user input as a variable), you can use the Storage API provided by Chrome for the extension

    // background.js
    chrome.runtime.onMessage.addListener(({ type, name }) = > {
      if (type === "set-name") { chrome.storage.local.set({ name }); }}); chrome.action.onClicked.addListener((tab) = > {
      chrome.storage.local.get(["name"].({ name }) = > {
        chrome.tabs.sendMessage(tab.id, { name });
      });
    });
    Copy the code
  • Use setTimeout or setInterval to implement delayed or periodic operations in the Manifest V2 version, which is not feasible in the Manifest V3 version of Service Workers. Because the Service Worker does not stay in the background for long periods of time, the scheduler cancels the timer when it terminates. Instead, we should use the Alarms API, which schedules code to run periodically or at specified times in the future. It also needs to be registered in the top-level scope of the background script.

    // background.js
    chrome.alarms.create({ delayInMinutes: 3 }); // set an alarm, which will dispatch onAlarm event after 3 mins
    
    // listen the onAlarm event and react to it
    chrome.alarms.onAlarm.addListener(() = > {
      chrome.action.setIcon({
        path: getRandomIconPath(),
      });
    });
    Copy the code

A Service Worker is actually a Web Worker, which can run independently of web pages in the browser. Generally, there is a global variable window in the execution context of web pages, through which you can access some APIS provided by browsers. For example, IndexedDB, cookie, localStorage, etc., but there is no such object in the Service Worker, so there are many limitations, such as DOM access in this environment, XMLHttpRequest cannot be initiated (but fetch is supported), Here are some ways to deal with limitations:

  • Since the Service Workers do not have access to the DOMParser API to parse HTML, we can create a TAB using the Chrome.windows.create () and crhome.tabs. Create () apis, To provide an environment with window objects, or use some library (such as Jsdom or UnDOM) to compensate for this lack.

  • Media resources cannot be played or captured in Service Workers. A TAB can be created using the Chrome.windows.create () and crhome.tabs. Create () apis to provide an environment with window objects. You can then control the multimedia playback of the page in the service worker through message Passing.

  • While the DOM is not accessible in the Service Worker, you can create a canvas through the OffscreenCanvas API. For more information about Touch Screen Anvas, please refer to here.

    // background.js
    // for MV3 service workers
    function buildCanvas(width, height) {
      const canvas = new OffscreenCanvas(width, height);
      return canvas;
    }
    Copy the code

Message Passing

Reference:

  • Message passing
  • Official example about Message Passing

Scripts are run on the web, “detached” from the extension, but communicate using message passing (between the Web page’s content Script and the extension). Chrome provides simple apis for one-time request-response communication, complex apis for long connection communication, and cross-extension communication based on ID.

💡 Any valid JSON-formatted data can be passed.

💡 can even communicate with the native system.

⚠️ Security issues need to be considered during information transmission:

  • Content scripts are more vulnerable to malicious web attacks and should limit the range of actions that can be triggered by information passed through Content Scripts

  • When extenders communicate with external resources, they should take the necessary actions to avoid cross-site scripting attacks

    chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
      // WARNING! Might be injecting a malicious script!
      document.getElementById("resp").innerHTML = response.farewell;
    });
    
    chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
      // innerText does not let the attacker inject HTML elements.
      document.getElementById("resp").innerText = response.farewell;
    });
    
    chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
      // JSON.parse does not evaluate the attacker's scripts.
      let resp = JSON.parse(response.farewell);
    });
    Copy the code

One-time request

To use chrome. Runtime. SendMessage () method or chrome. Tabs. The sendMessage () for a single request, this two methods can set the callback function, receives the returned response data as parameters by default.

// Send messages in the page script content script
chrome.runtime.sendMessage({greeting: "hello"}, function(response) {
  console.log(response.farewell);
});

// Send messages in the extension program
// The tabId needs to be obtained using query to specify which particular TAB page the request should be sent to
chrome.tabs.query({active: true.currentWindow: true}, function(tabs) {
  // Send to the currently active TAB page
  chrome.tabs.sendMessage(tabs[0].id, {greeting: "hello"}, function(response) {
    console.log(response.farewell);
  });
});
Copy the code

At the receiving end, embedded code the content of the page script and extension of the background code background script is the same, need to use chrome. Runtime. OnMessage. AddListener () to monitor the corresponding events to capture the request. The event handler takes three arguments. The first argument is the received message. The second argument is an object that contains information about the sender of this message. The third parameter is a method that sends the response content.

chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
  console.log(sender.tab ?
    "from a content script:" + sender.tab.url :
    "from the extension");
  if (message.greeting === "hello")
    sendResponse({farewell: "goodbye"}); });Copy the code

💡 in the above event handlers, the sendResponse is executed synchronously, that is, after receiving the message-triggered event handler, the response will be sent immediately when the sendResponse function is executed; If you want to send the response asynchronously (for example, to integrate data from other asynchronous requests), you can explicitly return true at the end of the onMessage event handler to tell the extender that the response will be executed asynchronously, so that the Message Channel will remain open. Until sendResponse is called.

💡 If you have multiple onMessage event listeners on different pages, the extender as a whole will execute the sendResponse() method at most once for each message request to ensure that there is only one response, and the response functions in other event handlers will be ignored.

A long connection

You can use the method chrome.runtime.connect() or the method chrome.tabs. Connect () to make a long connection between the content script and the extension. To distinguish multiple channels).

When a channel is created using the above method, a runtime.Port Port object is returned that contains methods and properties related to the information channel. Then can be sent through that channel portObj. PostMessage () and receive portObj. OnMessage. AddListener () information.

// Set up a long-connected message channel in the page script content script
let port = chrome.runtime.connect({name: "knockknock"});
// Send messages through this port
port.postMessage({joke: "Knock knock"});
// Set up an event listener to receive information through this port and take the received information as an input parameter
port.onMessage.addListener(function(msg) {
  if (msg.question === "Who's there?")
    port.postMessage({answer: "Madame"});
  else if (msg.question === "Madame who?")
    port.postMessage({answer: "Madame... Bovary"});
});
Copy the code

Similarly, if you use the chrome.tabs. Connect () method when setting up a long connection to send a message in an extension, you need to specify which particular TAB page the request is sent to.

The message channel is bi-directional, so in addition to creating the port at the originator, you also need to respond to the channel connection request at the receiver using the method Chrome.Runtime.onConnect () (as in the Content script and extender). When the channel initiates a Port call to the connect method, the listener on the receiving side invoks the callback function, which takes the corresponding Runtime. Port Port object as an input parameter. This Port can then be used to send and receive messages in the channel so that the interfaces on either side of the channel can receive and send messages to each other.

chrome.runtime.onConnect.addListener(function(port) {
  console.assert(port.name === "knockknock");
  port.onMessage.addListener(function(msg) {
    if (msg.joke === "Knock knock")
      port.postMessage({question: "Who's there?"});
    else if (msg.answer === "Madame")
      port.postMessage({question: "Madame who?"});
    else if (msg.answer === "Madame... Bovary")
      port.postMessage({question: "I don't get it."});
  });
});
Copy the code

Port life cycle

The information channel connection is long-term, but may also be disconnected for the following reasons:

  • Chrome.runtime.onconnect () failed to establish channel without setting listener on receiver

  • When you create a channel with Chrome.tabs.connect(), the page you want to connect to does not exist

  • All frames that received the port (via runtime.onConnect) have unloaded.

  • Port object method calls the chrome. Runtime. Port. The disconnect () the end connection

    💡 If multiple receive ports (multiple information channels) are established after the call connect is initiated on the port, onDisconnect event will only be triggered on the originating port and not on the other ports when one of the receive ports calls disconnect.

You can use the method port.onDisconnect() to listen for a disconnection event on that port (where port is the port object), which is the input parameter to the event handler function.

Communication across extensions

In addition to messaging within extensions, a similar Messaging API can be used to communicate between different extensions.

When you send request information, you must provide the ID of the extender so that other extenders can determine whether to respond. Method of use chrome. Runtime. SendMessage (id, message) send a one-time request; Usage Chrome.Runtime.connect (ID) initiates a channel connection request and returns a port object.

// The ID of the extension we want to talk to.
const laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// Make a simple request:
chrome.runtime.sendMessage(laserExtensionId, {getTargetData: true},
  function(response) {
    if (targetInRange(response.targetData))
      chrome.runtime.sendMessage(laserExtensionId, {activateLasers: true}); });// Start a long-running conversation:
letport = chrome.runtime.connect(laserExtensionId); port.postMessage(...) ;Copy the code

For one-off request, use chrome. Runtime. OnMessageExternal () to monitor and make a response; For long connection, use chrome. Runtime. OnConnectExternal () response to connect, and use the port object to send and receive information.

// For simple requests:
chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.id === blocklistedExtension)
      return;  // don't allow this extension access
    else if (request.getTargetData)
      sendResponse({targetData: targetData});
    else if (request.activateLasers) {
      var success = activateLasers();
      sendResponse({activateLasers: success}); }});// For long-lived connections:
chrome.runtime.onConnectExternal.addListener(function(port) {
  port.onMessage.addListener(function(msg) {
    // See other examples for sample onMessage handlers.
  });
});
Copy the code

Web communications

Similar to cross-extension communication, regular web pages can send messages to extensions. Json option, externally_connectable.matches, which external pages you want to connect to (regular expressions can be used to support a list of pages that conform to certain rules, but contain at least secondary fields).

{
  // ...
  "externally_connectable": {
    "matches": ["https://*.example.com/*"]}}Copy the code

In web usage chrome. Runtime. SendMessage () method or chrome. Runtime. The connect () send information (by ID to specify which an extension program to communicate with)

// The ID of the extension we want to talk to.
const editorExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// Make a simple request:
chrome.runtime.sendMessage(editorExtensionId, {openUrlInEditor: url},
  function(response) {
    if(! response.success) handleError(url); });Copy the code

In an extension program to use chrome. Runtime. OnMessageExternal () method or chrome. Runtime. OnConnectExternal ()

chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.url === blocklistedWebsite)
      return;  // don't allow this web page access
    if (request.openUrlInEditor) openUrl(request.openUrlInEditor);
});
Copy the code

💡 When the extension communicates with a regular web page, the long connection can only be initiated through the web page.

Permissions

Reference:

  • chrome.permissions API
  • Declare permissions
  • Declare permissions and warn users

Limiting the permission of an extender can not only reduce the possibility of the extender being used by malicious programs; You can also give the user active choice as to which permissions should be granted to the extension.

In order for the extension to be able to use some of the Chrome APIs and access external web pages, it needs to explicitly declare the required permissions in the manifest manifest.json. There are three options for declaring different permissions:

  • permissionsTo set the required permissions, all available permissions fields are available in thehereView, containing multiple fields as an array. To implement the basic functionality of the extender, ask the user at installation time
  • optional_permissionsUsed to set optional permissions, also in an array containing multiple fields. In order to perform some optional function (typically access to specific data or resources), the user is asked for permission only when the extender is running.host_permissionsSpecifically used to set which hosts the extension can access, including a series ofA regular expression used to match urls

Then, when the extension is installed or running, the browser will ask the user whether to allow it to obtain the corresponding permissions and access to specific resources, so that the user can have the initiative to protect their data. For details about which permissions will be notified to the user or ask the user to choose, you can check here.

{
  // ...
  "permissions": [
    "tabs"."bookmarks"."unlimitedStorage"]."optional_permissions": [
    "unlimitedStorage"]."host_permissions": [
    "http://www.blogger.com/"."http://*.google.com/"],}Copy the code

💡 All the permissions fields that can be declared in the manifest.json configuration can be viewed here, but some fields cannot be declared in the optional permissions optional_permissions option:

  • debugger
  • declarativeNetRequest
  • devtools
  • experimental
  • geolocation
  • mdns
  • proxy
  • tts
  • ttsEngine
  • wallpaper

In addition to the optional permissions can be set in the list of configuration, also can be in extension logic code, based on the active user interaction (for example) in the button click event handler method used in chrome. Permissions. The request () dynamic application. If the requested permission triggers a warning, a permission prompt box will pop up asking the user for permission and wait for the result to return before executing the subsequent code

document.querySelector('#my-button').addEventListener('click'.(event) = > {
  // Permissions must be requested from inside a user gesture, like a button's
  // click handler.
  chrome.permissions.request({
    permissions: ['tabs'].origins: ['https://www.google.com/']},(granted) = > {
    // The callback argument will be true if the user granted the permissions.
    if (granted) {
      doSomething();
    } else{ doSomethingElse(); }}); });Copy the code

If no longer need a permission, you can use the method of chrome. Permissions. Remove () to remove the permissions, if then how to use the chrome permissions. The request () dynamically add the corresponding permissions, don’t need to inform the user

chrome.permissions.remove({
  permissions: ['tabs'].origins: ['https://www.google.com/']},(removed) = > {
  if (removed) {
    // The permissions have been removed.
  } else {
    // The permissions have not been removed (e.g., you tried to remove
    // required permissions).}});Copy the code

💡 method can use chrome. Permissions. The contains () to see if add-in currently has a permission to use chrome. Permissions. The getAll () to obtain the add-in currently has all permissions

chrome.permissions.contains({
  permissions: ['tabs'].origins: ['https://www.google.com/']},(result) = > {
  if (result) {
    // The extension has the permissions.
  } else {
    // The extension doesn't have the permissions.}});Copy the code

💡 There are some permissions, there will be a pop-up warning, you can view the relevant permissions here.

To avoid slowing down installs due to excessive pop-up alerts when installing extensions, declare only the permissions required for core functions in the manifest.json option permissions, and include as many permissions as possible that require pop-up alerts. Instead, declare these permissions in the optional_Permissions option, and then use interactive controls such as buttons or checkboxes to allow the user to actively activate the optional features. Only then will the permissions alert box pop up and allow the user to actively choose whether to authorize or not. This interactive experience is better.

💡 If permissions are added to the extension when it is updated, the extension cannot be activated temporarily, and the user needs to allow permissions again. This can be prevented by placing the added permissions in the optional_Permissions option.

💡 declares activeTab permission to temporarily obtain permission to access the currently activeTab page and use the tabs related API on the current page (the path becomes inaccessible when you navigate to another url or close the current page), generally in place of

permission, Reach the current page that accesses any URL without a warning when the extension is installed.

💡 Permissions warnings are not displayed for Loading unzipped extensions in Developer mode. If you want to see the effect of the warning, you can package it relative to the developed extension. For details, see the official guide.

Storage

Extensions can store and retrieve data in the browser.

The Chrome browser provides the data store API Chrome.storeage for extensions, which provides similar functionality to localStorage, but with some differences:

  • usechrome.storage.syncRelated methods can be used in ChromesynchronousFunction to achieve the same account extension data synchronization between multiple devices. 💡 If you have logged in to Chrome offline, the data to be synchronized will be stored locally and synchronized after the browser goes online. If the user disables data synchronization in Chrome Settings, thenchrome.storage.syncThe function of correlation method andchrome.storage.localThe same
  • Bulk read and write data operations are performed asynchronously and therefore faster than blocking and serialization caused by localStorage
  • The data type stored can be objects, whereas localStorage only allows you to store strings
  • Enterprise policies configured by the administrator for the extension can be read (using storage.managed with a schema). The storage.managed storage is read-only.

💡 There are three different storage domains storageAreas

  • syncSynchronize stored data
  • localData stored locally
  • managedThe data set by the administrator is read-only

To use the API, you must first declare the registration permissions in the manifest.json option permissions

{
  // ...
  "permissions": [
    "storage"],}Copy the code

To store the extension’s data as key-value pairs (the data format is objects), use the method chrome.storage.local.set({key: Value}, callback()) stored locally, or using the method chrome.storage.sync.set({key: Value}, callback()) to synchronize the storage, and use chrom.storage.local.get () or chrom.storage.sync.get () to obtain the corresponding key data

// sync
chrome.storage.sync.set({key: value}, function() {
  console.log('Value is set to ' + value);
});

chrome.storage.sync.get(['key'].function(result) {
  console.log('Value currently is ' + result.key);
});
Copy the code
// local
chrome.storage.local.set({key: value}, function() {
  console.log('Value is set to ' + value);
});

chrome.storage.local.get(['key'].function(result) {
  console.log('Value currently is ' + result.key);
});
Copy the code

When 💡 gets the GET data, you can pass in not only the key string, but also an array of keys to return the corresponding set of data. If the key passed in is null when retrieving get data, all stored data is returned.

💡 If you want to delete data for a key, you can use the method chrome.storage.remove(key, callback()). To clear all stored data, use the method chrome.storage.clear(callback())

⚠️ Do not store confidential user data because data stored using this API is not encrypted.

The API allows a small amount of data to be stored. For sync data, the total size is allowed to be 100KB, and a maximum of 512 data items, each 8KB in size, can be stored. For locally stored data, a total size of 5MB is allowed (similar to the storage limit for localStorage), so they are generally used as storage, synchronous extender Settings.

When the data stored by the extender changes, the onChange event is triggered and can be listened for in response

// background.js
chrome.storage.onChanged.addListener(function (changes, namespace) {
  for (let [key, { oldValue, newValue }] of Object.entries(changes)) {
    console.log(
      `Storage key "${key}" in namespace "${namespace}" changed.`.`Old value was "${oldValue}", new value is "${newValue}". `); }});Copy the code