Ho ngola katoloso e sireletsehileng ea sebatli

Ho ngola katoloso e sireletsehileng ea sebatli

Ho fapana le meralo e tloaelehileng ea "client-server", lits'ebetso tse arotsoeng li khetholloa ka:

  • Ha ho na tlhoko ea ho boloka database e nang le mabitso a basebelisi le li-password. Lintlha tsa phihlello li bolokoa ke basebelisi ka bobona, 'me netefatso ea bonnete ba bona e etsahala maemong a protocol.
  • Ha ho hlokahale ho sebelisa seva. Tlhaloso ea kopo e ka etsoa ho marang-rang thibela, moo ho ka khonehang ho boloka palo e hlokahalang ea data.

Ho na le li-storage tse 2 tse batlang li bolokehile bakeng sa linotlolo tsa mosebelisi - li-wallet tsa hardware le li-extensions tsa sebatli. Li-wallet tsa Hardware hangata li bolokehile haholo, empa ho thata ho li sebelisa ebile li hole le mahala, empa likeketso tsa sebatli ke motsoako o phethahetseng oa ts'ireletso le boiketlo ba ts'ebeliso, hape e ka ba mahala ho basebelisi ba ho qetela.

Ha re ela hloko sena sohle, re ne re batla ho etsa katoloso e sireletsehileng ka ho fetesisa e nolofatsang nts'etsopele ea lits'ebetso tse arolelanoeng ka ho fana ka API e bonolo bakeng sa ho sebetsa ka litšebelisano le mesaeno.
Re tla u bolella ka phihlelo ena ka tlase.

Sengoliloeng se tla ba le litaelo tsa mohato ka mohato mabapi le mokhoa oa ho ngola katoloso ea sebatli, ka mehlala ea khoutu le li-screenshots. U ka fumana khoutu eohle ho bobolokelo. Boitlamo bo bong le bo bong ka nepo bo lumellana le karolo ea sengoloa sena.

Nalane e Khutšoanyane ea Katoloso ea Sebatli

Katoloso ea sebatli esale e le teng ka nako e telele. Li hlahile ho Internet Explorer morao koana ka 1999, ho Firefox ka 2004. Leha ho le joalo, ka nako e telele haholo ho ne ho se na tekanyetso e le 'ngoe ea ho atolosoa.

Re ka re e hlahile hammoho le likeketso tsa mofuta oa bone oa Google Chrome. Ha e le hantle, ho ne ho se na tlhaloso ka nako eo, empa e ne e le Chrome API e ileng ea fetoha motheo oa eona: kaha e hlōtse boholo ba 'maraka oa sebatli le ho ba le lebenkele le hahiloeng ka har'a lisebelisoa, Chrome e hlile e behile litekanyetso tsa lisebelisoa tsa sebatli.

Mozilla e ne e e-na le maemo a eona, empa ha e bona ho tsebahala ha li-extensions tsa Chrome, k'hamphani e ile ea etsa qeto ea ho etsa API e lumellanang. Ka selemo sa 2015, molemong oa Mozilla, ho ile ha theoa sehlopha se ikhethileng ka har'a World Wide Web Consortium (W3C) ho sebetsana le lintlha tsa katoloso ea sebatli.

Li-extensions tsa API tse teng bakeng sa Chrome li nkuoe e le motheo. Mosebetsi o ile oa etsoa ka tšehetso ea Microsoft (Google e hanne ho kenya letsoho ntlafatsong ea maemo), 'me ka lebaka leo ho ile ha hlaha moralo. tobileng.

Ka mokhoa o hlophisitsoeng, litlhaloso li tšehetsoa ke Edge, Firefox le Opera (hlokomela hore Chrome ha e lethathamong lena). Empa ha e le hantle, tekanyetso e lumellana haholo le Chrome, kaha e hlile e ngotsoe ho latela likeketso tsa eona. U ka bala haholoanyane ka WebExtensions API mona.

Sebopeho sa katoloso

Faele e le 'ngoe feela e hlokahalang bakeng sa katoloso ke manifesto (manifest.json). Hape ke "ho kena" ho katoloso.

Ponahatso

Ho latela litlhaloso, faele ea manifesto ke faele e nepahetseng ea JSON. Tlhaloso e felletseng ea linotlolo tsa manifest tse nang le leseli mabapi le hore na ke linotlolo life tse tšehelitsoeng moo sebatli se ka shejoang mona.

Linotlolo tse seng ka har'a "li ka hlokomolohuoa" (liphoso tsa tlaleho ea Chrome le Firefox, empa likeketso li ntse li tsoela pele ho sebetsa).

'Me ke rata ho lebisa tlhokomelo ho lintlha tse ling.

  1. semelo sa - ntho e kenyeletsang likarolo tse latelang:
    1. litokomane - letoto la mangolo a tla phethoa ho latela moelelo oa taba (re tla bua ka sena hamorao);
    2. leqepheng la - sebakeng sa mangolo a tla ngoloa leqepheng le se nang letho, o ka hlakisa html e nang le litaba. Tabeng ena, sebaka sa script se tla hlokomolohuoa 'me mangolo a tla hloka ho kenngoa leqepheng la litaba;
    3. phehella - folakha ea binary, haeba e sa hlalosoa, sebatli se tla "bolaea" ts'ebetso ea morao-rao ha se nahana hore ha se etse letho, ebe se qala hape ha ho hlokahala. Ho seng joalo, leqephe le tla laolloa feela ha sebatli se koetsoe. Ha e tšehetsoe ho Firefox.
  2. content_scripts - lintho tse ngata tse u lumellang ho kenya mangolo a fapaneng maqepheng a fapaneng a webo. Ntho e 'ngoe le e' ngoe e na le likarolo tse latelang tsa bohlokoa:
    1. litlholisano - url ea mohlala, e etsang qeto ea hore na mongolo o itseng oa litaba o tla kenyelletsoa kapa che.
    2. js - lethathamo la mangolo a tla kenngoa papaling ena;
    3. khaolla_match - ha e kenyeletse lebaleng match Li-URL tse tsamaellanang le sebaka sena.
  3. leqephe_ketso - ha e le hantle ke ntho e ikarabellang bakeng sa letšoao le bontšitsoeng haufi le sebaka sa aterese ho sebatli le ho sebelisana le sona. E boetse e u lumella ho hlahisa fensetere ea popup, e hlalosoang u sebelisa HTML, CSS le JS ea hau.
    1. default_popup - tsela ea faele ea HTML e nang le sebopeho sa popup, e kanna ea ba le CSS le JS.
  4. ditumello - sehlopha sa ho laola litokelo tsa katoloso. Ho na le mefuta e 3 ea litokelo, tse hlalositsoeng ka botlalo mona
  5. web_accessible_resource - lisebelisoa tse atolositsoeng tseo leqephe la webo le ka li kopang, mohlala, litšoantšo, JS, CSS, lifaele tsa HTML.
  6. ka ntle_e kgokahang - Mona o ka hlakisa ka ho hlaka li-ID tsa likeketso tse ling le libaka tsa maqephe a webo tseo u ka hokelang ho tsona. Sebaka sa marang-rang se ka ba boemong ba bobeli kapa ho feta. Ha e sebetse ho Firefox.

Boemo ba ho phethahatsa

Katoloso e na le maemo a mararo a ts'ebetso ea khoutu, ke hore, ts'ebeliso e na le likarolo tse tharo tse nang le maemo a fapaneng a phihlello ho sebatli sa API.

Boemo ba katoloso

Boholo ba API bo fumaneha mona. Tabeng ena ba "phela":

  1. Leqephe le ka morao — "backend" karolo ea katoloso. Faele e hlalositsoe ho manifest ho sebelisoa konopo ea "background".
  2. Leqephe la popup — leqephe la popup le hlahang ha o tobetsa aekhoneng ea katoloso. Ho manifesto browser_action -> default_popup.
  3. Leqephe le ikhethileng — leqephe la katoloso, "phela" ho tab e arohaneng ea pono chrome-extension://<id_расширения>/customPage.html.

Taba ena e teng ntle le lifensetere le li-tab tsa sebatli. Leqephe le ka morao e teng ka kopi e le 'ngoe' me e lula e sebetsa (mokhelo ke leqephe la ketsahalo, ha sengoloa sa bokamorao se qalisoa ke ketsahalo ebe se "shoa" kamora ts'ebetso ea sona). Leqephe la popup e teng ha fensetere ea popup e butsoe, le Leqephe le ikhethileng — ha tab ya e nang le yona e butswe. Ha ho na mokhoa oa ho fumana li-tab tse ling le litaba tsa tsona ho tsoa moelelong ona.

Boemo ba mongolo oa litaba

Faele ea script ea litaba e qalisoa hammoho le tab e 'ngoe le e' ngoe ea sebatli. E na le phihlello ea karolo ea API ea katoloso le ho sefate sa DOM sa leqephe la webo. Ke mangolo a litaba a ikarabellang bakeng sa ho sebelisana le leqephe. Li-extensions tse fetolang sefate sa DOM li etsa sena ho lingoliloeng tsa litaba - mohlala, li-ad blockers kapa bafetoleli. Hape, mongolo oa litaba o ka buisana le leqephe ka mokhoa o tloaelehileng postMessage.

Boemo ba leqephe la webo

Lena ke leqephe la sebele la tepo ka bolona. Ha e amane le katoloso mme ha e na phihlello moo, ntle le maemong ao domain name ea leqephe lena e sa hlahisoang ka ho hlaka ho manifest (ho feta mona ka tlase).

Phapanyetsano ea molaetsa

Likarolo tse fapaneng tsa kopo li tlameha ho fapanyetsana melaetsa. Ho na le API bakeng sa sena runtime.sendMessage ho romela molaetsa background и tabs.sendMessage ho romella molaetsa leqepheng (sengoloa sa litaba, popup kapa leqephe la webo haeba le fumaneha externally_connectable). Ka tlase ke mohlala ha u kena ho Chrome API.

// Сообщением может быть любой 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))
    }
)

Bakeng sa puisano e felletseng, o ka theha likhokahano ka runtime.connect. Ka karabo re tla fumana runtime.Port, eo, ha e ntse e butsoe, u ka romella palo efe kapa efe ea melaetsa. Ka lehlakoreng la bareki, mohlala, contentscript, e shebahala tjena:

// Опять же 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"});

Seva kapa bokamorao:

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

Ho boetse ho na le ketsahalo onDisconnect le mokhoa disconnect.

Setšoantšo sa kopo

Ha re etseng sebatli se bolokang linotlolo tsa poraefete, se fanang ka phihlello ea litaba tsa sechaba (aterese, senotlolo sa sechaba se buisana le leqephe mme se lumella lits'ebetso tsa mokha oa boraro ho kopa ho saena bakeng sa transaction.

Ntlafatso ea kopo

Sesebelisoa sa rona se tlameha ho sebelisana le mosebelisi le ho fana ka leqephe ka API ea ho letsetsa mekhoa (mohlala, ho saena litšebelisano). Etsa qeto ka e le 'ngoe feela contentscript e ke ke ea sebetsa, kaha e na le phihlello ea DOM feela, empa eseng ho JS ea leqephe. Hokela ka runtime.connect ha re khone, hobane API e hlokahala libakeng tsohle, 'me ke tse khethehileng feela tse ka hlalosoang ho ponts'o. Ka lebaka leo, setšoantšo se tla shebahala tjena:

Ho ngola katoloso e sireletsehileng ea sebatli

Ho tla ba le mongolo o mong - inpage, eo re tla e kenya leqepheng. E tla sebetsa maemong a eona mme e fane ka API bakeng sa ho sebetsa le katoloso.

Tšimoloho

Khoutu eohle ea katoloso ea sebatli e fumaneha ho GitHub. Nakong ea tlhaloso ho tla ba le lihokela tsa boitlamo.

Ha re qaleng ka manifesto:

{
  // Имя и описание, версия. Все это будет видно в браузере в 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"]
}

Etsa letho background.js, popup.js, inpage.js le contentscript.js. Re kenya popup.html - mme sesebelisoa sa rona se se se ka kenngoa ho Google Chrome mme re etsa bonnete ba hore se ea sebetsa.

Ho netefatsa sena, o ka nka khoutu ho tloha mona. Ntle le seo re se entseng, sehokelo se hlophisitse kopano ea projeke e sebelisa webpack. Ho kenyelletsa sesebelisoa ho sebatli, ho chrome: // li-extensions u hloka ho khetha mojaro o sa koaloang le foldara e nang le katoloso e lumellanang - molemong oa rona dist.

Ho ngola katoloso e sireletsehileng ea sebatli

Hona joale katoloso ea rona e kentsoe ebile e sebetsa. O ka tsamaisa lisebelisoa tsa nts'etsopele bakeng sa maemo a fapaneng ka tsela e latelang:

popup ->

Ho ngola katoloso e sireletsehileng ea sebatli

Ho fihlella script script console ho etsoa ka console ea leqephe leo e qalisoang ho lona.Ho ngola katoloso e sireletsehileng ea sebatli

Phapanyetsano ea molaetsa

Kahoo, re hloka ho theha liteishene tse peli tsa puisano: inpage <-> bokamorao le popup <-> bokamorao. Ehlile, o ka romella melaetsa boema-kepeng mme oa iqapela protocol ea hau, empa ke khetha mokhoa oo ke o boneng morerong oa mohloli o bulehileng oa metamask.

Ena ke katoloso ea sebatli bakeng sa ho sebetsa le marang-rang a Ethereum. Ho eona, likarolo tse fapaneng tsa ts'ebeliso li buisana ka RPC li sebelisa laeborari ea dnode. E u lumella ho hlophisa phapanyetsano kapele le ha bonolo haeba u e fana ka nodejs e le sepalangoang (e bolelang ntho e sebelisang sebopeho se tšoanang):

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

Joale re tla theha sehlopha sa kopo. E tla theha lintho tsa API bakeng sa popup le leqephe la sebaka sa marang-rang, ebe e ba etsetsa dnode:

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

Mona le ka tlase, sebakeng sa ntho ea lefats'e ea Chrome, re sebelisa extensionApi, e kenang Chrome ho sebatli sa Google le sebatli ho tse ling. Sena se etsoa molemong oa ho sebelisana le sebatli, empa molemong oa sengoloa sena motho a ka sebelisa 'chrome.runtime.connect'.

Ha re theheng mohlala oa ts'ebeliso ka har'a script e ka morao:

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

Kaha dnode e sebetsa le melapo, 'me re fumana kou, ho hlokahala sehlopha sa adaptara. E entsoe ka laebrari e baloang, e sebelisang melapo ea nodejs ho sebatli:

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

Joale ha re theheng khokahano ho 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;
    }
}

Ebe re theha khokahano ho script ea litaba:

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

Kaha ha re hloke API ho sengoloa sa litaba, empa ka kotloloho leqepheng, re etsa lintho tse peli:

  1. Re etsa melapo e 'meli. E 'ngoe - ho ea leqepheng, holim'a posoMolaetsa. Bakeng sa sena re sebelisa sena sephutheloana sena ho tsoa ho baetsi ba metamask. Molapo oa bobeli ke oa morao-rao holim'a boema-kepe bo amoheloang ho tsoa runtime.connect. Ha re li rekeng. Joale leqephe le tla ba le molapo o eang ka morao.
  2. Kenya mongolo ho DOM. Khoasolla sengoloa (ho se fihlelle ho lumelletsoe ho manifest) 'me u thehe tag script le dikahare tsa yona ka hare:

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

Joale re theha ntho ea api ho inpage ebe re e beha lefatšeng ka bophara:

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

Re lokile Remote Procedure Call (RPC) e nang le API e arohaneng ea leqephe le UI. Ha o hokela leqephe le lecha ho bokamorao re ka bona sena:

Ho ngola katoloso e sireletsehileng ea sebatli

Empty API le tšimoloho. Lehlakoreng la leqephe, re ka bitsa mosebetsi oa hello ka tsela ena:

Ho ngola katoloso e sireletsehileng ea sebatli

Ho sebetsa ka mesebetsi ea callback ho JS ea sejoale-joale ke mekhoa e mebe, kahoo ha re ngoleng mothusi e monyane ho theha dnode e u lumellang ho fetisa ntho ea API ho lisebelisoa.

Lintho tsa API joale li tla shebahala tjena:

export class SignerApp {

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

...

}

Ho fumana ntho ho tloha hole joalo ka:

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

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

'Me ho letsetsa mesebetsi ho khutlisa tšepiso:

Ho ngola katoloso e sireletsehileng ea sebatli

Phetolelo e nang le mesebetsi ea asynchronous e fumaneha mona.

Ka kakaretso, mokhoa oa RPC le oa ho phalla o bonahala o le bonolo: re ka sebelisa multiplexing ea mouoane 'me ra theha li-API tse fapaneng bakeng sa mesebetsi e fapaneng. Ha e le hantle, dnode e ka sebelisoa kae kapa kae, ntho e ka sehloohong ke ho phuthela lipalangoang ka mokhoa oa nodejs stream.

E 'ngoe ke mokhoa oa JSON, o sebelisang protocol ea JSON RPC 2. Leha ho le joalo, e sebetsa le lipalangoang tse itseng (TCP le HTTP(S)), tse sa sebetseng molemong oa rona.

Naha ea ka hare le Polokelo ea lehae

Re tla hloka ho boloka boemo ba ka hare ba kopo - bonyane linotlolo tsa ho saena. Re ka eketsa boemo habonolo ts'ebelisong le mekhoa ea ho e fetola ho popup API:

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

export class SignerApp {

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

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

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

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

    ...

} 

Ka morao, re tla phuthela ntho e 'ngoe le e' ngoe ka ts'ebetsong ebe re ngola ntho ea kopo fensetereng e le hore re ka sebetsa le eona ho tloha 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)
        }
    }
}

Ha re kenyeng linotlolo tse 'maloa ho tsoa ho khomphutha ea UI mme re bone se etsahalang ka mmuso:

Ho ngola katoloso e sireletsehileng ea sebatli

Boemo bo hloka ho etsoa bo phehellang e le hore linotlolo li se ke tsa lahleha ha li qala hape.

Re tla e boloka ho Local Storage, re e ngole ka liphetoho tsohle. Kamora moo, phihlello ea eona le eona e tla hlokahala bakeng sa UI, hape ke kopa ho ingolisa ho liphetoho. E ipapisitse le sena, ho tla ba bonolo ho theha polokelo e hlokomelehang le ho ingolisa ho liphetoho tsa eona.

Re tla sebelisa laeborari ea mobx (https://github.com/mobxjs/mobx). Khetho e ile ea oela ho eona hobane ke ne ke sa tlameha ho sebetsa le eona, empa ke ne ke hlile ke batla ho ithuta eona.

Ha re kenyelle tlhahiso ea boemo ba pele mme re etse hore lebenkele le bonahale:

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

    ...

}

"Ka tlas'a hood," mobx e nkile sebaka sa libaka tsohle tsa mabenkele ka proxy 'me e thibela mehala eohle ho bona. Ho tla khoneha ho ngolisa melaetsa ena.

Ka tlase ke tla sebelisa lentsoe "ha u fetoha", le hoja sena se sa nepahale ka ho feletseng. Mobx e latela mokhoa oa ho fihlella masimong. Ho sebelisoa li-getter le li-setter tsa lintho tsa proxy tseo laebrari e li etsang.

Bakhabiso ba liketso ba sebeletsa merero e 'meli:

  1. Ka mokhoa o thata le folakha ea enforceActions, mobx e thibela ho fetola naha ka kotloloho. Ho nkoa e le mokhoa o motle oa ho sebetsa tlas'a maemo a thata.
  2. Esita le haeba mosebetsi o fetola mmuso ka makhetlo a 'maloa - ka mohlala, re fetola masimo a' maloa meleng e 'maloa ea khoutu - bashebelli ba tsebisoa feela ha e qeta. Sena se bohlokoa haholo bakeng sa sebaka se ka pele, moo lintlafatso tse sa hlokahaleng tsa mmuso li lebisang phepelong e sa hlokahaleng ea likarolo. Tabeng ea rona, ha ho ea pele kapa ea bobeli e amehang ka ho khetheha, empa re tla latela mekhoa e metle. Ke tloaelo ho hokela bakhabi ba mesebetsi eohle e fetolang boemo ba masimo a hlokometsoeng.

Ka morao re tla kenyelletsa ho qala le ho boloka mmuso sebakeng sa polokelo ea lehae:

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

Mosebetsi oa karabelo oa thahasellisa mona. E na le likhohlano tse peli:

  1. Sekhethi sa data.
  2. Sebapali se tla bitsoa ka data ena nako le nako ha e fetoha.

Ho fapana le reux, moo re amohelang mmuso ka ho hlaka e le khang, mobx e hopola hore na ke lintho life tse bonoang tseo re li fihlelang ka har'a mokhethoa, 'me e letsetsa motho ea sebetsang ha a fetoha.

Ho bohlokoa ho utloisisa hantle hore na mobx e etsa qeto ea hore na ke lintho life tse bonoang tseo re ingolisang ho tsona. Haeba ke ngotse mokhethoa ka khoutu tjena() => app.store, joale karabelo e ke ke ea bitsoa, ​​kaha polokelo ka boeona ha e bonahale, ke masimo a eona feela.

Haeba ke e ngotse tjena () => app.store.keys, joale hape ha ho letho le neng le tla etsahala, kaha ha ho eketsa / ho tlosa likarolo tse ngata, ho buuoa ka eona ho ke ke ha fetoha.

Mobx e sebetsa e le mokhethoa khetlo la pele mme e boloka tlaleho ea lintho tse bonoang tseo re li fihletseng. Sena se etsoa ka li-proxy getters. Ka hona, mosebetsi o hahelletsoeng o sebelisoa mona toJS. E khutlisa ntho e ncha ka li-proxies tsohle tse nkeloang sebaka ke masimo a mantlha. Nakong ea ho bolaoa, e bala masimo ohle a ntho - ka hona, li-getter lia tsosoa.

Ka har'a popup console re tla eketsa linotlolo tse 'maloa. Lekhetlong lena li ile tsa qetella li le sebakeng sa polokelo ea lehae:

Ho ngola katoloso e sireletsehileng ea sebatli

Ha leqephe le ka morao le kenngoa bocha, lintlha li lula li le teng.

Khoutu eohle ea kopo ho fihlela ntlheng ena e ka bonoa mona.

Poloko e sireletsehileng ea linotlolo tsa poraefete

Ho boloka linotlolo tsa poraefete ka mongolo o hlakileng ha hoa sireletseha: kamehla ho na le monyetla oa hore u tla qhekelloa, u fihlelle komporo ea hau, joalo-joalo. Ka hona, sebakeng sa polokelo ea lehae re tla boloka linotlolo ka foromo e kentsoeng ka password.

Bakeng sa ts'ireletso e kholoanyane, re tla eketsa boemo bo notletsoeng ho kopo, moo ho ke keng ha e-ba le mokhoa oa ho fumana linotlolo ho hang. Re tla fetisetsa ka bohona katoloso sebakeng se notletsoeng ka lebaka la nako e felileng.

Mobx e u lumella ho boloka palo e fokolang feela ea data, 'me tse ling kaofela li baloa ka bohona ho latela eona. Tsena ke lintho tseo ho thoeng ke computed properties. Li ka bapisoa le lipono tse fumanehang ho database:

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

Hona joale re boloka feela linotlolo tse patiloeng le password. Tse ding tsohle di ballwe. Re etsa phetisetso ho boemo bo notletsoeng ka ho tlosa phasewete ho tsoa mmusong. API ea sechaba joale e na le mokhoa oa ho qala polokelo.

E ngoletsoe ho qoelisoa lisebelisoa tse sebelisang crypto-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)
}

Sebatli se na le API e sa sebetseng eo ka eona u ka ingolisang ketsahalong - liphetoho tsa maemo. Naha, ka hona, e ka ba idle, active и locked. Bakeng sa ho se sebetse o ka beha nako ea ho tsoa, ​​'me e notletsoe e behiloe ha OS ka boeona e koetsoe. Hape re tla fetola sekhetho bakeng sa ho boloka ho 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)
        }
    }
}

Khoutu pele mohato ona ke mona.

Litšebelisano

Kahoo, re tla ho ntho ea bohlokoa ka ho fetisisa: ho theha le ho saena litšebelisano ho blockchain. Re tla sebelisa WAVES blockchain le laeborari maqhubu-phepana.

Taba ea pele, a re kenyelletseng mmuso melaetsa e mengata e hlokang ho saena, ebe re eketsa mekhoa ea ho eketsa molaetsa o mocha, ho tiisa tekeno, le ho hana:

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

    ...
}

Ha re fumana molaetsa o mocha, re eketsa metadata ho oona, etsa observable le ho eketsa ho store.messages.

Haeba u sa etse joalo observable ka letsoho, ebe mobx e tla e etsa ka boeona ha e eketsa melaetsa ho sehlopha. Leha ho le joalo, e tla theha ntho e ncha eo re ke keng ra e bua, empa re tla e hloka bakeng sa mohato o latelang.

Ka mor'a moo, re khutlisetsa tšepiso e rarollang ha boemo ba molaetsa bo fetoha. Boemo bo hlahlojoa ke karabelo, e tla "ipolaea" ha boemo bo fetoha.

Mokhoa oa khoutu approve и reject e bonolo haholo: re mpa re fetola boemo ba molaetsa, re o saenetse pele ha ho hlokahala.

Re kenya Amohela le ho hana ho UI API, molaetsa o mocha leqepheng la 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)
        }
    }

    ...
}

Joale ha re leke ho saena transaction le katoloso:

Ho ngola katoloso e sireletsehileng ea sebatli

Ka kakaretso, ntho e 'ngoe le e' ngoe e lokile, e setseng ke eona feela eketsa UI e bonolo.

UI

Khokahano e hloka ho fihlella boemo ba ts'ebeliso. Ka lehlakoreng la UI re tla etsa joalo observable bolela le ho eketsa ts'ebetso ho API e tla fetola boemo bona. Ha re eketse observable ho ea ho ntho ea API e fumanoeng ka morao:

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

Qetellong re qala ho fana ka sebopeho sa kopo. Ena ke ts'ebeliso ea maikutlo. Ntho e ka morao e fetisoa feela ho sebelisoa lisebelisoa. E ka ba hantle, ho hlakile, ho etsa tšebeletso e arohaneng bakeng sa mekhoa le lebenkele la mmuso, empa molemong oa sehlooho sena sena se lekane:

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

Ka mobx ho bonolo haholo ho qala ho fana ha data e fetoha. Re mpa re fanyeha mokhabiso oa ho shebella ho tloha sephutheloana mobx-react karolong, 'me render e tla bitsoa ka tsela e iketsang ha lintho life kapa life tse bonoang tse boletsoeng ke karolo e fetoha. Ha u hloke mapaStateToProps kapa ho hokela joalo ka reux. Tsohle li sebetsa ka ntle ho lebokose:

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

Likarolo tse setseng li ka bonoa khoutu foldareng ea UI.

Hona joale sehlopheng sa kopo o hloka ho etsa khetho ea mmuso bakeng sa UI le ho tsebisa UI ha e fetoha. Ho etsa sena, a re kenye mokhoa getState и reactionletsa 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())

        })
    }

    ...
}

Ha o amohela ntho remote bōpiloe reaction ho fetola boemo bo bitsang tshebetso e lehlakoreng la UI.

Taba ea ho qetela ke ho kenyelletsa pontšo ea melaetsa e mecha ho lets'oao la katoloso:

function setupApp() {
...

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

...
}

Kahoo, kopo e se e loketse. Maqephe a Marang-rang a ka kopa ho saena bakeng sa lipehelo:

Ho ngola katoloso e sireletsehileng ea sebatli

Ho ngola katoloso e sireletsehileng ea sebatli

Khoutu e fumaneha mona kgokahanyo.

fihlela qeto e

Haeba u balile sengoloa ho fihlela qetellong, empa u ntse u e-na le lipotso, u ka li botsa ho polokelo e nang le katoloso. Mona u tla boela u fumane boitlamo bakeng sa mohato o mong le o mong o khethiloeng.

'Me haeba u thahasella ho sheba khoutu bakeng sa katoloso ea sebele, u ka fumana sena mona.

Khoutu, polokelo le tlhaloso ea mosebetsi ho tloha siemarell

Source: www.habr.com

Eketsa ka tlhaloso