
Till skillnad frÄn den vanliga "klient-server"-arkitekturen kÀnnetecknas decentraliserade applikationer av:
- Det finns inget behov av att lagra en databas med anvÀndarnamn och lösenord. à tkomstinformationen lagras enbart av anvÀndarna sjÀlva, och valideringen av deras identitet sker pÄ protokollnivÄ.
- Det finns inget behov av att anvÀnda en server. Applikationens logik kan köras i blockchain-nÀtverket, dÀr det ocksÄ Àr möjligt att lagra den nödvÀndiga datamÀngden.
Det finns 2 relativt sĂ€kra lagringsmetoder för anvĂ€ndarnas nycklar â hĂ„rdvaruplĂ„nböcker och webblĂ€sartillĂ€gg. HĂ„rdvaruplĂ„nböcker Ă€r i de flesta fall maximalt sĂ€kra, men svĂ„ra att anvĂ€nda och lĂ„ngt ifrĂ„n gratis, medan webblĂ€sartillĂ€gg erbjuder en perfekt kombination av sĂ€kerhet och anvĂ€ndarvĂ€nlighet, och kan ocksĂ„ vara helt gratis för slutanvĂ€ndare.
Med detta i Ätanke ville vi skapa ett sÄ sÀkert tillÀgg som möjligt, som förenklar utvecklingen av decentraliserade applikationer genom att erbjuda ett enkelt API för transaktioner och signaturer.
Detta Àr vad vi kommer att berÀtta för er nedan.
I artikeln kommer det att finnas en steg-för-steg guide om hur man skriver ett webblÀsartillÀgg, med kodexempel och skÀrmdumpar. All kod kan ni hitta i . Varje commit överensstÀmmer logiskt med avsnittet i denna artikel.
En kort historia om webblÀsartillÀgg
WebblĂ€sartillĂ€gg har funnits ganska lĂ€nge. De dök upp i Internet Explorer redan 1999, och i Firefox â 2004. Trots detta dröjde det lĂ€nge innan det fanns en enhetlig standard för tillĂ€gg.
Man kan sÀga att den uppkom samtidigt med tillÀggen i den fjÀrde versionen av Google Chrome. SjÀlvklart fanns det ingen specifikation dÄ, men just Chrome's API blev dess grund: med en stor del av webblÀsarmarknaden och en inbyggd appbutik satte Chrome i praktiken standarden för webblÀsartillÀgg.
Mozilla hade sin egen standard, men med tanke pÄ populariteten för tillÀgg till Chrome beslutade företaget att skapa ett kompatibelt API. à r 2015, pÄ initiativ av Mozilla, skapades en sÀrskild grupp inom World Wide Web Consortium (W3C) för att arbeta med specifikationerna för plattformsoberoende tillÀgg.
Grunden lades i en redan existerande API för Chrome-tillÀgg. Arbetet utfördes med stöd frÄn Microsoft (Google vÀgrade delta i standardutvecklingen), och resultatet blev ett utkast. .
Formellt stöds specifikationen av Edge, Firefox och Opera (notera att Chrome saknas i denna lista). Men i praktiken Àr standarden i stor utstrÀckning kompatibel Àven med Chrome, eftersom den faktiskt Àr skriven baserat pÄ dess tillÀgg. Mer om WebExtensions API kan lÀsas. .
Struktur för tillÀgget
Den enda fil som Àr absolut nödvÀndig för ett tillÀgg Àr manifestet (manifest.json). Det Àr ocksÄ 'ingÄngspunkten' för tillÀgget.
Manifest
Enligt specifikationen Àr manifestfilen en giltig JSON-fil. En fullstÀndig beskrivning av manifestnycklarna med information om vilka nycklar som stöds i vilken webblÀsare kan ses. .
Nycklar som inte finns i specifikationen 'kan' ignoreras (bÄde Chrome och Firefox rapporterar fel, men tillÀggen fortsÀtter att fungera).
Jag skulle vilja pÄpeka nÄgra punkter.
- background â ett objekt som innehĂ„ller följande fĂ€lt:
- scripts â en array av skripter som kommer att köras i background-context (vi pratar om detta lite senare);
- sidor â i stĂ€llet för skripter som kommer att köras pĂ„ en tom sida, kan man ange HTML med innehĂ„ll. I sĂ„ fall kommer skriptfĂ€ltet att ignoreras, och skripten mĂ„ste sĂ€ttas in i sidan med innehĂ„ll;
- bestĂ€ndiga â en binĂ€r flagga; om den inte anges, kommer webblĂ€saren att 'avsluta' bakgrundsprocessen nĂ€r den anser att den inte gör nĂ„got och starta om vid behov. Annars kommer sidan endast att laddas ur vid stĂ€ngning av webblĂ€saren. Inte stöd i Firefox.
- content_scripts â en array av objekt som möjliggör att ladda olika skript till olika webbsidor. Varje objekt innehĂ„ller följande viktiga fĂ€lt:
- matches â , som anvĂ€nds för att avgöra om ett specifikt content-script ska inkluderas eller inte.
- js â en lista över skript som kommer att laddas i det specifika matchningen;
- exclude_matches â utesluter fĂ€ltet
matchURL:er som uppfyller detta fÀlt.
- page_action â Ă€r i sjĂ€lva verket ett objekt som ansvarar för ikonen som visas bredvid adressfĂ€ltet i webblĂ€saren och interaktionen med den. Den möjliggör ocksĂ„ att visa ett popup-fönster, vilket anges med hjĂ€lp av sin egen HTML, CSS och JS.
- default_popup â sökvĂ€g till HTML-filen med popup-grĂ€nssnittet, kan innehĂ„lla CSS och JS.
- permissions â en array för att hantera rĂ€ttigheter för tillĂ€gg. Det finns 3 typer av rĂ€ttigheter, som beskrivs i detalj.
- web_accessible_resources â resurser frĂ„n tillĂ€gget som en webbsida kan begĂ€ra, sĂ„som bilder, JS-filer, CSS, HTML.
- externally_connectable â hĂ€r kan man uttryckligen ange ID:n för andra tillĂ€gg och domĂ€ner för webbsidor som kan ansluta. DomĂ€nen kan vara av andra nivĂ„er och högre. Fungerar inte i Firefox.
Körkontext
Ett tillÀgg har tre körkontexter för kod, vilket innebÀr att applikationen bestÄr av tre delar med olika nivÄer av Ätkomst till webblÀsarens API.
TillÀggskontext
HÀr Àr det mesta av API:t tillgÀngligt. I denna kontext «lever»:
- Bakgrundssida â âbackendâ-delen av tillĂ€gget. Filen anges i manifestet under nyckeln âbackgroundâ.
- Popup-sida â popup-sidan som visas nĂ€r man klickar pĂ„ tillĂ€ggsikonen. I manifestet
browser_action->default_popup. - Anpassad sida â tillĂ€ggssidan som «lever» pĂ„ en separat flik av typen
chrome-extension:///customPage.html.
Denna kontext existerar oberoende av webblĂ€sarens fönster och flikar. Bakgrundssida finns i ett enda exemplar och Ă€r alltid aktiv (undantag Ă€r event page, dĂ€r bakgrundsskriptet startas vid en hĂ€ndelse och «dör» efter att den har körts). Popup-sida existerar nĂ€r popup-fönstret Ă€r öppet, och Anpassad sida â sĂ„ lĂ€nge en flik med den Ă€r öppen. Ingen Ă„tkomst till andra flikar eller deras innehĂ„ll finns frĂ„n denna kontext.
InnehÄllsskriptkontext
InnehĂ„llsskriptfilen körs samtidigt med varje webblĂ€sartab. Den har tillgĂ„ng till delar av tillĂ€ggs-API:t och DOM-trĂ€det pĂ„ webbsidan. Det Ă€r innehĂ„llsskripten som ansvarar för interaktionen med sidan. TillĂ€gg som manipulerar DOM-trĂ€det gör detta i innehĂ„llsskripten â till exempel annonsblockerare eller översĂ€ttare. InnehĂ„llsskript kan ocksĂ„ kommunicera med sidan via standardmetoden postMessage.
Webbsidors kontext
Det Àr sjÀlva webbsidan. Den har ingen relation till tillÀgget och har ingen Ätkomst dit, förutom nÀr domÀnen för denna sida uttryckligen anges i manifestet (mer om detta nedan).
Meddelandebyte
Olika delar av applikationen mÄste utbyta meddelanden med varandra. För detta finns API:t runtime.sendMessage för att skicka meddelanden background och tabs.sendMessage för att skicka meddelanden till sidan (innehÄllsskriptet, popup:en eller webbsidan om externally_connectable). HÀr Àr ett exempel pÄ hur man anropar Chrome API.
// ĐĄĐŸĐŸĐ±ŃĐ”ĐœĐžĐ”ĐŒ ĐŒĐŸĐ¶Đ”Ń Đ±ŃŃŃ Đ»ŃĐ±ĐŸĐč JSON ŃĐ”ŃОалОзŃĐ”ĐŒŃĐč ĐŸĐ±ŃĐ”ĐșŃ
const msg = {a: 'foo', b: 'bar'};
// extensionId ĐŒĐŸĐ¶ĐœĐŸ ĐœĐ” ŃĐșазŃĐČаŃŃ, Đ”ŃлО ĐŒŃ Ń
ĐŸŃĐžĐŒ ĐżĐŸŃлаŃŃ ŃĐŸĐŸĐ±ŃĐ”ĐœĐžĐ” 'ŃĐČĐŸĐ”ĐŒŃ' ŃаŃŃĐžŃĐ”ĐœĐžŃ (Оз ui ОлО ĐșĐŸĐœŃĐ”ĐœŃ ŃĐșŃОпŃа)
chrome.runtime.sendMessage(extensionId, msg);
// йаĐș ĐČŃглŃĐŽĐžŃ ĐŸĐ±ŃĐ°Đ±ĐŸŃŃĐžĐș
chrome.runtime.onMessage.addListener((msg) => console.log(msg))
// ĐĐŸĐ¶ĐœĐŸ ŃлаŃŃ ŃĐŸĐŸĐ±ŃĐ”ĐœĐžŃ ĐČĐșлаЎĐșĐ°ĐŒ Đ·ĐœĐ°Ń ĐžŃ
id
chrome.tabs.sendMessage(tabId, msg)
// ĐĐŸĐ»ŃŃĐžŃŃ Đș ĐČĐșлаЎĐșĐ°ĐŒ Đž ĐžŃ
id ĐŒĐŸĐ¶ĐœĐŸ, ĐœĐ°ĐżŃĐžĐŒĐ”Ń, ĐČĐŸŃ ŃаĐș
chrome.tabs.query(
{currentWindow: true, active : true},
function(tabArray){
tabArray.forEach(tab => console.log(tab.id))
}
)För att möjliggöra fullstÀndig kommunikation kan man skapa anslutningar via runtime.connect. Som svar fÄr vi runtime.Port, i vilken vi kan skicka ett obegrÀnsat antal meddelanden sÄ lÀnge den Àr öppen. PÄ klientsidan, till exempel, contentscript, ser det ut sÄ hÀr:
// ĐĐżŃŃŃ Đ¶Đ” extensionId ĐŒĐŸĐ¶ĐœĐŸ ĐœĐ” ŃĐșазŃĐČаŃŃ ĐżŃĐž ĐșĐŸĐŒĐŒŃĐœĐžĐșаŃОО ĐČĐœŃŃŃĐž ĐŸĐŽĐœĐŸĐłĐŸ ŃаŃŃĐžŃĐ”ĐœĐžŃ. ĐĐŸĐŽĐșĐ»ŃŃĐ”ĐœĐžĐ” ĐŒĐŸĐ¶ĐœĐŸ ĐžĐŒĐ”ĐœĐŸĐČаŃŃ
const port = chrome.runtime.connect({name: "knockknock"});
port.postMessage({joke: "Knock knock"});
port.onMessage.addListener(function(msg) {
if (msg.question === "Who's there?")
port.postMessage({answer: "Madame"});
else if (msg.question === "Madame who?")
port.postMessage({answer: "Madame... Bovary"});
Server eller background:
// ĐбŃĐ°Đ±ĐŸŃŃĐžĐș ĐŽĐ»Ń ĐżĐŸĐŽĐșĐ»ŃŃĐ”ĐœĐžŃ 'ŃĐČĐŸĐžŃ
' ĐČĐșĐ»Đ°ĐŽĐŸĐș. ĐĐŸĐœŃĐ”ĐœŃ ŃĐșŃОпŃĐŸĐČ, 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) {
...
});Det finns ocksÄ en hÀndelse onDisconnect och metoden koppla ifrÄn.
App-schema
LÄt oss skapa en webblÀsartillÀgg som lagrar privata nycklar, ger tillgÄng till offentlig information (adress, offentlig nyckel kommunicerar med sidan) och tillÄter tredjepartsapplikationer att begÀra signaturer av transaktioner.
Utveckling av applikation
VÄr applikation bör bÄde interagera med anvÀndaren och tillhandahÄlla en API för sidan för att anropa metoder (till exempel för att signera transaktioner). Att endast anvÀnda contentscript gÄr inte, eftersom den bara har tillgÄng till DOM, men inte till JS pÄ sidan. Att koppla upp sig via runtime.connect kan vi inte göra, eftersom API:t behövs pÄ alla domÀner, men i manifestet kan man bara ange specifika. Slutresultatet kommer att se ut sÄ hÀr:

Det kommer att finnas ett annat skript â inpage, som vi kommer att injicera pĂ„ sidan. Det kommer att exekveras i dess kontext och tillhandahĂ„lla API för samarbete med tillĂ€gget.
Början
All kod för webblÀsartillÀgget finns tillgÀnglig pÄ . I beskrivningen kommer det att finnas lÀnkar till commits.
Vi börjar med manifestet:
{
// Namn och beskrivning, version. Allt detta kommer att synas i webblÀsaren pÄ chrome://extensions/?id=<extension-id>
"name": "Signer",
"description": "Extension demo",
"version": "0.0.1",
"manifest_version": 2,
// Skripten som kommer att köras i bakgrunden, det kan finnas flera
"background": {
"scripts": ["background.js"]
},
// Vilken HTML som ska anvÀndas för popup
"browser_action": {
"default_title": "My Extension",
"default_popup": "popup.html"
},
// InnehÄllsskript.
// Vi har ett objekt: för alla url:er som börjar med http eller https kör vi
// innehÄllsskript-kontext med skriptet contentscript.js. Starta omedelbart vid dokumentets mottagande för alla ramar
"content_scripts": [
{
"matches": [
"http:/*/*",
"https:/*/*"
],
"js": [
"contentscript.js"
],
"run_at": "document_start",
"all_frames": true
}
],
// Ă
tkomst till localStorage och idle API tillÄts
"permissions": [
"storage",
// "unlimitedStorage",
//"clipboardWrite",
"idle"
//"activeTab",
//"webRequest",
//"notifications",
//"tabs"
],
// HÀr anges resurser till vilka webbplatsen kommer att ha tillgÄng. Det betyder att de kan begÀras med fetch eller bara xhr
"web_accessible_resources": ["inpage.js"]
}Vi skapar tomma background.js, popup.js, inpage.js och contentscript.js. Vi lĂ€gger till popup.html â och vĂ„r applikation kan redan laddas upp i Google Chrome för att se om den fungerar.
För att bekrĂ€fta detta kan vi ta koden . Utöver det vi har gjort, sĂ„ Ă€r det en lĂ€nk för att sĂ€tta upp projektet med webpack. För att lĂ€gga till applikationen i webblĂ€saren, gĂ„ till chrome://extensions, vĂ€lj load unpacked och mappen med motsvarande tillĂ€gg â i vĂ„rt fall dist.

Nu Àr vÄrt tillÀgg installerat och fungerar. Utvecklarverktyg för olika kontexter kan startas pÄ följande sÀtt:
popup ->

TillgÄng till innehÄllsskriptets konsol görs via konsolen pÄ den sida dÀr det körs.
Meddelandebyte
SÄ vi behöver sÀtta upp tvÄ kommunikationskanaler: inpage background och popup background. Visst kan vi bara skicka meddelanden till porten och uppfinna vÄrt eget protokoll, men jag föredrar den metod jag sett i det öppna projektet metamask.
Detta Àr en webblÀsartillÀgg för att arbeta med Ethereum-nÀtverket. I det kommunicerar olika delar av applikationen via RPC med hjÀlp av biblioteket dnode. Det möjliggör snabb och bekvÀm kommunikation om vi tillhandahÄller nodejs-stream som transport (med det menar jag ett objekt som implementerar samma grÀnssnitt):
import Dnode from "dnode/browser";
// I det hÀr exemplet förutsÀtter vi att klienten fjÀrran anropar funktioner pÄ servern, Àven om inget hindrar oss frÄn att göra det tvÄvÀgs
// Server
// API som vi vill tillhandahÄlla
const dnode = Dnode({
hello: (cb) => cb(null, "world")
})
// Transport som dnode kommer att fungera ovanpÄ. Valfri nodejs-stream. I webblÀsaren finns biblioteket 'readable-stream'
connectionStream.pipe(dnode).pipe(connectionStream)
// Klient
const dnodeClient = Dnode() // Anrop utan argument betyder att vi inte tillhandahÄller API pÄ andra sidan
// Skriver ut 'world' i konsolen
dnodeClient.once('remote', remote => {
remote.hello((err, value) => console.log(value))
})Nu kommer vi att skapa en klass för applikationen. Den kommer att skapa API-objekt för popup och webbsidan samt skapa dnode för dem:
import Dnode from 'dnode/browser';
export class SignerApp {
// Return API object for UI
popupApi(){
return {
hello: cb => cb(null, 'world')
}
}
// Return API object for page
pageApi(){
return {
hello: cb => cb(null, 'world')
}
}
// Connects popup UI
connectPopup(connectionStream){
const api = this.popupApi();
const dnode = Dnode(api);
connectionStream.pipe(dnode).pipe(connectionStream);
dnode.on('remote', (remote) => {
console.log(remote)
})
}
// Connects page
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)
})
}
}HĂ€r och framĂ„t anvĂ€nder vi istĂ€llet det globala objektet Chrome som extentionApi, vilket adresserar Chrome i Googles webblĂ€sare och browser i andra. Detta görs för att sĂ€kerstĂ€lla cross-browser stöd, men inom ramen för denna artikel skulle man ocksĂ„ kunna anvĂ€nda âchrome.runtime.connectâ.
Skapa en instans av applikationen i bakgrundsskriptet:
import {extensionApi} from './utils/extensionApi';
import {PortStream} from './utils/PortStream';
import {SignerApp} from './SignerApp';
const app = new SignerApp();
// onConnect utlöses vid anslutning av 'processer' (contentscript, popup eller sida av tillÀgget)
extensionApi.runtime.onConnect.addListener(connectRemote);
function connectRemote(remotePort) {
const processName = remotePort.name;
const portStream = new PortStream(remotePort);
// Vid anslutning kan man ange namn, och det namnet anvÀnder vi för att identifiera vem som anslutit sig, contentscript eller UI
if (processName === 'contentscript'){
const origin = remotePort.sender.url
app.connectPage(portStream, origin)
}else{
app.connectPopup(portStream)
}
}Eftersom dnode arbetar med strömmar och vi tar emot en port, behövs en adapterklass. Den Àr skapad med hjÀlp av biblioteket readable-stream, som implementerar nodejs-strömmar i webblÀsaren:
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()
}
}Nu skapar vi en anslutning i 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(){
// Precis som i applikationsklassen skapar vi en port, lindar in den i en stream och gör en 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)
})
});
// Gör API-objektet tillgÀngligt frÄn konsolen
if (DEV_MODE){
global.background = background;
}
}DÀrefter skapar vi en anslutning i innehÄllsskriptet:
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 {
// injicera skript pÄ sidan
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('Injektion misslyckades.', e);
}
}Eftersom API:et inte behövs i innehÄllsskriptet utan direkt pÄ sidan, gör vi tvÄ saker:
- Skapar tvĂ„ streamar. En mot sidan, ovanför postMessage. För detta anvĂ€nder vi frĂ„n skaparna av metamask. Den andra streamen â till bakgrunden ovanför porten som erhĂ„llits frĂ„n
runtime.connect. Vi pipar dem. Nu kommer sidan ha en stream till bakgrunden. - Injicerar skriptet i DOM. Vi hÀmtar skriptet (Ätkomst till det har beviljats i manifestet) och skapar en tagg
scriptmed dess innehÄll inuti:
import PostMessageStream from 'post-message-stream';
import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
setupConnection();
injectScript();
function setupConnection(){
// Stream till bakgrunden
const backgroundPort = extensionApi.runtime.connect({name: 'contentscript'});
const backgroundStream = new PortStream(backgroundPort);
// Stream till sidan
const pageStream = new PostMessageStream({
name: 'content',
target: 'page',
});
pageStream.pipe(backgroundStream).pipe(pageStream);
}
function injectScript(){
try {
// injicera skript pÄ sidan
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('Injektion misslyckades.', e);
}
}Nu skapar vi ett API-objekt i inpage och gör det globalt:
import PostMessageStream from 'post-message-stream';
import Dnode from 'dnode/browser';
setupInpageApi().catch(console.error);
async function setupInpageApi() {
// Stream to the content script
const connectionStream = new PostMessageStream({
name: 'page',
target: 'content',
});
const dnode = Dnode();
connectionStream.pipe(dnode).pipe(connectionStream);
// Getting the API object
const pageApi = await new Promise(resolve => {
dnode.once('remote', api => {
resolve(api)
})
});
// Access through window
global.SignerApp = pageApi;
}Vi Àr klara . NÀr en ny sida kopplas till bakgrunden kan vi se detta:

Tom API och origin. PÄ sidan kan vi anropa funktionen hello sÄ hÀr:

Att arbeta med callback-funktioner i modern JS Àr förlegat, sÄ vi skriver en liten hjÀlpare för att skapa dnode, som möjliggör att överföra till API-objektet i utils.
API-objekten kommer nu att se ut sÄ hÀr:
export class SignerApp {
popupApi() {
return {
hello: async () => "world"
}
}
...
}FÄ objektet frÄn remote pÄ följande sÀtt:
import {cbToPromise, transformMethods} from "../src/utils/setupDnode";
const pageApi = await new Promise(resolve => {
dnode.once('remote', remoteApi => {
// Med hjÀlp av verktyg förÀndrar vi alla callbacks till promises
resolve(transformMethods(cbToPromise, remoteApi))
})
});Och anrop av funktioner returnerar ett promise:

Versionen med asynkrona funktioner Àr tillgÀnglig .
Ăverlag verkar tillvĂ€gagĂ„ngssĂ€ttet med RPC och strömmar tillrĂ€ckligt flexibelt: vi kan anvĂ€nda stream multiplexing och skapa flera olika API:er för olika uppgifter. I princip kan dnode anvĂ€ndas överallt, det viktigaste Ă€r att slĂ„ in transporten som en nodejs-stream.
Ett alternativ Àr JSON-formatet, som implementerar protokollet JSON RPC 2. Men det fungerar med specifika transportprotokoll (TCP och HTTP(S)), vilket i vÄrt fall inte Àr tillÀmpligt.
Intern status och localStorage
Vi kommer att behöva lagra den interna statusen för applikationen â minst nycklarna för signering. Vi kan ganska enkelt lĂ€gga till status i applikationen och metoder för att Ă€ndra den i popup API:
import {setupDnode} from "./utils/setupDnode";
export class SignerApp {
constructor(){
this.store = {
keys: [],
};
}
addKey(key){
this.store.keys.push(key)
}
removeKey(index){
this.store.keys.splice(index,1)
}
popupApi(){
return {
addKey: async (key) => this.addKey(key),
removeKey: async (index) => this.removeKey(index)
}
}
...
} I bakgrunden sveper vi allt i en funktion och skriver objektet för applikationen i window, sÄ att vi kan arbeta med det frÄn konsolen:
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)
}
}
}LÄt oss lÀgga till nÄgra nycklar frÄn UI-konsolen och se vad som har hÀnt med tillstÄndet:

TillstÄndet mÄste göras bestÀndigt sÄ att nycklarna inte gÄr förlorade vid omstart.
Vi ska lagra det i localStorage och skriva över det vid varje förÀndring. Senare kommer tillgÄng till det ocksÄ att vara nödvÀndig för UI, och vi vill Àven kunna prenumerera pÄ förÀndringar. UtifrÄn detta blir det praktiskt att skapa ett observerbart lager (observable storage) och prenumerera pÄ dess förÀndringar.
Vi kommer att anvÀnda mobx-biblioteket (). Valet föll pÄ det eftersom jag aldrig har arbetat med det och verkligen ville lÀra mig det.
LÄt oss lÀgga till initialisering av det initiala tillstÄndet och göra lagret observerbart:
import {observable, action} from 'mobx';
import {setupDnode} from "./utils/setupDnode";
export class SignerApp {
constructor(initState = {}) {
// Externt kommer lagret fortsatt att vara samma objekt, men nu har alla dess fÀlt blivit proxy som övervakar Ätkomst till dem
this.store = observable.object({
keys: initState.keys || [],
});
}
// Metoder som Àndrar observable brukar omges av dekorator
@action
addKey(key) {
this.store.keys.push(key)
}
@action
removeKey(index) {
this.store.keys.splice(index, 1)
}
...
}«Under huven» har mobx ersatt alla fÀlt i lagret med proxy och fÄngar alla Ätkomster till dem. Det Àr möjligt att prenumerera pÄ dessa Ätkomster.
Jag kommer senare att ofta anvĂ€nda termen âvid förĂ€ndringâ, Ă€ven om det inte Ă€r helt korrekt. Mobx övervakar just Ă„tkomst till fĂ€lten. Getter- och setter-metoder för proxy-objekten anvĂ€nds, som biblioteken skapar.
Dekoratorer för action tjÀnar tvÄ syften:
- I strikt lÀge med flaggan enforceActions förbjuder mobx att direkt Àndra tillstÄndet. Det anses vara god praxis att arbeta just i strikt lÀge.
- Ăven om funktionen Ă€ndrar tillstĂ„nd flera gĂ„nger â till exempel, om vi Ă€ndrar flera fĂ€lt pĂ„ flera rader kod â sĂ„ meddelas observatörerna endast nĂ€r den Ă€r klar. Detta Ă€r sĂ€rskilt viktigt för frontenden, dĂ€r onödiga uppdateringar av tillstĂ„ndet leder till oönskad rendering av element. I vĂ„rt fall Ă€r varken det första eller det andra sĂ€rskilt relevant, men vi kommer att följa bĂ€sta praxis. Dekoratorer brukar tillĂ€mpas pĂ„ alla funktioner som Ă€ndrar tillstĂ„ndet för observerade fĂ€lt.
I bakgrunden lÀgger vi till initialisering och sparande av tillstÄndet i localStorage:
import {reaction, toJS} from 'mobx';
import {extensionApi} from ".\/utils\/extensionApi";
import {PortStream} from ".\/utils\/PortStream";
import {SignerApp} from ".\/SignerApp";
// HjÀlpmetoder. Skrivs/lÀses objekt i/frÄn localStorage i form av en JSON-strÀng med nyckeln '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;
}
// InstÀllning av tillstÄndsparallelisering
// Resultatet av reaction tilldelas en variabel sÄ att prenumerationen kan avbrytas. Vi behöver inte detta, kvarlÀmnat som exempel
const localStorageReaction = reaction(
() => toJS(app.store), // Funktion för dataval
saveState // Funktion som kommer att anropas vid Àndring av data som returneras av valfunktion
);
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)
}
}
}Den intressanta funktionen hÀr Àr reaction. Den har tvÄ parametrar:
- Dataval
- En callback som kommer att anropas med dessa data varje gÄng de Àndras.
Till skillnad frÄn redux, dÀr vi tydligt fÄr tillstÄndet som en parameter, kommer mobx ihÄg vilka observerbara vi refererar till inom valfunktionen, och anropar endast callbacken nÀr de Àndras.
Det Àr viktigt att förstÄ hur mobx bestÀmmer vilka observerbara vi prenumererar pÄ. Om jag i koden skrev valfunktionen pÄ det sÀttet() => app.store, sÄ skulle reaction aldrig anropas, eftersom sjÀlva lagret inte Àr observerbart; endast dess fÀlt Àr det.
Om jag skrev den sÄ hÀr () => app.store.keys, sÄ skulle igen ingenting hÀnda, eftersom lÀnken till den inte skulle förÀndras vid tillÀgg/borttagning av element i arrayen.
Mobx utför för första gĂ„ngen en selektorfunktion och övervakar endast de observable som vi hade tillgĂ„ng till. Detta görs genom proxygetters. DĂ€rför anvĂ€nds en inbyggd funktion hĂ€r. toJS. Den returnerar ett nytt objekt dĂ€r alla proxies har ersatts av de ursprungliga fĂ€lten. Under körning lĂ€ser den alla fĂ€lt i objektet â dĂ€rmed aktiveras getternas funktioner.
I popupkonsolen lÀgger vi till nÄgra nycklar igen. Den hÀr gÄngen hamnar de ocksÄ i localStorage:

Vid omladdning av bakgrundssidan förblir informationen pÄ plats.
Hele applikationens kod fram till detta ögonblick kan ses. .
SÀker förvaring av privata nycklar
Att lagra privata nycklar i klartext Àr osÀkert: det finns alltid en risk att du blir hackad och fÄr tillgÄng till din dator med mera. DÀrför kommer vi att lagra nycklarna i lokal lagring i ett lösenordskrypterat format.
För ökad sÀkerhet kommer vi att lÀgga till ett lÄst tillstÄnd i applikationen, dÀr det inte kommer att finnas nÄgot tillgÄng till nycklarna överhuvudtaget. Vi kommer automatiskt att överföra tillÀgget till det lÄsta tillstÄndet efter en timeout.
Mobx gör det möjligt att lagra endast en minimal mÀngd data, och automatiskt berÀkna resten baserat pÄ dessa. Detta kallas för computed properties. De kan jÀmföras med views i databaser:
import {observable, action} from 'mobx';
import {setupDnode} from ".\/utils\/setupDnode";
// Verktyg för sÀker kryptering av strÀngar. AnvÀnder crypto-js
import {encrypt, decrypt} from ".\/utils\/cryptoUtils";
export class SignerApp {
constructor(initState = {}) {
this.store = observable.object({
// Lagrar lösenord och krypterade nycklar. Om lösenord Àr null - appen Àr lÄst
password: null,
vault: initState.vault,
// Getters för berÀknade fÀlt. Kan liknas vid vy i databasen.
get locked(){
return this.password == null
},
get keys(){
return this.locked ?
undefined :
SignerApp._decryptVault(this.vault, this.password)
},
get initialized(){
return this.vault !== undefined
}
})
}
// Initiera ett tomt förrÄd med ett nytt lösenord
@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
)
}
... // kod för anslutning och api
// privat
_checkPassword(password) {
SignerApp._decryptVault(this.store.vault, password);
}
_checkLocked() {
if (this.store.locked){
throw new Error('Appen Àr lÄst')
}
}
// Metoder för kryptering/dekryptering av förrÄdet
static _encryptVault(obj, pass){
const jsonString = JSON.stringify(obj)
return encrypt(jsonString, pass)
}
static _decryptVault(str, pass){
if (str === undefined){
throw new Error('FörrÄdet Àr inte initierat')
}
try {
const jsonString = decrypt(str, pass)
return JSON.parse(jsonString)
}catch (e) {
throw new Error('Fel lösenord')
}
}
}Nu lagrar vi endast krypterade nycklar och lösenord. Allt annat berĂ€knas. ĂvergĂ„ngen till lĂ„st tillstĂ„nd görs genom att ta bort lösenordet frĂ„n tillstĂ„ndet. I det publika API:t har en metod för att initiera förrĂ„det tillkommit.
För kryptering har :
import CryptoJS from 'crypto-js'
// AnvÀnds för att komplicera gissning av lösenord genom brute-force. För varje lösenord alternativ mÄste angriparen göra 5000 hashningar
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)
}WebblĂ€saren har ett Idle API som gör att man kan prenumerera pĂ„ hĂ€ndelsen â statusemĂ€ndringar. Status kan dĂ€rmed vara inaktiv, active och lĂ„st. För idle kan man stĂ€lla in en tidsgrĂ€ns, och lĂ„st sĂ€tts nĂ€r operativsystemet sjĂ€lv blockeras. Vi kommer ocksĂ„ att Ă€ndra selektorn för att spara 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;
}
\/\/ HÀr anropar vi tydligt fÀltet, som kommer att anvÀndas, reaktionen fungerar korrekt
reaction(
() => ({
vault: app.store.vault
}),
saveState
);
\/\/ TidsgrÀns för inaktivitet nÀr hÀndelsen intrÀffar
extensionApi.idle.setDetectionInterval(IDLE_INTERVAL);
\/\/ Om anvÀndaren lÄser skÀrmen eller Àr inaktiv under den angivna tidsgrÀnsen, lÄser vi applikationen
extensionApi.idle.onStateChanged.addListener(state => {
if (['locked', 'idle'].indexOf(state) > -1) {
app.lock()
}
});
\/\/ Anslut till andra sammanhang
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)
}
}
}Koden fram till detta steg finns .
Transaktioner
SÄ, vi har kommit till det viktigaste: skapandet och signeringen av transaktioner i blockkedjan. Vi kommer att anvÀnda WAVES blockkedjan och biblioteket .
För att börja lĂ€gger vi till en array av meddelanden i statusen som behöver signeras, sedan â metoder för att lĂ€gga till ett nytt meddelande, bekrĂ€fta signaturen och avböja:
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) {
// För varje meddelande skapar vi metadata med id, status, skapad tid osv.
const message = observable.object({
id: uuid(), // Identifierare, anvÀnder uuid
origin, // Origin kommer vi senare visa i grÀnssnittet
data, //
status: 'new', // Det kommer finnas fyra statusar: new, signed, rejected och failed
timestamp: Date.now()
});
console.log(`nytt meddelande: ${JSON.stringify(message, null, 2)}`);
this.store.messages.push(message);
// Vi returnerar ett löfte dÀr mobx övervakar Àndringar i meddelandet. SÄ snart status Àndras kommer vi att lösa det
return new Promise((resolve, reject) => {
reaction(
() => message.status, // Vi kommer att observera statusen av meddelandet
(status, reaction) => { // den andra argumentet Àr en referens till sjÀlva reaction, sÄ att den kan tas bort inuti anropet
switch (status) {
case 'signed':
resolve(message.data);
break;
case 'rejected':
reject(new Error('AnvÀndaren avslog meddelandet'));
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(`Inga meddelanden med 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(`Inga meddelanden med id:${id}`);
message.status = 'rejected'
}
...
}NÀr vi fÄr ett nytt meddelande lÀgger vi till metadata i det, vi gör observable och lÀgger till i store.messages.
Om vi inte gör det observable manuellt kommer mobx att göra det automatisk nÀr det lÀggs till i arrayen messages. Men den kommer att skapa ett nytt objekt, som vi inte kommer att ha en referens till, vilket behövs för nÀsta steg.
Vi returnerar sedan ett löfte som löses nÀr meddelandets status Àndras. Statusen övervakas av en reaction, som sjÀlvmant "dödas" vid statusÀndring.
Kod för metoderna approve och reject Àr mycket enkel: vi Àndrar bara meddelandets status, efter att ha signerat det om det behövs.
Vi hanterar godkĂ€nnande och avvisande i API UI, newMessage â pĂ„ API-sidan:
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)
}
}
...
}LÄt oss nu försöka signera en transaktion med tillÀgget:

I stort sett Àr allt klart, vi behöver bara .
UI
UI:t behöver Ätkomst till applikationens tillstÄnd. PÄ UI-sidan kommer vi att göra observable ett tillstÄnd och lÀgga till en funktion i API:t som kommer att Àndra detta tillstÄnd. Vi lÀgger till observable i API-objektet som fÄs frÄn bakgrunden:
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() {
// Ansluter till porten, skapar en ström frÄn den
const backgroundPort = extensionApi.runtime.connect({name: 'popup'});
const connectionStream = new PortStream(backgroundPort);
// Skapar en tom observable för bakgrundens tillstÄnd
let backgroundState = observable.object({});
const api = {
// Ger bakgrunden en funktion som kommer att uppdatera observable
updateState: async state => {
Object.assign(backgroundState, state)
}
};
// Skapar RPC-objekt
const dnode = setupDnode(connectionStream, api);
const background = await new Promise(resolve => {
dnode.once('remote', remoteApi => {
resolve(transformMethods(cbToPromise, remoteApi))
})
});
// LÀgger till observable med tillstÄndet i bakgrunden
background.state = backgroundState;
if (DEV_MODE) {
global.background = background;
}
// Startar UI:t
await initApp(background)
}
I slutet startar vi renderingen av applikationens grÀnssnitt. Det hÀr Àr en React-applikation. Bakgrundsobjektet överförs helt enkelt med props. För korrekthetens skull borde man göra en separat tjÀnst för metoder och lagra tillstÄndet, men inom ramen för denna artikel Àr det tillrÀckligt:
import {render} from 'react-dom'
import App from '.\/App'
import React from "react";
// Initierar applikationen med bakgrundsobjektet som props
export async function initApp(background){
render(
<App background={background} />,
document.getElementById('app-content')
);
}
Med mobx Àr det mycket enkelt att starta renderingen vid databasÀndringar. Vi sÀtter bara dekoren observer frÄn paketet till komponenten, och rendering kommer automatiskt att kallas nÀr nÄgra observable som komponenten refererar till Àndras. Ingen mapStateToProps eller connect behövs, som i redux. Allt fungerar direkt 'ur lÄdan':
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 \/\/ Komponenten med denna dekoration kommer automatiskt att anropa render-metoden om de observerbara som den hänvisar till ändras
export default class App extends Component {
\/\/ Det är korrekt att extrahera logiken för att rendera sidor till routing och inte använda inbäddade ternära operatorer,
\/\/ samt koppla observerbara och metoder till bakgrunden direkt till de komponenter som använder dem
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()}>Lås App</button>}
{initialized && <button onclick="{()" > deleteVault()}>Radera alla nycklar och initiera</button>}
</div>
</Fragment>
}
}Ăvriga komponenter kan ses i koden .
Nu mÄste klassen för applikationen göra en selektor för tillstÄndet för UI och meddela UI vid förÀndring. För detta kommer vi att lÀgga till metoden getState och reaction, som anropar 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 {
...
// offentlig
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) => {
// Skapa reaction pÄ förÀndringar av tillstÄndet, som gör ett anrop till en fjÀrrprocedur och uppdaterar tillstÄndet i UI-processen.
const updateStateReaction = reaction(
() => this.getState(),
(state) => remote.updateState(state),
// Tredje argumentet kan anvÀndas för att överföra parametrar. fireImmediately betyder att reaction kommer att köras första gÄngen direkt.
// Detta Àr nödvÀndigt för att fÄ det initiala tillstÄndet. Delay tillÄter att stÀlla in debounce.
{fireImmediately: true, delay: 500}
);
// Ta bort prenumerationen nÀr klienten kopplas bort
dnode.once('end', () => updateStateReaction.dispose())
})
}
...
}Vid mottagning av objektet remote skapas reaction pÄ förÀndring av tillstÄndet, som anropar funktionen pÄ UI-sidan.
Sista handen - vi lÀgger till en visning av nya meddelanden pÄ ikonen för tillÀgget:
function setupApp() {
...
// Reaction pÄ att stÀlla in texten pÄ badgen.
reaction(
() => app.store.newMessages.length > 0 ? app.store.newMessages.length.toString() : '',
text => extensionApi.browserAction.setBadgeText({text}),
{fireImmediately: true}
);
...
}SÄ applikationen Àr klar. Webbplatser kan begÀra signaturer av transaktioner:


Koden finns tillgÀnglig pÄ denna .
Slutsats
Om du har lÀst artikeln till slutet men har frÄgor kan du stÀlla dem i . DÀr hittar du Àven commits för varje angiven steg.
Och om du Àr intresserad av att se koden för ett riktigt tillÀgg, sÄ kan du hitta detta .
Koden, förrÄdet och beskrivningen av arbetet frÄn
KĂ€lla: habr.com
