Síneadh brabhsálaí slán a scríobh

Síneadh brabhsálaí slán a scríobh

Murab ionann agus an ailtireacht “cliaint-freastalaí”, is iad seo a leanas na tréithe a bhaineann le feidhmchláir dhíláraithe:

  • Ní gá bunachar sonraí a stóráil le logáil isteach úsáideoirí agus pasfhocail. Is iad na húsáideoirí féin amháin a stórálann an fhaisnéis rochtana, agus tarlaíonn dearbhú ar a mbarántúlacht ag leibhéal an phrótacail.
  • Níl gá le freastalaí a úsáid. Is féidir an loighic iarratais a fhorghníomhú ar líonra blockchain, áit ar féidir an méid sonraí is gá a stóráil.

Tá 2 stóras sách sábháilte ann le haghaidh eochracha úsáideora - sparán crua-earraí agus síntí brabhsálaí. Tá sparán crua-earraí den chuid is mó thar a bheith slán, ach deacair le húsáid agus i bhfad ó saor in aisce, ach is meascán foirfe slándála agus éasca le húsáid iad síntí brabhsálaí, agus is féidir leo a bheith go hiomlán saor in aisce freisin d’úsáideoirí deiridh.

Ag cur seo go léir san áireamh, bhíomar ag iarraidh an síneadh is sláine a dhéanamh a shimplíonn forbairt feidhmchlár díláraithe trí API simplí a sholáthar chun oibriú le hidirbhearta agus sínithe.
Inseoimid duit faoin taithí seo thíos.

Beidh treoracha céim ar chéim san alt maidir le conas síneadh brabhsálaí a scríobh, le samplaí cód agus screenshots. Is féidir leat teacht ar an gcód ar fad i stórtha. Freagraíonn gach gealltanas go loighciúil do chuid den alt seo.

Stair Achomair ar Eisínteachtaí Brabhsálaí

Tá síntí brabhsálaí thart le fada an lá. Bhí siad le feiceáil in Internet Explorer siar i 1999, i Firefox i 2004. Mar sin féin, ar feadh i bhfad ní raibh aon chaighdeán amháin le haghaidh síntí.

Is féidir linn a rá go raibh an chuma air chomh maith le síntí sa cheathrú leagan de Google Chrome. Ar ndóigh, ní raibh aon sonraíocht ann an uair sin, ach ba é an API Chrome a bhí mar bhunús leis: tar éis dó an chuid is mó den mhargadh brabhsálaí a shárú agus siopa feidhmchlár ionsuite a bheith aige, shocraigh Chrome an caighdeán le haghaidh síntí brabhsálaí.

Bhí a chaighdeán féin ag Mozilla, ach nuair a chonaic an chuideachta an tóir a bhí ar síntí Chrome, chinn an chuideachta API comhoiriúnach a dhéanamh. In 2015, ar thionscnamh Mozilla, cruthaíodh grúpa speisialta laistigh de Chuibhreannas an Ghréasáin Dhomhanda (W3C) chun oibriú ar shonraíochtaí síneadh trasbhrabhsálaí.

Glacadh leis na síntí API reatha do Chrome mar bhonn. Rinneadh an obair le tacaíocht ó Microsoft (dhiúltaigh Google páirt a ghlacadh i bhforbairt an chaighdeáin), agus mar thoradh air sin bhí dréacht le feiceáil sonraíochtaí.

Go foirmiúil, tacaíonn Edge, Firefox agus Opera leis an tsonraíocht (tabhair faoi deara nach bhfuil Chrome ar an liosta seo). Ach i ndáiríre, tá an caighdeán ag luí go mór le Chrome, ós rud é go bhfuil sé scríofa i ndáiríre bunaithe ar a chuid síntí. Is féidir leat tuilleadh a léamh faoin WebExtensions API anseo.

Struchtúr síneadh

Is é an t-aon chomhad atá ag teastáil don síneadh ná an forléiriú (manifest.json). Is é freisin an “pointe iontrála” don leathnú.

Manifesto

De réir na sonraíochta, is comhad JSON bailí é an comhad follasach. Cur síos iomlán ar na heochracha follasacha le faisnéis faoi na heochracha a dtacaítear leo inar féidir féachaint ar an mbrabhsálaí anseo.

D’fhéadfaí neamhaird a dhéanamh d’eochracha nach bhfuil sa tsonraíocht (is féidir earráidí a thuairisciú idir Chrome agus Firefox, ach leanann na síntí ar aghaidh ag obair).

Agus ba mhaith liom aird a tharraingt ar roinnt pointí.

  1. cúlra — réad a chuimsíonn na réimsí seo a leanas:
    1. scripteanna — sraith scripteanna a fhorghníomhófar sa chomhthéacs cúlra (beidh muid ag caint faoi seo beagán níos déanaí);
    2. leathanach - in ionad scripteanna a fhorghníomhófar ar leathanach folamh, is féidir leat html a shonrú le hábhar. Sa chás seo, déanfar neamhaird ar an réimse script, agus beidh gá na scripteanna a chur isteach sa leathanach ábhar;
    3. seasfaidh siad — bratach dhénártha, mura sonraítear é, “maróidh” an brabhsálaí an próiseas cúlra nuair a mheasann sé nach bhfuil aon rud á dhéanamh aige, agus atosóidh sé é más gá. Seachas sin, ní dhéanfar an leathanach a dhíluchtú ach amháin nuair a dhúntar an brabhsálaí. Ní thacaítear leis in Firefox.
  2. ábhar_scripteanna — raon ábhar a ligeann duit scripteanna éagsúla a lódáil chuig leathanaigh ghréasáin éagsúla. Tá na réimsí tábhachtacha seo a leanas i ngach réad:
    1. cluichí - url patrún, a chinneann an gcuirfear script ábhar ar leith san áireamh nó nach gcuirfear.
    2. js — liosta de na scripteanna a lódálfar isteach sa chluiche seo;
    3. eisiamh_matches - eisiamh ón réimse match URLanna a mheaitseálann an réimse seo.
  3. leathanach_gníomh - is réad é i ndáiríre atá freagrach as an deilbhín atá ar taispeáint in aice leis an mbarra seoltaí sa bhrabhsálaí agus idirghníomhú leis. Ligeann sé duit freisin fuinneog aníos a thaispeáint, a shainítear ag baint úsáide as do HTML, CSS agus JS féin.
    1. preabfhuinneog_réamhshocraithe — cosán chuig an gcomhad HTML leis an gcomhéadan aníos, féadfaidh CSS agus JS a bheith ann.
  4. Ceadanna — eagar chun cearta sínte a bhainistiú. Tá 3 chineál ceart ann, a gcuirtear síos go mion orthu anseo
  5. acmhainní_rochtana_gréasáin — acmhainní sínte is féidir le leathanach gréasáin a iarraidh, mar shampla, íomhánna, JS, CSS, comhaid HTML.
  6. go seachtrach_nasctha — anseo is féidir leat aitheantais na síntí agus na bhfearann ​​​​eile de leathanaigh ghréasáin a shonrú go sainráite ónar féidir leat nascadh. Is féidir le fearann ​​a bheith dara leibhéal nó níos airde. Ní oibríonn sé i Firefox.

Comhthéacs forghníomhaithe

Tá trí chomhthéacs forghníomhaithe cód ag an síneadh, is é sin, tá an t-iarratas comhdhéanta de thrí chuid le leibhéil éagsúla rochtana ar API an bhrabhsálaí.

Comhthéacs síneadh

Tá an chuid is mó den API ar fáil anseo. Sa chomhthéacs seo tá siad “beo”:

  1. Leathanach cúlra — cuid den síneadh “innill”. Sonraítear an comhad sa léiriú ag baint úsáide as an eochair "cúlra".
  2. Leathanach aníos — leathanach aníos a thaispeánfar nuair a chliceálann tú ar dheilbhín an tsínidh. Sa fhorógra browser_action -> default_popup.
  3. Leathanach saincheaptha — leathanach síneadh, “beo” i gcluaisín ar leith den radharc chrome-extension://<id_расширения>/customPage.html.

Tá an comhthéacs seo ann neamhspleách ar fhuinneoga agus cluaisíní brabhsálaí. Leathanach cúlra ann i gcóip amháin agus oibríonn sé i gcónaí (is í an eisceacht leathanach an imeachta, nuair a sheoltar an script chúlra ag imeacht agus “básaíonn sé” tar éis é a fhorghníomhú). Leathanach aníos ann nuair a bhíonn an fhuinneog aníos oscailte, agus Leathanach saincheaptha - cé go bhfuil an cluaisín leis oscailte. Níl aon rochtain ar chlubanna eile agus a n-ábhar ón gcomhthéacs seo.

Comhthéacs script ábhar

Seoltar an comhad script inneachair in éineacht le gach cluaisín brabhsálaí. Tá rochtain aige ar chuid de API an tsínidh agus ar chrann DOM an leathanaigh ghréasáin. Scripteanna ábhar atá freagrach as idirghníomhú leis an leathanach. Déanann síntí a ionramhálann an crann DOM é seo i scripteanna ábhar - mar shampla, bacóirí fógraí nó aistritheoirí. Chomh maith leis sin, is féidir leis an script ábhar cumarsáid a dhéanamh leis an leathanach trí chaighdeán postMessage.

Comhthéacs leathanach gréasáin

Is é seo an leathanach gréasáin féin. Níl baint ar bith aige leis an síneadh agus níl rochtain aige ann, ach amháin i gcásanna nach bhfuil fearann ​​an leathanaigh seo léirithe go sainráite sa fhorléiriú (tuilleadh air seo thíos).

Malartú teachtaireachta

Caithfidh codanna éagsúla den fheidhmchlár teachtaireachtaí a mhalartú lena chéile. Tá API ann chuige seo runtime.sendMessage chun teachtaireacht a sheoladh background и tabs.sendMessage chun teachtaireacht a sheoladh chuig leathanach (script inneachair, aníos nó leathanach gréasáin má tá fáil air externally_connectable). Seo thíos sampla agus tú ag rochtain ar an API Chrome.

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

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

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

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

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

Le haghaidh cumarsáide iomlán, is féidir leat naisc a chruthú trí runtime.connect. Mar fhreagra gheobhaidh muid runtime.Port, agus, cé go bhfuil sé oscailte, is féidir leat aon líon teachtaireachtaí a sheoladh. Ar thaobh an chliaint, mar shampla, contentscript, tá sé mar seo:

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

Freastalaí nó cúlra:

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

Tá imeacht ann freisin onDisconnect agus modh disconnect.

Léaráid iarratais

Déanaimis síneadh brabhsálaí a stórálann eochracha príobháideacha, a sholáthraíonn rochtain ar fhaisnéis phoiblí (seoladh, déanann eochair phoiblí cumarsáid leis an leathanach agus ligeann d’fheidhmchláir tríú páirtí síniú a iarraidh le haghaidh idirbheart.

Forbairt Feidhmchláir

Caithfidh ár n-iarratas idirghníomhú leis an úsáideoir agus API a sholáthar don leathanach chun modhanna a ghlaoch (mar shampla, chun idirbhearta a shíniú). Déan a dhéanamh le ceann amháin contentscript ní oibreoidh sé, mar níl rochtain aige ach ar an DOM, ach ní chuig JS an leathanaigh. Ceangail trí runtime.connect ní féidir linn, toisc go bhfuil an API ag teastáil ar gach fearann, agus ní féidir ach cinn shonracha a shonrú sa léiriú. Mar thoradh air sin, beidh cuma mar seo ar an léaráid:

Síneadh brabhsálaí slán a scríobh

Beidh script eile ann - inpage, a chuirfimid isteach sa leathanach. Rithfidh sé ina chomhthéacs agus cuirfidh sé API ar fáil chun oibriú leis an síneadh.

Tosaigh

Tá gach cód síneadh brabhsálaí ar fáil ag GitHub. Le linn na tuairisce beidh naisc le gealltanais.

Cuirimis tús leis an bhforógra:

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

Cruthaigh cúlra folamh.js, popup.js, inpage.js agus contentscript.js. Cuirimid popup.html leis - agus is féidir ár bhfeidhmchlár a luchtú isteach i Google Chrome cheana féin agus a chinntiú go n-oibríonn sé.

Chun é seo a fhíorú, is féidir leat an cód a ghlacadh dá bhrí sin. Chomh maith leis an méid a rinneamar, chumraigh an nasc tionól an tionscadail ag baint úsáide as webpack. Chun iarratas a chur leis an mbrabhsálaí, i chrome://extensions is gá duit a roghnú ualach unpacked agus an fillteán leis an síneadh comhfhreagrach - inár gcás dist.

Síneadh brabhsálaí slán a scríobh

Anois tá ár síneadh suiteáilte agus ag obair. Is féidir leat na huirlisí forbróra a rith le haghaidh comhthéacsanna éagsúla mar seo a leanas:

aníos ->

Síneadh brabhsálaí slán a scríobh

Déantar rochtain ar chonsól script an ábhair trí chonsól an leathanaigh féin ar a seoltar é.Síneadh brabhsálaí slán a scríobh

Malartú teachtaireachta

Mar sin, ní mór dúinn dhá bhealach cumarsáide a bhunú: inpage <-> cúlra agus aníos <-> cúlra. Is féidir leat, ar ndóigh, teachtaireachtaí a sheoladh chuig an gcalafort agus do phrótacal féin a chumadh, ach is fearr liom an cur chuige a chonaic mé sa tionscadal foinse oscailte metamask.

Is síneadh brabhsálaí é seo chun oibriú le líonra Ethereum. Inti, déanann codanna éagsúla den fheidhmchlár cumarsáid trí RPC ag baint úsáide as an leabharlann dnode. Ligeann sé duit malartú a eagrú go tapa agus go háisiúil má chuireann tú sruth nódejs ar fáil dó mar iompar (rud a chiallaíonn rud a chuireann an comhéadan céanna i bhfeidhm):

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

Anois cruthóimid rang iarratais. Cruthóidh sé oibiachtaí API don phreabfhuinneog agus don leathanach gréasáin, agus cruthóidh sé dnode dóibh:

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

Anseo agus thíos, in ionad an réad Chrome domhanda, úsáidimid extensionApi, a fhaigheann rochtain ar Chrome i mbrabhsálaí Google agus brabhsálaí i gcásanna eile. Déantar é seo ar mhaithe le comhoiriúnacht trasbhrabhsálaí, ach chun críocha an ailt seo d’fhéadfaí ‘chrome.runtime.connect’ a úsáid go simplí.

Cruthaímid sampla feidhmchláir sa script chúlra:

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

Ós rud é go n-oibríonn dnode le sruthanna, agus go bhfaighimid calafort, tá gá le haicme oiriúnaitheora. Déantar é ag baint úsáide as an leabharlann sruth inléite, a chuireann sruthanna nodejs i bhfeidhm sa bhrabhsálaí:

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

Anois cruthaimis nasc san Chomhéadain:

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

Ansin cruthaímid an nasc sa script ábhair:

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

Ós rud é go bhfuil an API ag teastáil uainn nach bhfuil sa script ábhair, ach go díreach ar an leathanach, déanaimid dhá rud:

  1. Cruthaímid dhá shruth. Amháin - i dtreo an leathanaigh, ar bharr an postMessage. Chun seo a úsáid againn seo pacáiste seo ó chruthaitheoirí metaamask. Is é an dara sruth ná cúlra an chalafoirt a fuarthas ó runtime.connect. A ligean ar iad a cheannach. Anois beidh sruth ag an leathanach go dtí an cúlra.
  2. Instealladh an script isteach sa DOM. Íoslódáil an script (cheadaíodh rochtain air sa léiriú) agus cruthaigh clib script lena bhfuil istigh:

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

Anois cruthaímid réad api ar leathanach agus socróimid go domhanda é:

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

Táimid réidh Glao ar Nós Imeachta Cianda (RPC) le API ar leith don leathanach agus don Chomhéadain. Agus leathanach nua á nascadh le cúlra is féidir linn é seo a fheiceáil:

Síneadh brabhsálaí slán a scríobh

API folamh agus tionscnaimh. Ar thaobh an leathanaigh, is féidir linn an fheidhm hello a ghlaoch mar seo:

Síneadh brabhsálaí slán a scríobh

Is drochbhéasach é oibriú le feidhmeanna aisghlaoch i JS nua-aimseartha, mar sin scríobhaimis cúntóir beag chun dnode a chruthú a ligeann duit réad API a chur ar aghaidh chuig uirlisí.

Beidh cuma mar seo ar na hoibiachtaí API anois:

export class SignerApp {

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

...

}

Rud a fháil ón gcianrialtán mar seo:

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

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

Agus tugann glaonna feidhmeanna gealltanas:

Síneadh brabhsálaí slán a scríobh

Leagan le feidhmeanna asincrónacha ar fáil anseo.

Tríd is tríd, is cosúil go bhfuil an cur chuige RPC agus sruth solúbtha go leor: is féidir linn ilphléacsáil gaile a úsáid agus roinnt APIanna éagsúla a chruthú le haghaidh tascanna éagsúla. I bprionsabal, is féidir dnode a úsáid in áit ar bith, is é an rud is mó ná an iompar a fhilleadh i bhfoirm sruth nódejs.

Rogha eile is ea an fhormáid JSON, a chuireann prótacal JSON RPC 2 i bhfeidhm. Mar sin féin, oibríonn sé le hiompar ar leith (TCP agus HTTP(S)), rud nach bhfuil infheidhme inár gcás.

Staid inmheánach agus stóráil áitiúil

Beidh orainn staid inmheánach an fheidhmchláir a stóráil - na heochracha sínithe ar a laghad. Is furasta dúinn staid a chur leis an bhfeidhmchlár agus na modhanna chun é a athrú san API aníos:

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

    ...

} 

Sa chúlra, cuirfimid gach rud i bhfeidhm agus scríobhfaimid réad an fheidhmchláir chuig an bhfuinneog ionas gur féidir linn oibriú leis ón gconsól:

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

Cuirfimid cúpla eochair leis ón gconsól Chomhéadain agus féachfaimid cad a tharlaíonn leis an stát:

Síneadh brabhsálaí slán a scríobh

Ní mór an stát a dhéanamh go leanúnach ionas nach gcailltear na heochracha nuair a atosófar iad.

Déanfaimid é a stóráil i localStorage, agus é á fhorscríobh le gach athrú. Ina dhiaidh sin, beidh rochtain ar an Chomhéadain riachtanach freisin, agus ba mhaith liom síntiús a íoc le hathruithe freisin. Bunaithe ar seo, beidh sé áisiúil stór inbhraite a chruthú agus liostáil lena athruithe.

Úsáidfimid an leabharlann mobx (https://github.com/mobxjs/mobx). Thit an rogha air mar ní raibh orm oibriú leis, ach bhí mé i ndáiríre ag iarraidh staidéar a dhéanamh air.

Cuirimis tús leis an staid tosaigh agus cuirfimid an siopa inbhraite:

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

    ...

}

“Faoi an cochall,” tá mobx tar éis seachfhreastalaí a chur in ionad na réimsí stórais go léir agus idircheapann sé gach glaoch orthu. Beifear in ann liostáil leis na teachtaireachtaí seo.

Anseo thíos úsáidfidh mé an téarma “nuair a athrú”, cé nach bhfuil sé seo go hiomlán ceart. Rianaíonn Mobx rochtain ar réimsí. Úsáidtear faighteoirí agus socraitheoirí seachfhreastalaí a chruthaíonn an leabharlann.

Tá dhá chuspóir ag maisitheoirí gníomhaíochta:

  1. I mód dian leis an mbratach enforceActions, cuireann mobx cosc ​​ar an stát a athrú go díreach. Meastar gur dea-chleachtas é oibriú faoi dhianchoinníollacha.
  2. Fiú má athraíonn feidhm an stát arís agus arís eile - mar shampla, athraíonn muid roinnt réimsí i roinnt línte cód - ní thugtar fógra do na breathnóirí ach amháin nuair a chríochnaíonn sé. Tá sé seo thar a bheith tábhachtach don aghaidh, nuair a bhíonn rindreáil neamhriachtanach na n-eilimintí mar thoradh ar nuashonruithe stáit nach bhfuil gá leo. Inár gcás, níl an chéad cheann ná an dara ceann thar a bheith ábhartha, ach leanfaimid na cleachtais is fearr. Is gnách maisitheoirí a cheangal le gach feidhm a athraíonn staid na réimsí breathnaithe.

Sa chúlra cuirfimid tús leis agus sábhálfaimid an stát i localStorage:

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

Tá feidhm an imoibrithe suimiúil anseo. Tá dhá argóint ann:

  1. Roghnóir sonraí.
  2. Láimhseálaí a ghlaofar leis na sonraí seo gach uair a athraíonn sé.

Murab ionann agus redux, áit a bhfaighimid an stát go sainráite mar argóint, cuimhníonn mobx ar na nithe inbhraite a bhfuil rochtain againn orthu laistigh den roghnóir, agus ní ghlaonn sé ar an láimhseálaí ach amháin nuair a athraíonn siad.

Tá sé tábhachtach a thuiscint go beacht conas a chinneann mobx cé na nithe inbhraite a bhfuilimid ag síntiús leo. Má scríobh mé roghnóir i gcód mar seo() => app.store, ansin ní bheidh imoibriú ar a dtugtar riamh, ós rud é nach bhfuil an stóráil féin inbhraite, ach a réimsí.

Má scríobh mé é mar seo () => app.store.keys, ansin arís ní tharlódh rud ar bith, mar nuair a chuirtear eilimintí eagar leis/á mbaintear iad, ní athrófar an tagairt dó.

Feidhmíonn Mobx mar roghnóir den chéad uair agus ní choinníonn sé ach súil ar rudaí inbhraite a bhfuil rochtain againn orthu. Déantar é seo trí sheachfhreastalaithe. Dá bhrí sin, úsáidtear an fheidhm ionsuite anseo toJS. Tugann sé rud nua ar ais agus cuirtear na bunréimsí in ionad gach seachvótálaí. Le linn an fhorghníomhaithe, léann sé réimsí uile an ruda - mar sin déantar na getters a tharraing.

Sa chonsól aníos cuirfimid roinnt eochracha arís. An uair seo chuaigh siad isteach sa Stóráil áitiúil freisin:

Síneadh brabhsálaí slán a scríobh

Nuair a athlódáiltear an leathanach cúlra, fanann an fhaisnéis i bhfeidhm.

Is féidir féachaint ar gach cód iarratais go dtí an pointe seo anseo.

Stóráil slán eochracha príobháideacha

Níl sé sábháilte eochracha príobháideacha a stóráil i dtéacs soiléir: bíonn seans i gcónaí go ndéanfar hackáil ort, go bhfaighidh tú rochtain ar do ríomhaire, agus mar sin de. Mar sin, in localStorage stórálfaimid na heochracha i bhfoirm atá criptithe le pasfhocal.

Ar mhaithe le slándáil níos fearr, cuirfimid staid faoi ghlas leis an bhfeidhmchlár, ina mbeidh aon rochtain ar na heochracha ar chor ar bith. Déanfaimid an síneadh a aistriú go huathoibríoch chuig an stát faoi ghlas mar gheall ar am istigh.

Ceadaíonn Mobx duit ach íosmhéid sonraí a stóráil, agus déantar an chuid eile a ríomh go huathoibríoch bunaithe air. Is iad seo na hairíonna ríomhairithe mar a thugtar orthu. Is féidir iad a chur i gcomparáid le radhairc i mbunachair shonraí:

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

Anois ní stóráilimid ach na heochracha criptithe agus pasfhocal. Ríomhtar gach rud eile. Déanaimid an t-aistriú go dtí stát faoi ghlas tríd an focal faire a bhaint as an stát. Tá modh ag an API poiblí anois chun an stóráil a thosú.

Scríofa le haghaidh criptithe fóntais ag baint úsáide as 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)
}

Tá API díomhaoin ag an mbrabhsálaí trínar féidir leat síntiús a íoc le himeacht - athruithe stáit. Luaigh, dá réir sin, d'fhéadfadh a bheith idle, active и locked. Le haghaidh díomhaoin is féidir leat teorainn ama a shocrú, agus socraítear faoi ghlas nuair a bhíonn an OS féin bac. Athróimid an roghnóir freisin lena shábháil chuig 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)
        }
    }
}

Tá an cód roimh an gcéim seo anseo.

Idirbhearta

Mar sin, déanaimid teacht ar an rud is tábhachtaí: idirbhearta a chruthú agus a shíniú ar an blockchain. Bainfimid úsáid as blockchain agus leabharlann WAVES tonnta-idirbhearta.

Ar dtús, cuirimis leis an stát sraith teachtaireachtaí a chaithfear a shíniú, ansin cuir modhanna chun teachtaireacht nua a chur leis, an síniú a dhearbhú, agus diúltú:

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

    ...
}

Nuair a fhaighimid teachtaireacht nua, cuirimid meiteashonraí leis, déan observable agus cur le store.messages.

Mura ndéanann tú observable de láimh, ansin déanfaidh mobx é féin agus teachtaireachtaí á gcur leis an eagar. Mar sin féin, cruthóidh sé réad nua nach mbeidh tagairt againn dó, ach beidh sé ag teastáil uainn don chéad chéim eile.

Ansin, tugaimid gealltanas ar ais a réitíonn nuair a athraíonn stádas na teachtaireachta. Déantar monatóireacht ar an stádas trí imoibriú, rud a “mharóidh é féin” nuair a athraíonn an stádas.

Cód modh approve и reject an-simplí: ní mór dúinn ach stádas na teachtaireachta a athrú, tar éis é a shíniú más gá.

Cuirimid Faomhadh agus diúltú san UI API, newMessage in API an leathanaigh:

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

    ...
}

Anois déanaimis iarracht an t-idirbheart a shíniú leis an síneadh:

Síneadh brabhsálaí slán a scríobh

Go ginearálta, tá gach rud réidh, níl fágtha ach cuir Chomhéadain simplí leis.

UI

Tá rochtain ag teastáil ón gcomhéadan ar staid an fheidhmchláir. Ar an taobh Chomhéadain déanfaimid observable luaigh agus cuir feidhm leis an API a athróidh an stát seo. Cuirimis observable chuig an oibiacht API a fuarthas ón gcúlra:

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

Ag an deireadh tosaímid ag rindreáil an chomhéadan iarratais. Is feidhmchlár imoibríoch é seo. Is é an rud cúlra a rith go simplí ag baint úsáide as frapaí. Bheadh ​​​​sé ceart, ar ndóigh, seirbhís ar leith a dhéanamh le haghaidh modhanna agus stór don stát, ach chun críocha an ailt seo is leor é seo:

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

Le mobx tá sé an-éasca tús a chur le rindreáil nuair a athraíonn sonraí. Ní dhéanaimid ach an maisitheoir breathnadóireachta a chrochadh ón bpacáiste mobx-imoibriú ar an gcomhpháirt, agus déanfar rindreáil a ghlaoch go huathoibríoch nuair a athraítear aon inbhraite dá dtagraítear ag an gcomhpháirt. Níl aon mapStateToProps ag teastáil uait nó nasc cosúil le in redux. Oibríonn gach rud díreach as an mbosca:

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

Is féidir na comhpháirteanna atá fágtha a fheiceáil sa chód san fhillteán Chomhéadain.

Anois sa rang iarratais ní mór duit roghnóir stáit a dhéanamh don Chomhéadain agus fógra a thabhairt don Chomhéadain nuair a athraíonn sé. Chun seo a dhéanamh, a ligean ar chur modh getState и reactionag glaoch 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())

        })
    }

    ...
}

Nuair a fhaigheann réad remote cruthaithe reaction a athrú ar an stát a ghlaonn an fheidhm ar an taobh Chomhéadain.

Is é an teagmháil dheireanach ná taispeáint teachtaireachtaí nua a chur leis an deilbhín síneadh:

function setupApp() {
...

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

...
}

Mar sin, tá an t-iarratas réidh. Féadfaidh leathanaigh ghréasáin síniú a iarraidh le haghaidh idirbhearta:

Síneadh brabhsálaí slán a scríobh

Síneadh brabhsálaí slán a scríobh

Tá an cód ar fáil anseo nasc.

Conclúid

Má tá an t-alt léite agat go dtí an deireadh, ach go bhfuil ceisteanna agat fós, is féidir leat iad a chur ag stórtha le síneadh. Gheobhaidh tú gealltanais ansin freisin do gach céim ainmnithe.

Agus má tá suim agat féachaint ar an gcód don síneadh iarbhír, is féidir leat é seo a fháil anseo.

Cód, stór agus cur síos ar an bpost ó siemarell

Foinse: will.com

Add a comment