从单独的脚本更新 WebExtension webRequest.onBeforeRequest 侦听器 URL 设置

Update WebExtension webRequest.onBeforeRequest listener URL settings from separate script

我目前正在创建一个 WebExtension,我在其中注册了一个针对正在发出的 Web 请求的侦听器,因此:

main.js:

chrome.webRequest.onBeforeRequest.addListener(main_function, {urls: sites}, ["blocking"]);

其中 sites 是一个包含从设置页面加载的 URL 列表的数组。

更改这些位于单独的 HTML 和 JavaScript 文件中的设置后,我想更新上述侦听器以现在包含新的站点列表。

我无法从设置 JavaScript 文件访问此侦听器,因为

 onBeforeRequest.removeListener(callback)

需要原始回调作为参数。

有谁知道我如何从不同的 JavaScript 文件更新此侦听器?

问题示例(3 个文件):

manifest.json:

{
    ...
    "permissions": ["webRequest", "webRequestBlocking", "storage"],
    "background: { "scripts": ["main.js"] },
    "options_ui": { "page":"settings.html" }
}

其中 settings.html 依次加载 settings.js.

main.js:

/* I wont type out the code to load this array from settings. You will have to imagine it being loaded */
all_urls = ["http://www.google.com", "http://www.whosebug.com"];

function example_callback() {
    console.log("Hello World!");
}

chome.webRequest.onBeforeRequest.addListener(example_callback, {urls: all_urls}, ["blocking"]);

settings.js:

/* Again, imagine that the settings are edited by this file and saved to storage. */
all_urls = ["https://www.google.com"];

/* This is where the problem lies. Listener cannot be removed, as callback is not available in this file */
chrome.webRequest.onBeforeRequest.removeListener(); // Wont work
chrome.webRequest.onBeforeRequest.addListener(example_callback, {urls: all_urls}, ["blocking"]);

解决方案是使用 WebExtension 消息 API,例如:

settings.js:

...
/* Settings now outdated */
chrome.runtime.sendMessage(message);
...

main.js

...
chrome.runtime.onMessage.addListener( (message) => {
    /* Update listener */
    chrome.webRequest.onBeforeRequest.removeListener(example_callback);
    chrome.webRequest.onBeforeRequest.addListener(example_callback, {urls: all_urls}, ["blocking"]);
});
...

文档中的相关部分:https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Content_scripts#Communicating_with_background_scripts

在您的选项页面(或面板)JavaScript 和后台脚本之间进行通信

选项页面或面板与后台脚本之间的通信有两种通用方法。

  1. 您可以直接从选项页面 JavaScript 访问后台脚本中的变量和函数,方法是首先获取后台页面的 Window object for your background scripts by using extension.getBackgroundPage(). If you want to directly access other pages from your background script you can use extension.getViews() to get the Window 对象,如果已定义、任何 popup/panel、选项页面或包含与扩展程序打包在一起的内容的选项卡。

    对于问题中的代码,你可以这样做:

let backgroundPage = chrome.extension.getBackgroundPage();
chrome.webRequest.onBeforeRequest.removeListener(backgroundPage.example_callback);
  1. 您可以使用 runtime.sendMessage(), runtime.onMessage, and/or runtime.connect() 在页面之间来回发送消息。

    如果您来回发送消息,则需要选择这些消息的用途及其内容。您是发送所有数据,还是仅发送一条数据已更新的消息?您打算将消息用于多种用途吗?如果是这样,您的听众将如何确定什么消息是针对您脚本的哪一部分的。您将需要对消息强加某种格式。您需要使用这些消息完成的事情越多,您需要采用的格式就越复杂。

示例代码:

以下扩展将 Web 请求记录到控制台。根据用户的选择,它将记录

  1. 所有请求 mozilla.org
  2. 所有网络请求

它实现了与 options_ui page and a default_popup for a browser_action 按钮相同的页面。用户可以select从以上3个日志选项以及选项数据如何传递到后台页面:

  1. 选项在options.js代码中存储到storage.local。然后options.js直接调用background.js文件中的getOptions()函数就有后台脚本重新阅读选项。
  2. 选项在options.js代码中存储到storage.local。然后,options.js 向后台脚本发送 optionsUpdated 消息,告知选项已更新。然后后台脚本重新读取选项。
  3. [= 83 =]与所有的选项。然后将选项存储到后台脚本中的 storage.local。存储选项后,后台脚本会将 optionsStored 消息发送回 options.js 代码。然后 options.js 代码向用户指示选项已保存。

background.jsoptions.js 之间发送的消息是具有以下格式的对象:

{
    type: //String describing the type of message:
          //  'optionsUpdated' 'optionsData', or 'optionsStored'
    data: //Object containing the options data
}

//Options data object:
{
    loggingUrls: //Array of URL match strings for webRequest requestFilter
    useDirect: //Number: 0, 1, 2 indicating the method of communication between
               // options.js and background.js
               // 0 = Directly invoke functions in background script from options/panel code
               // 1 = Send a message that data was updated
               // 2 = Send a message with all options data
}

该扩展已在 Firefox 和 Google 中进行测试 Chrome:

manifest.json:

{
    "description": "Demonstrate Changing webRequest.RequestFilter",
    "manifest_version": 2,
    "name": "webrequest.requestfilter-demo",
    "version": "0.1",

    "applications": {
        "gecko": {
             //Firefox: must define id to use option_ui:
            "id": "webrequestrequestfilter-demo@example.example",
            "strict_min_version": "42.0",
            "strict_max_version": "51.*"
        }
    },

    "permissions": [
        "storage",
        "webRequest",
        "webRequestBlocking",
        "<all_urls>" //Required for Google Chrome. Not, currently, needed for Firefox.
    ],

    "background": {
        "scripts": [
            "background.js"
        ]
    },

    "browser_action": {
        "default_icon": {
            "48": "myIcon.png"
        },
        "default_title": "Currently NOT logging. Click to start logging only mozilla.org",
        "browser_style": true,
        "default_popup": "options.html"
    },

    "options_ui": {
      "page": "options.html",
      "chrome_style": true
    }
}

background.js:

var webRequestExtraInfo = ["blocking"];

var useDirect=0; //Holds the state of how we communicate with options.js
const useDirectTypes=[ 'Directly invoke functions in background script'
                      ,'Send a message that data was updated'
                      ,'Send a message with all options data'];


//Register the message listener 
chrome.runtime.onMessage.addListener(receiveMessage);

function receiveMessage(message,sender,sendResponse){
    //Receives a message that must be an object with a property 'type'.
    //  This format is imposed because in a larger extension we may
    //  be using messages for multiple purposes. Having the 'type'
    //  provides a defined way for other parts of the extension to
    //  both indicate the purpose of the message and send arbitrary
    //  data (other properties in the object).
    console.log('Received message: ',message);
    if(typeof message !== 'object' || !message.hasOwnProperty('type')){
        //Message does not have the format we have imposed for our use.
        //Message is not one we understand.
        return;
    }
    if(message.type === "optionsUpdated"){
        //The options have been updated and stored by options.js.
        //Re-read all options.
        getOptions();
    }
    if(message.type === "optionsData"){
        saveOptionsSentAsData(message.data,function(){
            //Callback function executed once data is stored in storage.local
            console.log('Sending response back to options page/panel');
            //Send a message back to options.js that the data has been stored.
            sendResponse({type:'optionsStored'});
            //Re-read all options.
            getOptions();
        });
        //Return true to leave the message channel open so we can 
        //  asynchronously send a message back to options.js that the
        //  data has actually been stored.
        return true;
    }
}

function getOptions(){
    //Options are normally in storage.sync (sync'ed across the profile).
    //This example is using storage.local.
    //Firefox does not currently support storage.sync.
    chrome.storage.local.get({
        loggingUrls: [''],
        useDirect: 0
    }, function(items) {
        if(typeof items.useDirect !== 'number' || items.useDirect<0 || items.useDirect>2) {
            items.useDirect=0;
        }
        useDirect = items.useDirect;
        updateLogging(items.loggingUrls);
        console.log('useDirect=' + useDirectTypes[useDirect]);
    });
}

function saveOptionsSentAsData(data,callback) {
    //Options data received as a message from options.js is 
    //  stored in storeage.local.
    chrome.storage.local.set(data, function() {
        //Invoke a callback function if we were passed one.
        if(typeof callback === 'function'){
            callback();
        }
    });
}

function updateLogging(urlArray){
    //The match URLs for the webRequest listener are passed in as an 
    //  array.  Check to make sure it is an array, and forward to
    //  function that adds the listener as a requestFilter.
    if(typeof urlArray === "object" &&  Array.isArray(urlArray)
        && urlArray[0].length>0){
        startListeningToWebRequests({urls: urlArray});
    }else{
        //The argument was not an array
        stopListeningToWebRequests();
    }
}

function logURL(requestDetails) {
    //Log the webRequest to the Console.
    console.log("Loading: " + requestDetails.url);
    return {}; //Return object in case this is a  blocking listener
}

function stopListeningToWebRequests() {
    if(chrome.webRequest.onBeforeRequest.hasListener(logURL)) {
        //Don't really need to check for the listener, as removeListener for a 
        //  function which is not listening does nothing (no-op).
        chrome.webRequest.onBeforeRequest.removeListener(logURL);
        console.log("STOPPED logging all Web Requests");
    }
}

function startListeningToWebRequests(requestFilter) {
    stopListeningToWebRequests();
    //Start listening to webRequests
    chrome.webRequest.onBeforeRequest
                     .addListener(logURL,requestFilter,webRequestExtraInfo);
    //Log to the console the requestFilter that is being used
    console.log("Logging Web Requests:", requestFilter, "-->", requestFilter.urls);
}

//Read the options stored from prior runs of the extension.
getOptions();

//On Firefox, open the Browser Console:
//To determine if this is Chrome, multiple methods which are not implemented
//  in Firefox are checked. Multiple ones are used as Firefox will eventually 
//  support more APIs.
var isChrome = !!chrome.extension.setUpdateUrlData
               && !!chrome.runtime.reload
               && !!chrome.runtime.restart;
if(!isChrome) {
    //In Firefox cause the Browser Console to open by using alert()
    window.alert('Open the console. isChrome=' + isChrome);
}

options.js:

// Saves options to chrome.storage.local.
// It is recommended by Google that options be saved to chrome.storage.sync.
// Firefox does not yet support storage.sync.
function saveOptions(data, callback) {
    chrome.storage.local.set(data, function() {
        if(typeof callback === 'function'){
            callback();
        }
        // Update status to let user know options were saved.
        notifyOptionsSaved();
    });
}

function optionsChanged() {
    //Get the selected option values from the DOM
    let loggingUrls = document.getElementById('loggingUrls').value;
    let useDirectValue = document.getElementById('useDirect').value;
    useDirectValue = +useDirectValue; //Force to number, not string
    //Put all the option data in a single object
    let optionData = {
        loggingUrls: [loggingUrls],
        useDirect: useDirectValue
    }
    if(useDirectValue == 0 ) {
        //We save the options in the options page, or popup
        saveOptions(optionData, function(){
            //After the save is complete:
            //The getOptions() functon already exists to retrieve options from
            //  storage.local upon startup of the extension. It is easiest to use that.
            //  We could remove and add the listener here, but that code already
            //  exists in background.js. There is no reason to duplicate the code here.
            let backgroundPage = chrome.extension.getBackgroundPage();
            backgroundPage.getOptions();
        });
    } else if (useDirectValue == 1) {
        //We save the options in the options page, or popup
        saveOptions(optionData, function(){
            //Send a message to background.js that options in storage.local were updated.
            chrome.runtime.sendMessage({type:'optionsUpdated'});
        });
    } else {
        //Send all the options data to background.js and let it be dealt with there.
        chrome.runtime.sendMessage({
            type:'optionsData',
            data: optionData
        }, function(message){
            //Get a message back that may indicate we have stored the data.
            if(typeof message === 'object' && message.hasOwnProperty('type')){
                if(message.type === 'optionsStored') {
                    //The message received back indicated the option data has
                    //  been stored by background.js.
                    //Notify the user that the options have been saved.
                    notifyOptionsSaved();
                }
            }
        });
    }
}

// Restores select box using the preferences
// stored in chrome.storage.
function useStoredOptionsForDisplayInDOM() {
    chrome.storage.local.get({
        loggingUrls: [''],
        useDirect: 0
    }, function(items) {
        //Store retrieved options as the selected values in the DOM
        document.getElementById('loggingUrls').value = items.loggingUrls[0];
        document.getElementById('useDirect').value = items.useDirect;
    });
    //notifyStatusChange('Option read');
}

function notifyOptionsSaved(callback){
    //Notify the user that the options have been saved
    notifyStatusChange('Options saved.',callback);
}

function notifyStatusChange(newStatus,callback){
    let status = document.getElementById('status');
    status.textContent = newStatus;
    //Clear the notification after a second
    setTimeout(function() {
        status.textContent = '';
        if(typeof callback === 'function'){
            callback();
        }
    }, 1000);
}

document.addEventListener('DOMContentLoaded', useStoredOptionsForDisplayInDOM);
document.getElementById('optionsArea').addEventListener('change',optionsChanged);

options.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>WebRequest Logging Options</title>
    <style>
        body: { padding: 10px; }
    </style>
</head>

<body>
    <div id="optionsArea">
        Log Web Requests from:
        <select id="loggingUrls">
            <option value="">None</option>
            <option value="*://*.mozilla.org/*">Mozilla.org</option>
            <option value="<all_urls>">All URLs</option>
        </select>
        <br/>
        Communication with background page:
        <select id="useDirect">
            <option value="0">Direct</option>
            <option value="1">Message Updated</option>
            <option value="2">Message all Data</option>
        </select>
    </div>
    <div id="status" style="top:0px;display:inline-block;"></div>

    <script src="options.js"></script>
</body>
</html>

此答案中的代码是根据 question I answered here, and my answer here, which is based on code the OP for that question provided in a GitHub repository. The options.js and options.html files had their start with code found on developer.chrome.com 中的代码合并和修改的。