Tikteb estensjoni tal-browser sigura

Tikteb estensjoni tal-browser sigura

B'differenza mill-arkitettura komuni "klijent-server", l-applikazzjonijiet deċentralizzati huma kkaratterizzati minn:

  • M'hemmx bżonn li taħżen database b'logins u passwords tal-utent. L-informazzjoni tal-aċċess hija maħżuna esklussivament mill-utenti nfushom, u l-konferma tal-awtentiċità tagħhom isseħħ fil-livell tal-protokoll.
  • M'hemmx għalfejn tuża server. Il-loġika tal-applikazzjoni tista 'tiġi eżegwita fuq netwerk blockchain, fejn huwa possibbli li jinħażen l-ammont meħtieġ ta' data.

Hemm 2 ħażniet relattivament sikuri għaċ-ċwievet tal-utent - kartieri tal-ħardwer u estensjonijiet tal-browser. Il-kartieri tal-ħardwer huma l-aktar siguri ħafna, iżda diffiċli biex jintużaw u 'l bogħod milli ħielsa, iżda l-estensjonijiet tal-browser huma l-kombinazzjoni perfetta ta' sigurtà u faċilità ta 'użu, u jistgħu wkoll ikunu kompletament ħielsa għall-utenti finali.

Meta wieħed iqis dan kollu, ridna nagħmlu l-estensjoni l-aktar sigura li tissimplifika l-iżvilupp ta 'applikazzjonijiet deċentralizzati billi nipprovdu API sempliċi biex taħdem ma' tranżazzjonijiet u firem.
Aħna ngħidulek dwar din l-esperjenza hawn taħt.

L-artiklu se jkun fih struzzjonijiet pass pass dwar kif tikteb estensjoni tal-browser, b'eżempji ta' kodiċi u screenshots. Tista' ssib il-kodiċi kollu ġewwa repożitorji. Kull impenn jikkorrispondi loġikament għal taqsima ta 'dan l-artikolu.

Storja qasira tal-estensjonijiet tal-browser

L-estensjonijiet tal-browser ilhom jeżistu għal żmien twil. Huma dehru fl-Internet Explorer lura fl-1999, fil-Firefox fl-2004. Madankollu, għal żmien twil ħafna ma kien hemm l-ebda standard uniku għall-estensjonijiet.

Nistgħu ngħidu li deher flimkien ma 'estensjonijiet fir-raba' verżjoni ta 'Google Chrome. Naturalment, ma kien hemm l-ebda speċifikazzjoni dakinhar, iżda kienet l-API tal-Chrome li saret il-bażi tagħha: wara li rebaħ il-biċċa l-kbira tas-suq tal-brawżers u li kellu maħżen tal-applikazzjoni integrat, Chrome fil-fatt stabbilixxa l-istandard għall-estensjonijiet tal-browser.

Mozilla kellha l-istandard tagħha stess, iżda meta rat il-popolarità tal-estensjonijiet tal-Chrome, il-kumpanija ddeċidiet li tagħmel API kompatibbli. Fl-2015, fuq inizjattiva ta 'Mozilla, inħoloq grupp speċjali fi ħdan il-World Wide Web Consortium (W3C) biex jaħdem fuq speċifikazzjonijiet ta' estensjoni bejn il-browser.

L-estensjonijiet tal-API eżistenti għal Chrome ttieħdu bħala bażi. Ix-xogħol sar bl-appoġġ ta 'Microsoft (Google irrifjutat li tipparteċipa fl-iżvilupp tal-istandard), u bħala riżultat deher abbozz speċifikazzjonijiet.

Formalment, l-ispeċifikazzjoni hija appoġġjata minn Edge, Firefox u Opera (innota li Chrome mhuwiex f'din il-lista). Iżda fil-fatt, l-istandard huwa fil-biċċa l-kbira kompatibbli mal-Chrome, peress li fil-fatt huwa miktub ibbażat fuq l-estensjonijiet tiegħu. Tista' taqra aktar dwar il-WebExtensions API hawn.

Struttura ta 'estensjoni

L-uniku fajl li huwa meħtieġ għall-estensjoni huwa l-manifest (manifest.json). Huwa wkoll il-"punt tad-dħul" għall-espansjoni.

Manifest

Skont l-ispeċifikazzjoni, il-fajl manifest huwa fajl JSON validu. Deskrizzjoni sħiħa taċ-ċwievet manifest b'informazzjoni dwar liema ċwievet huma appoġġjati f'liema browser jista' jaraha hawn.

Ċwievet li mhumiex fl-ispeċifikazzjoni "jistgħu" jiġu injorati (kemm Chrome kif ukoll Firefox jirrappurtaw żbalji, iżda l-estensjonijiet ikomplu jaħdmu).

U nixtieq niġbed l-attenzjoni għal xi punti.

  1. isfond — oġġett li jinkludi l-oqsma li ġejjin:
    1. skripts — firxa ta' skripts li se jiġu eżegwiti fil-kuntest tal-isfond (se nitkellmu dwar dan ftit aktar tard);
    2. paġna - minflok skripts li se jiġu esegwiti f'paġna vojta, tista 'tispeċifika html bil-kontenut. F'dan il-każ, il-qasam tal-iskript se jiġi injorat, u l-iskripts iridu jiddaħħlu fil-paġna tal-kontenut;
    3. jippersistu — bandiera binarja, jekk ma tkunx speċifikata, il-browser “joqtol” il-proċess ta’ l-isfond meta jqis li m’hu qed jagħmel xejn, u jerġa’ jibda jekk ikun meħtieġ. Inkella, il-paġna tinħatt biss meta l-browser jingħalaq. Mhux appoġġjat fil-Firefox.
  2. content_scripts — firxa ta’ oġġetti li tippermettilek tagħbija skripts differenti għal paġni tal-web differenti. Kull oġġett fih l-oqsma importanti li ġejjin:
    1. logħbiet - url tal-mudell, li jiddetermina jekk skript ta' kontenut partikolari hux se jiġi inkluż jew le.
    2. js — lista ta' skripts li se jiġu mgħobbija f'din il-partita;
    3. exclude_matches - jeskludi mill-qasam match URLs li jaqblu ma' dan il-qasam.
  3. page_action - fil-fatt huwa oġġett li huwa responsabbli għall-ikona li tintwera ħdejn il-bar tal-indirizz fil-browser u l-interazzjoni magħha. Jippermettilek ukoll li turi tieqa popup, li hija definita bl-użu tal-HTML, CSS u JS tiegħek stess.
    1. default_popup — mogħdija għall-fajl HTML bl-interface popup, jista’ jkun fiha CSS u JS.
  4. permessi — firxa għall-ġestjoni tad-drittijiet ta' estensjoni. Hemm 3 tipi ta' drittijiet, li huma deskritti fid-dettall hawn
  5. riżorsi_aċċessibbli_web — riżorsi ta' estensjoni li paġna web tista' titlob, pereżempju, stampi, fajls JS, CSS, HTML.
  6. esternament_connectable — hawnhekk tista' tispeċifika b'mod espliċitu l-IDs ta' estensjonijiet u oqsma oħra ta' paġni tal-web li minnhom tista' tikkonnettja. Dominju jista' jkun it-tieni livell jew ogħla. Ma taħdimx fil-Firefox.

Kuntest ta' eżekuzzjoni

L-estensjoni għandha tliet kuntesti ta 'eżekuzzjoni tal-kodiċi, jiġifieri, l-applikazzjoni tikkonsisti fi tliet partijiet b'livelli differenti ta' aċċess għall-API tal-browser.

Kuntest ta' estensjoni

Ħafna mill-API hija disponibbli hawn. F'dan il-kuntest huma "jgħixu":

  1. Paġna ta 'sfond — parti “backend” tal-estensjoni. Il-fajl huwa speċifikat fil-manifest bl-użu taċ-ċavetta "background".
  2. Popup paġna — paġna popup li tidher meta tikklikkja fuq l-ikona tal-estensjoni. Fil-manifest browser_action -> default_popup.
  3. Paġna tad-dwana — paġna ta 'estensjoni, "living" f'tab separata tal-veduta chrome-extension://<id_расширения>/customPage.html.

Dan il-kuntest jeżisti indipendentement mit-twieqi u t-tabs tal-browser. Paġna ta 'sfond teżisti f'kopja waħda u dejjem taħdem (l-eċċezzjoni hija l-paġna tal-avveniment, meta l-iskript tal-isfond jiġi mniedi minn avveniment u "jmut" wara l-eżekuzzjoni tiegħu). Popup paġna teżisti meta t-tieqa popup tkun miftuħa, u Paġna tad-dwana — filwaqt li t-tab magħha tkun miftuħa. M'hemm l-ebda aċċess għal tabs oħra u l-kontenut tagħhom minn dan il-kuntest.

Kuntest tal-kitba tal-kontenut

Il-fajl tal-iskrittura tal-kontenut jitnieda flimkien ma' kull tab tal-browser. Għandu aċċess għal parti mill-API tal-estensjoni u għas-siġra DOM tal-paġna web. Huma skripts tal-kontenut li huma responsabbli għall-interazzjoni mal-paġna. L-estensjonijiet li jimmanipulaw is-siġra DOM jagħmlu dan fi skripts tal-kontenut - pereżempju, imblokkaturi tar-reklami jew tradutturi. Ukoll, l-iskrittura tal-kontenut tista 'tikkomunika mal-paġna permezz ta' standard postMessage.

Kuntest tal-paġna tal-web

Din hija l-paġna web attwali nnifisha. M'għandha x'taqsam xejn mal-estensjoni u m'għandhiex aċċess hemm, ħlief f'każijiet fejn id-dominju ta' din il-paġna ma jkunx indikat b'mod espliċitu fil-manifest (aktar dwar dan hawn taħt).

Skambju ta 'messaġġi

Partijiet differenti tal-applikazzjoni jridu jiskambjaw messaġġi ma' xulxin. Hemm API għal dan runtime.sendMessage biex tibgħat messaġġ background и tabs.sendMessage biex tibgħat messaġġ lil paġna (kitba tal-kontenut, popup jew paġna tal-web jekk disponibbli externally_connectable). Hawn taħt hemm eżempju meta taċċessa l-API 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))
    }
)

Għal komunikazzjoni sħiħa, tista 'toħloq konnessjonijiet permezz runtime.connect. Bi tweġiba se nirċievu runtime.Port, li għaliha, waqt li tkun miftuħa, tista 'tibgħat kull numru ta' messaġġi. Fuq in-naħa tal-klijent, pereżempju, contentscript, jidher bħal dan:

// Опять же 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 jew sfond:

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

Hemm ukoll avveniment onDisconnect u l-metodu disconnect.

Dijagramma tal-applikazzjoni

Ejja nagħmlu estensjoni tal-browser li taħżen ċwievet privati, tipprovdi aċċess għal informazzjoni pubblika (indirizz, ċavetta pubblika tikkomunika mal-paġna u tippermetti applikazzjonijiet ta 'partijiet terzi li jitolbu firma għat-tranżazzjonijiet.

Żvilupp ta' applikazzjoni

L-applikazzjoni tagħna għandha kemm jinteraġixxu mal-utent kif ukoll tipprovdi lill-paġna b'API biex issejjaħ metodi (pereżempju, biex tiffirma tranżazzjonijiet). Agħmel ma' wieħed biss contentscript mhux se jaħdem, peress li għandu biss aċċess għad-DOM, iżda mhux għall-JS tal-paġna. Qabbad permezz runtime.connect ma nistgħux, minħabba li l-API hija meħtieġa fuq id-dominji kollha, u dawk speċifiċi biss jistgħu jiġu speċifikati fil-manifest. Bħala riżultat, id-dijagramma se tidher bħal din:

Tikteb estensjoni tal-browser sigura

Se jkun hemm skript ieħor - inpage, li aħna se ninjettaw fil-paġna. Se taħdem fil-kuntest tagħha u tipprovdi API biex taħdem bl-estensjoni.

Bidu

Il-kodiċi kollu tal-estensjoni tal-browser huwa disponibbli fuq GitHub. Matul id-deskrizzjoni se jkun hemm links għall-commits.

Nibdew bil-manifest:

{
  // Имя и описание, версия. Все это будет видно в браузере в chrome://extensions/?id=<id расширения>
  "name": "Signer",
  "description": "Extension demo",
  "version": "0.0.1",
  "manifest_version": 2,

  // Скрипты, которые будут исполнятся в background, их может быть несколько
  "background": {
    "scripts": ["background.js"]
  },

  // Какой html использовать для popup
  "browser_action": {
    "default_title": "My Extension",
    "default_popup": "popup.html"
  },

  // Контент скрипты.
  // У нас один объект: для всех url начинающихся с http или https мы запускаем
  // contenscript context со скриптом contentscript.js. Запускать сразу по получении документа для всех фреймов
  "content_scripts": [
    {
      "matches": [
        "http://*/*",
        "https://*/*"
      ],
      "js": [
        "contentscript.js"
      ],
      "run_at": "document_start",
      "all_frames": true
    }
  ],
  // Разрешен доступ к localStorage и idle api
  "permissions": [
    "storage",
    // "unlimitedStorage",
    //"clipboardWrite",
    "idle"
    //"activeTab",
    //"webRequest",
    //"notifications",
    //"tabs"
  ],
  // Здесь указываются ресурсы, к которым будет иметь доступ веб страница. Тоесть их можно будет запрашивать fetche'м или просто xhr
  "web_accessible_resources": ["inpage.js"]
}

Oħloq background.js vojta, popup.js, inpage.js u contentscript.js. Aħna nżidu popup.html - u l-applikazzjoni tagħna diġà tista' titgħabba f'Google Chrome u kun żgur li taħdem.

Biex tivverifika dan, tista 'tieħu l-kodiċi għalhekk. Minbarra dak li għamilna, il-link kkonfigurat l-assemblaġġ tal-proġett bl-użu tal-webpack. Biex iżżid applikazzjoni mal-browser, fil-chrome://extensions trid tagħżel it-tagħbija mhux ippakkjata u l-folder bl-estensjoni korrispondenti - fil-każ tagħna dist.

Tikteb estensjoni tal-browser sigura

Issa l-estensjoni tagħna hija installata u taħdem. Tista' tħaddem l-għodda tal-iżviluppatur għal kuntesti differenti kif ġej:

popup ->

Tikteb estensjoni tal-browser sigura

L-aċċess għall-console tal-iskript tal-kontenut jitwettaq permezz tal-console tal-paġna nnifisha li fuqha titnieda.Tikteb estensjoni tal-browser sigura

Skambju ta 'messaġġi

Għalhekk, għandna bżonn nistabbilixxu żewġ kanali ta 'komunikazzjoni: inpage <-> background u popup <-> background. Tista', ovvjament, tibgħat messaġġi lill-port u tivvinta l-protokoll tiegħek stess, imma nippreferi l-approċċ li rajt fil-proġett metamask open source.

Din hija estensjoni tal-browser biex taħdem man-netwerk Ethereum. Fiha, partijiet differenti tal-applikazzjoni jikkomunikaw permezz tal-RPC bl-użu tal-librerija dnode. Jippermettilek torganizza skambju pjuttost malajr u b'mod konvenjenti jekk tipprovdih bi fluss nodejs bħala trasport (jiġifieri oġġett li jimplimenta l-istess interface):

import Dnode from "dnode/browser";

// В этом примере условимся что клиент удаленно вызывает функции на сервере, хотя ничего нам не мешает сделать это двунаправленным

// Cервер
// API, которое мы хотим предоставить
const dnode = Dnode({
    hello: (cb) => cb(null, "world")
})
// Транспорт, поверх которого будет работать dnode. Любой nodejs стрим. В браузере есть бибилиотека 'readable-stream'
connectionStream.pipe(dnode).pipe(connectionStream)

// Клиент
const dnodeClient = Dnode() // Вызов без агрумента значит что мы не предоставляем API на другой стороне

// Выведет в консоль world
dnodeClient.once('remote', remote => {
    remote.hello(((err, value) => console.log(value)))
})

Issa se noħolqu klassi ta 'applikazzjoni. Se toħloq oġġetti API għall-popup u l-paġna web, u toħloq dnode għalihom:

import Dnode from 'dnode/browser';

export class SignerApp {

    // Возвращает объект API для ui
    popupApi(){
        return {
            hello: cb => cb(null, 'world')
        }
    }

    // Возвращает объет API для страницы
    pageApi(){
        return {
            hello: cb => cb(null, 'world')
        }
    }

    // Подключает popup ui
    connectPopup(connectionStream){
        const api = this.popupApi();
        const dnode = Dnode(api);

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

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

    // Подключает страницу
    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)
        })
    }
}

Hawn u hawn taħt, minflok l-oġġett globali tal-Chrome, nużaw extensionApi, li taċċessa Chrome fil-brawżer ta' Google u l-browser f'oħrajn. Dan isir għall-kompatibilità bejn il-browser, iżda għall-finijiet ta' dan l-artikolu wieħed jista' sempliċement juża 'chrome.runtime.connect'.

Ejja noħolqu istanza ta' applikazzjoni fl-iskrittura tal-isfond:

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

const app = new SignerApp();

// onConnect срабатывает при подключении 'процессов' (contentscript, popup, или страница расширения)
extensionApi.runtime.onConnect.addListener(connectRemote);

function connectRemote(remotePort) {
    const processName = remotePort.name;
    const portStream = new PortStream(remotePort);
    // При установке соединения можно указывать имя, по этому имени мы и оппределяем кто к нам подлючился, контентскрипт или ui
    if (processName === 'contentscript'){
        const origin = remotePort.sender.url
        app.connectPage(portStream, origin)
    }else{
        app.connectPopup(portStream)
    }
}

Peress li d-dnode jaħdem ma 'flussi, u nirċievu port, hija meħtieġa klassi ta' adapter. Huwa magħmul bl-użu tal-librerija li tinqara, li timplimenta n-nodejs streams fil-browser:

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

Issa ejja noħolqu konnessjoni fl-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(){
    // Также, как и в классе приложения создаем порт, оборачиваем в stream, делаем  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)
        })
    });

    // Делаем объект API доступным из консоли
    if (DEV_MODE){
        global.background = background;
    }
}

Imbagħad noħolqu l-konnessjoni fl-iskrittura tal-kontenut:

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 {
        // inject in-page script
        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('Injection failed.', e);
    }
}

Peress li għandna bżonn l-API mhux fl-iskript tal-kontenut, iżda direttament fuq il-paġna, nagħmlu żewġ affarijiet:

  1. Noħolqu żewġ flussi. Wieħed - lejn il-paġna, fuq il-postMessage. Għal dan nużaw dan dan il-pakkett mill-ħallieqa tal-metamask. It-tieni fluss huwa l-isfond fuq il-port riċevut minn runtime.connect. Ejja nixtruhom. Issa l-paġna se jkollha nixxiegħa fl-isfond.
  2. Injetta l-iskrittura fid-DOM. Niżżel l-iskrittura (l-aċċess għalih kien permess fil-manifest) u oħloq tikketta script bil-kontenut tiegħu ġewwa:

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

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 {
        // inject in-page script
        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('Injection failed.', e);
    }
}

Issa noħolqu oġġett api f'inpage u nissettjawh għal globali:

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

setupInpageApi().catch(console.error);

async function setupInpageApi() {
    // Стрим к контентскрипту
    const connectionStream = new PostMessageStream({
        name: 'page',
        target: 'content',
    });

    const dnode = Dnode();

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

    // Получаем объект API
    const pageApi = await new Promise(resolve => {
        dnode.once('remote', api => {
            resolve(api)
        })
    });

    // Доступ через window
    global.SignerApp = pageApi;
}

Aħna lesti Sejħa ta' Proċedura Remota (RPC) b'API separata għall-paġna u l-UI. Meta ngħaqqdu paġna ġdida mal-isfond nistgħu naraw dan:

Tikteb estensjoni tal-browser sigura

API vojta u oriġini. Fuq in-naħa tal-paġna, nistgħu nsejħu l-funzjoni hello bħal din:

Tikteb estensjoni tal-browser sigura

Li taħdem b'funzjonijiet ta 'callback f'JS moderni hija manjieri ħżiena, allura ejja nikteb helper żgħir biex toħloq dnode li jippermettilek tgħaddi oġġett API lill-utils.

L-oġġetti API issa se jidhru bħal dan:

export class SignerApp {

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

...

}

Ikseb oġġett mill-bogħod bħal dan:

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

const pageApi = await new Promise(resolve => {
    dnode.once('remote', remoteApi => {
        // С помощью утилит меняем все callback на promise
        resolve(transformMethods(cbToPromise, remoteApi))
    })
});

U s-sejħa tal-funzjonijiet tagħti wegħda:

Tikteb estensjoni tal-browser sigura

Verżjoni b'funzjonijiet mhux sinkroniċi disponibbli hawn.

B'mod ġenerali, l-RPC u l-approċċ tan-nixxiegħa jidher pjuttost flessibbli: nistgħu nużaw multiplexing tal-fwar u noħolqu diversi APIs differenti għal kompiti differenti. Fil-prinċipju, dnode jista 'jintuża kullimkien, il-ħaġa prinċipali hija li tkebbeb it-trasport fil-forma ta' nixxiegħa nodejs.

Alternattiva hija l-format JSON, li jimplimenta l-protokoll JSON RPC 2. Madankollu, jaħdem ma 'trasporti speċifiċi (TCP u HTTP(S)), li mhux applikabbli fil-każ tagħna.

Stat intern u lokali Ħażna

Ikollna bżonn naħżnu l-istat intern tal-applikazzjoni - tal-inqas iċ-ċwievet tal-iffirmar. Nistgħu faċilment inżidu stat mal-applikazzjoni u metodi biex nibdluha fl-API popup:

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

    ...

} 

Fl-isfond, aħna ser nagħżlu kollox f'funzjoni u niktbu l-oġġett tal-applikazzjoni fit-tieqa sabiex inkunu nistgħu naħdmu miegħu mill-console:

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

Ejja nżidu ftit ċwievet mill-console UI u naraw x'jiġri bl-istat:

Tikteb estensjoni tal-browser sigura

L-istat jeħtieġ li jsir persistenti sabiex iċ-ċwievet ma jintilfux meta jerġgħu jibdew.

Aħna se naħżnuh fil-lokalStorage, billi nikteb fuqu ma 'kull bidla. Sussegwentement, l-aċċess għaliha se jkun meħtieġ ukoll għall-UI, u nixtieq ukoll tabbona għal bidliet. Ibbażat fuq dan, ikun konvenjenti li tinħoloq ħażna osservabbli u tabbona għall-bidliet tagħha.

Se nużaw il-librerija mobx (https://github.com/mobxjs/mobx). L-għażla waqgħet fuqha għax ma kellix għalfejn naħdem magħha, imma ridt ħafna nistudjaha.

Ejja nżidu l-inizjalizzazzjoni tal-istat inizjali u nagħmlu l-maħżen osservabbli:

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

export class SignerApp {

    constructor(initState = {}) {
        // Внешне store так и останется тем же объектом, только теперь все его поля стали proxy, которые отслеживают доступ к ним
        this.store =  observable.object({
            keys: initState.keys || [],
        });
    }

    // Методы, которые меняют observable принято оборачивать декоратором
    @action
    addKey(key) {
        this.store.keys.push(key)
    }

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

    ...

}

"Taħt il-kappa," mobx issostitwixxa l-oqsma kollha tal-maħżen bi prokura u jinterċetta s-sejħiet kollha lilhom. Ikun possibbli li tabbona għal dawn il-messaġġi.

Hawn taħt ħafna drabi se nuża t-terminu "meta tinbidel", għalkemm dan mhux kompletament korrett. Mobx isegwi l-aċċess għall-oqsma. Jintużaw getters u setters ta' oġġetti proxy li toħloq il-librerija.

Id-dekoraturi tal-azzjoni jservu żewġ skopijiet:

  1. B'mod strett bil-bandiera enforceActions, mobx jipprojbixxi l-bidla tal-istat direttament. Hija kkunsidrata prattika tajba li taħdem taħt kundizzjonijiet stretti.
  2. Anke jekk funzjoni tbiddel l-istat diversi drabi - pereżempju, aħna nibdlu diversi oqsma f'diversi linji ta 'kodiċi - l-osservaturi jiġu nnotifikati biss meta titlesta. Dan huwa speċjalment importanti għall-frontend, fejn aġġornamenti tal-istat bla bżonn iwasslu għal rendering bla bżonn tal-elementi. Fil-każ tagħna, la l-ewwel u lanqas it-tieni mhuma partikolarment rilevanti, iżda aħna se nsegwu l-aħjar prattiki. Hija drawwa li jitwaħħlu dekoraturi mal-funzjonijiet kollha li jibdlu l-istat tal-oqsma osservati.

Fl-isfond se nżidu l-inizjalizzazzjoni u nsalvaw l-istat fil-lokalStorage:

import {reaction, toJS} from 'mobx';
import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {SignerApp} from "./SignerApp";
// Вспомогательные методы. Записывают/читают объект в/из localStorage виде JSON строки по ключу '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;
    }

    // Setup state persistence

    // Результат reaction присваивается переменной, чтобы подписку можно было отменить. Нам это не нужно, оставлено для примера
    const localStorageReaction = reaction(
        () => toJS(app.store), // Функция-селектор данных
        saveState // Функция, которая будет вызвана при изменении данных, которые возвращает селектор
    );

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

Il-funzjoni ta 'reazzjoni hija interessanti hawn. Għandu żewġ argumenti:

  1. Selettur tad-dejta.
  2. Maniġer li se jissejjaħ b'din id-dejta kull darba li tinbidel.

B'differenza mir-redux, fejn nirċievu l-istat b'mod espliċitu bħala argument, mobx jiftakar liema osservabbli għandna aċċess ġewwa s-selettur, u jsejjaħ biss lill-handler meta jinbidlu.

Huwa importanti li nifhmu eżattament kif il-mobx jiddeċiedi liema osservabbli nissottoskrivu għalihom. Jekk ktibt selettur f'kodiċi bħal dan() => app.store, allura reazzjoni qatt mhu se tissejjaħ, peress li l-ħażna nnifisha mhix osservabbli, l-oqsma tagħha biss huma.

Jekk ktibtha hekk () => app.store.keys, imbagħad għal darb'oħra xejn ma jiġri, peress li meta żżid/tneħħi elementi ta' firxa, ir-referenza għaliha mhux se tinbidel.

Mobx jaġixxi bħala selettur għall-ewwel darba u jżomm biss rekord ta 'l-osservabbli li aħna aċċessajna. Dan isir permezz ta' proxy getters. Għalhekk, il-funzjoni integrata tintuża hawn toJS. Jirritorna oġġett ġdid bil-prokuri kollha sostitwiti bl-oqsma oriġinali. Matul l-eżekuzzjoni, jaqra l-oqsma kollha tal-oġġett - għalhekk il-getters jiġu attivati.

Fil-console popup nerġgħu nżidu diversi ċwievet. Din id-darba spiċċaw ukoll fil-lokalStorage:

Tikteb estensjoni tal-browser sigura

Meta l-paġna ta' l-isfond terġa' tiġi mgħobbija, l-informazzjoni tibqa' f'postha.

Il-kodiċi tal-applikazzjoni kollu sa dan il-punt jista 'jaraha hawn.

Ħażna sikura ta 'ċwievet privati

Il-ħażna taċ-ċwievet privati ​​f'test ċar mhuwiex sigur: dejjem ikun hemm ċans li tiġi hacked, tikseb aċċess għall-kompjuter tiegħek, eċċ. Għalhekk, f'localStorage se naħżnu ċ-ċwievet f'forma kriptata bil-password.

Għal aktar sigurtà, aħna se nżidu stat msakkra mal-applikazzjoni, li fiha ma jkun hemm l-ebda aċċess għaċ-ċwievet. Aħna se tittrasferixxi awtomatikament l-estensjoni għall-istat msakkra minħabba timeout.

Mobx jippermettilek taħżen biss sett minimu ta 'dejta, u l-bqija huwa awtomatikament ikkalkulat ibbażat fuqha. Dawn huma l-hekk imsejħa proprjetajiet komputati. Jistgħu jitqabblu ma' fehmiet fid-databases:

import {observable, action} from 'mobx';
import {setupDnode} from "./utils/setupDnode";
// Утилиты для безопасного шифрования строк. Используют crypto-js
import {encrypt, decrypt} from "./utils/cryptoUtils";

export class SignerApp {
    constructor(initState = {}) {
        this.store = observable.object({
            // Храним пароль и зашифрованные ключи. Если пароль null - приложение locked
            password: null,
            vault: initState.vault,

            // Геттеры для вычислимых полей. Можно провести аналогию с view в бд.
            get locked(){
                return this.password == null
            },
            get keys(){
                return this.locked ?
                    undefined :
                    SignerApp._decryptVault(this.vault, this.password)
            },
            get initialized(){
                return this.vault !== undefined
            }
        })
    }
    // Инициализация пустого хранилища новым паролем
    @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
        )
    }

    ... // код подключения и api

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

    _checkLocked() {
        if (this.store.locked){
            throw new Error('App is locked')
        }
    }

    // Методы для шифровки/дешифровки хранилища
    static _encryptVault(obj, pass){
        const jsonString = JSON.stringify(obj)
        return encrypt(jsonString, pass)
    }

    static _decryptVault(str, pass){
        if (str === undefined){
            throw new Error('Vault not initialized')
        }
        try {
            const jsonString = decrypt(str, pass)
            return JSON.parse(jsonString)
        }catch (e) {
            throw new Error('Wrong password')
        }
    }
}

Issa aħna naħżnu biss iċ-ċwievet u l-password encrypted. Kollox ieħor huwa kkalkulat. Aħna nagħmlu t-trasferiment għal stat imsakkar billi neħħi l-password mill-istat. L-API pubblika issa għandha metodu għall-inizjalizzazzjoni tal-ħażna.

Miktub għall-encryption utilitajiet li jużaw kripto-js:

import CryptoJS from 'crypto-js'

// Используется для осложнения подбора пароля перебором. На каждый вариант пароля злоумышленнику придется сделать 5000 хешей
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)
}

Il-browser għandu API idle li permezz tagħha tista' tabbona għal avveniment - bidliet fl-istat. Stat, għalhekk, jista 'jkun idle, active и locked. Għal idle tista 'tissettja timeout, u msakkra hija ssettjata meta l-OS innifsu huwa mblukkat. Aħna se nbiddlu wkoll is-selettur għall-iffrankar għal 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;
    }

    // Теперь мы явно узываем поле, которому будет происходить доступ, reaction отработает нормально
    reaction(
        () => ({
            vault: app.store.vault
        }),
        saveState
    );

    // Таймаут бездействия, когда сработает событие
    extensionApi.idle.setDetectionInterval(IDLE_INTERVAL);
    // Если пользователь залочил экран или бездействовал в течение указанного интервала лочим приложение
    extensionApi.idle.onStateChanged.addListener(state => {
        if (['locked', 'idle'].indexOf(state) > -1) {
            app.lock()
        }
    });

    // Connect to other contexts
    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)
        }
    }
}

Il-kodiċi qabel dan il-pass huwa hawn.

Tranżazzjonijiet

Allura, naslu għall-iktar ħaġa importanti: il-ħolqien u l-iffirmar ta 'tranżazzjonijiet fuq il-blockchain. Se nużaw il-blockchain u l-librerija WAVES mewġ-transazzjonijiet.

L-ewwel, ejja nżidu mal-istat firxa ta’ messaġġi li jridu jiġu ffirmati, imbagħad żid metodi biex iżżid messaġġ ġdid, nikkonferma l-firma, u tirrifjuta:

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) {
        // Для каждого сообщения создаем метаданные с id, статусом, выременем создания и тд.
        const message = observable.object({
            id: uuid(), // Идентификатор, используюю uuid
            origin, // Origin будем впоследствии показывать в интерфейсе
            data, //
            status: 'new', // Статусов будет четыре: new, signed, rejected и failed
            timestamp: Date.now()
        });
        console.log(`new message: ${JSON.stringify(message, null, 2)}`);

        this.store.messages.push(message);

        // Возвращаем промис внутри которого mobx мониторит изменения сообщения. Как только статус поменяется мы зарезолвим его
        return new Promise((resolve, reject) => {
            reaction(
                () => message.status, //Будем обсервить статус сообщеня
                (status, reaction) => { // второй аргумент это ссылка на сам reaction, чтобы его можно было уничтожть внутри вызова
                    switch (status) {
                        case 'signed':
                            resolve(message.data);
                            break;
                        case 'rejected':
                            reject(new Error('User rejected message'));
                            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(`No msg with 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(`No msg with id:${id}`);
        message.status = 'rejected'
    }

    ...
}

Meta nirċievu messaġġ ġdid, inżidu l-metadata miegħu, do observable u żid mal store.messages.

Jekk inti ma observable manwalment, allura mobx se tagħmel dan innifsu meta żżid messaġġi mal-firxa. Madankollu, se toħloq oġġett ġdid li mhux se jkollna referenza għalih, iżda ser ikollna bżonnu għall-pass li jmiss.

Sussegwentement, nirritornaw wegħda li ssolvi meta l-istatus tal-messaġġ jinbidel. L-istatus huwa mmonitorjat permezz ta 'reazzjoni, li "joqtol lilu nnifsu" meta l-istatus jinbidel.

Kodiċi tal-metodu approve и reject sempliċi ħafna: aħna sempliċement nibdlu l-istatus tal-messaġġ, wara li niffirmawh jekk meħtieġ.

Poġġejna Approva u tirrifjuta fl-API UI, Messaġġ ġdid fil-paġna 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)
        }
    }

    ...
}

Issa ejja nippruvaw niffirmaw it-tranżazzjoni bl-estensjoni:

Tikteb estensjoni tal-browser sigura

B'mod ġenerali, kollox lest, dak kollu li jibqa 'huwa żid UI sempliċi.

UI

L-interface jeħtieġ aċċess għall-istat tal-applikazzjoni. Fuq in-naħa UI se nagħmlu observable istat u żid funzjoni mal-API li se tbiddel dan l-istat. Ejja nżidu observable għall-oġġett API riċevut mill-isfond:

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() {
    // Подключаемся к порту, создаем из него стрим
    const backgroundPort = extensionApi.runtime.connect({name: 'popup'});
    const connectionStream = new PortStream(backgroundPort);

    // Создаем пустой observable для состояния background'a
    let backgroundState = observable.object({});
    const api = {
        //Отдаем бекграунду функцию, которая будет обновлять observable
        updateState: async state => {
            Object.assign(backgroundState, state)
        }
    };

    // Делаем RPC объект
    const dnode = setupDnode(connectionStream, api);
    const background = await new Promise(resolve => {
        dnode.once('remote', remoteApi => {
            resolve(transformMethods(cbToPromise, remoteApi))
        })
    });

    // Добавляем в background observable со стейтом
    background.state = backgroundState;

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

    // Запуск интерфейса
    await initApp(background)
}

Fl-aħħar nibdew nirrendu l-interface tal-applikazzjoni. Din hija applikazzjoni ta' reazzjoni. L-oġġett ta 'l-isfond huwa sempliċement mgħoddi bl-użu ta' props. Ikun korrett, ovvjament, li jsir servizz separat għall-metodi u maħżen għall-istat, iżda għall-finijiet ta 'dan l-artikolu dan huwa biżżejjed:

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

// Инициализируем приложение с background объектом в качест ве props
export async function initApp(background){
    render(
        <App background={background}/>,
        document.getElementById('app-content')
    );
}

Bil-mobx huwa faċli ħafna li tibda tirrendi meta d-dejta tinbidel. Aħna sempliċiment hang-dekoratur osservatur mill-pakkett mobx-react fuq il-komponent, u render se jissejjaħ awtomatikament meta xi osservabbli referenzjati mill-komponent jinbidel. M'għandekx bżonn l-ebda mapStateToProps jew qabbad bħal f'redux. Kollox jaħdem dritt barra mill-kaxxa:

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 // У Компонета с этим декоратом будет автоматически вызван метод render, если будут изменены observable на которые он ссылается
export default class App extends Component {

    // Правильно конечно вынести логику рендера страниц в роутинг и не использовать вложенные тернарные операторы,
    // и привязывать observable и методы background непосредственно к тем компонентам, которые их используют
    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 > 0
                        ?
                        <Sign keys={keys} message={messages[messages.length - 1]} onApprove={approve} onReject={reject}/>
                        :
                        <Keys keys={keys} onAdd={addKey} onRemove={removeKey}/>
            }
            <div>
                {!locked && <button onClick={() => lock()}>Lock App</button>}
                {initialized && <button onClick={() => deleteVault()}>Delete all keys and init</button>}
            </div>
        </Fragment>
    }
}

Il-komponenti li jifdal jistgħu jitqiesu fil-kodiċi fil-folder UI.

Issa fil-klassi tal-applikazzjoni trid tagħmel selettur tal-istat għall-UI u tinnotifika lill-UI meta tinbidel. Biex tagħmel dan, ejja żid metodu getState и reactionsejħa 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) => {
            // Создаем reaction на изменения стейта, который сделает вызовет удаленну процедуру и обновит стейт в ui процессе
            const updateStateReaction = reaction(
                () => this.getState(),
                (state) => remote.updateState(state),
                // Третьим аргументом можно передавать параметры. fireImmediatly значит что reaction выполниться первый раз сразу.
                // Это необходимо, чтобы получить начальное состояние. Delay позволяет установить debounce
                {fireImmediately: true, delay: 500}
            );
            // Удалим подписку при отключении клиента
            dnode.once('end', () => updateStateReaction.dispose())

        })
    }

    ...
}

Meta tirċievi oġġett remote hija maħluqa reaction biex tibdel l-istat li jsejjaħ il-funzjoni fuq in-naħa UI.

L-aħħar touch huwa li żżid il-wiri ta 'messaġġi ġodda fuq l-ikona tal-estensjoni:

function setupApp() {
...

    // Reaction на выставление текста беджа.
    reaction(
        () => app.store.newMessages.length > 0 ? app.store.newMessages.length.toString() : '',
        text => extensionApi.browserAction.setBadgeText({text}),
        {fireImmediately: true}
    );

...
}

Għalhekk, l-applikazzjoni hija lesta. Paġni tal-web jistgħu jitolbu firma għal tranżazzjonijiet:

Tikteb estensjoni tal-browser sigura

Tikteb estensjoni tal-browser sigura

Il-kodiċi huwa disponibbli hawn rabta.

Konklużjoni

Jekk qrajt l-artiklu sal-aħħar, imma għad għandek mistoqsijiet, tista’ tistaqsihom fuq repożitorji b'estensjoni. Hemmhekk issib ukoll kommits għal kull pass magħżul.

U jekk inti interessat li tħares lejn il-kodiċi għall-estensjoni attwali, tista 'ssib dan hawn.

Kodiċi, repożitorju u deskrizzjoni tax-xogħol minn siemarell

Sors: www.habr.com

Żid kumment