Ysgrifennu estyniad porwr diogel

Ysgrifennu estyniad porwr diogel

Yn wahanol i bensaernïaeth gyffredin “gweinydd cleient”, nodweddir cymwysiadau datganoledig gan:

  • Nid oes angen storio cronfa ddata gyda mewngofnodi defnyddwyr a chyfrineiriau. Mae gwybodaeth mynediad yn cael ei storio gan y defnyddwyr eu hunain yn unig, ac mae cadarnhad o'u dilysrwydd yn digwydd ar lefel y protocol.
  • Nid oes angen defnyddio gweinydd. Gellir gweithredu rhesymeg y cais ar rwydwaith blockchain, lle mae'n bosibl storio'r swm gofynnol o ddata.

Mae yna 2 storfa gymharol ddiogel ar gyfer allweddi defnyddwyr - waledi caledwedd ac estyniadau porwr. Mae waledi caledwedd ar y cyfan yn hynod o ddiogel, ond yn anodd eu defnyddio ac ymhell o fod yn rhad ac am ddim, ond mae estyniadau porwr yn gyfuniad perffaith o ddiogelwch a rhwyddineb defnydd, a gallant hefyd fod yn hollol rhad ac am ddim i ddefnyddwyr terfynol.

Gan ystyried hyn i gyd, roeddem am wneud yr estyniad mwyaf diogel sy'n symleiddio datblygiad cymwysiadau datganoledig trwy ddarparu API syml ar gyfer gweithio gyda thrafodion a llofnodion.
Byddwn yn dweud wrthych am y profiad hwn isod.

Bydd yr erthygl yn cynnwys cyfarwyddiadau cam wrth gam ar sut i ysgrifennu estyniad porwr, gydag enghreifftiau cod a sgrinluniau. Gallwch ddod o hyd i'r holl god yn storfeydd. Mae pob ymrwymiad yn cyfateb yn rhesymegol i adran o'r erthygl hon.

Hanes Byr o Estyniadau Porwr

Mae estyniadau porwr wedi bod o gwmpas ers amser maith. Fe wnaethant ymddangos yn Internet Explorer yn ôl yn 1999, yn Firefox yn 2004. Fodd bynnag, am amser hir iawn nid oedd un safon sengl ar gyfer estyniadau.

Gallwn ddweud ei fod wedi ymddangos ynghyd ag estyniadau yn y bedwaredd fersiwn o Google Chrome. Wrth gwrs, nid oedd unrhyw fanyleb bryd hynny, ond yr API Chrome a ddaeth yn sail iddo: ar ôl goresgyn y rhan fwyaf o'r farchnad borwyr a chael storfa gymwysiadau adeiledig, gosododd Chrome y safon ar gyfer estyniadau porwr mewn gwirionedd.

Roedd gan Mozilla ei safon ei hun, ond o weld poblogrwydd estyniadau Chrome, penderfynodd y cwmni wneud API cydnaws. Yn 2015, ar fenter Mozilla, crëwyd grŵp arbennig o fewn Consortiwm y We Fyd Eang (W3C) i weithio ar fanylebau estyniad traws-borwr.

Cymerwyd yr estyniadau API presennol ar gyfer Chrome fel sail. Gwnaethpwyd y gwaith gyda chefnogaeth Microsoft (gwrthododd Google gymryd rhan yn natblygiad y safon), ac o ganlyniad ymddangosodd drafft manylebau.

Yn ffurfiol, cefnogir y fanyleb gan Edge, Firefox ac Opera (sylwch nad yw Chrome ar y rhestr hon). Ond mewn gwirionedd, mae'r safon yn gydnaws i raddau helaeth â Chrome, gan ei fod wedi'i ysgrifennu mewn gwirionedd yn seiliedig ar ei estyniadau. Gallwch ddarllen mwy am yr API WebExtensions yma.

Strwythur ymestyn

Yr unig ffeil sydd ei hangen ar gyfer yr estyniad yw'r maniffest (manifest.json). Dyma hefyd y “pwynt mynediad” i'r ehangu.

Maniffest

Yn ôl y fanyleb, mae'r ffeil maniffest yn ffeil JSON ddilys. Disgrifiad llawn o fysellau maniffest gyda gwybodaeth am ba allweddi sy'n cael eu cynnal y gellir eu gweld yn y porwr yma.

Efallai y bydd allweddi nad ydynt yn y fanyleb “yn cael eu hanwybyddu” (mae Chrome a Firefox yn adrodd am wallau, ond mae'r estyniadau'n parhau i weithio).

A hoffwn dynnu sylw at rai pwyntiau.

  1. cefndir — gwrthrych sy'n cynnwys y meysydd canlynol:
    1. sgriptiau — amrywiaeth o sgriptiau a fydd yn cael eu gweithredu yn y cyd-destun cefndirol (byddwn yn siarad am hyn ychydig yn ddiweddarach);
    2. dudalen - yn lle sgriptiau a fydd yn cael eu gweithredu mewn tudalen wag, gallwch chi nodi html gyda chynnwys. Yn yr achos hwn, bydd y maes sgript yn cael ei anwybyddu, a bydd angen gosod y sgriptiau yn y dudalen gynnwys;
    3. parhau — baner ddeuaidd, os nad yw wedi'i nodi, bydd y porwr yn “lladd” y broses gefndir pan fydd yn ystyried nad yw'n gwneud unrhyw beth, a'i ailgychwyn os oes angen. Fel arall, dim ond pan fydd y porwr ar gau y bydd y dudalen yn cael ei dadlwytho. Heb ei gefnogi yn Firefox.
  2. cynnwys_scripts - amrywiaeth o wrthrychau sy'n eich galluogi i lwytho gwahanol sgriptiau i wahanol dudalennau gwe. Mae pob gwrthrych yn cynnwys y meysydd pwysig canlynol:
    1. gemau - url patrwm, sy'n pennu a fydd sgript cynnwys penodol yn cael ei chynnwys ai peidio.
    2. js — rhestr o sgriptiau a fydd yn cael eu llwytho i mewn i'r gêm hon;
    3. eithrio_matches - yn eithrio o'r maes match URLs sy'n cyfateb i'r maes hwn.
  3. tudalen_gweithred - mewn gwirionedd yn wrthrych sy'n gyfrifol am yr eicon sy'n cael ei arddangos wrth ymyl y bar cyfeiriad yn y porwr a rhyngweithio ag ef. Mae hefyd yn caniatáu ichi arddangos ffenestr naid, a ddiffinnir gan ddefnyddio eich HTML, CSS a JS eich hun.
    1. naidlen_diofyn — llwybr i'r ffeil HTML gyda'r rhyngwyneb naid, gall gynnwys CSS a JS.
  4. caniatadau — arae ar gyfer rheoli hawliau estyn. Mae 3 math o hawliau, a ddisgrifir yn fanwl yma
  5. adnoddau_hygyrch_gwe — adnoddau estyn y gall tudalen we ofyn amdanynt, er enghraifft, delweddau, JS, CSS, ffeiliau HTML.
  6. yn allanol_connectable — yma gallwch chi nodi'n benodol IDau estyniadau a pharthau eraill o dudalennau gwe y gallwch chi gysylltu â nhw. Gall parth fod yn ail lefel neu'n uwch. Nid yw'n gweithio yn Firefox.

Cyd-destun gweithredu

Mae gan yr estyniad dri chyd-destun gweithredu cod, hynny yw, mae'r rhaglen yn cynnwys tair rhan gyda gwahanol lefelau o fynediad i API y porwr.

Cyd-destun ymestyn

Mae'r rhan fwyaf o'r API ar gael yma. Yn y cyd-destun hwn maent yn “byw”:

  1. Tudalen gefndir — rhan “ôl” o'r estyniad. Mae'r ffeil wedi'i nodi yn y maniffest gan ddefnyddio'r allwedd “cefndir”.
  2. Tudalen popup - tudalen naid sy'n ymddangos pan gliciwch ar eicon yr estyniad. Yn y maniffesto browser_action -> default_popup.
  3. Tudalen personol — tudalen estyniad, “byw” mewn tab ar wahân o'r olygfa chrome-extension://<id_расширения>/customPage.html.

Mae'r cyd-destun hwn yn bodoli yn annibynnol ar ffenestri a thabiau porwr. Tudalen gefndir yn bodoli mewn un copi ac yn gweithio bob amser (yr eithriad yw tudalen y digwyddiad, pan fydd y sgript gefndir yn cael ei lansio gan ddigwyddiad ac yn “marw” ar ôl ei gweithredu). Tudalen popup yn bodoli pan fydd y ffenestr naid ar agor, a Tudalen personol - tra bod y tab gydag ef ar agor. Nid oes mynediad i dabiau eraill a'u cynnwys o'r cyd-destun hwn.

Cyd-destun sgript cynnwys

Mae'r ffeil sgript cynnwys yn cael ei lansio ynghyd â phob tab porwr. Mae ganddo fynediad i ran o API yr estyniad ac i goeden DOM y dudalen we. Sgriptiau cynnwys sy'n gyfrifol am ryngweithio â'r dudalen. Mae estyniadau sy'n trin y goeden DOM yn gwneud hyn mewn sgriptiau cynnwys - er enghraifft, atalyddion hysbysebion neu gyfieithwyr. Hefyd, gall y sgript cynnwys gyfathrebu â'r dudalen trwy safon postMessage.

Cyd-destun tudalen we

Dyma'r dudalen we ei hun. Nid oes ganddo unrhyw beth i'w wneud â'r estyniad ac nid oes ganddo fynediad yno, ac eithrio mewn achosion lle nad yw parth y dudalen hon wedi'i nodi'n benodol yn y maniffest (mwy am hyn isod).

Cyfnewid negeseuon

Rhaid i wahanol rannau o'r cais gyfnewid negeseuon â'i gilydd. Mae API ar gyfer hyn runtime.sendMessage i anfon neges background и tabs.sendMessage i anfon neges i dudalen (sgript cynnwys, naidlen neu dudalen we os yw ar gael externally_connectable). Isod mae enghraifft wrth gyrchu'r 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))
    }
)

Ar gyfer cyfathrebu llawn, gallwch greu cysylltiadau drwy runtime.connect. Mewn ymateb byddwn yn derbyn runtime.Port, y gallwch chi anfon unrhyw nifer o negeseuon ato tra ei fod ar agor. Ar ochr y cleient, er enghraifft, contentscript, mae'n edrych fel hyn:

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

Gweinydd neu gefndir:

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

Mae yna ddigwyddiad hefyd onDisconnect a dull disconnect.

Diagram cais

Gadewch i ni wneud estyniad porwr sy'n storio allweddi preifat, yn darparu mynediad i wybodaeth gyhoeddus (cyfeiriad, allwedd gyhoeddus yn cyfathrebu â'r dudalen ac yn caniatáu i gymwysiadau trydydd parti ofyn am lofnod ar gyfer trafodion.

Datblygu cais

Rhaid i'n cymhwysiad ryngweithio â'r defnyddiwr a darparu API i'r dulliau galw i'r dudalen (er enghraifft, i lofnodi trafodion). Gwnewch ei wneud gydag un yn unig contentscript Ni fydd yn gweithio, gan mai dim ond mynediad i'r DOM sydd ganddo, ond nid i JS y dudalen. Cysylltwch drwy runtime.connect ni allwn, oherwydd mae angen yr API ar bob parth, a dim ond rhai penodol y gellir eu nodi yn y maniffest. O ganlyniad, bydd y diagram yn edrych fel hyn:

Ysgrifennu estyniad porwr diogel

Bydd sgript arall - inpage, y byddwn yn ei chwistrellu i'r dudalen. Bydd yn rhedeg yn ei gyd-destun ac yn darparu API ar gyfer gweithio gyda'r estyniad.

Dechrau

Mae cod estyniad pob porwr ar gael yn GitHub. Yn ystod y disgrifiad bydd dolenni i ymrwymiadau.

Gadewch i ni ddechrau gyda'r maniffesto:

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

Creu cefndir gwag.js, popup.js, inpage.js a contentscript.js. Rydym yn ychwanegu popup.html - a gellir llwytho ein cais i mewn i Google Chrome eisoes a gwneud yn siŵr ei fod yn gweithio.

I wirio hyn, gallwch chi gymryd y cod felly. Yn ogystal â'r hyn a wnaethom, fe wnaeth y ddolen ffurfweddu cydosod y prosiect gan ddefnyddio pecyn gwe. I ychwanegu cais at y porwr, yn chrome: // estyniadau mae angen i chi ddewis llwyth heb ei bacio a'r ffolder gyda'r estyniad cyfatebol - yn ein hachos ni dist.

Ysgrifennu estyniad porwr diogel

Nawr mae ein estyniad wedi'i osod ac yn gweithio. Gallwch redeg yr offer datblygwr ar gyfer gwahanol gyd-destunau fel a ganlyn:

popup ->

Ysgrifennu estyniad porwr diogel

Mae mynediad i'r consol sgript cynnwys yn cael ei wneud trwy gonsol y dudalen ei hun y mae'n cael ei lansio arni.Ysgrifennu estyniad porwr diogel

Cyfnewid negeseuon

Felly, mae angen i ni sefydlu dwy sianel gyfathrebu: tu mewn <-> cefndir a ffenestr naid <-> cefndir. Gallwch, wrth gwrs, anfon negeseuon i'r porthladd a dyfeisio'ch protocol eich hun, ond mae'n well gennyf y dull a welais yn y prosiect ffynhonnell agored metamask.

Estyniad porwr yw hwn ar gyfer gweithio gyda rhwydwaith Ethereum. Ynddo, mae gwahanol rannau o'r cais yn cyfathrebu trwy RPC gan ddefnyddio'r llyfrgell dnode. Mae'n caniatáu ichi drefnu cyfnewid yn eithaf cyflym a chyfleus os ydych chi'n darparu ffrwd nodejs iddo fel cludiant (sy'n golygu gwrthrych sy'n gweithredu'r un rhyngwyneb):

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

Nawr byddwn yn creu dosbarth cais. Bydd yn creu gwrthrychau API ar gyfer y naidlen a'r dudalen we, ac yn creu dnod ar eu cyfer:

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

Yma ac isod, yn lle'r gwrthrych Chrome byd-eang, rydym yn defnyddio estyniadApi, sy'n cyrchu Chrome ym mhorwr Google a porwr mewn eraill. Gwneir hyn ar gyfer cydweddoldeb traws-borwr, ond at ddibenion yr erthygl hon, gallai un ddefnyddio 'chrome.runtime.connect'.

Gadewch i ni greu enghraifft cais yn y sgript gefndir:

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

Gan fod dnode yn gweithio gyda nentydd, ac rydym yn derbyn porthladd, mae angen dosbarth addasydd. Fe'i gwneir gan ddefnyddio'r llyfrgell ffrwd ddarllenadwy, sy'n gweithredu ffrydiau nodejs yn y porwr:

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

Nawr, gadewch i ni greu cysylltiad yn yr UI:

import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import Dnode from 'dnode/browser';

const DEV_MODE = process.env.NODE_ENV !== 'production';

setupUi().catch(console.error);

async function setupUi(){
    // Также, как и в классе приложения создаем порт, оборачиваем в stream, делаем  dnode
    const backgroundPort = extensionApi.runtime.connect({name: 'popup'});
    const connectionStream = new PortStream(backgroundPort);

    const dnode = Dnode();

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

    const background = await new Promise(resolve => {
        dnode.once('remote', api => {
            resolve(api)
        })
    });

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

Yna rydyn ni'n creu'r cysylltiad yn y sgript cynnwys:

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

Gan fod angen yr API arnom nid yn y sgript cynnwys, ond yn uniongyrchol ar y dudalen, rydym yn gwneud dau beth:

  1. Rydyn ni'n creu dwy ffrwd. Un - tuag at y dudalen, ar ben y postMessage. Ar gyfer hyn rydym yn defnyddio hyn y pecyn hwn oddi wrth y crewyr metamask. Mae'r ail ffrwd i gefndir dros y porthladd a dderbyniwyd oddi wrth runtime.connect. Gadewch i ni eu prynu. Nawr bydd gan y dudalen ffrwd i'r cefndir.
  2. Chwistrellwch y sgript i'r DOM. Lawrlwythwch y sgript (caniatawyd mynediad iddo yn y maniffest) a chreu tag script gyda'i gynnwys y tu mewn:

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

Nawr rydyn ni'n creu gwrthrych api ar y tu mewn a'i osod i fyd-eang:

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

Rydyn ni'n barod Galwad Gweithdrefn Anghysbell (RPC) gydag API ar wahân ar gyfer tudalen ac UI. Wrth gysylltu tudalen newydd â chefndir gallwn weld hyn:

Ysgrifennu estyniad porwr diogel

API gwag a tharddiad. Ar ochr y dudalen, gallwn alw'r swyddogaeth helo fel hyn:

Ysgrifennu estyniad porwr diogel

Mae gweithio gyda swyddogaethau galw yn ôl yn JS modern yn foesau drwg, felly gadewch i ni ysgrifennu cynorthwyydd bach i greu dnode sy'n eich galluogi i drosglwyddo gwrthrych API i gyfleustodau.

Bydd y gwrthrychau API nawr yn edrych fel hyn:

export class SignerApp {

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

...

}

Cael gwrthrych o bell fel hyn:

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

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

Ac mae swyddogaethau galw yn dychwelyd addewid:

Ysgrifennu estyniad porwr diogel

Fersiwn gyda swyddogaethau asyncronaidd ar gael yma.

Ar y cyfan, mae'r dull RPC a ffrwd yn ymddangos yn eithaf hyblyg: gallwn ddefnyddio amlblecsio stêm a chreu sawl API gwahanol ar gyfer gwahanol dasgau. Mewn egwyddor, gellir defnyddio dnode yn unrhyw le, y prif beth yw lapio'r cludiant ar ffurf ffrwd nodejs.

Dewis arall yw fformat JSON, sy'n gweithredu protocol JSON RPC 2. Fodd bynnag, mae'n gweithio gyda chludiant penodol (TCP a HTTP(S)), nad yw'n berthnasol yn ein hachos ni.

Cyflwr mewnol a storfa leol

Bydd angen i ni storio cyflwr mewnol y cais - o leiaf yr allweddi arwyddo. Gallwn yn eithaf hawdd ychwanegu cyflwr i'r cymhwysiad a'r dulliau ar gyfer ei newid yn yr API pop-up:

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

    ...

} 

Yn y cefndir, byddwn yn lapio popeth mewn swyddogaeth ac yn ysgrifennu gwrthrych y cymhwysiad i'r ffenestr fel y gallwn weithio gydag ef o'r consol:

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

Gadewch i ni ychwanegu ychydig o allweddi o'r consol UI a gweld beth sy'n digwydd gyda'r wladwriaeth:

Ysgrifennu estyniad porwr diogel

Mae angen gwneud y cyflwr yn barhaus fel nad yw'r allweddi'n cael eu colli wrth ailgychwyn.

Byddwn yn ei storio mewn storfa leol, gan ei drosysgrifo gyda phob newid. Yn dilyn hynny, bydd mynediad iddo hefyd yn angenrheidiol ar gyfer yr UI, a hoffwn hefyd danysgrifio i newidiadau. Yn seiliedig ar hyn, bydd yn gyfleus creu storfa arsylladwy a thanysgrifio i'w newidiadau.

Byddwn yn defnyddio'r llyfrgell mobx (https://github.com/mobxjs/mobx). Syrthiodd y dewis arno oherwydd nid oedd yn rhaid i mi weithio gydag ef, ond roeddwn i wir eisiau ei astudio.

Gadewch i ni ychwanegu ymgychwyniad o'r cyflwr cychwynnol a gwneud y storfa yn weladwy:

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

    ...

}

“O dan y cwfl,” mae mobx wedi disodli pob maes siop â dirprwy ac wedi rhyng-gipio pob galwad iddynt. Bydd modd tanysgrifio i'r negeseuon hyn.

Isod byddaf yn aml yn defnyddio’r term “wrth newid”, er nad yw hyn yn gwbl gywir. Mae Mobx yn olrhain mynediad i gaeau. Defnyddir derbynwyr a gosodwyr gwrthrychau dirprwyol y mae'r llyfrgell yn eu creu.

Mae dau ddiben i addurnwyr gweithredu:

  1. Yn y modd llym gyda'r faner enforceActions, mae mobx yn gwahardd newid y wladwriaeth yn uniongyrchol. Ystyrir ei bod yn arfer da gweithio dan amodau llym.
  2. Hyd yn oed os yw swyddogaeth yn newid y cyflwr sawl gwaith - er enghraifft, rydym yn newid sawl maes mewn sawl llinell o god - dim ond pan fydd wedi'i gwblhau y caiff yr arsylwyr eu hysbysu. Mae hyn yn arbennig o bwysig ar gyfer y blaen, lle mae diweddariadau cyflwr diangen yn arwain at rendro elfennau diangen. Yn ein hachos ni, nid yw'r cyntaf na'r ail yn arbennig o berthnasol, ond byddwn yn dilyn yr arferion gorau. Mae'n arferol atodi addurnwyr i bob swyddogaeth sy'n newid cyflwr y meysydd a arsylwyd.

Yn y cefndir byddwn yn ychwanegu ymgychwyn ac arbed y wladwriaeth yn 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)
        }
    }
}

Mae'r swyddogaeth adwaith yn ddiddorol yma. Mae ganddo ddwy ddadl:

  1. Dewisydd data.
  2. Triniwr a fydd yn cael ei alw gyda'r data hwn bob tro y bydd yn newid.

Yn wahanol i redux, lle rydym yn derbyn y wladwriaeth yn benodol fel dadl, mae mobx yn cofio pa bethau arsylladwy rydyn ni'n eu cyrchu y tu mewn i'r dewisydd, a dim ond yn galw'r triniwr pan fyddant yn newid.

Mae'n bwysig deall yn union sut mae mobx yn penderfynu pa bethau arsylladwy rydym yn tanysgrifio iddynt. Pe bawn i'n ysgrifennu dewisydd mewn cod fel hyn() => app.store, yna ni fydd adwaith byth yn cael ei alw, gan nad yw'r storfa ei hun yn weladwy, dim ond ei feysydd.

Pe bawn i'n ei ysgrifennu fel hyn () => app.store.keys, yna eto ni fyddai unrhyw beth yn digwydd, oherwydd wrth ychwanegu / dileu elfennau arae, ni fydd y cyfeiriad ato yn newid.

Mae Mobx yn gweithredu fel dewisydd am y tro cyntaf a dim ond yn cadw golwg ar bethau y gellir eu gweld yr ydym wedi eu cyrchu. Gwneir hyn trwy ddirprwywyr. Felly, defnyddir y swyddogaeth adeiledig yma toJS. Mae'n dychwelyd gwrthrych newydd gyda'r holl ddirprwyon yn cael eu disodli gan y meysydd gwreiddiol. Yn ystod y dienyddiad, mae'n darllen holl feysydd y gwrthrych - felly mae'r getters yn cael eu sbarduno.

Yn y consol pop-up byddwn eto'n ychwanegu sawl allwedd. Y tro hwn fe wnaethon nhw hefyd ddod i'r storfa leol:

Ysgrifennu estyniad porwr diogel

Pan fydd y dudalen gefndir yn cael ei hail-lwytho, mae'r wybodaeth yn parhau yn ei lle.

Gellir gweld yr holl god cais hyd at y pwynt hwn yma.

Storio allweddi preifat yn ddiogel

Nid yw storio allweddi preifat mewn testun clir yn ddiogel: mae siawns bob amser y cewch eich hacio, cael mynediad i'ch cyfrifiadur, ac ati. Felly, yn localStorage byddwn yn storio'r allweddi ar ffurf wedi'i hamgryptio gan gyfrinair.

Er mwyn sicrhau mwy o ddiogelwch, byddwn yn ychwanegu cyflwr cloi i'r cais, lle na fydd mynediad at yr allweddi o gwbl. Byddwn yn trosglwyddo'r estyniad yn awtomatig i'r cyflwr dan glo oherwydd terfyn amser.

Mae Mobx yn caniatáu ichi storio set leiaf o ddata yn unig, ac mae'r gweddill yn cael ei gyfrifo'n awtomatig yn seiliedig arno. Dyma'r priodweddau cyfrifiadurol fel y'u gelwir. Gellir eu cymharu â golygfeydd mewn cronfeydd data:

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

Nawr rydyn ni'n storio'r allweddi a'r cyfrinair wedi'u hamgryptio yn unig. Mae popeth arall yn cael ei gyfrifo. Rydyn ni'n gwneud y trosglwyddiad i gyflwr dan glo trwy dynnu'r cyfrinair o'r wladwriaeth. Bellach mae gan yr API cyhoeddus ddull ar gyfer cychwyn y storfa.

Ysgrifennwyd ar gyfer amgryptio cyfleustodau gan ddefnyddio 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)
}

Mae gan y porwr API segur y gallwch chi danysgrifio i ddigwyddiad - newidiadau cyflwr. Nodwch, yn unol â hynny, efallai idle, active и locked. Ar gyfer segur gallwch osod terfyn amser, a chlo yn cael ei osod pan fydd yr OS ei hun yn cael ei rwystro. Byddwn hefyd yn newid y dewisydd ar gyfer arbed i 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)
        }
    }
}

Y cod cyn y cam hwn yw yma.

Trafodiad

Felly, rydym yn dod at y peth pwysicaf: creu a llofnodi trafodion ar y blockchain. Byddwn yn defnyddio blockchain a llyfrgell WAVES tonnau-trafodion.

Yn gyntaf, gadewch i ni ychwanegu at y cyflwr amrywiaeth o negeseuon y mae angen eu llofnodi, yna ychwanegu dulliau ar gyfer ychwanegu neges newydd, cadarnhau'r llofnod, a gwrthod:

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

    ...
}

Pan fyddwn yn derbyn neges newydd, rydym yn ychwanegu metadata ato, gwnewch observable ac ychwanegu at store.messages.

Os na wnewch chi observable â llaw, yna bydd mobx yn ei wneud ei hun wrth ychwanegu negeseuon at yr arae. Fodd bynnag, bydd yn creu gwrthrych newydd na fydd gennym gyfeiriad ato, ond bydd ei angen arnom ar gyfer y cam nesaf.

Nesaf, rydym yn dychwelyd addewid sy'n datrys pan fydd statws y neges yn newid. Mae'r statws yn cael ei fonitro gan adwaith, a fydd yn "lladd ei hun" pan fydd y statws yn newid.

Cod dull approve и reject syml iawn: rydym yn syml yn newid statws y neges, ar ôl ei llofnodi os oes angen.

Rydyn ni'n rhoi Cymeradwyo a gwrthod yn yr UI API, newMessage yn API y dudalen:

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

    ...
}

Nawr, gadewch i ni geisio llofnodi'r trafodiad gyda'r estyniad:

Ysgrifennu estyniad porwr diogel

Yn gyffredinol, mae popeth yn barod, y cyfan sydd ar ôl yw ychwanegu UI syml.

UI

Mae angen mynediad i gyflwr y cais ar y rhyngwyneb. Ar yr ochr UI byddwn yn ei wneud observable nodi ac ychwanegu swyddogaeth i'r API a fydd yn newid y cyflwr hwn. Gadewch i ni ychwanegu observable i'r gwrthrych API a dderbyniwyd o'r cefndir:

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

Ar y diwedd rydym yn dechrau rendro'r rhyngwyneb cais. Cais adweithiol yw hwn. Mae'r gwrthrych cefndir yn cael ei basio gan ddefnyddio propiau. Byddai'n gywir, wrth gwrs, gwneud gwasanaeth ar wahân ar gyfer dulliau a storfa ar gyfer y wladwriaeth, ond at ddibenion yr erthygl hon mae hyn yn ddigon:

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

Gyda mobx mae'n hawdd iawn dechrau rendro pan fydd data'n newid. Yn syml, rydyn ni'n hongian yr addurnwr arsylwr o'r pecyn mobx-ymateb ar y gydran, a bydd rendrad yn cael ei alw'n awtomatig pan fydd unrhyw bethau y mae'r gydran yn cyfeirio atynt yn newid. Nid oes angen unrhyw mapStateToProps na chyswllt fel yn redux. Mae popeth yn gweithio'n iawn allan o'r bocs:

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

Gellir gweld y cydrannau sy'n weddill yn y cod yn y ffolder UI.

Nawr yn y dosbarth cais mae angen i chi wneud dewisydd cyflwr ar gyfer yr UI a hysbysu'r UI pan fydd yn newid. I wneud hyn, gadewch i ni ychwanegu dull getState и reactionyn galw 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())

        })
    }

    ...
}

Wrth dderbyn gwrthrych remote creu reaction i newid y cyflwr sy'n galw'r swyddogaeth ar yr ochr UI.

Y cyffyrddiad olaf yw ychwanegu arddangosiad negeseuon newydd ar yr eicon estyniad:

function setupApp() {
...

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

...
}

Felly, mae'r cais yn barod. Gall tudalennau gwe ofyn am lofnod ar gyfer trafodion:

Ysgrifennu estyniad porwr diogel

Ysgrifennu estyniad porwr diogel

Mae'r cod ar gael yma cyswllt.

Casgliad

Os ydych wedi darllen yr erthygl hyd y diwedd, ond bod gennych gwestiynau o hyd, gallwch eu gofyn yn storfeydd gydag estyniad. Yno fe welwch hefyd ymrwymiadau ar gyfer pob cam dynodedig.

Ac os oes gennych ddiddordeb mewn edrych ar y cod ar gyfer yr estyniad gwirioneddol, gallwch ddod o hyd i hwn yma.

Cod, ystorfa a disgrifiad swydd o siemarell

Ffynhonnell: hab.com

Ychwanegu sylw