Vytvořte rozšíření Google Chrome, část 2:Image Grabber

Obsah

Úvod
Vytvořte a otevřete webovou stránku se seznamem obrázků
Otevřete novou kartu se stránkou místního rozšíření
Vytvořte stránku.html
Otevřete novou kartu prohlížeče
Odešlete na stránku data adres URL obrázků
Na stránce přijímat údaje o adresách URL obrázků
Vytvořte rozhraní pro stahování obrázků
Vytvořte uživatelské rozhraní pro zobrazení a výběr obrázků
Implementujte funkci Vybrat vše
Implementujte funkci stahování
Získejte vybrané adresy URL obrázků
Stáhněte si obrázky podle URL
Určete názvy souborů pro obrázky
Vytvořte archiv ZIP
Stáhněte si archiv ZIP
Čištění kódu
Úprava stylu stránky rozšíření
Publikovat a distribuovat rozšíření
Závěr

Úvod

Toto je druhá část článku, kde ukážu, jak vytvořit Image Grabber Rozšíření Google Chrome. Image Grabber je rozšíření, které umožňuje extrahovat všechny nebo vybrané obrázky z libovolné webové stránky zobrazené v prohlížeči Chrome a stáhnout je jako jediný archiv ZIP.
Než si jej přečtete, musíte si přečíst první část tohoto článku zde:

https://dev.to/andreygermanov/create-a-google-chrome-extension-part-1-image-grabber-1foa

V předchozím díle jsme tedy vytvořili rozšíření, které zobrazuje vyskakovací okno s tlačítkem „UHRNI NYNÍ“. Když uživatel stiskne toto tlačítko, rozšíření vloží skript na webovou stránku otevřenou na aktuální kartě prohlížeče, která z této stránky získá všechny značky , extrahuje adresy URL všech obrázků a vrátí je zpět do rozšíření. Poté rozšíření zkopírovalo tento seznam adres URL do schránky.

V této části toto chování změníme. Místo kopírování do schránky rozšíření otevře webovou stránku se seznamem obrázků a tlačítkem "Stáhnout". Poté si uživatel může vybrat, které obrázky chce stáhnout. Nakonec po stisknutí tlačítka "Stáhnout" na této stránce skript stáhne všechny vybrané obrázky a zkomprimuje je do archivu s názvem images.zip a vyzve uživatele k uložení tohoto archivu na místní počítač.

Takže na konci tohoto článku, pokud provedete všechny kroky, budete mít rozšíření, které vypadá a funguje jako zobrazené na dalším videu.

Během tohoto tutoriálu se naučíte důležité koncepty výměny dat mezi různými částmi webového prohlížeče Chrome, některé nové funkce Javascript API z chrome jmenný prostor prohlížeče, koncepty práce s daty binárních souborů v Javascriptu včetně ZIP archivů a nakonec vysvětlím, jak rozšíření připravit pro publikování do Chrome Web Store - globálního úložiště rozšíření Google Chrome, které jej zpřístupní pro kohokoli na světě.

Takže, pojďme začít.

Vytvořte a otevřete webovou stránku se seznamem obrázků

Poslední krok popup.js skript v předchozí části byl onResult funkce, která shromáždila pole adres URL obrázků a zkopírovala je do schránky. V současné fázi tato funkce vypadá takto:

/**
 * Executed after all grabImages() calls finished on 
 * remote page
 * Combines results and copy a list of image URLs 
 * to clipboard
 * 
 * @param {[]InjectionResult} frames Array 
 * of grabImage() function execution results
 */
function onResult(frames) {
    // If script execution failed on remote end 
    // and could not return results
    if (!frames || !frames.length) { 
        alert("Could not retrieve images");
        return;
    }
    // Combine arrays of image URLs from 
    // each frame to a single array
    const imageUrls = frames.map(frame=>frame.result)
                            .reduce((r1,r2)=>r1.concat(r2));
    // Copy to clipboard a string of image URLs, delimited by 
    // carriage return symbol  
    window.navigator.clipboard
          .writeText(imageUrls.join("\n"))
          .then(()=>{
             // close the extension popup after data 
             // is copied to the clipboard
             window.close();
          });
}

Takže odstraníme vše za // Copy to clipboard ... řádek komentáře obsahující tento samotný řádek a místo toho implementujte funkci, která otevře stránku se seznamem obrázků:

function onResult(frames) {
    // If script execution failed on remote end 
    // and could not return results
    if (!frames || !frames.length) { 
        alert("Could not retrieve images");
        return;
    }
    // Combine arrays of image URLs from 
    // each frame to a single array
    const imageUrls = frames.map(frame=>frame.result)
                            .reduce((r1,r2)=>r1.concat(r2));
    // Open a page with a list of images and send imageUrls to it
    openImagesPage(imageUrls)
}

/**
 * Opens a page with a list of URLs and UI to select and
 * download them on a new browser tab and send an
 * array of image URLs to this page
 * 
 * @param {*} urls - Array of Image URLs to send
 */
function openImagesPage(urls) {
    // TODO: 
    // * Open a new tab with a HTML page to display an UI
    // * Send `urls` array to this page
}

Nyní implementujme openImagesPage funkce krok za krokem.

Otevřete novou kartu se stránkou místního rozšíření

Pomocí chrome.tabs.create funkce Google Chrome API, můžete vytvořit novou kartu v prohlížeči s libovolnou URL. Může to být jakákoli adresa URL na internetu nebo místní Html stránka rozšíření.

Vytvořte HTML stránky

Vytvořme stránku, kterou chceme otevřít. Vytvořte soubor HTML s jednoduchým názvem page.html a následující obsah. Poté jej uložte do kořenového adresáře Image Grabber složka rozšíření:

<!DOCTYPE html>
<html>
    <head>
        <title>Image Grabber</title>
    </head>
    <body>
        <div class="header">
            <div>
                <input type="checkbox" id="selectAll"/>&nbsp;
                <span>Select all</span>
            </div>
            <span>Image Grabber</span>
            <button id="downloadBtn">Download</button>    
        </div>
        <div class="container">      
        </div>
    </body>
</html>

Toto označení definuje stránku, která se skládá ze dvou částí (dvou divů):header div a container div, které mají příslušné třídy, které budou později použity v šabloně stylů CSS. Header část má ovládací prvky pro výběr všech obrázků ze seznamu a jejich stažení. Container část, která je nyní prázdná, bude dynamicky naplněna obrázky pomocí řady adres URL. Nakonec, po použití stylů CSS na tuto stránku, bude vypadat takto:

Otevřete novou kartu prohlížeče

Je tedy čas začít psát openImagesPage(urls) funkce v popup.js , který jsme definovali dříve. Použijeme chrome.tabs.create otevřete novou kartu s page.html v něm.

Syntaxe chrome.tabs.create funkce je následující:

chrome.tabs.create(createProperties,callback)
  • createProperties je objekt s parametry, které Chrome říkají, kterou kartu otevřít a jak. Konkrétně má url parametr, který bude použit k určení, která stránka se má otevřít na kartě

  • callback je funkce, která bude volána po vytvoření karty. Tato funkce má jeden argument tab , který obsahuje objekt vytvořené záložky, který mimo jiné obsahuje id parametr této karty, abyste s ní mohli později komunikovat.

Takže vytvoříme kartu:

function openImagesPage(urls) {
    // TODO: 
    // * Open a new tab with a HTML page to display an UI    
    chrome.tabs.create({"url": "page.html"},(tab) => {        
        alert(tab.id)
        // * Send `urls` array to this page
    });
}

Pokud nyní spustíte rozšíření a na libovolné stránce prohlížeče s obrázky stisknete tlačítko „Chytit nyní“, mělo by se otevřít page.html na nové kartě a aktivujte tuto kartu. Na nové kartě by se měl zobrazit následující obsah:

Jak vidíte v předchozím kódu, definovali jsme callback funkce, která by měla být později použita k odeslání urls pole na tuto stránku, ale nyní by se mělo zobrazit upozornění s vytvořeným ID karty. Pokud se to však pokusíte spustit nyní, nestane se to kvůli jednomu zajímavému efektu, o kterém je třeba diskutovat, abyste pochopili, co se stalo, a poté pochopit, jak to opravit.

Takže ve vyskakovacím okně stisknete tlačítko „Chytit nyní“, čímž se zobrazí nová karta. A v okamžiku, kdy se objeví a aktivuje nová karta, vyskakovací okno zmizí a je zničeno. Bylo zničeno PŘED provedením zpětného volání. To se stane, když se aktivuje nová karta a získá fokus. Abychom to napravili, měli bychom kartu vytvořit, ale neaktivovat ji, dokud neprovedeme všechny požadované akce ve zpětném volání. Teprve po dokončení všech akcí zpětného volání je třeba kartu ručně aktivovat.

První věc, kterou je třeba udělat, je specifikovat v chrome.tabs.create funkci automaticky nevybrat vytvořenou kartu. Chcete-li to provést, musíte nastavit selected parametr createProperties na false :

chrome.tabs.create({url: 'page.html', selected: false}, ...

Poté je třeba v rámci zpětného volání spustit všechny akce, které je třeba spustit (zobrazit upozornění nebo odeslat seznam adres URL) a v posledním řádku tohoto zpětného volání kartu ručně aktivovat.

Pokud jde o rozhraní API Chrome, activate karta znamená update the tab status . Chcete-li aktualizovat stav karty, musíte použít chrome.tabs.update funkce s velmi podobnou syntaxí:

chrome.tabs.update(tabId,updateProperties,callback)
  • tabId je ID karty k aktualizaci
  • updateProperties definuje, které vlastnosti karty se mají aktualizovat.
  • callback funkce volaná po dokončení operace aktualizace. Chcete-li aktivovat kartu pomocí této funkce, musíte provést toto volání:
chrome.tabs.update(tab.id,{active:true});

Vynecháme zpětné volání, protože je nepotřebujeme. Vše, co je potřeba udělat s touto záložkou, by mělo být provedeno na předchozích řádcích této funkce.

function openImagesPage(urls) {
    // TODO: 
    // * Open a new tab with a HTML page to display an UI    
    chrome.tabs.create(
        {"url": "page.html",selected:false},(tab) => {        
            alert(tab.id)
            // * Send `urls` array to this page
            chrome.tabs.update(tab.id,{active: true});
        }
    );
}

Pokud nyní spustíte rozšíření a stisknete tlačítko „Uchopte nyní“, vše by mělo fungovat podle očekávání:karta se vytvoří, zobrazí se upozornění, karta bude vybrána a vyskakovací okno nakonec zmizí.

Nyní odeberme dočasné alert a definovat, jak odeslat seznam adres URL obrázků na novou stránku a jak zobrazit rozhraní pro jejich správu.

Odeslat na stránku data adres URL obrázků

Nyní musíme vytvořit skript, který vygeneruje značku HTML pro zobrazení seznamu obrázků uvnitř container div na stránce.

Na první pohled můžeme jít stejnou cestou jako v předchozí části tohoto článku. Můžeme použít chrome.scripting API o vložení skriptu na kartu s page.html a tento skript bude používat obrázek urls pro generování seznamu obrázků uvnitř kontejneru. Ale vkládání skriptů není skutečný způsob. Je to druh hackování. Není to úplně správné a legální. Skript bychom měli definovat v místě, kde se bude provádět, neměli bychom "posílat skripty". Jediný důvod, proč jsme to dělali dříve, je ten, že jsme neměli přístup ke zdrojovému kódu stránek webů, ze kterých jsme stahovali obrázky. Ale v aktuálním případě máme plnou kontrolu nad page.html a všechny skripty v něm a proto by měl být skript, který pro to generuje rozhraní, definován v page.html . Vytvořme tedy prázdný page.js Javascript, vložte jej do stejné složky s page.html a zahrňte jej do page.html tudy:

<!DOCTYPE html>
<html>
    <head>
        <title>Image Grabber</title>
    </head>
    <body>
        <div class="header">
            <div>
                <input type="checkbox" id="selectAll"/>&nbsp;
                <span>Select all</span>
            </div>
            <span>Image Grabber</span>
            <button id="downloadBtn">Download</button>    
        </div>
        <div class="container">      
        </div>
        <script src="/page.js"></script>        
    </body>
</html>

Nyní můžeme psát v page.js cokoliv je potřeba k inicializaci a vytvoření rozhraní. Stále však potřebujeme data z popup.js - pole urls pro zobrazení obrázků. Stále tedy potřebujeme odeslat tato data do skriptu, který jsme právě vytvořili.

Toto je okamžik k představení důležité funkce rozhraní Chrome API, kterou lze použít ke komunikaci mezi různými částmi rozšíření:messaging . Jedna část pobočky může odeslat zprávu s daty jiné části pobočky a ta druhá může zprávu přijmout, zpracovat přijatá data a odpovědět odesílající části. V zásadě je rozhraní API pro zasílání zpráv definováno pod chrome.runtime jmenný prostor a oficiální dokumentaci si můžete přečíst zde:https://developer.chrome.com/docs/extensions/mv3/messaging/.

Konkrétně se jedná o chrome.runtime.onMessage událost. Pokud je pro tuto událost ve skriptu definován posluchač, bude tento skript přijímat všechny události, které mu ostatní skripty pošlou.

Pro účely Image Grabber musíme odeslat zprávu se seznamem adres URL z popup.js skript na kartu s page.html strana. Skript na této stránce by měl přijmout tuto zprávu, extrahovat z ní data a poté na ni odpovědět, aby potvrdil, že data byla zpracována správně. Nyní je čas představit API, které je k tomu zapotřebí.

chrome.tabs.sendMessage(tabId, message, responseFn)
  • tabId je ID karty, na kterou bude zpráva odeslána
  • message samotnou zprávu. Může to být jakýkoli objekt Javascript.
  • callback je funkce, která je volána, když přijatá strana odpověděla na tuto zprávu. Tato funkce má pouze jeden argument responseObject který obsahuje cokoli, tento příjemce odeslal jako odpověď.

Toto je to, co potřebujeme zavolat v popup.js pro odeslání seznamu adres URL jako zprávy:

function openImagesPage(urls) {
    // TODO: 
    // * Open a new tab with a HTML page to display an UI    
    chrome.tabs.create(
        {"url": "page.html",selected:false},(tab) => {        
            // * Send `urls` array to this page
            chrome.tabs.sendMessage(tab.id,urls,(resp) => {
                chrome.tabs.update(tab.id,{active: true});
            });                            
        }
    );
}

Na této kartě odešleme urls jako zprávu na stránku a aktivujte tuto stránku až po obdržení odpovědi na tuto zprávu.

Doporučuji zabalit tento kód do setTimeout funkce počkat několik milisekund před odesláním zprávy. Je třeba dát nějaký čas inicializaci nové karty:

function openImagesPage(urls) {
    // TODO: 
    // * Open a new tab with a HTML page to display an UI    
    chrome.tabs.create(
        {"url": "page.html",selected:false},(tab) => {        
            // * Send `urls` array to this page
            setTimeout(()=>{
                chrome.tabs.sendMessage(tab.id,urls,(resp) => {
                    chrome.tabs.update(tab.id,{active: true});
                });                            
            },100);
        }
    );
}

Příjem dat adres URL obrázků na stránce

Pokud toto spustíte nyní, vyskakovací okno nezmizí, protože by mělo zmizet až po obdržení odpovědi z přijímací stránky. Abychom tuto zprávu obdrželi, musíme definovat chrome.runtime.onMessage posluchač událostí v page.js skript:

chrome.runtime.onMessage
    .addListener(function(message,sender,sendResponse) { 
        addImagesToContainer(message);               
        sendResponse("OK");
    });

/**
 * Function that used to display an UI to display a list 
 * of images
 * @param {} urls - Array of image URLs
 */
function addImagesToContainer(urls) {
    // TODO Create HTML markup inside container <div> to
    // display received images and to allow to select 
    // them for downloading
    document.write(JSON.stringify(urls));
}

Chcete-li přijmout zprávu, měl by cílový skript přidat posluchače do chrome.runtime.onMessage událost. Posluchač je funkce se třemi argumenty:

  • message - objekt přijaté zprávy, přenesený tak, jak je. (pole urls v tomto případě)
  • sender - objekt, který identifikuje odesílatele této zprávy.
  • sendResponse - funkce, kterou lze použít k odeslání odpovědi odesílateli. Jediným parametrem této funkce je cokoliv, co chceme odeslat odesílateli.

Zde tedy tento posluchač předá přijatou zprávu na addImagesToContainer funkce, která bude použita k vytvoření značky HTML pro zobrazení obrázků. Ale právě teď zapisuje řetězcovou reprezentaci přijatého pole adres URL. Poté posluchač odpoví odesílateli sendResponse funkce. Jako odpověď odešle pouze řetězec "OK", protože nezáleží na tom, jak reagovat. V tomto případě je důležitá jediná odpověď.

Po dokončení, když kliknete na tlačítko "UHNOUT" z rozšíření, nová stránka by se měla otevřít s něčím takovým, jako je obsah:(podle toho, na kterou kartu jste klikli):

Vytvořit rozhraní pro stahování obrázků

Obdrželi jsme řadu adres URL obrázků ke stažení z vyskakovacího okna do skriptu připojeného k page.html a to je vše, co jsme potřebovali od popup.js . Nyní je čas vytvořit rozhraní pro zobrazení těchto obrázků a umožnění jejich stahování.

Vytvořte uživatelské rozhraní pro zobrazení a výběr obrázků

Funkce addImagesToContainer(urls) již vytvořený se zástupným kódem. Změňme to tak, aby skutečně přidávaly obrázky do kontejneru

:

/**
 * Function that used to display an UI to display a list 
 * of images
 * @param {} urls - Array of image URLs
 */
function addImagesToContainer(urls) {
    if (!urls || !urls.length) {
        return;
    }
    const container = document.querySelector(".container");
    urls.forEach(url => addImageNode(container, url))
}

/**
 * Function dynamically add a DIV with image and checkbox to 
 * select it to the container DIV
 * @param {*} container - DOM node of a container div 
 * @param {*} url - URL of image 
 */
function addImageNode(container, url) {
    const div = document.createElement("div");
    div.className = "imageDiv";
    const img = document.createElement("img");
    img.src = url;
    div.appendChild(img);
    const checkbox = document.createElement("input");
    checkbox.type = "checkbox";
    checkbox.setAttribute("url",url);            
    div.appendChild(checkbox);
    container.appendChild(div)
}

Pojďme si tento kód ujasnit krok za krokem.

  • addImagesToContainer funkce zkontroluje, zda pole URL není prázdné, a zastaví se, pokud nic neobsahuje.
  • Potom se dotazuje DOM, aby získal uzel div prvek s container třída. Pak bude tento prvek kontejneru použit ve funkci k připojení všech obrázků.
  • Dále zavolá addImageNode funkce pro každou adresu URL. Prochází kódem container k němu a samotné URL
  • Nakonec addImageNode dynamicky vytvoří HTML pro každý obrázek a připojí ho ke kontejneru.

Pro každou adresu URL obrázku vytvoří následující HTML:

<div class="imageDiv">
    <img src={url}/>
    <input type="checkbox" url={url}/>
</div>

Připojí div s třídou imageDiv pro každý obrázek. Tento div obsahuje samotný obrázek se zadaným url a zaškrtávací políčko pro jeho výběr. Toto zaškrtávací políčko má vlastní atribut s názvem url , který bude později použit funkcí stahování k identifikaci, kterou adresu URL použít ke stažení obrázku.

Pokud to spustíte právě teď pro stejný seznam obrázků jako na předchozím snímku obrazovky, stránka by měla zobrazovat něco jako následující:

Zde můžete vidět, že hned za záhlavím se zaškrtávacím políčkem „Vybrat vše“ a tlačítkem „Stáhnout“ je seznam obrázků se zaškrtávacími políčky pro ruční výběr každého z nich.

Toto je úplný kód page.js soubor, který se používá k příjmu a zobrazení tohoto seznamu:

chrome.runtime.onMessage
    .addListener((message,sender,sendResponse) => { 
        addImagesToContainer(message)
        sendResponse("OK");
    });

/**
 * Function that used to display an UI to display a list 
 * of images
 * @param {} urls - Array of image URLs
 */
function addImagesToContainer(urls) {
    if (!urls || !urls.length) {
        return;
    }
    const container = document.querySelector(".container");
    urls.forEach(url => addImageNode(container, url))
}

/**
 * Function dynamically add a DIV with image and checkbox to 
 * select it to the container DIV
 * @param {*} container - DOM node of a container div 
 * @param {*} url - URL of image 
 */
function addImageNode(container, url) {
    const div = document.createElement("div");
    div.className = "imageDiv";
    const img = document.createElement("img");
    img.src = url;
    div.appendChild(img);
    const checkbox = document.createElement("input");
    checkbox.type = "checkbox";
    checkbox.setAttribute("url",url);            
    div.appendChild(checkbox);
    container.appendChild(div)
}

V tomto kroku můžeme každý obrázek vybrat ručně. Nyní je čas aktivovat zaškrtávací políčko „Vybrat vše“ a vybrat/zrušit výběr všech najednou.

Implementace funkce Vybrat vše

Pokud se vraťte na page.html rozvržení, uvidíte, že zaškrtávací políčko "Vybrat vše" je vstupní pole s selectAll id. Musíme tedy reagovat na kliknutí uživatelů na něj. Když jej uživatel zapne, měla by se zapnout všechna zaškrtávací políčka obrázků. Když jej uživatel vypne, všechna zaškrtávací políčka pro obrázky by také měla zhasnout. Jinými slovy, měli bychom poslouchat událost „onChange“ zaškrtávacího políčka „#selectAll“ a v obslužné rutině této události nastavit stav „zaškrtnuto“ všech zaškrtávacích políček tak, aby byl stejný jako stav „Vybrat vše“. "zaškrtávací políčko. Takto by to mohlo být implementováno v page.js skript:

document.getElementById("selectAll")
        .addEventListener("change", (event) => {
    const items = document.querySelectorAll(".container input");
    for (let item of items) {
        item.checked = event.target.checked;
    };
});

Funkce naslouchání přijímá instanci onChange událost jako event argument funkce. Tato instance má odkaz na samotný uzel "Vybrat vše" v target parametr, který můžeme použít k určení aktuálního stavu tohoto checkboxu.

Poté vybereme všechna "vstupní" pole uvnitř div s container třída, např. všechna zaškrtávací políčka obrázků, protože v tomto kontejneru nejsou žádná další vstupní pole.

Poté nastavíme zaškrtnutý stav každého z těchto zaškrtávacích políček na stav zaškrtávacího políčka „Vybrat vše“. Takže pokaždé, když uživatel změní stav tohoto zaškrtávacího políčka, všechna ostatní zaškrtávací políčka tuto změnu odrážejí.

Nyní, pokud znovu spustíte rozšíření, můžete vybrat obrázky ke stažení buď ručně, nebo automaticky.

V této sekci zbývá pouze stažení vybraných obrázků. K tomu potřebujeme vytvořit Download tlačítko práce.

Implementujte funkci stahování

Poté, co uživatel vybere obrázky, měl by stisknout Download tlačítko, které by mělo spustit onClick posluchač událostí tohoto tlačítka. Download tlačítko lze identifikovat podle downloadBtn ID. K tomuto tlačítku tedy můžeme připojit funkci posluchače pomocí tohoto ID. Tato funkce by měla dělat tři věci:

  • Získejte adresy URL všech vybraných obrázků,
  • Stáhněte si je a zkomprimujte do archivu ZIP
  • Vyzvěte uživatele ke stažení tohoto archivu.

Definujme tvar této funkce:

document.getElementById("downloadBtn")
        .addEventListener("click", async() => {
            try {
                const urls = getSelectedUrls();
                const archive = await createArchive(urls);
                downloadArchive(archive);
            } catch (err) {
                alert(err.message)
            }
        })

function getSelectedUrls() {
    // TODO: Get all image checkboxes which are checked,
    // extract image URL from each of them and return
    // these URLs as an array
}

async function createArchive(urls) {
    // TODO: Create an empty ZIP archive, then, using 
    // the array of `urls`, download each image, put it 
    // as a file to the ZIP archive and return that ZIP
    // archive
}

function downloadArchive(archive) {
    // TODO: Create an <a> tag
    // with link to an `archive` and automatically
    // click this link. This way, the browser will show
    // the "Save File" dialog window to save the archive
}

Posluchač spouští přesně ty akce, definované výše, jednu po druhé.

Vložil jsem celé tělo posluchače do pokusu/chytit blok, abych implementoval jednotný způsob, jak zvládnout všechny chyby, které se mohou stát v jakémkoli kroku. Pokud je během zpracování seznamu adres URL nebo komprimace souborů vyvolána výjimka, bude tato chyba zachycena a zobrazena jako výstraha.

Součástí akcí, které tato funkce provede, jsou také asynchronní a návratové sliby. Používám async/await přístup k řešení slibů, namísto potom/chytit, aby byl kód jednodušší a čistší. Pokud tento moderní přístup neznáte, hledejte jednoduché vysvětlení zde:https://javascript.info/async-await. To je důvod, proč být schopen vyřešit sliby pomocí await , funkce posluchače je definována jako async() , stejně jako createArchive funkce.

Získejte vybrané adresy URL obrázků

getSelectedUrls() funkce by se měla dotazovat na všechna zaškrtávací políčka obrázků v .container div, pak je filtrujte, aby zůstaly pouze zaškrtnuté, a poté extrahujte url atribut těchto zaškrtávacích políček. V důsledku toho by tato funkce měla vrátit pole těchto adres URL. Takto by tato funkce mohla vypadat:

function getSelectedUrls() {
    const urls = 
        Array.from(document.querySelectorAll(".container input"))
             .filter(item=>item.checked)
             .map(item=>item.getAttribute("url"));
    if (!urls || !urls.length) {
        throw new Error("Please, select at least one image");
    }
    return urls;
}

Kromě toho vyvolá výjimku, pokud nejsou vybrána žádná zaškrtávací políčka. Poté je tato výjimka správně zpracována ve funkci upstream.

Stahování obrázků podle adres URL

createArchive funkce používá urls argument ke stažení obrazových souborů pro každý url . Chcete-li stáhnout soubor z internetu, musíte provést požadavek GET HTTP na adresu tohoto souboru. Existuje mnoho způsobů z Javascriptu, ale nejjednotnější a nejmodernější je použití fetch() funkce. Tato funkce může být jednoduchá nebo složitá. V závislosti na druhu požadavku, který potřebujete vykonat, můžete vytvořit velmi specifické objekty požadavků, které se předají dané funkci, a poté analyzovat vrácené odpovědi. V jednoduchém formuláři vyžaduje zadání adresy URL pro vyžádání a vrátí příslib s objektem Response:

response = await fetch(url);

Tento formulář použijeme pro Image Grabber. Úplný popis fetch funkci a její API naleznete v oficiálních dokumentech:https://www.javascripttutorial.net/javascript-fetch-api/.

Volání funkce výše se buď převede na response objekt nebo hodit výjimku v případě problémů. response je objekt HTTP Response, který obsahuje surový přijatý obsah a různé vlastnosti a metody, které s ním umožňují nakládat. Odkaz na něj najdete také v oficiálních dokumentech:https://developer.mozilla.org/en-US/docs/Web/API/Response.

Tento objekt obsahuje metody pro získání obsahu v různých formách v závislosti na tom, co se očekává, že bude přijat. Například response.text() převede odpověď na textový řetězec response.json() převede jej na prostý Javascriptový objekt. Potřebujeme však získat binární data obrázku, abychom jej mohli uložit do souboru. Typ objektu, který se obvykle používá pro práci s binárními daty v JavaScriptu, je Blob - Binární velký objekt. Metoda pro získání obsahu odpovědi jako blob je response.blob() .

Nyní implementujme část createArchive funkce ke stažení obrázků jako Blob objekty:

async function createArchive(urls) {
    for (let index in urls) {
        const url = urls[index];
        try {
            const response = await fetch(url);
            const blob = await response.blob();
            console.log(blob);
        } catch (err) {
            console.error(err);
        }
    };
}

V této funkci procházíme každou položku vybraného urls pole, stáhněte každý z nich na response poté převeďte response na blob . Nakonec stačí přihlásit každý objekt blob do konzoly.

A blob je objekt, který obsahuje binární data samotného souboru a také některé vlastnosti těchto dat, které mohou být důležité, zejména:

  • typ – Typ souboru. Toto je obsah typu MIME – https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types. V závislosti na typu MIME můžeme zkontrolovat, zda jde skutečně o obrázek nebo ne. Budeme muset filtrovat soubory podle jejich typů MIME a ponechat pouze image/jpeg , image/png nebo image/gif . Uděláme to později, v další sekci.

  • velikost – Velikost obrázku v bajtech. Tento parametr je také důležitý, protože pokud je velikost 0 nebo menší než 0, nemá smysl tento obrázek ukládat do souboru.

Odkaz se všemi parametry a metodami Blob objekty naleznete zde:https://developer.mozilla.org/en-US/docs/Web/API/Blob.

Pokud si toto přečtete, nenajdete name nebo file name vlastnictví. Blob je pouze o obsahu, nezná název souboru, protože obsah vrácený fetch() nemusí to být soubor. Názvy obrázků však nějak potřebujeme mít. V další části vytvoříme obslužnou funkci, která bude použita k vytvoření názvu souboru, přičemž budeme znát pouze blob.

Určete názvy souborů pro obrázky

Abychom mohli vložit soubory do archivu, musíme pro každý soubor zadat název souboru. Abychom tyto soubory později otevřeli jako obrázky, potřebujeme znát příponu každého souboru. Abychom tuto úlohu zvládli, nadefinujeme funkci utility s následující syntaxí:

function checkAndGetFileName(index, blob)

Kde index je index položky z urls pole a blob je objekt BLOB s obsahem souboru.

Chcete-li získat name souboru použijeme pouze index adresy URL ve vstupním poli. Samotnou URL nepoužijeme, protože může být divná a obsahovat různá časová razítka a další odpadky. Názvy souborů tedy budou jako '1.jpeg', '2.png' a tak dále.

Chcete-li získat extension souboru, použijeme typ MIME blob objekt tohoto souboru, který je uložen v blob.type parametr.

Kromě toho tato funkce nejen vytvoří název souboru, ale také zkontroluje, zda má objekt blob správný size a typu MIME. Vrátí název souboru pouze v případě, že má kladné size a správný typ MIME obrázku. Správné typy MIME pro obrázky vypadají takto:image/jpeg , image/png nebo image/gif ve kterém je první částí slovo image a druhá část je rozšířením obrázku.

Funkce tedy analyzuje typ MIME a vrátí název souboru s příponou pouze v případě, že typ mime začíná image . Název souboru je index a přípona souboru je druhá část jeho typu MIME:

Takto může funkce vypadat:

function checkAndGetFileName(index, blob) {
    let name = parseInt(index)+1;
    const [type, extension] = blob.type.split("/");
    if (type != "image" || blob.size <= 0) {
        throw Error("Incorrect content");
    }
    return name+"."+extension;
}

Nyní, když máme názvy obrázků a jejich binární obsah, nic nám nemůže zabránit v tom, abychom to prostě vložili do archivu ZIP.

Vytvořte archiv ZIP

ZIP je jedním z nejčastěji používaných formátů pro kompresi a archivaci dat. Pokud komprimujete soubory pomocí ZIP a pošlete je někam, můžete si být na 100% jisti, že je přijímající strana bude moci otevřít. Tento formát vytvořila a vydala společnost PKWare v roce 1989:https://en.wikipedia.org/wiki/ZIP_(formát_souboru). Naleznete zde nejen historii, ale také strukturu ZIP souboru a popis algoritmu, pomocí kterého lze touto metodou implementovat kompresi a dekompresi binárních dat. Zde však nebudeme znovu objevovat kolo, protože je již implementováno pro všechny nebo téměř všechny programovací jazyky, včetně Javascriptu. Využijeme pouze stávající externí knihovnu - JSZip. Najdete ho zde:https://stuk.github.io/jszip/.

Musíme si tedy stáhnout skript knihovny JSZip a zahrnout jej do page.html , před page.js . Přímý odkaz ke stažení je následující:http://github.com/Stuk/jszip/zipball/master. Stáhne archiv se všemi zdrojovými kódy a verzemi. Toto je velký archiv, ale ve skutečnosti z něj potřebujete pouze jeden soubor:dist/jszip.min.js .

Vytvořte lib složku uvnitř cesty rozšíření, extrahujte do ní tento soubor a zahrňte tento skript do page.html , před page.js :

<!DOCTYPE html>
<html>
    <head>
        <title>Image Grabber</title>
    </head>
    <body>
        <div class="header">
            <div>
                <input type="checkbox" id="selectAll"/>&nbsp;
                <span>Select all</span>
            </div>
            <span>Image Grabber</span>
            <button id="downloadBtn">Download</button>    
        </div>
        <div class="container">      
        </div>
        <script src="/lib/jszip.min.js"></script>
        <script src="/page.js"></script>        
    </body>
</html>

Když je zahrnut, vytvoří globální JSZip třídy, kterou lze použít ke konstrukci ZIP archivů a přidávání obsahu do nich. Tento proces lze popsat následujícím kódem:

const zip = new JSZip();
zip.file(filename1, blob1);
zip.file(filename2, blob2);
.
.
.
zip.file(filenameN, blobN);
const blob = await zip.generateAsync({type:'blob'});

Nejprve vytvoří prázdný zip objekt. Poté do něj začne přidávat soubory. Soubor definovaný názvem a blob s binárním obsahem tohoto souboru. Nakonec generateAsync metoda se používá ke generování ZIP archivu z dříve přidaných souborů. V tomto případě vrací vygenerovaný archiv jako blob, protože už víme, co je BLOB a jak s ním pracovat. Můžete se však naučit dokumentaci rozhraní JSZip API pro další možnosti:https://stuk.github.io/jszip/documentation/api_jszip.html.

Nyní můžeme tento kód integrovat do createArchive funkce pro vytvoření archivu ze všech obrazových souborů a vrácení BLOB tohoto archivu:

async function createArchive(urls) {
    const zip = new JSZip();
    for (let index in urls) {
        try {
            const url = urls[index];
            const response = await fetch(url);
            const blob = await response.blob();
            zip.file(checkAndGetFileName(index, blob),blob);
        } catch (err) {
            console.error(err);
        }
    };
    return await zip.generateAsync({type:'blob'});
}

function checkAndGetFileName(index, blob) {
    let name = parseInt(index)+1;
    [type, extension] = blob.type.split("/");
    if (type != "image" || blob.size <= 0) {
        throw Error("Incorrect content");
    }
    return name+"."+extension;
}

Zde při přidávání každého souboru obrázku do zip , používáme dříve vytvořený checkAndGetFileName funkce pro vygenerování názvu souboru pro tento soubor.

Tělo smyčky je také umístěno do bloku try/catch, takže každá výjimka, která je vyvolána jakýmkoli řádkem kódu, bude zpracována uvnitř této smyčky. Rozhodl jsem se nezastavovat proces v případě výjimek zde, ale pouze přeskočit soubor, což vedlo k výjimce a zobrazit pouze chybovou zprávu do konzole.

A nakonec vrátí vygenerovaný BLOB se zip archivem, který je připraven ke stažení.

Stáhněte si archiv ZIP

Obvykle, když chceme pozvat uživatele ke stažení souboru, ukážeme jim odkaz ukazující na tento soubor a požádáme je, aby na něj klikli, aby si tento soubor stáhli. V tomto případě potřebujeme mít odkaz, který ukazuje na BLOB archivu. Objekty BLOB mohou být velmi velké, proto je webový prohlížeč někam ukládá a naštěstí je v Javascriptu funkce, která umožňuje získat odkaz na objekt BLOB:

window.URL.createObjectURL(blob)

Můžeme tedy vytvořit odkaz na blob archivu ZIP. Navíc můžeme automaticky kliknout na tento odkaz, abychom uživatele o to nežádali, protože již na začátku klikli na tlačítko "Stáhnout".

Konečně, takto je downloadArchive funkce vypadá:

function downloadArchive(archive) {
    const link = document.createElement('a');
    link.href = URL.createObjectURL(archive);
    link.download = "images.zip";        
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);    
}

Tento kód dynamicky vytvoří prvek „a“ a nasměruje jej na adresu URL archive kapka. Také nastaví název stahovaného souboru na images.zip . Poté vloží tento neviditelný odkaz do dokumentu a klepne na něj. Tím prohlížeč spustí buď zobrazení okna „Uložení souboru“, nebo automatické uložení souboru s názvem images.zip a obsah archivu ZIP. Nakonec funkce odstraní tento odkaz z dokumentu, protože jej po kliknutí již nepotřebujeme.

Vyčištění kódu

Toto je poslední krok implementace funkce "Download". Pojďme vyčistit, okomentovat a zapamatovat si celý kód, který jsme vytvořili v page.js :

/**
 * Listener that receives a message with a list of image
 * URL's to display from popup.
 */
chrome.runtime.onMessage
    .addListener((message,sender,sendResponse) => { 
        addImagesToContainer(message)
        sendResponse("OK");
    });

/**
 * Function that used to display an UI to display a list 
 * of images
 * @param {} urls - Array of image URLs
 */
function addImagesToContainer(urls) {
    if (!urls || !urls.length) {
        return;
    }
    const container = document.querySelector(".container");
    urls.forEach(url => addImageNode(container, url))
}

/**
 * Function dynamically add a DIV with image and checkbox to 
 * select it to the container DIV
 * @param {*} container - DOM node of a container div 
 * @param {*} url - URL of image 
 */
function addImageNode(container, url) {
    const div = document.createElement("div");
    div.className = "imageDiv";
    const img = document.createElement("img");
    img.src = url;
    div.appendChild(img);
    const checkbox = document.createElement("input");
    checkbox.type = "checkbox";
    checkbox.setAttribute("url",url);            
    div.appendChild(checkbox);
    container.appendChild(div)
}

/**
 * The "Select All" checkbox "onChange" event listener
 * Used to check/uncheck all image checkboxes
 */
document.getElementById("selectAll")
         .addEventListener("change", (event) => {
    const items = document.querySelectorAll(".container input");
    for (let item of items) {
        item.checked = event.target.checked;
    };
});

/**
 * The "Download" button "onClick" event listener
 * Used to compress all selected images to a ZIP-archive 
 * and download this ZIP-archive
 */
document.getElementById("downloadBtn")
        .addEventListener("click", async() => {
            try {
                const urls = getSelectedUrls();
                const archive = await createArchive(urls);
                downloadArchive(archive);
            } catch (err) {
                alert(err.message)
            }
        })

/**
 * Function used to get URLs of all selected image
 * checkboxes
 * @returns Array of URL string 
 */
function getSelectedUrls() {
    const urls = 
        Array.from(document.querySelectorAll(".container input"))
             .filter(item=>item.checked)
             .map(item=>item.getAttribute("url"));
    if (!urls || !urls.length) {
        throw new Error("Please, select at least one image");
    }
    return urls;
}

/**
 * Function used to download all image files, identified 
 * by `urls`, and compress them to a ZIP
 * @param {} urls - list of URLs of files to download
 * @returns a BLOB of generated ZIP-archive
 */
async function createArchive(urls) {
    const zip = new JSZip();
    for (let index in urls) {
        try {
            const url = urls[index];
            const response = await fetch(url);
            const blob = await response.blob();
            zip.file(checkAndGetFileName(index, blob),blob);
        } catch (err) {
            console.error(err);
        }
    };
    return await zip.generateAsync({type:'blob'});
}

/**
 * Function used to return a file name for
 * image blob only if it has a correct image type
 * and positive size. Otherwise throws an exception.
 * @param {} index - An index of URL in an input
 * @param {*} blob - BLOB with a file content 
 * @returns 
 */
function checkAndGetFileName(index, blob) {
    let name = parseInt(index)+1;
    const [type, extension] = blob.type.split("/");
    if (type != "image" || blob.size <= 0) {
        throw Error("Incorrect content");
    }
    return name+"."+extension.split("+").shift();
}

/**
 * Triggers browser "Download file" action
 * using a content of a file, provided by 
 * "archive" parameter
 * @param {} archive - BLOB of file to download
 */
function downloadArchive(archive) {
    const link = document.createElement('a');
    link.href = URL.createObjectURL(archive);
    link.download = "images.zip";        
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);    
}

Nyní můžete kliknout na tlačítko „UHRAŤ HNED“, poté buď automaticky nebo ručně vybrat obrázky ke stažení, stisknout tlačítko „Stáhnout“ a uložit archiv ZIP s těmito obrázky:

Nevypadá to však dokonale. To je v praxi téměř nemožné použít. Upravme tuto stránku správně.

Úprava stylu stránky rozšíření

V současné fázi jsou všechny značky a funkce stránky rozšíření připraveny. Všechny třídy a ID jsou definovány v HTML. Je čas přidat CSS, nastylovat to. Vytvořte page.css soubor ve stejné složce s page.html a další a přidejte tuto šablonu stylů do page.html :

<!DOCTYPE html>
<html>
    <head>
        <title>Image Grabber</title>
        <link href="/page.css" rel="stylesheet" type="text/css"/>
    </head>
    <body>
        <div class="header">
            <div>
                <input type="checkbox" id="selectAll"/>&nbsp;
                <span>Select all</span>
            </div>
            <span>Image Grabber</span>
            <button id="downloadBtn">Download</button>    
        </div>
        <div class="container">      
        </div>
        <script src="/lib/jszip.min.js"></script>
        <script src="/page.js"></script>        
    </body>
</html>

Poté přidejte následující obsah do page.css :

body {
    margin:0px;
    padding:0px;
    background-color: #ffffff;
}

.header {    
    display:flex;
    flex-wrap: wrap;
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
    width:100%;
    position: fixed;
    padding:10px;
    background: linear-gradient( #5bc4bc, #01a9e1);
    z-index:100;
    box-shadow: 0px 5px 5px #00222266;
}

.header > span {
    font-weight: bold;
    color: black;
    text-transform: uppercase;
    color: #ffffff;
    text-shadow: 3px 3px 3px #000000ff;
    font-size: 24px;
}

.header > div {
    display: flex;
    flex-direction: row;
    align-items: center;
    margin-right: 10px;
}

.header > div > span {
    font-weight: bold;
    color: #ffffff;
    font-size:16px;
    text-shadow: 3px 3px 3px #00000088;
}

.header input {
    width:20px;
    height:20px;
}

.header > button {
    color:white;
    background:linear-gradient(#01a9e1, #5bc4bc);
    border-width:0px;
    border-radius:5px;
    padding:10px;
    font-weight: bold;
    cursor:pointer;
    box-shadow: 2px 2px #00000066;
    margin-right: 20px;
    font-size:16px;
    text-shadow: 2px 2px 2px#00000088;
}

.header > button:hover {
    background:linear-gradient( #5bc4bc,#01a9e1);
    box-shadow: 2px 2px #00000066;
}

.container {
    display: flex;
    flex-wrap: wrap;
    flex-direction: row;
    justify-content: center;
    align-items: flex-start;
    padding-top: 70px;
}

.imageDiv {
    display:flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    position:relative;
    width:150px;
    height:150px;
    padding:10px;
    margin:10px;
    border-radius: 5px;
    background: linear-gradient(#01a9e1, #5bc4bc);
    box-shadow: 5px 5px 5px #00222266;
}

.imageDiv:hover {
    background: linear-gradient(#5bc4bc,#01a9e1);
    box-shadow: 10px 10px 10px #00222266;
}

.imageDiv img {
    max-width:100%;
    max-height:100%;
}

.imageDiv input {
    position:absolute;
    top:10px;
    right:10px;
    width:20px;
    height:20px;
}

Po body styling, definuje styl pro sadu selektorů obsahu .header div a poté pro sadu selektorů obsahu .container div. Klíčovou součástí tohoto stylu je použití Flexbox rozložení s možností „flex-wrap“. Používá se jak pro záhlaví, tak pro kontejner. Díky tomu je celé rozložení responzivní. Komponenty se správně uspořádají na obrazovce jakékoli velikosti:

.

O použití rozvržení Flexbox si můžete přečíst například zde:https://css-tricks.com/snippets/css/a-guide-to-flexbox/. Informace o všech ostatních použitých stylech CSS můžete snadno najít v jakékoli příručce CSS.

Publikovat a distribuovat rozšíření

Nyní je práce dokončena a rozšíření je připraveno k vydání. Jak to ukázat ostatním lidem? Pošlete jim tuto složku se soubory a vysvětlete, jak nainstalovat rozbalené rozšíření pomocí chrome://extensions karta? Samozřejmě ne, toto není správný způsob distribuce rozšíření Chrome. Správným způsobem je publikovat rozšíření na Chrome Web Store a pošlete odkaz na stránku, kde bude zveřejněn každému, koho chcete, a publikujte tento odkaz na všech svých online zdrojích.

Toto je například odkaz na Image Reader rozšíření, které jsem nedávno vytvořil a zveřejnil:

https://chrome.google.com/webstore/detail/image-reader/acaljenpmopdeajikpkgbilhbkddjglh

V Internetovém obchodě Chrome to vypadá takto:

Lidé si mohou přečíst popis rozšíření, prohlédnout si snímky obrazovky a nakonec stisknout Add to Chrome tlačítko pro instalaci.

Jak vidíte zde, k publikování rozšíření musíte poskytnout nejen samotné rozšíření, ale také obrázek rozšíření, snímky obrazovky, popis, specifikovat kategorii rozšíření a další parametry.

Pravidla publikování se čas od času mění, proto je lepší použít oficiální web Google a podívat se na návod, jak nastavit účet vývojáře webu Chrome, nahrát do něj rozšíření a poté jej publikovat. Toto je kořen informací v oficiální dokumentaci:https://developer.chrome.com/docs/webstore/publish/. Google zde popisuje vše, co musíte udělat, a aktualizuje tuto stránku, když se pravidla změní.

Pro snadné zahájení zde mohu uvést seznam klíčových bodů. (Aktuální je však pouze dnes, možná za týden nebo později se něco v pravidlech Google změní, takže na tento seznam příliš nespoléhejte, použijte ho jen jako obecnou informaci):

  • Archivujte složku rozšíření do souboru zip

  • Zaregistrujte se jako vývojář Internetového obchodu Chrome na této stránce:https://chrome.google.com/webstore/devconsole/ . Můžete použít stávající účet Google (pokud máte například účet používaný pro Gmail, bude fungovat).

  • Zaplaťte jednorázový registrační poplatek 5 $

  • Pomocí Vývojářské konzole Internetového obchodu Chrome v ní vytvořte nový produkt a nahrajte do něj vytvořený archiv ZIP.

  • Fill required fields in a product form with information about product name and description. Upload a product picture and screenshots of different sizes. This information can be variable, that is why I think that you will need to prepare it in a process of filling out this form.

  • It's not required to fill all fields in a single run. You can complete part of the form and press the "Save Draft" button. Then, return back, select your product and continue filling.

  • After all fields are completed, press the "Submit for Review" button, and, if the form is completed without mistakes, the extension will be sent to Google for review. The review can take time. The status of the review will be displayed on the products list.

  • You have to check from time to time the status of your submission because Google does not send any notifications by email about review progress.

  • After successful review, the status of the product will change to "Published" and it will be available on Google Chrome Web Store:https://chrome.google.com/webstore/. People will be able to find it and install it.

In the case of my extension on the screenshot above, the Google review took two days and it was published successfully. I hope the same will be with you, or even faster. Hodně štěstí!

Závěr

Creating Google Chrome Extensions is an easy way to distribute your web application worldwide, using a global worldwide platform, that just works and does not require any support and promotion. This way you can easily deliver your online ideas almost at no cost. What is more, you can enrich the features of your existing websites with browser extensions to make your users feel more comfortable working with your online resources. For example, the extension, which I recently published, used to work with an online text recognition service - "Image Reader" (https://ir.germanov.dev). Using this service, you can get an image from any website, paste it to the interface and recognize a text on it. The browser extension for this service helps to send images from any browser tab to this service automatically. Without the extension, the user needs to make 5 mouse clicks to do that, but with extension, the same can be done in just two mouse clicks. This is a great productivity improvement. You can watch this video to see, how that extension helps to deliver images to the web service using the context menu:

I believe that you can find a lot of ways how to use web browser automation via extensions to increase the productivity and comfort level of your online users, to make their work with your online resources better, faster, and smarter. I hope that my tutorial opened the world of web browser extensions for you. However, I did not clarify even a few percent of the features, that exist in this area. Perhaps I will write more about this soon.

Full source code of the Image Grabber extension you can clone from my GitHub repository:

https://github.com/AndreyGermanov/image_grabber.

Please write if you have something to add or found bugs or what to improve.

Feel free to connect and follow me on social networks where I publish announcements about my new articles, similar to this one and other software development news:

LinkedIn:https://www.linkedin.com/in/andrey-germanov-dev/
Facebook:https://web.facebook.com/AndreyGermanovDev
Twitter:https://twitter.com/GermanovDev

My online services website:https://germanov.dev

Hodně štěstí při kódování!