Nivîsandina pêvekek gerokek ewledar

Nivîsandina pêvekek gerokek ewledar

Berevajî mîmariya hevpar "muwekîl-server", serîlêdanên nenavendî bi vî rengî têne destnîşan kirin:

  • Ne hewce ye ku databasek bi têketin û şîfreyên bikarhêner hilîne. Agahdariya gihîştinê tenê ji hêla bikarhêneran ve têne hilanîn, û pejirandina rastiya wan di asta protokolê de pêk tê.
  • Ne hewce ye ku serverek bikar bînin. Mantiqa serîlêdanê dikare li ser torgilokek blokek were darve kirin, ku li wir gengaz e ku mîqdara pêdivî ya daneyê hilîne.

Ji bo bişkojkên bikarhêner 2 depoyên bi ewledar hene - berîkên hardware û pêvekên gerokê. Bersivên hardware bi piranî zehf ewledar in, lê karanîna wan dijwar e û ji belaş dûr in, lê pêvekên gerokê tevliheviyek bêkêmasî ya ewlehî û karanîna hêsan e, û di heman demê de dikare ji bo bikarhênerên dawîn jî bi tevahî belaş be.

Bi girtina van hemîyan, me xwest ku dirêjkirina herî ewledar a ku pêşkeftina serîlêdanên nenavendî hêsan dike bi peydakirina APIyek hêsan a ji bo xebata bi danûstendin û îmzeyan re çêbikin.
Em ê li jêr vê serpêhatiyê ji we re vebêjin.

Gotar dê rêwerzên gav-bi-gav li ser çawaniya nivîsandina pêvekek gerokê, bi nimûneyên kod û dîmenan vehewîne. Hûn dikarin hemî kodê tê de bibînin depoyên. Her commit bi mantiqî bi beşek vê gotarê re têkildar e.

Kurtîyek Dîroka Pêşveçûnên Gerokê

Berfirehkirina gerokê ji demek dirêj ve heye. Ew di 1999-an de di Internet Explorer-ê de, di 2004-an de di Firefox-ê de xuya bûn. Lêbelê, ji bo demek pir dirêj ji bo dirêjkirinê standardek yekane tune bû.

Em dikarin bibêjin ku ew digel pêvekan di guhertoya çaremîn a Google Chrome de xuya bû. Bê guman, wê hingê diyariyek tune bû, lê ew API-ya Chrome bû ku bû bingeha wê: ku piraniya bazara gerokê zeft kir û xwedan firotgehek serîlêdanê ya çêkirî bû, Chrome bi rastî standard ji bo dirêjkirina gerokê destnîşan kir.

Mozilla xwedan standardek xwe bû, lê bi dîtina populerbûna pêvekên Chrome, pargîdanî biryar da ku API-yek hevaheng çêbike. Di sala 2015-an de, bi înîsiyatîfa Mozilla, komek taybetî di nav Konsorsiyûma Tevna Berfireh a Cîhanê (W3C) de hate afirandin ku li ser taybetmendiyên dirêjkirina gerokê bixebite.

Zêdekirinên API-ya heyî yên ji bo Chrome wekî bingeh hatin girtin. Kar bi piştgiriya Microsoft-ê hate kirin (Google red kir ku beşdarî pêşkeftina standardê bibe), û di encamê de pêşnumayek xuya bû taybetmendiyên.

Bi fermî, taybetmendî ji hêla Edge, Firefox û Opera ve tê piştgirî kirin (bibînin ku Chrome ne di vê navnîşê de ye). Lê bi rastî, standard bi gelemperî bi Chrome-ê re hevaheng e, ji ber ku ew bi rastî li gorî pêvekên wê hatî nivîsandin. Hûn dikarin di derbarê WebExtensions API de bêtir bixwînin vir.

Struktura Extension

Tenê pelê ku ji bo dirêjkirinê hewce ye, manifest e (manifest.json). Ew di heman demê de "xala têketinê" ya berfirehbûnê ye.

Manîfestoye

Li gorî diyardeyê, pelê manifest pelek JSON derbasdar e. Danasînek bêkêmasî ya bişkokên manîfestoyê digel agahdariya li ser kîjan bişkojan têne piştgirî kirin li kîjan gerokê dikare were dîtin vir.

Bişkojkên ku di taybetmendiyê de ne "dibe" werin paşguh kirin (hem Chrome û Firefox jî xeletiyan radigihînin, lê pêvekirin berdewam dikin).

Û ez dixwazim balê bikişînim ser hin xalan.

  1. paşî - Tiştek ku zeviyên jêrîn vedigire:
    1. nivîsandin - rêzek nivîsarên ku dê di çarçoveyek paşerojê de bêne darve kirin (em ê hinekî paşê li ser vê biaxivin);
    2. rûpel - li şûna skrîptên ku dê di rûpelek vala de bêne darve kirin, hûn dikarin html-ê bi naverokê diyar bikin. Di vê rewşê de, qada skrîptê dê were paşguh kirin, û pêdivî ye ku nivîsar di nav rûpelê naverokê de bêne danîn;
    3. li sersekinîn - ala binary, heke ne diyar be, gerok dê pêvajoya paşîn "bikuje" gava ku ew bifikire ku ew tiştek nake, û ger hewce bike wê ji nû ve bide destpêkirin. Wekî din, rûpel tenê dema gerok were girtin dê were rakirin. Di Firefoxê de nayê piştgirî kirin.
  2. naveroka_scripts - komek tiştan ku dihêle hûn nivîsarên cihêreng li rûpelên malperê yên cihêreng bar bikin. Her tişt qadên girîng ên jêrîn vedigire:
    1. maçên - url nimûne, ku diyar dike ka dê nivîsarek naverokek taybetî were nav kirin an na.
    2. js - navnîşek nivîsarên ku dê di vê maçê de werin barkirin;
    3. exclude_matches - ji qadê derdixe match URLên ku vê qadê li hev dikin.
  3. page_action - bi rastî tiştek e ku ji îkona ku li kêleka barika navnîşan a gerokê tê xuyang kirin û pêwendiya pê re berpirsiyar e. Di heman demê de ew dihêle hûn pencereyek popupê jî nîşan bidin, ku bi karanîna HTML, CSS û JS-ya xwe ve hatî destnîşan kirin.
    1. default_popup - riya pelê HTML-ê bi navgîniya popupê, dibe ku CSS û JS-ê hebe.
  4. destûrên - komek ji bo birêvebirina mafên dirêjkirinê. 3 cureyên mafan hene, ku bi hûrgilî têne vegotin vir
  5. web_accessible_resources - Çavkaniyên dirêjkirinê yên ku rûpelek malperê dikare daxwaz bike, mînakî, wêne, pelên JS, CSS, HTML.
  6. externally_connectable - Li vir hûn dikarin bi eşkere nasnameyên pêvek û domên din ên rûpelên malperê yên ku hûn dikarin jê ve girêbidin diyar bikin. Domanek dikare asta duyemîn an jî bilindtir be. Di Firefoxê de naxebite.

Çarçoveya darvekirinê

Pêşveçûn sê çarçoveyên darvekirina kodê hene, ango serîlêdan ji sê beşan bi astên cûda yên gihîştina API-ya gerokê pêk tê.

Çarçoveya dirêjkirinê

Piraniya API-ê li vir heye. Di vê çarçoveyê de ew "dijîn":

  1. Rûpelê paşîn - Beşa "paşve" ya dirêjkirinê. Pelê di manîfestoyê de bi bişkoja "paşverû" tê destnîşan kirin.
  2. Rûpelê popup - rûpelek popupê ya ku gava ku hûn li ser îkonê dirêjkirinê bitikînin xuya dibe. Di manîfestoyê de browser_action -> default_popup.
  3. rûpela xwerû - rûpela dirêjkirinê, "jiyanê" di tabloyek veqetandî ya dîtinê de chrome-extension://<id_расширения>/customPage.html.

Ev çarçoveyek serbixwe ji pace û tabloyên gerokê heye. Rûpelê paşîn di kopiyek yekane de heye û her gav dixebite (îstîsna rûpela bûyerê ye, dema ku skrîpta paşîn ji hêla bûyerek ve hatî destpêkirin û piştî darvekirina wê "mire"). Rûpelê popup heye dema ku pencereya popup vekirî ye, û rûpela xwerû - dema ku tabloya pê re vekirî ye. Ji vê çarçovê de tu gihîştina tabloyên din û naveroka wan tune.

Çarçoveya nivîsara naverokê

Pelê skrîpta naverokê digel her tabloya gerokê tê destpêkirin. Ew gihîştina beşek ji API-ya dirêjkirinê û dara DOM-ê ya rûpelê malperê heye. Ew nivîsarên naverokê ne ku berpirsiyariya danûstendina bi rûpelê re ne. Berfirehkirinên ku dara DOM-ê manîpule dikin vê yekê di nivîsarên naverokê de dikin - mînakî, astengkerên reklamê an wergêran. Di heman demê de, skrîpta naverokê dikare bi rûpelê standard re têkilî daynin postMessage.

Têkiliya rûpela malperê

Ev rûpela malperê ya rastîn bixwe ye. Ti pêwendiya wê bi dirêjkirinê re nîne û gihêştina wê tune ye, ji bilî rewşên ku domaina vê rûpelê bi eşkere di manîfestoyê de nehatiye destnîşan kirin (li ser vê yekê li jêr bêtir).

Danûstendina peyamê

Divê beşên cûda yên serîlêdanê bi hev re peyaman biguhezînin. Ji bo vê yekê API heye runtime.sendMessage ji bo şandina peyamekê background и tabs.sendMessage ji bo şandina peyamek ji rûpelek re (nivîsa naverokê, popup an rûpela malperê heke hebe externally_connectable). Li jêr mînakek heye dema ku meriv bigihîje API-ya 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))
    }
)

Ji bo pêwendiya tevahî, hûn dikarin pêwendiyan bi rê ve biafirînin runtime.connect. Di bersivê de em ê bistînin runtime.Port, ku, dema ku ew vekirî ye, hûn dikarin her hejmarek peyaman bişînin. Li aliyê xerîdar, wek nimûne, contentscript, bi vî rengî xuya dike:

// Опять же 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 an paşnav:

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

Bûyerek jî heye onDisconnect û rêbaz disconnect.

Diagrama sepanê

Ka em pêvekek gerokek çêbikin ku mifteyên taybet hilîne, gihîştina agahdariya gelemperî peyda dike (navnîşan, mifteya giştî bi rûpelê re têkilî daynin û destûrê dide serlêdanên sêyemîn ku ji bo danûstendinan îmzeyek daxwaz bikin.

Pêşveçûna sepanê

Pêdivî ye ku serîlêdana me hem bi bikarhêner re têkilî daynin û hem jî rûpelek bi API-yek peyda bike da ku rêbazan bang bike (mînak, danûstendinan îmze bike). Bi tenê yekê bikin contentscript dê nexebite, ji ber ku ew tenê gihîştina DOM-ê heye, lê ne JS-ya rûpelê. Girêdana bi rêya runtime.connect em nikarin, ji ber ku API li ser hemî domanan hewce ye, û tenê yên taybetî dikarin di manîfestoyê de bêne diyar kirin. Wekî encamek, diagram dê bi vî rengî xuya bike:

Nivîsandina pêvekek gerokek ewledar

Dê senaryoyek din hebe - inpage, ku em ê di nav rûpelê de derzî bikin. Ew ê di çarçoveya xwe de bixebite û ji bo xebata bi dirêjkirinê re API peyda bike.

Destpêka

Hemî koda dirêjkirina gerokê li vir heye GitHub. Di dema danasînê de dê lînkên commitan hebin.

Ka em bi manîfestoyê dest pê bikin:

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

Background.js, popup.js, inpage.js û contentscript.js vala ava bikin. Em popup.html lê zêde dikin - û serîlêdana me jixwe dikare di Google Chrome de were barkirin û pê ewle bibe ku ew dixebite.

Ji bo verastkirina vê, hûn dikarin kodê bigirin ji vir. Digel tiştê ku me kir, lînkê bi karanîna webpackê kombûna projeyê mîheng kir. Ji bo ku serîlêdanek li gerokê zêde bikin, di chrome://extensions de hûn hewce ne ku barkirina nepakkirî û peldanka bi dirêjkirina têkildar hilbijêrin - di doza me de dist.

Nivîsandina pêvekek gerokek ewledar

Niha pêveka me hatiye sazkirin û kar dike. Hûn dikarin amûrên pêşdebiran ji bo şertên cûda yên jêrîn bimeşînin:

popup ->

Nivîsandina pêvekek gerokek ewledar

Gihîştina konsolê skrîpta naverokê bi navgîniya konsolê rûpela ku ew li ser hatî destpêkirin ve tête kirin.Nivîsandina pêvekek gerokek ewledar

Danûstendina peyamê

Ji ber vê yekê, pêdivî ye ku em du kanalên ragihandinê ava bikin: paşîn <-> paşperdeya hundurîn û paşperdeya popup <->. Bê guman, hûn dikarin tenê peyaman bişînin portê û protokola xwe îcad bikin, lê ez nêzîkatiya ku min di projeya çavkaniya vekirî ya metamask de dît tercîh dikim.

Ev pêvekek gerokek e ku bi tora Ethereum re dixebite. Di wê de, beşên cihêreng ên serîlêdanê bi RPC-ê bi karanîna pirtûkxaneya dnode re têkilî daynin. Ew dihêle hûn bi lez û bez danûstendinek organîze bikin heke hûn wê wekî veguhêzek nodejs peyda bikin (tê wateya tiştek ku heman navbeynê pêk tîne):

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

Niha em ê çînek serîlêdanê çêbikin. Ew ê ji bo popup û rûpela malperê tiştên API-ê biafirîne, û ji wan re dnode biafirîne:

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

Li vir û jêr, li şûna objeya gerdûnî ya Chrome, em extensionApi bikar tînin, ku di geroka Google-ê de Chrome-ê û li yên din geroka digihîje Chrome. Ev ji bo lihevhatina gerokê ya cross-gerokê tê kirin, lê ji bo mebestên vê gotarê, hûn dikarin bi tenê 'chrome.runtime.connect' bikar bînin.

Ka em di skrîpta paşîn de mînakek serîlêdanê biafirînin:

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

Ji ber ku dnode bi çeman re dixebite, û em portek distînin, çînek adapter hewce ye. Ew bi karanîna pirtûkxaneya stream-a xwendinê ve hatî çêkirin, ku di gerokê de herikên nodejs bicîh tîne:

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

Naha em têkiliyek di UI-yê de biafirînin:

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

Dûv re em di skrîpta naverokê de pêwendiyê diafirînin:

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

Ji ber ku em hewceyê API-ê ne ne di naverokê de, lê rasterast li ser rûpelê, em du tiştan dikin:

  1. Em du çeman ava dikin. Yek - ber bi rûpelê, li jora postMessage. Ji bo vê em vê bikar tînin vê pakêtê ji afirînerên metamaskê. Herikîna duyemîn ew e ku li ser porta ku jê hatî wergirtin paşverû ye runtime.connect. Werin em wan bikirin. Naha dê rûpelek li paşperdeyê hebe.
  2. Skrîptê di DOM-ê de derxînin. Skrîptê dakêşin (gehiştina wê di manîfestoyê de destûr bû) û etîketek biafirînin script bi naveroka xwe ya hundir:

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

Naha em di hundurê rûpelê de tiştek api diafirînin û wê li gerdûnî bicîh dikin:

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

Em amade ne Banga Pêvajoya Dûr (RPC) bi API-ya cihêreng ji bo rûpel û UI. Dema ku rûpelek nû bi paşnavê ve girêdide em dikarin vê yekê bibînin:

Nivîsandina pêvekek gerokek ewledar

API û eslê vala. Li aliyê rûpelê, em dikarin fonksiyona hello bi vî rengî bang bikin:

Nivîsandina pêvekek gerokek ewledar

Karkirina bi fonksiyonên paşvekişînê di JS-ya nûjen de şêwazek xirab e, ji ber vê yekê em arîkarek piçûk binivîsin da ku dnode biafirînin ku destûrê dide we ku hûn tiştek API-yê ji kargêran re derbas bikin.

Tiştên API-ê dê nuha bi vî rengî xuya bikin:

export class SignerApp {

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

...

}

Ji dûr ve tiştek bi vî rengî wergirtin:

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

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

Û bangkirina fonksiyonan sozek vedigerîne:

Nivîsandina pêvekek gerokek ewledar

Guhertoya bi fonksiyonên asynchronous heye vir.

Bi tevayî, nêzîkatiya RPC û stream pir maqûl xuya dike: em dikarin pirrengkirina steam bikar bînin û ji bo karên cihê çend API-yên cihêreng biafirînin. Di prensîbê de, dnode dikare li her deverê were bikar anîn, ya sereke ev e ku veguheztinê di forma çemek nodejs de pêça.

Alternatîfek formata JSON e, ku protokola JSON RPC 2 pêk tîne. Lêbelê, ew bi veguheztinên taybetî (TCP û HTTP(S)) re dixebite, ku di doza me de ne derbasdar e.

Dewleta Navxweyî û Storage herêmî

Em ê hewce ne ku rewşa navxweyî ya serîlêdanê hilînin - bi kêmanî bişkojkên îmzekirinê. Em dikarin bi hêsanî dewletek li serîlêdan û awayên guheztina wê di API-ya popupê de zêde bikin:

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

    ...

} 

Di paşerojê de, em ê her tiştî di fonksiyonek de bipêçin û tiştê serîlêdanê li pencereyê binivîsin da ku em ji konsolê pê re bixebitin:

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

Ka em çend bişkokên ji konsolê UI lê zêde bikin û bibînin ka bi dewletê re çi qewimî:

Nivîsandina pêvekek gerokek ewledar

Pêdivî ye ku dewlet bi israr were çêkirin da ku gava ji nû ve dest pê bike kilît winda nebin.

Em ê wê li localStorage hilînin, bi her guhertinê re wê binivîsînin. Dûv re, gihîştina wê dê ji bo UI-yê jî hewce be, û ez jî dixwazim bibim abone li guhertinan. Li ser vê yekê, ew ê hêsan be ku meriv hilanînek berbiçav biafirîne û li guhertinên wê were qeyd kirin.

Em ê pirtûkxaneya mobx bikar bînin (https://github.com/mobxjs/mobx). Hilbijartin li ser wê ket ji ber ku ez neçar bûm ku pê re bixebitim, lê min bi rastî dixwest ku ez wê bixwînim.

Ka em destpêkirina rewşa destpêkê lê zêde bikin û firotgehê binihêrin:

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

    ...

}

"Di bin serpêhatiyê de," mobx li şûna hemî qadên firotgehê bi proxy veguhezandiye û hemî bangên ji wan re digire. Dê bibe abone li van peyaman.

Li jêr ez ê pir caran peyva "dema ku diguhezim" bikar bînim, her çend ev bi tevahî ne rast e. Mobx gihîştina zeviyan dişopîne. Bergir û sazkerên tiştên proxy yên ku pirtûkxane diafirîne têne bikar anîn.

Decoratorên çalakiyê du armancan xizmet dikin:

  1. Di moda hişk de bi ala EnforceActions, mobx guhertina dewletê rasterast qedexe dike. Karkirina di bin şert û mercên hişk de pratîkek baş tê hesibandin.
  2. Tewra heke fonksiyonek çend caran rewşê biguherîne - mînakî, em di çend rêzikên kodê de çend qadan biguhezînin - dema ku ew temam dibe çavdêr têne agahdar kirin. Ev bi taybetî ji bo pêşiyê girîng e, ku nûvekirinên dewletê yên nepêwist rê li ber veguheztina nehewce ya hêmanan digirin. Di doza me de, ne yekem û ne jî ya duyemîn bi taybetî têkildar e, lê em ê pratîkên çêtirîn bişopînin. Adet e ku meriv dekoratoran bi hemî fonksiyonên ku rewşa zeviyên çavdêriyê diguhezîne ve girêbide.

Di paşerojê de em ê destpêkkirin û tomarkirina dewletê li Herêmî Storage zêde bikin:

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

Fonksiyona reaksiyonê li vir balkêş e. Du argumanên wê hene:

  1. Hilbijêra daneyê.
  2. Desthilatdarek ku her gava ku ew diguhezîne dê bi vê daneyê re were gazî kirin.

Berevajî redux, ku em bi eşkere dewletê wekî argumanek distînin, mobx bi bîr tîne ku em di hundurê hilbijêran de xwe bigihînin kîjan çavdêran, û tenê gava ku ew diguhezin gazî hander dike.

Girîng e ku meriv tam fêm bike ka mobx çawa biryar dide ku em kîjan çavdêran abone dikin. Ger min hilbijêrek bi kodek weha nivîsand() => app.store, wê hingê dê reaksiyonê qet neyê gotin, ji ber ku hilanînê bixwe nayê dîtin, tenê zeviyên wê ne.

Ger min wisa binivîsanda () => app.store.keys, wê hingê dê dîsa tiştek neqewime, ji ber ku dema ku hêmanên rêzê lê zêdekirin/rakirin, referansa wê nayê guhertin.

Mobx ji bo cara yekem wekî hilbijarker tevdigere û tenê çavdêriyên ku me gihîştine dişopîne. Ev bi rêya proxy getters pêk tê. Ji ber vê yekê, fonksiyona çêkirî li vir tê bikar anîn toJS. Ew bi hemî proxeyên ku bi qadên orîjînal ve hatine veguheztin tiştek nû vedigerîne. Di dema darvekirinê de, ew hemî qadên objektê dixwîne - ji ber vê yekê wergir têne derxistin.

Di konsola popupê de em ê dîsa çend bişkokan lê zêde bikin. Vê carê ew jî di Storage herêmî de bi dawî bûn:

Nivîsandina pêvekek gerokek ewledar

Dema ku rûpela paşnavê ji nû ve tê barkirin, agahdarî di cîh de dimîne.

Hemî koda serîlêdanê heya vê gavê dikare were dîtin vir.

Depokirina ewle ya mifteyên taybet

Hilgirtina kilîtên taybet di nivîsa zelal de ne ewle ye: her gav şansek heye ku hûn werin hack kirin, bigihîjin komputera xwe û hwd. Ji ber vê yekê, di localStorage de em ê mifteyan bi rengek şîfrekirî hilînin.

Ji bo ewlehiya mezintir, em ê rewşek kilîtkirî li serîlêdanê zêde bikin, ku tê de qet gihandina bişkokan tune. Em ê ji ber demek dirêjkirinê bixweber veguhezînin rewşa girtî.

Mobx dihêle hûn tenê komek hindiktirîn daneyan hilînin, û yên mayî bixweber li ser bingeha wê têne hesibandin. Vana bi navê taybetmendiyên hesabkirî ne. Ew dikarin bi dîtinên di databases de bêne berhev kirin:

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

Naha em tenê kilît û şîfreya şîfrekirî hilînin. Her tiştê din tê hesibandin. Em bi rakirina şîfreyê ji dewletê veguheztina dewletek girtî dikin. API-ya gelemperî nuha rêbazek ji bo destpêkirina hilanînê heye.

Ji bo şîfrekirinê hatiye nivîsandin karûbarên ku crypto-js bikar tînin:

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

Gerok xwedan API-yek neçalak e ku bi navgîniya wê hûn dikarin bibin abone li bûyerek - guhertinên dewletê. Dewlet, li gorî wê, dibe idle, active и locked. Ji bo betaliyê hûn dikarin wextek demdirêj destnîşan bikin, û dema ku OS bixwe tê asteng kirin kilît tê danîn. Em ê her weha hilbijêrê ji bo tomarkirina li Storage herêmî biguherînin:

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

Koda berî vê gavê ye vir.

Transactions

Ji ber vê yekê, em gihîştin tiştê herî girîng: çêkirin û îmzekirina danûstendinên li ser blokê. Em ê bloka û pirtûkxaneya WAVES bikar bînin pêlên-danûstandin.

Pêşî, bila em rêzek peyamên ku divê bêne îmze kirin li dewletê zêde bikin, dûv re rêbazên lê zêdekirina peyamek nû, pejirandina îmzeyê û redkirina zêde bikin:

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

    ...
}

Dema ku em peyamek nû distînin, em metadata lê zêde dikin, bikin observable û lê zêde bike store.messages.

Heke hûn nekin observable bi destan, wê hingê mobx dema ku peyaman li rêzê zêde bike wê bixwe bike. Lêbelê, ew ê tiştek nû biafirîne ku em ê jê re referansek nebin, lê em ê ji bo gava paşîn jê re hewce bikin.

Dûv re, em sozek vedigerînin ku dema ku statûya peyamê diguhezîne çareser dibe. Rewş ji hêla reaksiyonê ve tê şopandin, ku dema ku statû biguhere dê "xwe bikuje".

Koda rêbazê approve и reject pir hêsan: em bi tenê statûya peyamê diguhezînin, piştî ku heke pêwîst be, wê îmze bikin.

Em di API-ya UI-yê de Pejirandin û red kirin, di API-ya rûpelê de peyama nû:

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

    ...
}

Naha em hewl bidin ku danûstendinê bi dirêjkirinê re îmze bikin:

Nivîsandina pêvekek gerokek ewledar

Bi gelemperî, her tişt amade ye, ya ku dimîne ew e UI-ya hêsan lê zêde bike.

UI

Pêdivî ye ku navbeynkar bigihîje rewşa serîlêdanê. Li aliyê UI em ê bikin observable dewlet û fonksiyonek li API-ê zêde bike ku dê vê rewşê biguherîne. Em lê zêde bikin observable ji bo tiştê API-ê ku ji paşnavê hatî wergirtin:

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

Di dawiyê de em dest bi pêşkêşkirina navrûya serîlêdanê dikin. Ev serîlêdana reaksiyonê ye. Tişta paşerojê bi tenê bi karanîna pêşniyaran tê derbas kirin. Bê guman, ew ê rast be ku ji bo rêbazan û dikanek ji bo dewletê karûbarek veqetandî were çêkirin, lê ji bo mebestên vê gotarê ev bes e:

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

Digel mobx-ê gava ku dane diguhezin pir hêsan e ku meriv dest bi vegotinê bike. Em tenê dekoratorê çavdêr ji pakêtê daliqînin mobx-react li ser hêmanê, û dema ku çavdêriyên ku ji hêla pêkhateyê ve têne referans kirin biguhezînin dê bixweber were gazî kirin. Hûn ne hewce ne mapStateToProps in an jî mîna redux-ê ve girêdin. Her tişt rast ji qutiyê dixebite:

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

Parçeyên mayî dikarin di kodê de werin dîtin di peldanka UI de.

Naha di pola serîlêdanê de hûn hewce ne ku ji bo UI-yê hilbijêrek dewletê çêbikin û gava ku ew diguhezîne UI-yê agahdar bikin. Ji bo vê yekê, bila rêbazek lê zêde bike getState и reactionbang dikin 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())

        })
    }

    ...
}

Dema ku tiştek wergirtin remote afirandin reaction ji bo guherandina rewşa ku fonksiyonê li aliyê UI-yê vedixwîne.

Têkiliya paşîn ev e ku meriv nîşana peyamên nû li ser îkonê dirêjkirinê zêde bike:

function setupApp() {
...

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

...
}

Ji ber vê yekê, serîlêdan amade ye. Dibe ku rûpelên malperê ji bo danûstandinan îmzeyek daxwaz bikin:

Nivîsandina pêvekek gerokek ewledar

Nivîsandina pêvekek gerokek ewledar

Kod li vir heye link.

encamê

Ger we gotar heya dawiyê xwendibe, lê dîsa jî pirsên we hebin, hûn dikarin ji wan bipirsin depoyên bi dirêjkirinê. Li wir hûn ê ji bo her gavê destnîşankirî jî peymanan bibînin.

Û heke hûn bala we dikin ku li kodê ji bo dirêjkirina rastîn binihêrin, hûn dikarin vê bibînin vir.

Kod, depo û danasîna kar ji siemarell

Source: www.habr.com

Add a comment