Krijojmë një zgjerim të sigurt për shfletuesin

Krijojmë një zgjerim të sigurt për shfletuesin

Në krahasim me arkitekturën e zakonshme "klient-server", aplikacionet decentralizuara karakterizohen nga:

  • Mungesa e nevojës për të ruajtur një bazë të dhënash me identifikuesit dhe fjalëkalimet e përdoruesve. Informacioni për qasje ruhet ekskluzivisht nga vetë përdoruesit, ndërsa konfirmimi 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ë ekzekutohet në rrjetin e blockchain, ku gjithashtu është e mundur ruajtja e sasisë së nevojshme të të dhënave.

Ekzistojnë 2 depozita relativisht të sigurta për çelësat e përdoruesve - portofolat harduerikë dhe zgjerimet e shfletuesit. Portofolat harduerikë janë në shumicë maksimalisht të sigurt, megjithatë janë të komplikuar për t'u përdorur dhe aspak falas, ndërsa zgjerimet e shfletuesit janë një kombinim ideal i sigurisë dhe thjeshtësisë në përdorim dhe gjithashtu mund të jenë plotësisht falas për përdoruesit e fundit.

Duke të gjitha këto, ne vendosëm të zhvillojmë një zgjerim maksimalisht të sigurt, që thjeshton zhvillimin e aplikacioneve të decentralizuara, duke ofruar një API të lehtë për të punuar me transaksionet dhe nënshkrimet.
Për këtë përvojë do t'ju flasim më poshtë.

Në këtë artikull do të ketë një udhëzues hapat-hapa se si të shkruani një zgjerim për shfletuesin, me shembuj kodi dhe screenshot-e. Të gjithë kodin e gjeni në repozitorit. Çdo commit korrespondon logjikisht me një seksion të këtij artikulli.

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

Zgjerimet e shfletuesve kanë ekzistuar prej kohësh. Ato shfaqen në Internet Explorer që në vitin 1999, dhe në Firefox – në vitin 2004. Megjithatë, për një kohë të gjatë, nuk kishte një standard të vetëm për zgjerimet.

Êshtë e drejtë të thuhet se ai u shfaq së bashku me zgjerimet në versionin e katërt të Google Chrome. Sigurisht, nuk kishte asnjë specifikim atëherë, por pikërisht API i Chrome u bë baza e saj: duke ndarë një pjesë të madhe të tregut të shfletuesve dhe duke pasur një dyqan aplikacionesh të integruar, Chrome në thelb përcaktoi standardin për zgjerimet e shfletuesve.

Mozilla kishte standardin e vet, por, duke parë popullaritetin e zgjerimeve për Chrome, kompania vendosi të krijojë një API të kompatibilshëm. Në vitin 2015, me iniciativën e Mozilla-s, u formua një grup i veçantë në kuadër të World Wide Web Consortium (W3C) për të punuar mbi specifikimet e zgjerimeve multidixhitale.

Si bazë u mor API-ja ekzistuese e zgjerimeve për Chrome. Punimet u mbështetën nga Microsoft (Google refuzoi të merrte pjesë në zhvillimin e standardit), dhe rezultat i kësaj ishte një draft specifikacioni.

Formalisht, specifikimin e mbështesin Edge, Firefox dhe Opera (vini re se në këtë listë mungon Chrome). Por në të vërtetë, standardi është në shumë aspekte kompatibil edhe me Chrome, pasi është shkruar faktikisht mbi bazën e zgjerimeve të tij. Më shumë mbi WebExtensions API mund të lexoni këtu.

Struktura e zgjerimit

Skeda e vetme që nevojitet për një zgjerim është manifesti (manifest.json). Ai gjithashtu është ‘pika e hyrjes’ në zgjerim.

Manifesti

Sipas specifikimit, skeda e manifestit është një skedë JSON e vlefshme. Një përshkrim i plotë i çelësave të manifestit me informacion mbi se cilët çelësa mbështeten në cilin shfletues mund të shikohet këtu.

Çelësat që nuk janë në specifikim, "mund" të injorohen (si Chrome ashtu edhe Firefox shfaqin gabime, por zgjerimet vazhdojnë të funksionojnë).

Dëshiroj të theksoj disa pika.

  1. background — një objekt që përfshin fushat e mëposhtme:
    1. scripts — një array skriptesh që do të ekzekutohen në kontekstin e background (do të flasim për këtë më vonë);
    2. page — në vend të skripteve që do të ekzekutohen në një faqe të zbrazët, mund të caktoni HTML me përmbajtje. Në këtë rast fusha script do të injorohet, dhe skripti do të duhet të integrohet në faqen me përmbajtje;
    3. persistent — një flamur biner, nëse nuk përcaktohet, atëherë shfletuesi do të "vrasë" procesin e background, kur të mendojë se ai nuk bën asgjë, dhe do ta rilidhë atë sipas nevojës. Në të kundërt, faqa do të shkarkohet vetëm kur të mbyllej shfletuesi. Nuk mbështetet në Firefox.
  2. content_scripts — një array objektesh që lejon ngarkimin e skripteve të ndryshme në faqe të ndryshme web. Çdo objekt përmban fushat e rëndësishme si më poshtë:
    1. matchespatterni url, sipas të cilit përcaktohet nëse do të përfshihet një skript specifik përmbajtjeje apo jo.
    2. js — një listë skriptesh që do të ngarkohen në këtë përputhje;
    3. exclude_matches — përjashton nga kjo fushë përputhje URL-të që përmbushin këtë fushë.
  3. veprimi_i_faqes — në fakt është një objekt që është përgjegjës për ikonën që shfaqet afër shiritit të adresës në shfletues dhe ndërveprimeve me të. Lejon gjithashtu të tregoni një dritare popup, e cila përcaktohet me HTML, CSS dhe JS të saj.
    1. popup_default — rruga e skedarit HTML me ndërfaqen popup, mund të përmbajë CSS dhe JS.
  4. lejet — një varg për menaxhimin e të drejtave të zgjerimit. Ekzistojnë 3 lloje të të drejtave, të cilat janë përshkruar në detaje këtu
  5. burime_që_mund_të_përfitojnë_web — burimet e zgjerimit që mund të kërkohen nga faqe të uebit, për shembull, figura, skedarë JS, CSS, HTML.
  6. të_konektueshëm_jashtë — këtu mund të specifikoni qartë ID-të e zgjerimeve të tjera dhe domainet e faqeve web nga të cilat mund të lidheni. Domaini mund të jetë i nivelit të dytë dhe më lart. Nuk funksionon në Firefox.

Konteksti i ekzekutimit

Zgjerimi ka tre kontekste të ekzekutimit të kodit, pra, aplikacioni përbëhet nga tri pjesë me nivele të ndryshme qasjeje në API-në e shfletuesit.

Konteksti i Zgjerimit

Këtu është e disponueshme shumica e API-ve. Në këtë kontekst "banojnë":

  1. Faqja e sfondit — pjesa “backend” e zgjerimit. Skedari指定在manniLa Që "sfondi".
  2. Faqja Popup — faqja popup që shfaqet kur klikoni në ikonën e shtesës. Në manifest browser_action -> popup_default.
  3. Faqe e Personalizuar — faqja e shtesës, "jeton" në një skedë të veçantë të tipit chrome-extension:///customPage.html.

Ky kontekst ekziston pavarësisht nga dritaret dhe skedat e shfletuesit. Faqja e sfondit ekziston në një kopje të vetme dhe punon gjithmonë (përjashtim — faqja e ngjarjeve, kur skedari i prapambetjes aktivizohet nga një ngjarje dhe "vdes" pas përfundimit të saj). Faqja Popup ekziston kur është hapur një dritare popup, dhe Faqe e Personalizuar — ndërsa skeda e saj është e hapur. Nuk ka qasje në skeda të tjera dhe përmbajtjen e tyre nga ky kontekst.

Konteksi i skenarëve të përmbajtjes

Skedari i skenarit të përmbajtjes ekzekutohet së bashku me çdo skedë të shfletuesit. Ai ka qasje në një pjesë të API-së së shtesës dhe në pemën DOM të faqes në internet. Skenarët e përmbajtjes janë përgjegjës për ndërveprimin me faqen. Shtesat që manipulojnë pemën DOM e bëjnë këtë në skenarët e përmbajtjes – për shembull, bllokuesit e reklamave ose përkthyesit. Gjithashtu, skenari i përmbajtjes mund të komunikojë me faqen përmes standardit postMessage.

Konteksi i faqes në internet

Kjo është vetë faqja e internetit. Ajo nuk ka asnjë lidhje dhe qasje në zgjatimin, përveç rasteve kur në manifest është e shprehur qartë domene i kësaj faqeje (për këtë, do flasim më poshtë).

Shkëmbimi i mesazheve

Pjesët e 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 (skripti i përmbajtjes, popup ose faqja e internetit nëse është e pranishme të_konektueshëm_jashtë). Më poshtë është një shembull kur i drejtohemi API-t të 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 një komunikim të plotë, mund të krijoni lidhje përmes runtime.connect. Në përgjigje do të marrim runtime.Port, në të cilin, derisa të jetë 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"});

Serveri 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 gjithashtu një ngjarje onDisconnect dhe metodën disconnect.

Skema e aplikacionit

Le të krijojmë një zgjatje për shfletuesin, që ruan çelësat privatë, ofron akses në informacionin publik (adresa, çelësi publik komunikon me faqen dhe lejon aplikacioneve të jashtme të kërkojnë nënshkrimin e transaksioneve.

Zhvillimi i aplikacionit

Aplikacioni ynë duhet të ndërveprojë me përdoruesin dhe të ofrojë një API për thirrjen e metodave (p.sh., për nënshkrimin e transaksioneve). Një i vetëm contentscript nuk do të jetë i mjaftueshëm, sepse ai ka qasje vetëm në DOM, por jo në JS-në e faqes. Nuk mund të lidhemi përmes runtime.connect sepse API-ja është e nevojshme në të gjitha domenet, ndërsa në manifest mund të specifikohen vetëm të caktuara. Në përfundim, skema do të duket kështu:

Krijojmë një zgjerim të sigurt për shfletuesin

Do të ketë një skenar tjetër — inpage, të cilin ne do ta injektojmë në faqe. Ai do të ekzekutohet në kontekstin e saj dhe do të ofrojë një API për të punuar me zgjerimin.

Fillimi

E gjithë kodi i zgjerimit të shfletuesit është në dispozicion në GitHub. Gjatë përshkrimit do të ketë lidhje në komitetet.

Të fillojmë me manifestin:

{
  // Emri dhe përshkrimi, versioni. Të gjitha këto do të shihen në shfletuesin në chrome://extensions/?id=<idh i zgjerimit>
  "name": "Signer",
  "description": "Demo i zgjerimit",
  "version": "0.0.1",
  "manifest_version": 2,

  // Skriptet që do të ekzekutohen në background, mund të ketë disa
  "background": {
    "scripts": ["background.js"]
  },

  // Cilën html do të përdorim për popup
  "browser_action": {
    "default_title": "Zgjerimi im",
    "default_popup": "popup.html"
  },

  // Skriptet e përmbajtjes.
  // Ne kemi një objekt: për të gjitha url që fillojnë me http ose https ne aktivizojmë
  // kontekstin e skriptit të përmbajtjes me skriptin contentscript.js. Aktivizohet menjëherë pas marrjes së dokumentit për të gjitha iframe-t
  "content_scripts": [
    {
      "matches": [
        "http://*/*",
        "https://*/*"
      ],
      "js": [
        "contentscript.js"
      ],
      "run_at": "document_start",
      "all_frames": true
    }
  ],
  // Lejohet qasja në localStorage dhe idle api
  "permissions": [
    "storage",
    // "unlimitedStorage",
    //"clipboardWrite",
    "idle"
    //"activeTab",
    //"webRequest",
    //"notifications",
    //"tabs"
  ],
  // Këtu specifikohen burimet, të cilat do të kenë akses faqja në web. Pra, ato mund të kërkohen me fetche' m apo thjesht xhr
  "web_accessible_resources": ["inpage.js"]
}

Krijojmë skedarë të 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'u siguruar se funksionon.

Për të siguruar këtë, mund të merrni kodin këtu. Përveç asaj që bëmë, në lidhje është konfiguruar një ndërtim projekti me ndihmën e webpack. Për të shtuar aplikacionin në shfletues, në chrome://extensions duhet të zgjidhni 'load unpacked' dhe folderin me zgjerimin përkatës — në rastin tonë dist.

Krijojmë një zgjerim të sigurt për shfletuesin

Tani zgjerimi ynë është instaluar dhe funksionon. Të aktivizoni mjetet për zhvilluesit për kontekste të ndryshme mund të bëhet në këtë mënyrë:

popup ->

Krijojmë një zgjerim të sigurt për shfletuesin

Qasja në konsolën e skriptit të përmbajtjes realizohet përmes konsolës së vetë faqes, ku është aktivizuar.Krijojmë një zgjerim të sigurt për shfletuesin

Shkëmbimi i mesazheve

Pra, na nevojitet të vendosim dy kanale komunikimi: inpage <-> background dhe popup <-> background. Sigurisht, mund të dërgoni mesazhe në port dhe të shpikni protokollin tuaj, por më pëlqen më shumë qasja që kam parë në projektin me kod të hapur metamask.

Ky është një zgjerim shfletues për të punuar me rrjetin Ethereum. Në të, pjesët e ndryshme të aplikacionit komunikojnë përmes RPC me ndihmën e bibliotekës dnode. Ajo e bën mjaft të shpejtë dhe të lehtë organizimin e shkëmbimit, nëse si transport i ofron stream të nodejs (kuptojmë objektin që implementon të njëjtin ndërfaqe):

import Dnode from "dnode/browser";

// Në këtë shembull, le të supozojmë se klienti thërret funksionet në server në distancë, megjithatë, asgjë nuk na pengon ta bëjmë këtë dyanshëm.

// Serveri
// API që ne duam të ofrojmë
const dnode = Dnode({
    hello: (cb) => cb(null, "world")
})
// Transporti në të cilin do të punojë dnode. Çdo stream nodejs. Në shfletues ka bibliotekën 'readable-stream'.
connectionStream.pipe(dnode).pipe(connectionStream)

// Klienti
const dnodeClient = Dnode() // Thirrja pa argument do të thotë që ne nuk ofrojmë API nga ana tjetër.

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

Tani ne do të krijojmë një klasë aplikacioni. Ajo do të krijojë objekte API për popup dhe faqe web, si dhe do të krijojë dnode për to:

import Dnode from 'dnode/browse';

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')
        }
    }

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

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

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

    // Lidhen faqja
    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)
        })
    }
}

Këtu dhe tutje, në vend të objektit global Chrome, ne përdorim extentionApi, i cili i qaset Chrome në shfletuesin Google dhe browser në të tjerët. Ky veprim bëhet për kromit të shumë shfletuesve, por në kuadër të këtij artikulli, mund të përdorim thjesht 'chrome.runtime.connect'.

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

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

const app = new SignerApp();

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

function connectRemote(remotePort) {
    const processName = remotePort.name;
    const portStream = new PortStream(remotePort);
    // Në momentin e vendosjes së lidhjes, mund të caktosh emrin, me këtë emër ne përcaktojmë kush është lidhur me ne, contentscript apo ui
    if (processName === 'contentscript'){
        const origin = remotePort.sender.url
        app.connectPage(portStream, origin)
    }else{
        app.connectPopup(portStream)
    }
}

Duke qenë se dnode punon me rrjedha, dhe ne marrim një port, na nevojitet një klasë-adapter. Ajo është realizuar me anë të bibliotekës readable-stream, e cila implementon rrjedhat nodejs 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 tani krijojmë lidhjen 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 ashtu, si në klasën e aplikacionit krijojmë port, e mbështjellim në stream, bëjmë 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)
        })
    });

    // 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 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('Injekti dështoi.', e);
    }
}

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

  1. Krijojmë dy kanale. Njëri — në drejtim të faqes, mbi postMessage. Për këtë ne përdorim këtë paketë nga krijuesit e metamask. Kanalin e dytë — në drejtim të background-it mbi portin e marrë nga runtime.connect. I lidhim ata. Tani faqja do të ketë një kanal me bbackground.
  2. Injektim skripti në DOM. Shkarkojmë skriptin (qasje e tij është lejuar në manifest) dhe krijojmë etiketën 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(){
    // Kthemi në background
    const backgroundPort = extensionApi.runtime.connect({name: 'contentscript'});
    const backgroundStream = new PortStream(backgroundPort);

    // Kthemi 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('Injeksioni dështoi.', e);
    }
}

Tani krijojmë një objekt api në inpage dhe e vendosim në global:

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

setupInpageApi().catch(console.error);

async function setupInpageApi() {
    // Kthemi në kontent skript
    const connectionStream = new PostMessageStream({
        name: 'page',
        target: 'content',
    });

    const dnode = Dnode();

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

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

    // Qasje përmes window
    global.SignerApp = pageApi;
}

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

Krijojmë një zgjerim të sigurt për shfletuesin

API bosh dhe burimi. Në anën e faqeve, ne mund të thërrasim funksionin hello në këtë mënyrë:

Krijojmë një zgjerim të sigurt për shfletuesin

Të punosh me callback-funksione në JS moderne është e papërshtatshme, prandaj do të shkruajmë një ndihmës të vogël për krijimin e dnode, i cili lejon të dërgosh 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 i ndërrojmë të gjitha callback në promise
        resolve(transformMethods(cbToPromise, remoteApi))
    })
});

Dhe thirrjet e funksioneve kthejnë një promise:

Krijojmë një zgjerim të sigurt për shfletuesin

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

Në përgjithësi, qasja me RPC dhe stream-et duket mjaft fleksibile: ne mund të përdorim stream multiplexing dhe të krijojmë disa API të ndryshme për detyra të ndryshme. Në thelb, dnode mund të përdoret kudo, gjëja kryesore është të mbështjellim transportin si një stream në nodejs.

Një alternativë është formati JSON, i cili implementon protokollin JSON RPC 2. Megjithatë, ai punon me transportet specifike (TCP dhe HTTP(S)), që në rastin tonë nuk është i aplikueshëm.

Stresi i brendshëm dhe localStorage

Na nevojitet të ruajmë stresin e brendshëm të aplikacionit — së paku, çelësat për nënshkrim. Mund ta shtojmë mjaft lehtë këtë stres dhe metodat për ta ndryshuar në API-në e popup-it:

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 mbajmë gjithçka në një funksion dhe do ta regjistrojmë objektin e aplikacionit në window, për ta pasur të mundur 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 dhe do të shohim se çfarë kemi arritur me stresin:

Krijojmë një zgjerim të sigurt për shfletuesin

Stati duhet të bëhet persistent, në mënyrë që çelësat të mos humbasin gjatë rinisjes.

Do ta ruajmë në localStorage, duke e tejkaluar me çdo ndryshim. Më vonë do të nevojitet gjithashtu për UI, dhe do të doja të abonohesha për ndryshimet. Nga kjo do të ishte e dobishme të bëhej një ruajtje e dukshme (observable storage) dhe të abonoheshim për ndryshime.

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 doja shumë ta studijoja.

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, që monitorojnë aksesin e tyre
        this.store = observable.object({
            keys: initState.keys || [],
        });
    }

    // Metodat që ndryshojnë observable priren të mbështillen 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ësoi të gjitha fushat e magazinës me proxy dhe kap të gjitha qasjet ndaj tyre. Këto qasje mund të abonohet.

Më vonë do të përdor shpesh termin “në ndryshim”, megjithatë, kjo nuk është krejtësisht e saktë. Mobx ndjek pikërisht qasjen në fushat. Përdoren getter dhe setter të objekteve proxy që krijon biblioteka.

Dekoratorët e veprimit kanë dy qëllime:

  1. Në mënyrë strikte me flamurin enforceActions, mobx ndalon ndryshimin e gjendjes drejtpërdrejt. Konsiderohet një praktikë e mirë që të punohet pikërisht në mënyrë strikte.
  2. Pavarësisht nëse funksioni ndryshon gjendjen disa herë – për shembull, ne ndryshojmë disa fusha në disa rreshta kodi, – observuesit njoftohen vetëm pas përfundimit të tij. Kjo është veçanërisht e rëndësishme për frontend-in, ku ndryshimet e panevojshme të gjendjes çonin në renderimin aksidental të elementeve. Në rastin tonë, as e para, as e dyta nuk janë shumë të aplikueshme, megjithatë, ne do të ndjekim praktikat më të mira. Dekoratorët zakonisht përdoren për 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. Shkruan/lexon objektin në/nga localStorage si një varg 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;
    }

    // Konfiguroni ruajtjen e gjendjes

    // Rezultati i reaction i jepet një variabli, në mënyrë që abonimi të mund të anulohet. Na nevojitet kjo për shembuj
    const localStorageReaction = reaction(
        () => toJS(app.store), // Funksioni-selektor i të dhënave
        saveState // Funksioni, i cili 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 këtu është interesant. Ai ka dy argumente:

  1. Selektoni të dhënat.
  2. Një trajtim që do të thirret me këto të dhëna çdo herë që ato ndryshojnë.

Në ndryshim nga redux, ku ne marrim qartë gjendjen si një argument, mobx mban mend se cilat observable po përdorim brenda selektorit, dhe vetëm me ndryshimin e tyre thërret trajtuesin.

Është e rëndësishme të kuptojmë se si mobx vendos për të cilat observable ne jemi të regjistruar. Nëse do të kisha shkruar selektorin si kjo() => app.store, atëherë reagimi nuk do të thirrej kurrë, pasi vetë depozita nuk është e vëzhgueshme, vetëm fushat e saj janë të tilla.

Nëse do të kisha shkruar kështu () => app.store.keys, edhe përsëri nuk do të ndodhte asgjë, pasi me shtimin/heqjen e elementeve të shumës, referenca ndaj saj nuk do të ndryshonte.

Mobx për herë të parë ekzekuton funksionin selektor dhe ndjek vetëm ato observable, të cilat kemi pasur qasje. Kjo bëhet përmes getter-ëve të proxy. toJS. Ajo kthen një objekt të ri, ku të gjitha proksit janë zëvendësuar me fushat origjinale. Gjatë ekzekutimit ajo lexon të gjitha fushat e objektit – si pasojë, aktivizohen getter-ët.

Në konzolën pop-up, përsëri do të shtojmë disa çelësa. Këtë herë ata ranë në localStorage gjithashtu:

Krijojmë një zgjerim të sigurt për shfletuesin

Kur rifreskojmë faqen e prapme, informatat mbeten në vend.

Të gjitha kodet e aplikacionit deri në këtë pikë mund të shihen këtu.

Ruajtje e sigurt e çelësave privatë

Të ruash çelësat privatë në formë të hapur është e pasigurt: gjithmonë ekziston mundësia që të thyhen, të marrin qasje në kompjuterin tuaj dhe kështu me radhë. Prandaj, në localStorage, ne do të ruajmë çelësat në formë të koduar me fjalëkalim.

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

Mobx lejon që të ruhen vetëm minimalet e dhënave, ndërsa pjesa tjetër llogaritet automatikisht në bazë të tyre. Këto janë ato që quhen properties të llogaritura. Ato mund të krahasohen me view në bazat e të dhënave:

import {observable, action} from 'mobx';
import {setupDnode} from "./utils/setupDnode";
// Utilitet për enkriptimin e sigurt të stringjeve. Përdor 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ë i bllokuar
            password: null,
            vault: initState.vault,

            // Getterët për fushat e llogaritura. Mund të bëhet 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
            }
        })
    }
    // Inizializimi i 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/dekriptimin e depozitës
    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ë inizializuar')
        }
        try {
            const jsonString = decrypt(str, pass)
            return JSON.parse(jsonString)
        } catch (e) {
            throw new Error('Fjalëkalim i gabuar')
        }
    }
}

Tani tani ruajmë vetëm çelësat e enkriptuar dhe fjalëkalimin. Çdo gjë tjetër llogaritet. Ne bëjmë kalimin në gjendjen e bllokuar duke hequr fjalëkalimin nga gjendja. Në API-në publike ka një metodë për inicializimin e ruajtjes.

Për enkriptim janë shkruar utilita me përdorimin e crypto-js:

import CryptoJS from 'crypto-js'

// Përdoret për të komplikuar gjetjen e fjalëkalimit përmes provimeve. Për çdo variant fjalëkalimi, sulmuesit do t'i duhet të bëjnë 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)
}

Bashkimi i shfletuesit ka idle API, përmes së cilës mund të abonoheni në ngjarjen — ndryshimi i gjendjes. Gjendja, përkatësisht, mund të jetë idle, active dhe locked. Për idle mund të konfiguroni një kohëzgjatje, ndërsa locked vendoset kur vetë OS 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 do ta bëjmë qartë se do të thërrasim fushën që do të ketë akses, reagimi do të funksionojë normalisht
    reaction(
        () => ({
            vault: app.store.vault
        }),
        saveState
    );

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

    // Lidhu 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 ndaj, kemi arritur në të rëndësishmen: 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ë gjendje një array mesazhesh që duhen nënshkruar, pastaj – metoda për shtimin e një mesazhi të ri, konfirmimin e nënshkrimit dhe refuzimin:

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) {
        // Krijojmë metadata për secilën mesazh me id, status, kohë krijimi, etj.
        const message = observable.object({
            id: uuid(), // Identifikues, duke përdorur uuid
            origin, // Origjina do të shfaqet më vonë në ndërfaqe
            data, //
            status: 'new', // Do të ketë katër status: 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 së cilës mobx monitoron ndryshimet e mesazhit. Sapokë statusi të ndryshojë, ne do ta mbarojmë atë
        return new Promise((resolve, reject) => {
            reaction(
                () => message.status, // Do të vëzhgojmë statusin e mesazhit
                (status, reaction) => { // argumenti i dytë është referenca për vetë reaksionin, që mund ta shkatërrojmë brenda thirrjes
                    switch (status) {
                        case 'signed':
                            resolve(message.data);
                            break;
                        case 'rejected':
                            reject(new Error('Përdoruesi refuzoi 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 merrni një mesazh të ri, ne shtojmë metadata në të, bëjmë observable dhe e shtojmë në store.messages.

Nëse nuk e bëni këtë observable manualisht, atëherë mobx do ta bëjë vetë gjatë shtimit në masivin messages. Megjithatë, ai do të krijojë një objekt të ri, për të cilin nuk do të kemi një referencë, e cila do të jetë e nevojshme për hapin tjetër.

Pastaj kthejmë një premisë, e cila zgjidhet kur ndryshon statusi i mesazhit. Statusin e monitoron një reagim, i cili do të "vrasë" veten kur të ndodhë ndryshimi i statusit.

Kodi i metodave approve dhe reject është shumë i thjeshtë: ne thjesht ndryshojmë statusin e mesazhit, duke e nënshkruar atë nëse është e nevojshme.

Approve dhe reject i siham në API UI, newMessage — në API të faqes:

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 ta provojmë të nënshkruajmë një transaksion me zgjerimin:

Krijojmë një zgjerim të sigurt për shfletuesin

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

UI

Interfejsi ka nevojë për qasje në gjendjen e aplikacionit. Në anën e UI do të bëjmë observable gjendjen dhe do të shtojmë një funksion në API që do ta ndryshojë këtë gjendje. Do të shtojmë observable në objektin API, i 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() {
    // Lidheni me portin, krijoni një stream prej tij
    const backgroundPort = extensionApi.runtime.connect({name: 'popup'});
    const connectionStream = new PortStream(backgroundPort);

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

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

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

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

    // Nisja e interfejsit
    await initApp(background)
}

Në fund, ne fillojmë renderimin e ndërfaqes së aplikacionit. Ky është një aplikacion react. Objekti background thjesht kalon përmes props. Sigurisht, do të ishte më mirë të krijonim një shërbim të veçantë për metodat dhe një store për gjendjen, por në kuadrin e këtij artikulli, kjo është e mjaftueshme:

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

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

Me mobx, është shumë e lehtë të fillosh renderimin kur ndryshojnë të dhënat. Ne thjesht përdorim dekoratorin observer nga paketa mobx-react në komponent, dhe renderimi do të thirret automatikisht kur ndodhin ndryshime në çdo observable të referuar nga komponenti. Nuk nevojiten 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 // Komponenti me këtë dekorator do të thërrasë automatikisht metodën render, nëse ndodhin ndryshime në observable të cilat ai referohet
export default class App extends Component {

    // Është e saktë të nxjerrësh logjikën e renditjes së faqeve në routing dhe të mos përdorësh operatorë ternarë të thelluar,
    // dhe të lidhësh observable dhe metodat background direkt me ato komponentë 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;Fshij të gjithë çelësat dhe inico</button>}
            </div>
        </Fragment>
    }
}

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

Tani në klasën e aplikacionit, është e nevojshme të bëjmë një selektor gjendjeje për UI dhe të njoftojmë UI-në kur ndryshohet. Për këtë do të shtojmë metodën getState dhe reaction, që 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 {

    ...

    // public
    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 ndryshimin e gjendjes, i cili do të thërrasë procedurën e largët dhe do të përditësojë gjendjen në procesin UI
            const updateStateReaction = reaction(
                () => this.getState(),
                (state) => remote.updateState(state),
                // Argumenti i tretë mund të kalojë parametrat. fireImmediatly do të thotë që reagimi do të zbatohet të parën herë menjëherë.
                // Kjo është e nevojshme për të marrë gjendjen fillestare. Delay lejon vendosjen e debounce
                {fireImmediately: true, delay: 500}
            );
            // Do të heqim abonimin kur klienti shkyçet
            dnode.once('end', () => updateStateReaction.dispose())

        })
    }

    ...
}

Kur merrni një objekt remote krijohet reaction për ndryshime në gjendje, e cila thërret funksionin në anën e UI.

Detaji i fundit — le të shtojmë shfaqjen e mesazheve të reja në ikonën e shtrirjes:

function setupApp() {
...

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

...
}

Prandaj, aplikacioni është gati.Faqet në internet mund të kërkojnë nënshkrimin e transaksioneve:

Krijojmë një zgjerim të sigurt për shfletuesin

Krijojmë një zgjerim të sigurt për shfletuesin

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

Përfundimi

Nëse e keni lexuar artikullin deri në fund, por keni pyetje, mund t'i bëni ato në repozitari me shtesën. Atje do të gjeni gjithashtu commit-et për secilën hap të shënuar.

Dhe nëse jeni të interesuar të shikoni kodin e një shtese të vërtetë, do të mund ta gjeni këtë këtu.

Kodi, repo i kësaj dhe përshkrimi i funksionimit nga siemarell

Burimi: habr.com

Bleni hostim të besueshëm për faqe me mbrojtje nga DDoS, serverë VPS VDS 🔥 Bleni hostim të besueshëm për faqe me mbrojtje nga DDoS, serverë VPS VDS | ProHoster