
Im Gegensatz zur üblichen "Client-Server" -Architektur zeichnen sich dezentrale Anwendungen aus durch:
- Sie müssen keine Datenbank mit Benutzeranmeldungen und Kennwörtern speichern. Zugriffsinformationen werden ausschließlich von den Benutzern selbst gespeichert, und die Bestätigung ihrer Authentizität erfolgt auf Protokollebene.
- Sie müssen keinen Server verwenden. Die Anwendungslogik kann in einem Blockchain-Netzwerk ausgeführt werden, in dem die erforderliche Datenmenge gespeichert werden kann.
Es gibt zwei relativ sichere Repositorys für Benutzerschlüssel - Hardware-Wallets und Browser-Erweiterungen. Die meisten Hardware-Portemonnaies sind so sicher wie möglich, aber schwierig zu verwenden und alles andere als kostenlos. Browser-Erweiterungen sind jedoch die perfekte Kombination aus Sicherheit und Benutzerfreundlichkeit und können auch für Endbenutzer völlig kostenlos sein.
Vor diesem Hintergrund wollten wir die sicherste Erweiterung entwickeln, die die Entwicklung dezentraler Anwendungen vereinfacht und eine einfache API für die Arbeit mit Transaktionen und Signaturen bietet.
Wir werden Ihnen unten über diese Erfahrung berichten.
Der Artikel enthält schrittweise Anweisungen zum Schreiben einer Browsererweiterung mit Codebeispielen und Screenshots. Sie finden den gesamten Code im Repository . Jedes Commit entspricht logischerweise einem Abschnitt dieses Artikels.
Eine kurze Geschichte der Browsererweiterungen
Browser-Erweiterungen gibt es schon seit geraumer Zeit. In Internet Explorer erschienen sie 1999, in Firefox - 2004. Lange Zeit gab es jedoch keinen einheitlichen Standard für Erweiterungen.
Wir können sagen, dass es zusammen mit Erweiterungen in der vierten Version von Google Chrome erschien. Natürlich gab es damals keine Spezifikation, aber es war die Chrome-API, die ihre Grundlage wurde: Nachdem Chrome einen großen Teil des Browsermarktes erobert und über einen integrierten Anwendungsspeicher verfügt hatte, setzte Chrome tatsächlich den Standard für Browsererweiterungen.
Mozilla hatte seinen eigenen Standard, aber angesichts der Beliebtheit von Erweiterungen für Chrome entschied sich das Unternehmen für eine kompatible API. Auf Initiative von Mozilla wurde 2015 eine spezielle Gruppe innerhalb des World Wide Web Consortium (W3C) gegründet, um Spezifikationen für browserübergreifende Erweiterungen zu erarbeiten.
Basierend auf den bereits vorhandenen API-Erweiterungen für Chrome. Die Arbeit wurde von Microsoft unterstützt (Google weigerte sich, an der Entwicklung des Standards teilzunehmen), und als Ergebnis erschien ein Entwurf einer Spezifikation .
Formal wird die Spezifikation von Edge, Firefox und Opera unterstützt (beachten Sie, dass Chrome nicht in dieser Liste enthalten ist). Tatsächlich ist der Standard jedoch weitgehend mit Chrome kompatibel, da er tatsächlich auf der Grundlage seiner Erweiterungen geschrieben wurde. Weitere Informationen zur WebExtensions-API finden Sie hier .
Erweiterungsstruktur
Die einzige Datei, die für die Erweiterung erforderlich ist, ist das Manifest (manifest.json). Er ist der "Einstiegspunkt" in die Erweiterung.
Manifest
Gemäß der Spezifikation ist die Manifestdatei eine gültige JSON-Datei. Eine vollständige Beschreibung der Manifestschlüssel mit Informationen darüber, welche Schlüssel in welchem Browser unterstützt werden, finden Sie hier .
Schlüssel, die nicht in der Spezifikation enthalten sind, werden möglicherweise "ignoriert" (sowohl Chrome als auch Firefox melden Fehler, aber die Erweiterungen funktionieren weiterhin).
Und ich möchte auf einige Punkte aufmerksam machen.
- Hintergrund - Ein Objekt, das die folgenden Felder enthält:
- Skripte - eine Reihe von Skripten, die im Hintergrund ausgeführt werden (darüber werden wir etwas später sprechen);
- Seite - Anstelle von Skripten, die auf einer leeren Seite ausgeführt werden, können Sie HTML mit Inhalt angeben. In diesem Fall wird das Skriptfeld ignoriert und die Skripte müssen mit dem Inhalt in die Seite eingefügt werden.
- persistent - ein binäres Flag, falls nicht angegeben, beendet der Browser den Hintergrundprozess, wenn er der Ansicht ist, dass er nichts tut, und startet ihn gegebenenfalls neu. Andernfalls wird die Seite nur entladen, wenn der Browser geschlossen wird. Wird in Firefox nicht unterstützt.
- content_scripts - Ein Array von Objekten, mit denen Sie verschiedene Skripte auf verschiedene Webseiten laden können. Jedes Objekt enthält die folgenden wichtigen Felder:
- Übereinstimmungen - URL-Muster, anhand dessen bestimmt wird, ob ein bestimmtes Inhaltsskript enthalten sein wird oder nicht.
- js - eine Liste von Skripten, die in diese Übereinstimmung geladen werden;
- exclude_matches - Schließt
match
URLs aus dem match
aus, die diesem Feld entsprechen.
- page_action - Tatsächlich ist es das Objekt, das für das Symbol neben der Adressleiste im Browser und die Interaktion mit diesem verantwortlich ist. Außerdem können Sie ein Popup-Fenster anzeigen, das mithilfe von HTML, CSS und JS festgelegt wird.
- default_popup - Pfad zur HTML-Datei mit einer Popup-Oberfläche, kann CSS und JS enthalten.
- Berechtigungen - Ein Array zum Verwalten von Erweiterungsrechten. Es gibt drei Arten von Rechten, die hier ausführlich beschrieben werden.
- web_accessible_resources - Erweiterungsressourcen, die eine Webseite anfordern kann, z. B. Bilder, JS-, CSS- und HTML-Dateien.
- extern_connectable - Hier können Sie explizit die IDs anderer Erweiterungen und die Domänen von Webseiten angeben, von denen aus Sie eine Verbindung herstellen können. Eine Domain kann eine zweite Ebene oder höher sein. Funktioniert nicht in Firefox.
Ausführungskontext
Die Erweiterung verfügt über drei Kontexte für die Codeausführung, dh die Anwendung besteht aus drei Teilen mit unterschiedlichen Zugriffsebenen auf die Browser-API.
Erweiterungskontext
Die meisten APIs sind hier verfügbar. In diesem Zusammenhang "leben":
- Hintergrundseite - Backend-Teil der Erweiterung. Die Datei wird im Manifest durch die Taste "Hintergrund" angezeigt.
- Popup-Seite - Popup-Seite, die angezeigt wird, wenn Sie auf das Erweiterungssymbol klicken. Im Manifest
browser_action
-> default_popup
. - Benutzerdefinierte Seite - Erweiterungsseite, "Leben" in einer separaten Registerkarte des Formulars
chrome-extension://<id_>/customPage.html
.
Dieser Kontext existiert unabhängig von Browserfenstern und Registerkarten. Die Hintergrundseite ist in einer einzigen Kopie vorhanden und funktioniert immer (die Ausnahme ist die Ereignisseite, wenn das Hintergrundskript für ein Ereignis gestartet wird und nach seiner Ausführung stirbt). Die Popup- Seite ist vorhanden, wenn das Popup-Fenster geöffnet ist, und die benutzerdefinierte Seite, solange die Registerkarte geöffnet ist. In diesem Kontext besteht kein Zugriff auf andere Registerkarten und deren Inhalt.
Inhaltsskriptkontext
Die Inhaltsskriptdatei wird zusammen mit jeder Browser-Registerkarte gestartet. Er hat Zugriff auf einen Teil der Erweiterungs-API und auf den DOM-Baum der Webseite. Inhaltsskripte sind für die Interaktion mit der Seite verantwortlich. Erweiterungen, die den DOM-Baum bearbeiten, tun dies in Inhaltsskripten - beispielsweise in Werbeblockern oder Übersetzern. Das Inhaltsskript kann auch über Standard- postMessage
mit der Seite kommunizieren.
Webseitenkontext
Dies ist eigentlich die Webseite selbst. Es hat nichts mit der Erweiterung zu tun und hat dort keinen Zugriff, es sei denn, die Domain dieser Seite ist im Manifest nicht explizit angegeben (mehr dazu weiter unten).
Messaging
Verschiedene Teile der Anwendung müssen Nachrichten miteinander austauschen. Zu diesem runtime.sendMessage
gibt es eine runtime.sendMessage
API zum Senden einer background
und tabs.sendMessage
zum Senden einer Nachricht an eine Seite (Inhaltsskript, Popup oder Webseite, falls tabs.sendMessage
vorhanden ist). Das folgende Beispiel zeigt den Zugriff auf die Chrome-API.
Für eine vollständige Kommunikation können Sie Verbindungen über runtime.connect
erstellen. Als Antwort erhalten wir runtime.Port
, in das Sie, solange es geöffnet ist, eine beliebige Anzahl von Nachrichten senden können. Auf der Clientseite sieht es beispielsweise wie folgt aus:
Server oder Hintergrund:
Es gibt auch ein onDisconnect
Ereignis und eine disconnect
.
Anwendungsübersicht
Lassen Sie uns eine Browser-Erweiterung erstellen, die private Schlüssel speichert, Zugriff auf öffentliche Informationen bietet (Adresse, öffentlicher Schlüssel kommuniziert mit der Seite und Anwendungen von Drittanbietern ermöglicht, eine Transaktionssignatur anzufordern.
Anwendungsentwicklung
Unsere Anwendung sollte sowohl mit dem Benutzer interagieren als auch eine API-Seite zum Aufrufen von Methoden (z. B. zum Signieren von Transaktionen) bereitstellen. Es funktioniert nicht nur mit contentscript
, da es nur Zugriff auf das DOM hat, nicht jedoch auf die JS-Seite. Wir können keine Verbindung über runtime.connect
, da die API für alle Domänen benötigt wird und nur bestimmte im Manifest angegeben werden können. Infolgedessen sieht das Schema folgendermaßen aus:

Es wird eine weitere Skript- inpage
, die wir in die Seite inpage
werden. Es wird in seinem Kontext ausgeführt und bietet eine API für die Arbeit mit der Erweiterung.
Starten Sie
Der gesamte Browser-Erweiterungscode ist auf GitHub verfügbar. Im Beschreibungsprozess werden Links zu Commits angezeigt.
Beginnen wir mit dem Manifest:
{
Erstellen Sie leere background.js, popup.js, inpage.js und contentcript.js. Fügen Sie popup.html hinzu - und unsere Anwendung kann bereits in Google Chrome heruntergeladen werden und stellen Sie sicher, dass sie funktioniert.
Um dies zu überprüfen, können Sie den Code von hier übernehmen . Zusätzlich zu dem, was wir getan haben, ist der Link so konfiguriert, dass das Projekt mithilfe von Webpack erstellt wird. Um dem Browser eine Anwendung hinzuzufügen, müssen Sie in chrome: // -Erweiterungen load entpacked und den Ordner mit der entsprechenden Erweiterung auswählen - in unserem Fall dist.

Jetzt ist unsere Erweiterung installiert und funktioniert. Sie können Entwicklertools für verschiedene Kontexte wie folgt ausführen:
Popup ->

Der Zugriff auf die Konsole des Inhaltsskripts erfolgt über die Konsole der Seite selbst, auf der es gestartet wird. 
Messaging
Wir müssen also zwei Kommunikationskanäle einrichten: Inpage <-> Hintergrund und Popup <-> Hintergrund. Sie können natürlich einfach Nachrichten an den Port senden und Ihr Protokoll erfinden, aber ich bevorzuge den Ansatz, den ich beim Open-Source-Metamask-Projekt ausspioniert habe.
Dies ist eine Browser-Erweiterung für die Arbeit mit dem Ethereum-Netzwerk. Darin kommunizieren verschiedene Teile der Anwendung über RPC unter Verwendung der dnode-Bibliothek. Sie können einen Austausch schnell und bequem organisieren, wenn Sie nodejs stream als Transport bereitstellen (dh ein Objekt, das dieselbe Schnittstelle implementiert):
import Dnode from "dnode/browser";
Jetzt erstellen wir eine Anwendungsklasse. Es werden API-Objekte für Popup und Webseite erstellt und auch ein Knoten für sie erstellt:
import Dnode from 'dnode/browser'; export class SignerApp {
Im Folgenden verwenden wir anstelle des globalen Chrome-Objekts extentionApi, das im Browser von Google auf Chrome und in anderen auf den Browser verweist. Dies geschieht aus Gründen der Cross-Browser-Kompatibilität. Im Rahmen dieses Artikels kann jedoch auch chrome.runtime.connect verwendet werden.
Erstellen Sie die Anwendungsinstanz im Hintergrundskript:
import {extensionApi} from "./utils/extensionApi"; import {PortStream} from "./utils/PortStream"; import {SignerApp} from "./SignerApp"; const app = new SignerApp();
Da dnode mit Streams arbeitet und wir den Port erhalten, wird eine Adapterklasse benötigt. Es wird mithilfe der Bibliothek für lesbare Streams erstellt, die NodeJS-Streams im Browser implementiert:
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() } }
Erstellen Sie nun eine Verbindung in der Benutzeroberfläche:
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(){
Dann erstellen wir eine Verbindung im Inhaltsskript:
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 {
Da wir die API nicht im Inhaltsskript, sondern direkt auf der Seite benötigen, machen wir zwei Dinge:
- Wir erstellen zwei Streams. Eine befindet sich in Richtung der Seite oben auf postMessage. Dafür verwenden wir dieses Paket der Macher von Metamask. Der zweite Stream befindet sich im Hintergrund über dem von
runtime.connect
empfangenen runtime.connect
. Pip sie. Jetzt hat die Seite einen Stream im Hintergrund. - Injizieren Sie das Skript in das DOM. Wir pumpen das Skript aus (der Zugriff darauf war im Manifest zulässig) und erstellen ein
script
Tag mit dem Inhalt darin:
import PostMessageStream from 'post-message-stream'; import {extensionApi} from "./utils/extensionApi"; import {PortStream} from "./utils/PortStream"; setupConnection(); injectScript(); function setupConnection(){
Erstellen Sie nun ein API-Objekt in der Inpage und starten Sie es global:
import PostMessageStream from 'post-message-stream'; import Dnode from 'dnode/browser'; setupInpageApi().catch(console.error); async function setupInpageApi() {
Wir sind bereit für Remote Procedure Call (RPC) mit einer separaten API für die Seite und die Benutzeroberfläche . Wenn Sie eine neue Seite mit dem Hintergrund verbinden, sehen wir Folgendes:

Leere API und Herkunft. Auf der Seite können wir die Hallo-Funktion folgendermaßen aufrufen:

Die Arbeit mit Rückruffunktionen in modernem JS ist eine schlechte Idee. Daher schreiben wir einen kleinen Helfer, um einen Knoten zu erstellen, mit dem Sie APIs an Utils in einem Objekt übergeben können.
API-Objekte sehen nun folgendermaßen aus:
export class SignerApp { popupApi() { return { hello: async () => "world" } } ... }
Abrufen eines Objekts von der Fernbedienung wie folgt:
import {cbToPromise, transformMethods} from "../../src/utils/setupDnode"; const pageApi = await new Promise(resolve => { dnode.once('remote', remoteApi => {
Ein Funktionsaufruf gibt ein Versprechen zurück:

Eine Version mit asynchronen Funktionen finden Sie hier .
Im Allgemeinen scheint der Ansatz mit RPC und Streams recht flexibel zu sein: Wir können Steam-Multiplexing verwenden und mehrere verschiedene APIs für verschiedene Aufgaben erstellen. Im Prinzip kann dnode überall verwendet werden. Die Hauptsache besteht darin, den Transport in Form eines Nodejs-Streams zu verpacken.
Eine Alternative ist das JSON-Format, das das JSON RPC 2-Protokoll implementiert. Es funktioniert jedoch mit bestimmten Transporten (TCP und HTTP (S)), die in unserem Fall nicht anwendbar sind.
Interner Status und localStorage
Wir müssen den internen Status der Anwendung speichern - zumindest die Schlüssel zum Signieren. Wir können den Status einfach zur Anwendung und zu den Methoden zum Ändern in der Popup-API hinzufügen:
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) } } ... }
Im Hintergrund werden wir alles in eine Funktion einschließen und das Anwendungsobjekt in das Fenster schreiben, damit Sie von der Konsole aus damit arbeiten können:
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) } } }
Fügen Sie ein paar Schlüssel von der UI-Konsole hinzu und sehen Sie, was mit dem Status passiert ist:

Der Status muss dauerhaft sein, damit beim Neustart die Schlüssel nicht verloren gehen.
Wir werden es in localStorage speichern und bei jeder Änderung überschreiben. Anschließend ist auch für die Benutzeroberfläche ein Zugriff darauf erforderlich, und ich möchte die Änderungen auch abonnieren. Auf dieser Grundlage ist es zweckmäßig, einen beobachtbaren Speicher zu erstellen und seine Änderungen zu abonnieren.
Wir werden die Mobx-Bibliothek verwenden ( https://github.com/mobxjs/mobx ). Die Wahl fiel auf sie, da ich nicht mit ihr arbeiten musste, aber ich wollte sie wirklich studieren.
Fügen Sie die Initialisierung des Anfangszustands hinzu und machen Sie den Speicher beobachtbar:
import {observable, action} from 'mobx'; import {setupDnode} from "./utils/setupDnode"; export class SignerApp { constructor(initState = {}) {
"Unter der Haube" mobx ersetzte alle Speicherfelder durch Proxy und fängt alle Anrufe an sie ab. Sie können diese Appelle abonnieren.
Außerdem werde ich oft den Begriff „bei Änderung“ verwenden, obwohl dies nicht ganz richtig ist. Mobx verfolgt den Zugriff auf Felder. Die von der Bibliothek erstellten Getter und Setter von Proxy-Objekten werden verwendet.
Action-Dekorateure dienen zwei Zwecken:
- Im strengen Modus mit dem Flag assertceActions verbietet mobx das direkte Ändern des Status. Es wird als gute Praxis angesehen, im strengen Modus zu arbeiten.
- Selbst wenn die Funktion den Status mehrmals ändert - wir ändern beispielsweise mehrere Felder in mehrere Codezeilen - werden Beobachter erst benachrichtigt, wenn sie vollständig sind. Dies ist besonders wichtig für das Frontend, wo unnötige Statusaktualisierungen zu unnötigem Rendern von Elementen führen. In unserem Fall ist weder die erste noch die zweite besonders relevant. Wir werden jedoch Best Practices befolgen. Die Dekorateure entschieden sich, an allen Funktionen festzuhalten, die den Status der beobachteten Felder ändern.
Fügen Sie im Hintergrund die Initialisierung hinzu und speichern Sie den Status in localStorage:
import {reaction, toJS} from 'mobx'; import {extensionApi} from "./utils/extensionApi"; import {PortStream} from "./utils/PortStream"; import {SignerApp} from "./SignerApp";
Die Reaktionsfunktion ist hier interessant. Sie hat zwei Argumente:
- Datenauswahl.
- Ein Handler, der bei jeder Änderung mit diesen Daten aufgerufen wird.
Im Gegensatz zu Redux, bei dem wir den Status explizit als Argument erhalten, merkt sich mobx, auf welche Observable wir uns im Selektor beziehen, und ruft nur dann den Handler auf, wenn sie geändert werden.
Es ist wichtig, genau zu verstehen, wie mobx entscheidet, welches Observable wir abonnieren. Wenn ich einen Selektor in den Code wie diesen () => app.store
, wird die Reaktion niemals aufgerufen, da das Repository selbst nicht beobachtbar ist, sondern nur seine Felder.
Wenn ich so geschrieben hätte () => app.store.keys
, würde nichts mehr passieren, da sich beim Hinzufügen / Entfernen von Elementen des Arrays der Link dazu nicht ändert.
Zum ersten Mal führt Mobx die Funktion eines Selektors aus und überwacht nur die beobachtbaren Objekte, auf die wir Zugriff haben. Dies erfolgt über Proxy-Getter. toJS
. , . – , .
popup . localStorage:

background- .
.
: , , . localStorage .
locked, . locked .
Mobx , . — computed properties. view :
import {observable, action} from 'mobx'; import {setupDnode} from "./utils/setupDnode";
. . locked . API .
rypto-js :
import CryptoJS from 'crypto-js'
idle API, — . , , idle
, active
locked
. idle , locked , . 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; }
.
Transaktionen
, : . WAVES waves-transactions .
, , — , :
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) {
, observable
store.messages
.
observable
, mobx messages. , , .
, . reaction, "" .
approve
reject
: , , .
Approve reject API UI, newMessage — API :
export class SignerApp { ... popupApi() { return { addKey: async (key) => this.addKey(key), removeKey: async (index) => this.removeKey(index), lock: async () => this.lock(), unlock: async (password) => this.unlock(password), initVault: async (password) => this.initVault(password), approve: async (id, keyIndex) => this.approve(id, keyIndex), reject: async (id) => this.reject(id) } } pageApi(origin) { return { signTransaction: async (txParams) => this.newMessage(txParams, origin) } } ... }
:

, UI .
UI
. UI observable
API , . observable
API, background:
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() {
. react-. Background- props. , , store , :
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') ); }
mobx . observer mobx-react , observable, . mapStateToProps connect, redux. " ":
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> } }
UI .
UI UI. getState
reaction
, 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 { ...
remote
reaction
, UI.
— :
function setupApp() { ...
, . - :


.
Fazit
, , . .
, .
, siemarell