Un sito può richiamare un’estensione del browser?

Sono un novizio dello sviluppo dell’estensione del browser e capisco il concetto di estensioni del browser che altera la pagina e inserisce codici in esso.

C’è un modo in cui questa direzione può essere invertita? Scrivo un’estensione che fornisce un insieme di API e i siti Web che desiderano utilizzare la mia estensione possono rilevare la sua presenza e, se è presente, il sito Web può chiamare i miei metodi API come var extension = Extenion(foo, bar) . È ansible in Chrome, Firefox e Safari?

Esempio:

  1. Google ha creato una nuova estensione chiamata BeautifierExtension. Ha un set di API come oggetti JS.

  2. L’utente va a reddit.com. Reddit.com rileva BeautifierExtension e invoca l’API chiamando beautifer = Beautifier();

Vedi # 2 – normalmente è l’estensione che rileva i siti corrispondenti e altera le pagine. Quello che mi interessa sapere è se il n. 2 è ansible.

Poiché Chrome ha introdotto externally_connectable , questo è abbastanza facile da fare in Chrome. Innanzitutto, specifica il dominio consentito nel file manifest.json :

 "externally_connectable": { "matches": ["*://*.example.com/*"] } 

Usa chrome.runtime.sendMessage per inviare un messaggio dalla pagina:

 chrome.runtime.sendMessage(editorExtensionId, {openUrlInEditor: url}, function(response) { // ... }); 

Infine, ascolta la tua pagina di background con chrome.runtime.onMessageExternal :

 chrome.runtime.onMessageExternal.addListener( function(request, sender, sendResponse) { // verify `sender.url`, read `request` object, reply with `sednResponse(...)`... }); 

Se non si ha accesso al supporto externally_connectable collegabile, la risposta originale segue:

Risponderò da una prospettiva incentrata su Chrome, sebbene i principi qui descritti (iniezioni di script di pagine Web, script in background di lunga durata, passaggio di messaggi) siano applicabili praticamente a tutti i framework di estensione del browser.

Da un livello elevato, quello che vuoi fare è iniettare uno script di contenuto in ogni pagina web, che aggiunge un’API, accessibile alla pagina web. Quando il sito chiama l’API, l’API triggers lo script di contenuto per fare qualcosa, come inviare messaggi alla pagina di sfondo e / o inviare un risultato allo script di contenuto, tramite callback asincrono.

La principale difficoltà qui è che gli script di contenuto che sono “iniettati” in una pagina web non possono alterare direttamente l’ ambiente di esecuzione JavaScript di una pagina. Condividono il DOM, quindi gli eventi e le modifiche alla struttura DOM sono condivisi tra lo script del contenuto e la pagina Web, ma le funzioni e le variabili non sono condivise. Esempi:

  • Manipolazione DOM: se uno script di contenuto aggiunge un elemento

    a una pagina, funzionerà come previsto. Sia lo script di contenuto che la pagina vedranno il nuovo

    .

  • Eventi: se uno script di contenuto imposta un listener di eventi, ad esempio, per i clic su un elemento, il listener verrà triggersto correttamente quando si verifica l’evento. Se la pagina imposta un listener per gli eventi personalizzati triggersti ​​dallo script di contenuto, questi verranno ricevuti correttamente quando lo script di contenuto triggers quegli eventi.

  • Funzioni: se lo script di contenuto definisce una nuova funzione globale foo() (come si potrebbe provare quando si imposta una nuova API). La pagina non può vedere o eseguire foo , perché foo esiste solo nell’ambiente di esecuzione dello script di contenuto, non nell’ambiente della pagina.

Quindi, come puoi configurare un’API corretta? La risposta arriva in molti passaggi:

  1. A livello basso, rendi il tuo evento basato su eventi . La pagina Web triggers eventi DOM personalizzati con dispatchEvent e gli script di contenuto li ascolta con addEventListener , intervenendo quando vengono ricevuti. Ecco una semplice API di archiviazione basata sugli eventi che una pagina Web può utilizzare per avere l’estensione per archiviare i dati per esso:

    content_script.js (nella tua estensione):

     // an object used to store things passed in from the API internalStorage = {}; // listen for myStoreEvent fired from the page with key/value pair data document.addEventListener('myStoreEvent', function(event) { var dataFromPage = event.detail; internalStorage[dataFromPage.key] = dataFromPage.value }); 

    Pagina web non di estensione, utilizzando l’API basata sugli eventi:

     function sendDataToExtension(key, value) { var dataObj = {"key":key, "value":value}; var storeEvent = new CustomEvent('myStoreEvent', {"detail":dataObj}); document.dispatchEvent(storeEvent); } sendDataToExtension("hello", "world"); 

    Come puoi vedere, la normale pagina web sta triggersndo eventi a cui lo script di contenuto può vedere e reactjs, perché condividono il DOM. Gli eventi hanno dati allegati, aggiunti nel costruttore CustomEvent . Il mio esempio qui è pietosamente semplice – puoi ovviamente fare molto di più nello script di contenuto una volta che ha i dati dalla pagina (molto probabilmente lo passi alla pagina di sfondo per un’ulteriore elaborazione).

  2. Tuttavia, questa è solo metà della battaglia. Nel mio esempio precedente, la pagina Web ordinaria doveva creare sendDataToExtension stesso. La creazione e l’triggerszione di eventi personalizzati è abbastanza dettagliata (il mio codice occupa 3 righe ed è relativamente breve). Non vuoi forzare un sito a scrivere codice di triggerszione eventi arcano solo per utilizzare la tua API. La soluzione è un po ‘sgradevole: aggiungi un tag al tuo DOM condiviso che aggiunge il codice di triggerszione degli eventi all'ambiente di esecuzione della pagina principale.

    Dentro content_script.js:

     // inject a script from the extension's files // into the execution environment of the main page var s = document.createElement('script'); s.src = chrome.extension.getURL("myapi.js"); document.documentElement.appendChild(s); 

    Qualsiasi funzione definita in myapi.js diventerà accessibile alla pagina principale. (Se stai utilizzando "manifest_version":2 , dovrai includere myapi.js nell'elenco di web_accessible_resources del tuo manifest).

    myapi.js:

     function sendDataToExtension(key, value) { var dataObj = {"key":key, "value":value}; var storeEvent = new CustomEvent('myStoreEvent', {"detail":dataObj}); document.dispatchEvent(storeEvent); } 

    Ora la semplice pagina web può semplicemente fare:

     sendDataToExtension("hello", "world"); 
  3. C'è un ulteriore problema nel nostro processo API: lo script myapi.js non sarà disponibile esattamente al momento del caricamento. Invece, verrà caricato un po 'di tempo dopo il caricamento della pagina. Pertanto, la semplice pagina web deve sapere quando è ansible chiamare in sicurezza la propria API. Puoi risolvere questo problema facendo myapi.js che myapi.js un evento "API pronto", che la tua pagina ascolta.

    myapi.js:

     function sendDataToExtension(key, value) { // as above } // since this script is running, myapi.js has loaded, so let the page know var customAPILoaded = new CustomEvent('customAPILoaded'); document.dispatchEvent(customAPILoaded); 

    Semplice pagina web utilizzando l'API:

     document.addEventListener('customAPILoaded', function() { sendDataToExtension("hello", "world"); // all API interaction goes in here, now that the API is loaded... }); 
  4. Un'altra soluzione al problema della disponibilità di script al momento del caricamento è l'impostazione run_at proprietà run_at dello script di contenuto in manifest a "document_start" questo modo:

    manifest.json:

      "content_scripts": [ { "matches": ["https://example.com/*"], "js": [ "myapi.js" ], "run_at": "document_start" } ], 

    Estratto da documenti :

    Nel caso di "document_start", i file vengono iniettati dopo qualsiasi file da css, ma prima che venga creato qualsiasi altro DOM o qualsiasi altro script venga eseguito.

    Per alcuni contenuti che potrebbero essere più appropriati e meno impegnativi dell'evento "A carico dell'API".

  5. Per inviare i risultati alla pagina, è necessario fornire una funzione di callback asincrona. Non esiste alcun modo per restituire in modo sincrono un risultato dall'API, poiché l'triggerszione / l'ascolto degli eventi è intrinsecamente asincrono (ovvero, la funzione dell'API del sito termina prima che lo script di contenuto ottenga l'evento con la richiesta dell'API).

    myapi.js:

     function getDataFromExtension(key, callback) { var reqId = Math.random().toString(); // unique ID for this request var dataObj = {"key":key, "reqId":reqId}; var fetchEvent = new CustomEvent('myFetchEvent', {"detail":dataObj}); document.dispatchEvent(fetchEvent); // get ready for a reply from the content script document.addEventListener('fetchResponse', function respListener(event) { var data = event.detail; // check if this response is for this request if(data.reqId == reqId) { callback(data.value); document.removeEventListener('fetchResponse', respListener); } } } 

    content_script.js (nella tua estensione):

     // listen for myFetchEvent fired from the page with key // then fire a fetchResponse event with the reply document.addEventListener('myStoreEvent', function(event) { var dataFromPage = event.detail; var responseData = {"value":internalStorage[dataFromPage.key], "reqId":data.reqId}; var fetchResponse = new CustomEvent('fetchResponse', {"detail":responseData}); document.dispatchEvent(fetchResponse); }); 

    pagina web ordinaria:

     document.addEventListener('customAPILoaded', function() { getDataFromExtension("hello", function(val) { alert("extension says " + val); }); }); 

    Il reqId è necessario nel caso in cui tu abbia richieste multiple in una sola volta, in modo che non leggano le risposte sbagliate.

E penso che sia tutto! Quindi, non per i deboli di cuore, e forse non ne vale la pena, se consideri che altre estensioni possono anche bind gli ascoltatori ai tuoi eventi per intercettare come una pagina sta usando la tua API. So tutto questo solo perché ho realizzato un'API di crittografia proof-of-concept per un progetto scolastico (e successivamente ho appreso le principali insidie ​​di sicurezza ad esso associate).

In breve: uno script di contenuto può ascoltare eventi personalizzati da una normale pagina Web e lo script può anche iniettare un file di script con funzioni che rendono più semplice per le pagine Web triggersre quegli eventi. Lo script di contenuto può passare i messaggi a una pagina di sfondo, che quindi memorizza, trasforma o trasmette i dati dal messaggio.