Të shkruajmë një shfletues të sigurt për zgjerimin

Të shkruajmë një shfletues të sigurt për zgjerimin

Në ndryshim nga arkitektura e njohur «klient-server», aplikacionet e decentralizuara karakterizohen nga:

  • Mungesa e nevojës për të ruajtur një bazë të dhënash me identitetet dhe fjalëkalimet e përdoruesve. Informacioni për aksesin ruhet ekskluzivisht nga vetë përdoruesit, ndërsa verifikimi i saktësisë së tyre ndodh në nivelin e protokollit.
  • Mungesa e nevojës për të përdorur një server. Logjika e aplikacionit mund të realizohet në rrjetin e blockchain, ku gjithashtu është e mundur ruajtja e sasisë së nevojshme të të dhënave.

Ekzistojnë 2 ruajtje relativisht të sigurta për çelësat e përdoruesve — portofolët harduerikë dhe zgjerimet e shfletuesit. Portofolët harduerikë në shumicën e rasteve janë maksimalisht të sigurt, megjithatë janë të komplikuar për t'u përdorur dhe aspak falas, ndërsa zgjerimet e shfletuesve janë një kombinim ideal i sigurisë dhe lehtësisë së përdorimit, dhe gjithashtu mund të jenë krejtësisht falas për përdoruesit përfundimtarë.

Duke marrë parasysh të gjitha këto, ne dëshironim të bënim një zgjerim sa më të sigurt, që thjeshton zhvillimin e aplikacioneve të decentralizuara, duke ofruar një API të thjeshtë për punën me transaksionet dhe nënshkrimet.
Për këtë përvojë ne do t'ju tregojmë më poshtë.

Në artikull do të ketë një udhëzim hap pas hapi se si të shkruhet një zgjerim shfletuesi, me shembuj kodi dhe screenshot-e. Të gjithë kodin mund ta gjeni në repotitë e tij. Çdo commit logjikisht i korrespondon njërit seksion të këtij artikulli.

Një histori e shkurtër e zgjerimeve të shfletuesve

Zgjerimet e shfletuesve ekzistojnë që prej një kohe mjaft të gjatë. Në Internet Explorer ato u shfaqën që në vitin 1999, në Firefox — në vitin 2004. Megjithatë, për një kohë të gjatë nuk kishte një standard të vetëm për zgjerimet.

Mund të thuhet se ai u shfaq për herë të parë me zgjerimet në versionin e katërt të Google Chrome. Natyrisht, nuk kishte asnjë specifikim atëherë, por pikërisht API i Chrome u bë baza e tij: kur fitoi pjesën më të madhe të tregut të shfletuesve dhe kishte një dyqan aplikacionesh të integruar, Chrome në fakt vendosi standardin për zgjerimet e shfletuesve.

Mozilla kishte standardin e saj, por duke parë popullaritetin e zgjerimeve për Chrome, kompania vendosi të bëjë një API të përputhshëm. Në vitin 2015, me iniciativën e Mozilla, në kuadër të World Wide Web Consortium (W3C) u krijua një grup special për të punuar mbi specifikimet e zgjerimeve ndër-browsing.

Baza është një API ekzistues për shtesa për Chrome. Puna u krye me mbështetje të Microsoft-it (Google refuzoi të merrte pjesë në zhvillimin e standardit), dhe rezultati ishte një draft. specifikimet.

Formalish, specifikimi mbështetet nga Edge, Firefox dhe Opera (vini re se në këtë listë nuk është Chrome). Por në të vërtetë standardi është në një masë të madhe i përputhshëm me Chrome, pasi në të vërtetë është shkruar mbi bazën e shtesave të tij. Më shumë informacione rreth API të WebExtensions mund të lexoni. këtu.

Struktura e shtesës.

Skedari i vetëm që është domosdoshmërisht i nevojshëm për shtesën është manifesti (manifest.json). Ai është gjithashtu "pikë e hyrjes" në shtesë.

Manifesti.

Sipas specifikimit, skedari i manifestit është një skedar JSON i vlefshëm. Përshkrimi i plotë i çelësave të manifestit me informacionin se cilët çelësa mbështeten në cilin shfletues, mund të shihet. këtu.

Çelësat, të cilët nuk janë në specifikim, "mund" të injorohen (si Chrome ashtu edhe Firefox shkruajnë për gabimet, por shtesat vazhdojnë të funksionojnë).

Dua të theksoj disa pika.

  1. background — objekt që përfshin fushat e mëposhtme:
    1. scripts — një array skenarësh, të cilat do të ekzekutohen në kontekstin e background-it (do të flasim për këtë pak më vonë);
    2. page — në vend të skenarëve që do të ekzekutohen në një faqe të zbrazët, mund të vendosni HTML me përmbajtje. Në këtë rast, fusha e skenarit do të injorohet, dhe skenarët do të duhet të futen në faqen me përmbajtjen;
    3. persistente — një flag binar, nëse nuk specifikohet, shfletuesi do të "vrasë" procesin e background-it kur ta mendojë se ai nuk po bën asgjë, dhe do ta rindizet sipas nevojës. Në të kundërt, faqja do të shkarkohet vetëm kur të mbyllet shfletuesi. Nuk mbështetet në Firefox.
  2. content_scripts — një array objektesh që lejon ngarkimin e skenarëve të ndryshëm në faqe të ndryshme web. Çdo objekt përmban fushat e rëndësishme të mëposhtme:
    1. matches — modeli url, me të cilin përcaktohet nëse do të përfshihet skenari konkret i përmbajtjes apo jo.
    2. js — lista e skenarëve që do të ngarkohen në këtë ndeshje;
    3. exclude_matches — përjashton nga fusha match URL-të që i përgjigjen kësaj fushe.
  3. page_action — në të vërtetë është një objekt që përgjigjet për ikonën që shfaqet pranë shiritit të adresës në shfletues dhe ndërveprimin me të. Lejon gjithashtu të shfaqë një popup, e cila përcaktohet me HTML, CSS dhe JS.
    1. default_popup — rruga deri në skedarin HTML me ndërfaqen popup, mund të përmbajë CSS dhe JS.
  4. permissions — një array për menaxhimin e të drejtave të shtesës. Ekzistojnë 3 lloje të të drejtave, të cilat përshkruhen në detaj këtu
  5. web_accessible_resources — resurset e shtesës që mund të kërkohen nga faqja e internetit, si imazhe, skedarë JS, CSS, HTML.
  6. externally_connectable — këtu mund të specifikoni në mënyrë të qartë ID-në e shtesave të tjera dhe domainet e faqeve të internetit me të cilat mund të lidheni. Domeni mund të jetë i nivelit të dytë dhe më lart. Nuk punon në Firefox.

Konteksti i ekzekutimit

Shtesa ka tre kontekste ekzekutimi të kodit, që do të thotë se aplikacioni përbëhet nga tre pjesë me nivele të ndryshme aksesimi në API-në e shfletuesit.

Konteksti i Shtesës

Këtu është e aksesueshme pjesa më e madhe e API-së. Në këtë kontekst “jetojnë”:

  1. Faqja e sfondit — pjesa “backend” e shtesës. Skedari specifikohet në manifest sipas çelësit “background”.
  2. Faqja Popup — faqja popup që shfaqet kur klikoni në ikonën e shtesës. Në manifest browser_action -> default_popup.
  3. Faqja e personalizuar — faqja e shtesës, që “jeton” në një tab të veçantë të tipit chrome-extension:///customPage.html.

Ky kontekst ekziston në mënyrë të pavarur nga dritaret dhe tab-et e shfletuesit. Faqja e sfondit ekziston si një ekzemplar të vetëm dhe funksionon gjithmonë (përjashtim — faqja e ngjarjeve, kur skripti i sfondit aktivizohet nga një ngjarje dhe “vdes” pas përfundimit të saj). Faqja Popup ekziston kur është e hapur një dritare popup, dhe Faqja e personalizuar — derisa të jetë e hapur një tab me të. Së këndejmi nuk ka akses në tab-et e tjera dhe përmbajtjen e tyre nga ky kontekst.

Konteksti i skriptit të përmbajtjes

Skedari i skriptit të përmbajtjes aktivizohet për çdo tab të shfletuesit. Ai ka akses në një pjesë të API-së së shtesës dhe në pemën DOM të faqes së internetit. Është skriptet e përmbajtjes që përgjigjen për ndërveprimin me faqen. Shtesat që manipulojnë pemën DOM e bëjnë këtë në skriptet e përmbajtjes – për shembull, bllokuesit e reklamave ose përkthyesit. Gjithashtu, skripti i përmbajtjes mund të komunikojë me faqen përmes standardit postMessage.

Konteksti i faqes së internetit

Kjo është vetë faqja e internetit. Ajo nuk ka asnjë lidhje me shtesën dhe nuk ka akses në të, përveç rasteve kur në manifest eksplicitisht nuk është spesifikuar domeni i kësaj faqeje (për këtë — më poshtë).

Këmbimi i mesazheve

Pjesë të ndryshme të aplikacionit duhet të shkëmbejnë mesazhe me njëra-tjetrën. Për këtë ekziston API runtime.sendMessage për të dërguar një mesazh background dhe tabs.sendMessage për të dërguar një mesazh në faqen (në skriptin e përmbajtjes, popup ose në faqen e internetit në prani të externally_connectable). Më poshtë është një shembull gjatë qasjes në API-në Chrome.

// Сообщением может быть любой JSON сериализуемый объект
const msg = {a: 'foo', b: 'bar'};

// extensionId можно не указывать, если мы хотим послать сообщение 'своему' расширению (из ui или контент скрипта)
chrome.runtime.sendMessage(extensionId, msg);

// Так выглядит обработчик
chrome.runtime.onMessage.addListener((msg) => console.log(msg))

// Можно слать сообщения вкладкам зная их id
chrome.tabs.sendMessage(tabId, msg)

// Получить к вкладкам и их id можно, например, вот так
chrome.tabs.query(
    {currentWindow: true, active : true},
    function(tabArray){
      tabArray.forEach(tab => console.log(tab.id))
    }
)

Për komunikim të plotë, mund të krijoni lidhje nëpërmjet runtime.connect. Në përgjigje do të marrim runtime.Port, në të cilin, për sa kohë që është i hapur, mund të dërgoni çdo numër mesazhesh. Në anën e klientit, për shembull, contentscript, kjo duket kështu:

// Опять же extensionId можно не указывать при коммуникации внутри одного расширения. Подключение можно именовать
const port = chrome.runtime.connect({name: "knockknock"});
port.postMessage({joke: "Knock knock"});
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"});

Server ose background:

// Обработчик для подключения 'своих' вкладок. Контент скриптов, popup или страниц расширения
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."});
    });
});

// Обработчик для подключения внешних вкладок. Других расширений или веб страниц, которым разрешен доступ в манифесте
chrome.runtime.onConnectExternal.addListener(function(port) {
    ...
});

Ka edhe një ngjarje onDisconnect dhe metodën çkëputje.

Skema e aplikacionit

Le të bëjmë një shtesë të shfletuesit që ruan çelësa privatë, ofron akses në informacionin publik (adresa, çelësi publik komunikon me faqen dhe lejon aplikacione të treta të kërkojnë nënshkrim të transaksioneve.

Zhvillimi i aplikacionit

Aplikacioni ynë duhet të ndërveprojë me përdoruesin, si dhe të ofrojë API për faqen për thirrjen e metodave (p.sh., për nënshkrimin e transaksioneve). Nuk mund të mbështetemi vetëm në contentscript këtë, sepse ai ka akses vetëm në DOM, por jo në JS-në e faqes. Nuk mund të lidhemi përmes runtime.connect sepse API-ja duhet të jetë në të gjitha domainet, ndërsa në manifest mund të specifikojmë vetëm specifik. Si rezultat, skema do të duket kështu:

Të shkruajmë një shfletues të sigurt për zgjerimin

Do të ketë një script tjetër — inpage, i cili do ta injektonim në faqe. Ai do të ekzekutohet në kontekstin e saj dhe do të ofrojë një API për të punuar me shtesën.

Fillimi

Të gjithë kodin e shtesës së shfletuesit mund ta gjeni në GitHub. Gjatë përshkrimit do të ketë lidhje në commit-e.

Le të fillojmë me manifestin:

{
  // Emri dhe përshkrimi, versioni. Të gjitha këto do të jenë të dukshme në shfletues në chrome://extensions/?id=<id e shtesës>
  "name": "Signer",
  "description": "Demo i shtesës",
  "version": "0.0.1",
  "manifest_version": 2,

  // Skripti që do të ekzekutohet në background, mund të jenë disa
  "background": {
    "scripts": ["background.js"]
  },

  // Cili HTML të përdorim për popup
  "browser_action": {
    "default_title": "Shtesa Ime",
    "default_popup": "popup.html"
  },

  // Skripte për përmbajtjen.
  // Kemi një objekt: për të gjitha url që fillojnë me http ose https aktivizojmë
  // kontekstin e contentscript me skriptin contentscript.js. Aktivizohet menjëherë pas marrjes së dokumentit për të gjitha frames
  "content_scripts": [
    {
      "matches": [
        "http:/*/*",
        "https:/*/*"
      ],
      "js": [
        "contentscript.js"
      ],
      "run_at": "document_start",
      "all_frames": true
    }
  ],
  // Aksesi në localStorage dhe idle api është i lejuar
  "permissions": [
    "storage",
    // "unlimitedStorage",
    //"clipboardWrite",
    "idle"
    //"activeTab",
    //"webRequest",
    //"notifications",
    //"tabs"
  ],
  // Këtu specificohen burimet, të cilat do të kenë akses faqja web. Domethënë, ato do të mund të kërkohen me fetch ose thjesht xhr
  "web_accessible_resources": ["inpage.js"]
}

Krijojmë skedarët e zbrazët background.js, popup.js, inpage.js dhe contentscript.js. Shtojmë popup.html — dhe aplikacioni ynë tashmë mund të ngarkohet në Google Chrome për të verifikuar nëse funksionon.

Për ta verifikuar këtë, mund të marrim kodin këtu. Për më tepër, përveç asaj që bëmë, në lidhje është konfigurimi i ndërtimit të projektit me webpack. Për të shtuar aplikacionin në shfletues, në chrome://extensions duhet të zgjidhni ngarko të paketuara dhe dosjen me zgjerimin përkatës — në rastin tonë dist.

Të shkruajmë një shfletues të sigurt për zgjerimin

Tani zgjerimi ynë është instaluar dhe funksionon. Të aktivizoni mjetet për zhvilluesit për kontekste të ndryshme mund të bëhet si vijon:

popup ->

Të shkruajmë një shfletues të sigurt për zgjerimin

Qasja në konsolën e skenarit të përmbajtjes bëhet përmes konsolës së vetë faqes ku është aktivizuar.Të shkruajmë një shfletues të sigurt për zgjerimin

Këmbimi i mesazheve

Pra, na nevojitet të vendosim dy kanale komunikimi: inpage <-> background dhe popup <-> background. Natyrisht, mund të dërgojmë thjesht mesazhe në port dhe të shpikim protokollin tonë, por më pëlqen më shumë qasja që e kam parë në një projekt me kod të hapur, metamask.

Ky është një zgjerim shfletuesi për të punuar me rrjetin Ethereum. Në të, pjesët e ndryshme të aplikacionit komunikojnë përmes RPC duke përdorur bibliotekën dnode. Ajo lejon organizimin e shkëmbimit mjaft shpejt dhe lehtësisht, nëse si transport i ofrojmë një stream të nodejs (kuptohet objekti që implementon të njëjtin ndërfaqe):

import Dnode from "dnode/browser";

// Në këtë shembull, le të pranojmë se klienti e thërret larg funksionet në server, megjithëse asgjë nuk na pengon ta bëjmë këtë dyanshëm.

// Server
// API që duam të ofrojmë
const dnode = Dnode({
    hello: (cb) => cb(null, "world")
})
// Transporti mbi të cilin do të funksionojë dnode. Çdo stream nodejs. Në shfletues ekziston biblioteka 'readable-stream'
connectionStream.pipe(dnode).pipe(connectionStream)

// Klienti
const dnodeClient = Dnode() // Thirrja pa argument do të thotë se ne nuk ofrojmë API në anën tjetër

// Do të shfaqë në konsolë world
dnodeClient.once('remote', remote => {
    remote.hello(((err, value) => console.log(value)))
})

Tani do të krijojmë klasën e aplikacionit. Ajo do të krijojë objekte API për popup dhe faqen web, si dhe do të krijojë dnode për ta:

import Dnode from 'dnode/browser';

export class SignerApp {

    // Kthen objekti API për ui
    popupApi(){
        return {
            hello: cb => cb(null, 'world')
        }
    }

    // Kthen objekti API për faqen
    pageApi(){
        return {
            hello: cb => cb(null, 'world')
        }
    }

    // Shkone popup ui
    connectPopup(connectionStream){
        const api = this.popupApi();
        const dnode = Dnode(api);

        connectionStream.pipe(dnode).pipe(connectionStream);

        dnode.on('remote', (remote) => {
            console.log(remote)
        })
    }

    // Shkone faqen
    connectPage(connectionStream, origin){
        const api = this.popupApi();
        const dnode = Dnode(api);

        connectionStream.pipe(dnode).pipe(connectionStream);

        dnode.on('remote', (remote) => {
            console.log(origin);
            console.log(remote)
        })
    }
}

Nga tani e tutje, në vend të objektit global Chrome, ne përdorim extentionApi, i cili i qaset Chrome në shfletuesin e Google dhe browser në të tjerët. Kjo bëhet për qëllime ndër-bazë, por brenda këtij artikulli mund të ishte përdorur thjesht 'chrome.runtime.connect'.

Le të krijojmë një instancë të aplikacionit në skriptin background:

import {extensionApi} from './utils/extensionApi';
import {PortStream} from './utils/PortStream';
import {SignerApp} from './SignerApp';

const app = new SignerApp();

// onConnect aktivizohet kur 'proceset' (contentscript, popup, ose faqja e zgjerimit) lidhen
extensionApi.runtime.onConnect.addListener(connectRemote);

function connectRemote(remotePort) {
    const processName = remotePort.name;
    const portStream = new PortStream(remotePort);
    // Në vendosjen e lidhjes mund të specifikohet emri, me këtë emër ne identifikojmë kush është lidhur me ne, kontent script apo ui
    if (processName === 'contentscript'){
        const origin = remotePort.sender.url
        app.connectPage(portStream, origin)
    }else{
        app.connectPopup(portStream)
    }
}

Pasi dnode funksionon me stream-et, dhe ne marrim portin, na duhet një klasë-adapter. Ajo është krijuar duke përdorur bibliotekën readable-stream, e cila implementon nodejs-stream-et në shfletues:

import {Duplex} from 'readable-stream';

export class PortStream extends Duplex{
    constructor(port){
        super({objectMode: true});
        this._port = port;
        port.onMessage.addListener(this._onMessage.bind(this));
        port.onDisconnect.addListener(this._onDisconnect.bind(this))
    }

    _onMessage(msg) {
        if (Buffer.isBuffer(msg)) {
            delete msg._isBuffer;
            const data = new Buffer(msg);
            this.push(data)
        } else {
            this.push(msg)
        }
    }

    _onDisconnect() {
        this.destroy()
    }

    _read(){}

    _write(msg, encoding, cb) {
        try {
            if (Buffer.isBuffer(msg)) {
                const data = msg.toJSON();
                data._isBuffer = true;
                this._port.postMessage(data)
            } else {
                this._port.postMessage(msg)
            }
        } catch (err) {
            return cb(new Error('PortStream - disconnected'))
        }
        cb()
    }
}

Tani krijojmë një lidhje në UI:

import {extensionApi} from ".\/utils\/extensionApi";
import {PortStream} from ".\/utils\/PortStream";
import Dnode from 'dnode\/browser';

const DEV_MODE = process.env.NODE_ENV !== 'production';

setupUi().catch(console.error);

async function setupUi(){
    \/\/ Po të njëjtën mënyrë si në klasën e aplikacionit, krijojmë një port, e mbështjellim në stream, krijojmë dnode
    const backgroundPort = extensionApi.runtime.connect({name: 'popup'});
    const connectionStream = new PortStream(backgroundPort);

    const dnode = Dnode();

    connectionStream.pipe(dnode).pipe(connectionStream);

    const background = await new Promise(resolve => {
        dnode.once('remote', api => {
            resolve(api)
        })
    });

    \/\/ E bëjmë objektin API të aksesueshëm nga konsola
    if (DEV_MODE){
        global.background = background;
    }
}

Më pas krijojmë një lidhje në skriptin e përmbajtjes:

import {extensionApi} from ".\/utils\/extensionApi";
import {PortStream} from ".\/utils\/PortStream";
import PostMessageStream from 'post-message-stream';

setupConnection();
injectScript();

function setupConnection(){
    const backgroundPort = extensionApi.runtime.connect({name: 'contentscript'});
    const backgroundStream = new PortStream(backgroundPort);

    const pageStream = new PostMessageStream({
        name: 'content',
        target: 'page',
    });

    pageStream.pipe(backgroundStream).pipe(pageStream);
}

function injectScript(){
    try {
        \/\/ injekto scriptin në faqe
        let script = document.createElement('script');
        script.src = extensionApi.extension.getURL('inpage.js');
        const container = document.head || document.documentElement;
        container.insertBefore(script, container.children[0]);
        script.onload = () => script.remove();
    } catch (e) {
        console.error('Injekimi dështoi.', e);
    }
}

Pasi që API na nevojitet jo në skriptin e përmbajtjes, por drejtpërdrejt në faqe, ne bëjmë dy gjëra:

  1. Krijojmë dy streame. Njëri — drejt faqes, mbi postMessage. Për këtë ne përdorim këtë paketë nga krijuesit e metamask. Streami i dytë — në background përmes portit të marrë nga runtime.connect. I lidhim ata. Tani faqja do të ketë një stream deri në background.
  2. Injektojmë skriptin në DOM. Shkarkojmë skriptin (që aksesimi për të ishte i lejuar në manifest) dhe krijojmë tag script me përmbajtjen e tij brenda:

import PostMessageStream from 'post-message-stream';
import {extensionApi} from ".\/utils\/extensionApi";
import {PortStream} from ".\/utils\/PortStream";

setupConnection();
injectScript();

function setupConnection(){
    \/\/ Streami në background
    const backgroundPort = extensionApi.runtime.connect({name: 'contentscript'});
    const backgroundStream = new PortStream(backgroundPort);

    \/\/ Streami në faqe
    const pageStream = new PostMessageStream({
        name: 'content',
        target: 'page',
    });

    pageStream.pipe(backgroundStream).pipe(pageStream);
}

function injectScript(){
    try {
        \/\/ injekto skriptin në faqe
        let script = document.createElement('script');
        script.src = extensionApi.extension.getURL('inpage.js');
        const container = document.head || document.documentElement;
        container.insertBefore(script, container.children[0]);
        script.onload = () => script.remove();
    } catch (e) {
        console.error('Injekimi dështoi.', e);
    }
}

Tani krijojmë objektin api në inpage dhe e vendosim global:

import PostMessageStream from 'post-message-stream';
import Dnode from 'dnode/browser';

setupInpageApi().catch(console.error);

async function setupInpageApi() {
    // Stream to content script
    const connectionStream = new PostMessageStream({
        name: 'page',
        target: 'content',
    });

    const dnode = Dnode();

    connectionStream.pipe(dnode).pipe(connectionStream);

    // Getting the API object
    const pageApi = await new Promise(resolve => {
        dnode.once('remote', api => {
            resolve(api)
        })
    });

    // Access via window
    global.SignerApp = pageApi;
}

Kemi gati Remote Procedure Call (RPC) me një API të veçantë për faqen dhe UI. Kur lidhet një faqe e re me background, mund ta shohim këtë:

Të shkruajmë një shfletues të sigurt për zgjerimin

API i zbrazët dhe origjina. Në anën e faqes mund të thërrasim funksionin hello kështu:

Të shkruajmë një shfletues të sigurt për zgjerimin

Të punuarit me funksione klimë në JS-në moderne është një mjet i tepruar, prandaj le të shkruajmë një ndihmës të vogël për krijimin e dnode, i cili lejon kalimin në objektin API në utils.

Objektet API tani do të duken kështu:

export class SignerApp {

    popupApi() {
        return {
            hello: async () => "world"
        }
    }

...

}

Marrja e objektit nga remote në këtë mënyrë:

import {cbToPromise, transformMethods} from "../../src/utils/setupDnode";

const pageApi = await new Promise(resolve => {
    dnode.once('remote', remoteApi => {
        // Me ndihmën e utiliteteve, ne ndryshojmë të gjitha callback në promise
        resolve(transformMethods(cbToPromise, remoteApi))
    })
});

Dhe thirrjet e funksioneve kthejnë një promise:

Të shkruajmë një shfletues të sigurt për zgjerimin

Versioni me funksione asinkrone është në dispozicion këtu.

Në përgjithësi, qasja me RPC dhe stream-et duket mjaft fleksibël: mund të përdorim shumëfishimin e stream-eve dhe të krijojmë disa API të ndryshme për detyra të ndryshme. Në parim, dnode mund të përdoret kudo, gjëja më e rëndësishme është të mbështjellim transportin në formën e një stream-i nodejs.

Një alternativë është formati JSON, i cili realizon protokollin JSON RPC 2. Megjithatë, ai punon me transportet specifike (TCP dhe HTTP(S)), të cilat në rastin tonë nuk janë të aplikueshme.

Stati i brendshëm dhe localStorage

Na nevojitet të ruajmë staten e brendshëm të aplikacionit — të paktën, çelësat për nënshkrim. Mund të shtojmë mjaft lehtë staten në aplikacion dhe metodat për ta ndryshuar atë në popup API:

import {setupDnode} from "./utils/setupDnode";

export class SignerApp {

    constructor() {
        this.store = {
            keys: [],
        };
    }

    addKey(key) {
        this.store.keys.push(key)
    }

    removeKey(index) {
        this.store.keys.splice(index, 1)
    }

    popupApi() {
        return {
            addKey: async (key) => this.addKey(key),
            removeKey: async (index) => this.removeKey(index)
        }
    }

    ...

} 

Në background do ta mbështjellim gjithçka në një funksion dhe do ta shkruajmë objektin e aplikacionit në window, që të mund të punojmë me të nga konsola:

import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {SignerApp} from "./SignerApp";

const DEV_MODE = process.env.NODE_ENV !== 'production';

setupApp();

function setupApp() {
    const app = new SignerApp();

    if (DEV_MODE) {
        global.app = app;
    }

    extensionApi.runtime.onConnect.addListener(connectRemote);

    function connectRemote(remotePort) {
        const processName = remotePort.name;
        const portStream = new PortStream(remotePort);
        if (processName === 'contentscript') {
            const origin = remotePort.sender.url;
            app.connectPage(portStream, origin)
        } else {
            app.connectPopup(portStream)
        }
    }
}

Do të shtojmë disa çelësa nga konsola UI dhe do të shohim se çfarë ndodhi me gjendjen:

Të shkruajmë një shfletues të sigurt për zgjerimin

Gjatë rinisjes, gjendja duhet të bëhet persistente, në mënyrë që çelësat të mos humbasin.

Do ta ruajmë në localStorage, duke e riparë çdo herë që ndodhin ndryshime. Në vazhdim, qendra do të ketë gjithashtu nevojë për qasje në të, dhe dëshirojmë gjithashtu të abonojmë për ndryshimet. Bazuar në këtë, do të ishte e lehtë të krijonim një magazinë observuese (observable storage) dhe të abonoheshim për ndryshimet e saj.

Do të përdorim bibliotekën mobx (https://github.com/mobxjs/mobx). Zgjedhja ra mbi të, pasi nuk kam punuar me të më parë, dhe isha shumë i interesuar ta studjoja.

Do të shtojmë inicializimin e gjendjes fillestare dhe do ta bëjmë magazinën observable:

import {observable, action} from 'mobx';
import {setupDnode} from "./utils/setupDnode";

export class SignerApp {

    constructor(initState = {}) {
        // Jashtë, magazina do të mbetet po ai objekt, vetëm se tani të gjitha fushat e saj janë proxy, të cilat mbajnë nën mbikëqyrje qasjen ndaj tyre
        this.store =  observable.object({
            keys: initState.keys || [],
        });
    }

    // Metodat që ndryshojnë observable zakonisht mbyllen me dekorator
    @action
    addKey(key) {
        this.store.keys.push(key)
    }

    @action
    removeKey(index) {
        this.store.keys.splice(index, 1)
    }

    ...

}

«Nën kapak», mobx zëvendëson të gjitha fushat e magazinës me proxy dhe kap të gjitha qasjet ndaj tyre. Këto qasje mund të regjistrohen.

Më tutje, do të përdor shpesh termin "me ndryshim", megjithëse kjo nuk është krejtësisht e saktë. Mobx ndjek qasjen pikërisht në fusha. Përdoren getter dhe setter të objekteve proxy, të cilat krijon biblioteka.

Dekoratorët e veprimit shërbejnë për dy qëllime:

  1. Në modin e rreptë me flamurin enforceActions, mobx ndalon ndryshimin e gjendjes drejtpërdrejt. Një sjellje e mirë është të punosh pikërisht në modin e rreptë.
  2. Edhe nëse funksioni ndryshon gjendjen disa herë – për shembull, ne ndryshojmë disa fusha në disa rreshta kodi – oborserët njoftohen vetëm pas përfundimit të tij. Kjo është veçanërisht e rëndësishme për frontend-in, ku përditësimet e tepërta të gjendjes çojnë në renderim të panevojshëm të elementeve. Në rastin tonë, as e para as e dyta nuk janë shumë të rëndësishme, megjithatë do të ndjekim praktikat më të mira. Dekoratorët zakonisht ngjiten në të gjitha funksionet që ndryshojnë gjendjen e fushave të vëzhguara.

Në background do të shtojmë inicializimin dhe ruajtjen e gjendjes në localStorage:

import {reaction, toJS} from 'mobx';
import {extensionApi} from ". /utils/extensionApi";
import {PortStream} from ". /utils/PortStream";
import {SignerApp} from ". /SignerApp";
// Metoda ndihmëse. Shkruajnë / lexojnë objektin në / nga localStorage në formatin e një stringu JSON me çelësin 'store'
import {loadState, saveState} from ". /utils/localStorage";

const DEV_MODE = process.env.NODE_ENV !== 'production';

setupApp();

function setupApp() {
    const initState = loadState();
    const app = new SignerApp(initState);

    if (DEV_MODE) {
        global.app = app;
    }

    // Konfigurimi i qëndrueshmërisë së gjendjes

    // Rezultati i reaksionit caktohet një variabli që mund të anulojë abonimin. Ne nuk na nevojitet këtë, është lënë si shembull
    const localStorageReaction = reaction(
        () => toJS(app.store), // Funksioni-selektor i të dhënave
        saveState // Funksioni që do të thirret me ndryshimin e të dhënave që kthehen nga selektori
    );

    extensionApi.runtime.onConnect.addListener(connectRemote);

    function connectRemote(remotePort) {
        const processName = remotePort.name;
        const portStream = new PortStream(remotePort);
        if (processName === 'contentscript') {
            const origin = remotePort.sender.url
            app.connectPage(portStream, origin)
        } else {
            app.connectPopup(portStream)
        }
    }
}

Funksioni reaction është interesant këtu. Ai ka dy argumente:

  1. Seletori i të dhënave.
  2. Një trajtim që do të thirret me këto të dhëna çdo herë që ato ndryshojnë.

Ndryshe nga redux, ku ne qartë marrim gjendjen si një argument, mobx mban mend se në cilat observable po iu qasemi brenda selektorit, dhe vetëm me ndryshimin e tyre thërret trajtimin.

Është e rëndësishme të kuptohet se si mobx e vendos se në cilat observable ne abonuemi. Nëse në kod do të shkruaja selektorin kështu() => app.store, atëherë reakcioni nuk do të thirret kurrë, pasi vetë magazina nuk është e vëzhgueshme, vetëm fushat e saj janë të tilla.

Nëse do të shkruaja kështu () => app.store.keys, atëherë përsëri nuk do të ndodhte asgjë, pasi në rast se shtojmë / heqim elemente nga lista, lidhja e saj nuk do të ndryshojë.

Mobx e për herë të parë ekzekuton funksionin e selektorit dhe monitoron vetëm ato observable, të cilave u qasëm. Kjo bëhet përmes getter proxy. Prandaj, këtu është përdorur funksioni i integruar toJS. Ky funksion kthen një objekt të ri, ku të gjithë proxy zëvendësohen me fushat origjinale. Gjatë ekzekutimit, ai lexon të gjitha fushat e objektit – pra, aktivizohen getter-t.

Në konsolën popup përsëri do të shtojmë disa çelësa. Këtë herë ata përfshihen edhe në localStorage:

Të shkruajmë një shfletues të sigurt për zgjerimin

Kur të rindizet faqja e background-it, informacioni mbetet në vend.

I gjithë kodi i aplikacionit deri në këtë moment mund të shikohet këtu.

Ruajtja e sigurta e çelësave privatë

Të ruash çelësat privatë në formë të hapur është e pasigurt: gjithmonë ekziston mundësia që dikush të të hakojë, të fitojë qasje në kompjuterin tënd, e kështu me radhë. Prandaj, në localStorage ne do të ruajmë çelësat në formë të enkriptuar me fjalëkalim.

Për më shumë siguri, do të shtojmë një gjendje locked në aplikacion, në të cilën nuk do të ketë qasje në çelësa fare. Ne do ta kalojmë automatikisht zgjerimin në gjendjen locked pas një kohë.

Mobx lejon ruajtjen e vetëm një grupi minimal të të dhënave, ndërsa pjesa tjetër llogaritet automatikisht mbi bazën e tyre. Këto janë ato që quhen computed properties. Ato mund të krahasohen me view në bazat e të dhënave:

import {observable, action} from 'mobx';
import {setupDnode} from ".\/utils\/setupDnode";
// Utilitete për enkriptimin e sigurt të vargjeve. Përdorim crypto-js
import {encrypt, decrypt} from ".\/utils\/cryptoUtils";

export class SignerApp {
    constructor(initState = {}) {
        this.store = observable.object({
            // Ruajmë fjalëkalimin dhe çelësat e enkriptuar. Nëse fjalëkalimi është null - aplikacioni është locked
            password: null,
            vault: initState.vault,

            // Getter për fushat e llogaritura. Mund të tërheqim një analogji me view në db.
            get locked(){
                return this.password == null
            },
            get keys(){
                return this.locked ?
                    undefined :
                    SignerApp._decryptVault(this.vault, this.password)
            },
            get initialized(){
                return this.vault !== undefined
            }
        })
    }
    // Një vendosje e një depoje të zbrazët me një fjalëkalim të ri
    @action
    initVault(password){
        this.store.vault = SignerApp._encryptVault([], password)
    }
    @action
    lock() {
        this.store.password = null
    }
    @action
    unlock(password) {
        this._checkPassword(password);
        this.store.password = password
    }
    @action
    addKey(key) {
        this._checkLocked();
        this.store.vault = SignerApp._encryptVault(this.store.keys.concat(key), this.store.password)
    }
    @action
    removeKey(index) {
        this._checkLocked();
        this.store.vault = SignerApp._encryptVault([
                ...this.store.keys.slice(0, index),
                ...this.store.keys.slice(index + 1)
            ],
            this.store.password
        )
    }

    ... // kodi i lidhjes dhe api

    // private
    _checkPassword(password) {
        SignerApp._decryptVault(this.store.vault, password);
    }

    _checkLocked() {
        if (this.store.locked){
            throw new Error('App është e bllokuar')
        }
    }

    // Metoda për enkriptimin/deenkriptimin e depozita
    static _encryptVault(obj, pass){
        const jsonString = JSON.stringify(obj)
        return encrypt(jsonString, pass)
    }

    static _decryptVault(str, pass){
        if (str === undefined){
            throw new Error('Depozita nuk është inicializuar')
        }
        try {
            const jsonString = decrypt(str, pass)
            return JSON.parse(jsonString)
        }catch (e) {
            throw new Error('Fjalëkalim i gabuar')
        }
    }
}

Tani ne ruajmë vetëm çelësat e enkriptuar dhe fjalëkalimin. E gjithë e tjera llogaritet. Kalimi në gjendjen locked e bëjmë duke hequr fjalëkalimin nga gjendja. Në API-në publike ka dalë një metodë për inicializimin e depozitës.

Për enkriptimin janë shkruar utilitetet duke përdorur crypto-js:

import CryptoJS from 'crypto-js'

// Përdoret për të vështirësuar thyerjen e fjalëkalimit përmes brute force. Për çdo variant fjalëkalimi sulmuesi do të duhet të bëjë 5000 hash-e.
function strengthenPassword(pass, rounds = 5000) {
    while (rounds-- > 0){
        pass = CryptoJS.SHA256(pass).toString()
    }
    return pass
}

export function encrypt(str, pass){
    const strongPass = strengthenPassword(pass);
    return CryptoJS.AES.encrypt(str, strongPass).toString()
}

export function decrypt(str, pass){
    const strongPass = strengthenPassword(pass)
    const decrypted = CryptoJS.AES.decrypt(str, strongPass);
    return decrypted.toString(CryptoJS.enc.Utf8)
}

Shfletuesi ka një idle API, nëpërmjet të cilit mund të subscribohet për ngjarjen — ndryshimi i state-it. State, përkatësisht, mund të jetë i lirë, aktiv dhe locked. Për idle mund të konfiguroni një time-out, ndërsa locked vendoset kur OS vetë bllokohet. Gjithashtu do të ndryshojmë selektorin për ruajtjen në localStorage:

import {reaction, toJS} from 'mobx';
import {extensionApi} from ". /utils /extensionApi";
import {PortStream} from ". /utils /PortStream";
import {SignerApp} from ". /SignerApp";
import {loadState, saveState} from ". /utils /localStorage";

const DEV_MODE = process.env.NODE_ENV !== 'production';
const IDLE_INTERVAL = 30;

setupApp();

function setupApp() {
    const initState = loadState();
    const app = new SignerApp(initState);

    if (DEV_MODE) {
        global.app = app;
    }

    // Tani e thërrasim qartë fushën, të cilës do të ketë qasje, reagimi do të funksionojë siç duhet
    reaction(
        () => ({
            vault: app.store.vault
        }),
        saveState
    );

    // Time-out i paaktivitetit, kur do të ndodhë ngjarja
    extensionApi.idle.setDetectionInterval(IDLE_INTERVAL);
    // Nëse përdoruesi ka bllokuar ekranin ose ka qenë pa aktivitet për intervalin e caktuar, e bllokojmë aplikacionin
    extensionApi.idle.onStateChanged.addListener(state => {
        if (['locked', 'idle'].indexOf(state) > -1) {
            app.lock()
        }
    });

    // Konektimi me kontekste të tjera
    extensionApi.runtime.onConnect.addListener(connectRemote);

    function connectRemote(remotePort) {
        const processName = remotePort.name;
        const portStream = new PortStream(remotePort);
        if (processName === 'contentscript') {
            const origin = remotePort.sender.url
            app.connectPage(portStream, origin)
        } else {
            app.connectPopup(portStream)
        }
    }
}

Kodi deri në këtë hap ndodhet këtu.

Transaksionet

Pra, arritëm deri te e rëndësishmja: krijimi dhe nënshkrimi i transaksioneve në blockchain. Ne do të përdorim blockchain WAVES dhe bibliotekën waves-transactions.

Për të filluar, do të shtojmë në state një array mesazhesh që duhet nënshkruar, pastaj — metodat për të shtuar një mesazh të ri, për të konfirmuar nënshkrimin dhe për të refuzuar:

import {action, observable, reaction} from 'mobx';
import uuid from 'uuid/v4';
import {signTx} from '@waves/waves-transactions'
import {setupDnode} from "./utils/setupDnode";
import {decrypt, encrypt} from "./utils/cryptoUtils";

export class SignerApp {

    ...

    @action
    newMessage(data, origin) {
        // Për çdo mesazh krijojmë metadata me id, status, kohën e krijimit etj.
        const message = observable.object({
            id: uuid(), // Identifikues, duke përdorur uuid
            origin, // Origjina do të mund ta shohim më vonë në ndërfaqe
            data, //
            status: 'new', // Do të ketë katër statuse: new, signed, rejected dhe failed
            timestamp: Date.now()
        });
        console.log(`mesazh i ri: ${JSON.stringify(message, null, 2)}`);

        this.store.messages.push(message);

        // Kthejmë një premis brenda të cilit mobx monitoron ndryshimet e mesazhit. Sa herë që statusi ndryshon ne do ta zgjidhim atë
        return new Promise((resolve, reject) => {
            reaction(
                () => message.status, // Do të monitorojmë statusin e mesazhit
                (status, reaction) => { // argumenti i dytë është një referencë në vetë reagimin, për ta shkatërruar brenda thirrjes
                    switch (status) {
                        case 'signed':
                            resolve(message.data);
                            break;
                        case 'rejected':
                            reject(new Error('Përdoruesi hodhi poshtë mesazhin'));
                            break;
                        case 'failed':
                            reject(new Error(message.err.message));
                            break;
                        default:
                            return
                    }
                    reaction.dispose()
                }
            )
        })
    }
    @action
    approve(id, keyIndex = 0) {
        const message = this.store.messages.find(msg => msg.id === id);
        if (message == null) throw new Error(`Nuk ka mesazh me id:${id}`);
        try {
            message.data = signTx(message.data, this.store.keys[keyIndex]);
            message.status = 'signed'
        } catch (e) {
            message.err = {
                stack: e.stack,
                message: e.message
            };
            message.status = 'failed'
            throw e
        }
    }
    @action
    reject(id) {
        const message = this.store.messages.find(msg => msg.id === id);
        if (message == null) throw new Error(`Nuk ka mesazh me id:${id}`);
        message.status = 'rejected'
    }

    ...
}

Kur marrim një mesazh të ri, ne i shtojmë atij metadata, bëjmë observable dhe e shtojmë në store.messages.

Nëse nuk e bëjmë këtë manualisht, mobx do ta bëjë vetë kur e shtojmë në array-n e mesazheve. Megjithatë, do të krijojë një objekt të ri, për të cilin nuk do të kemi një referencë, dhe ajo do të nevojitet për hapat e ardhshëm. observable Më pas kthejmë një premis, i cili zgjidhet kur statusi i mesazhit ndryshon. Statusin e monitoron reagimi, i cili do të "vrasë" veten e tij kur të ndryshojë statusi.

Kodi i metodave

reject approve dhe është shumë i thjeshtë: thjesht ndërrimi statusit të mesazhit, pas nënshkrimit të tij, nëse është e nevojshme. është shumë i thjeshtë: thjesht ndryshojmë statusin e mesazhit, pasi ta kemi nënshkruar atë, nëse është e nevojshme.

Mirato dhe refuzo i çojmë në API UI, newMessage — në faqen e API:

export class SignerApp {
    ...
    popupApi() {
        return {
            addKey: async (key) => this.addKey(key),
            removeKey: async (index) => this.removeKey(index),

            lock: async () => this.lock(),
            unlock: async (password) => this.unlock(password),
            initVault: async (password) => this.initVault(password),

            approve: async (id, keyIndex) => this.approve(id, keyIndex),
            reject: async (id) => this.reject(id)
        }
    }

    pageApi(origin) {
        return {
            signTransaction: async (txParams) => this.newMessage(txParams, origin)
        }
    }

    ...
}

Tani do të provojmë të nënshkruajmë një transaksion me ndihmën e extension:

Të shkruajmë një shfletues të sigurt për zgjerimin

Në thelb, gjithçka është gati, mbetet të shtojmë një UI të thjeshtë.

UI

Ndërfaqes i nevojitet akses në gjendjen e aplikacionit. Në anën e UI do të bëjmë observable gjendjen dhe do të shtojmë në API një funksion që do ta ndryshojë këtë gjendje. Do të shtojmë observable në objektin API, e marrë nga background:

import {observable} from 'mobx'
import {extensionApi} from ".\/utils\/extensionApi";
import {PortStream} from ".\/utils\/PortStream";
import {cbToPromise, setupDnode, transformMethods} from ".\/utils\/setupDnode";
import {initApp} from ".\/ui\/index";

const DEV_MODE = process.env.NODE_ENV !== 'production';

setupUi().catch(console.error);

async function setupUi() {
    // Lidhni në port, krijoni një stream prej tij
    const backgroundPort = extensionApi.runtime.connect({name: 'popup'});
    const connectionStream = new PortStream(backgroundPort);

    // Krijoni një observable bosh për gjendjen e background-it
    let backgroundState = observable.object({});
    const api = {
        // Japim background-it një funksion që do të përditësojë observable
        updateState: async state => {
            Object.assign(backgroundState, state)
        }
    };

    // Krijojmë objektin RPC
    const dnode = setupDnode(connectionStream, api);
    const background = await new Promise(resolve => {
        dnode.once('remote', remoteApi => {
            resolve(transformMethods(cbToPromise, remoteApi))
        })
    });

    // Shtojmë në background observable me gjendjen
    background.state = backgroundState;

    if (DEV_MODE) {
        global.background = background;
    }

    // Nisja e ndërfaqes
    await initApp(background)
}

Në fund, ne nisnim renderimin e ndërfaqes së aplikacionit. Ky është një aplikacion react. Objekti background thjesht kalon përmes props. E drejta, natyrisht, është të bëjmë një shërbim të veçantë për metodat dhe një store për gjendjen, por në kuadër të këtij artikulli kjo është e mjaftueshme:

import {render} from 'react-dom'
import App from '.\/App'
import React from "react";

// Inicializoni aplikacionin me objektin background si props
export async function initApp(background){
    render(
        ,
        document.getElementById('app-content')
    );
}

Me ndihmën e mobx, është shumë e thjeshtë të nisni renderimin kur të ndryshojnë të dhënat. Ne thjesht e varim dekoratorin observer nga paketa mobx-react se komponenti, dhe renderimi do të thirret automatikisht kur ndodhin ndryshime në çdo observable që referohet komponentit. Nuk nevojiten as mapStateToProps ose connect, si në redux. E gjithë kjo funksionon menjëherë «nga kutia»:

import React, {Component, Fragment} from 'react'
import {observer} from "mobx-react";
import Init from '.\/components\/Initialize'
import Keys from '.\/components\/Keys'
import Sign from '.\/components\/Sign'
import Unlock from '.\/components\/Unlock'

@observer \/\/ Një komponent me këtë dekorator do të thotë se metodi render do të thirret automatikisht nëse ka ndryshime në observable të cilat ai i referohet
export default class App extends Component {

    \/\/ Në të vërtetë, është e saktë të nxjerrësh logjikën e renderimit të faqeve në routing dhe të mos përdorësh operatorë ternerë të ngulitur,
    \/\/ dhe të lidhen observable dhe metodat background drejtpërdrejt me ato komponente që i përdorin ato
    render() {
        const {keys, messages, initialized, locked} = this.props.background.state;
        const {lock, unlock, addKey, removeKey, initVault, deleteVault, approve, reject} = this.props.background;

        return <fragment>
            {!initialized
                ?
                <init oninit="{initVault}/">
                :
                locked
                    ?
                    <unlock onunlock="{unlock}/">
                    :
                    messages.length &gt; 0
                        ?
                        <sign keys="{keys}" message="{messages[messages.length" - 1]} onapprove="{approve}" onreject="{reject}/">
                        :
                        <keys keys="{keys}" onadd="{addKey}" onremove="{removeKey}/">
            }
            <div>
                {!locked &amp;&amp; <button onclick="{()" > lock()}&gt;Blloko Aplikacionin</button>}
                {initialized &amp;&amp; <button onclick="{()" > deleteVault()}&gt;Fshi të gjitha çelësat dhe inico</button>}
            </div>
        </Fragment>
    }
}

Komponentët e tjerë mund të shihen në kod në dosjen UI.

Tani në klasën e aplikacionit duhet të krijojmë një selektor të gjendjes për UI dhe, kur ajo ndryshon, të njoftojmë UI-në. Për këtë do të shtojmë metodën getState dhe reaction, e cila thërret remote.updateState:

import {action, observable, reaction} from 'mobx';
import uuid from 'uuid/v4';
import {signTx} from '@waves/waves-transactions';
import {setupDnode} from './utils/setupDnode';
import {decrypt, encrypt} from './utils/cryptoUtils';

export class SignerApp {

    ...

    // publike
    getState() {
        return {
            keys: this.store.keys,
            messages: this.store.newMessages,
            initialized: this.store.initialized,
            locked: this.store.locked
        }
    }

    ...

    //
    connectPopup(connectionStream) {
        const api = this.popupApi();
        const dnode = setupDnode(connectionStream, api);

        dnode.once('remote', (remote) => {
            // Krijojmë një reagim për ndryshimet në gjendje, i cili do të thërrasë procedurën e largët dhe do të përditësojë gjendjen në procesin e UI
            const updateStateReaction = reaction(
                () => this.getState(),
                (state) => remote.updateState(state),
                // Argumenti i tretë mund të ndihmojë në kalimin e parametrave. fireImmediatly do të thotë që reagimi do të ekzekutohet menjëherë herën e parë.
                // Kjo është e nevojshme për të marrë gjendjen fillestare. Delay lejon të vendosim debounce
                {fireImmediately: true, delay: 500}
            );
            // Të heqim abonimin kur klienti ndërpritet
            dnode.once('end', () => updateStateReaction.dispose())

        })
    }

    ...
}

Kur marrim objektin remote krijohet reaction për ndryshimin e gjendjes, që thërret funksionin në anën e UI.

Prekja e fundit — do të shtojmë shfaqjen e mesazheve të reja në ikonen e zgjerimit:

function setupApp() {
...

    // Reagimi për vendosjen e tekstit të badge-it.
    reaction(
        () => app.store.newMessages.length > 0 ? app.store.newMessages.length.toString() : '',
        text => extensionApi.browserAction.setBadgeText({text}),
        {fireImmediately: true}
    );

...
}

Tani, aplikacioni është gati. Web-faqet mund të kërkojnë nënshkrimin e transaksioneve:

Të shkruajmë një shfletues të sigurt për zgjerimin

Të shkruajmë një shfletues të sigurt për zgjerimin

Kodi është i disponueshëm në këtë linkun.

Përfundim

Nëse e keni lexuar artikullin deri në fund, por keni pyetje të tjera, mund t'i bëni ato në repo me zgjerimin. Aty do të gjeni commit-e për secilën hap të shënuar.

Nëse jeni të interesuar të shihni kodin e një zgjerimi të vërtetë, atëherë do të mund ta gjeni këtë këtu.

Kodi, repozitori dhe përshkrimi i punës nga siemarell

Burimi: habr.com

Blini hosting të besueshëm për faqe interneti me mbrojtje nga DDoS, serverë VPS VDS 🔥 Blini hosting të besueshëm për faqe interneti me mbrojtje nga DDoS, serverë VPS VDS | ProHoster