Ke kākau ʻana i kahi hoʻonui polokalamu kele pūnaewele palekana

Ke kākau ʻana i kahi hoʻonui polokalamu kele pūnaewele palekana

ʻAʻole like me ka hoʻolālā "client-server" maʻamau, ʻike ʻia nā noi decentralized e:

  • ʻAʻohe pono e mālama i kahi waihona me nā mea hoʻohana a me nā ʻōlelo huna. Mālama ʻia ka ʻike ʻike e nā mea hoʻohana ponoʻī, a ʻo ka hōʻoia ʻana o ko lākou ʻoiaʻiʻo e loaʻa ma ka pae protocol.
  • ʻAʻole pono e hoʻohana i kahi kikowaena. Hiki ke hoʻokō ʻia ka loiloi noi ma kahi pūnaewele blockchain, kahi e hiki ai ke mālama i ka nui o ka ʻikepili i makemake ʻia.

Aia he 2 mau waihona palekana no nā kī mea hoʻohana - nā wallets hardware a me nā hoʻonui pūnaewele. Paʻa loa nā ʻeke paʻa paʻa, akā paʻakikī ke hoʻohana a mamao loa mai ka manuahi, akā ʻo ka hoʻonui ʻia ʻana o ka polokalamu kele pūnaewele ʻo ia ka hui kūpono o ka palekana a me ka maʻalahi o ka hoʻohana ʻana, a hiki ke manuahi loa no nā mea hoʻohana hope.

Ma ka noʻonoʻo ʻana i kēia mau mea a pau, makemake mākou e hana i ka hoʻonui paʻa loa e hoʻomaʻamaʻa i ka hoʻomohala ʻana i nā noi decentralized ma o ka hāʻawi ʻana i kahi API maʻalahi no ka hana ʻana me nā kālepa a me nā pūlima.
E haʻi mākou iā ʻoe e pili ana i kēia ʻike ma lalo nei.

E loaʻa i ka ʻatikala nā ʻōlelo aʻoaʻo i kēlā me kēia ʻanuʻu e pili ana i ke kākau ʻana i kahi hoʻonui polokalamu, me nā laʻana code a me nā kiʻi paʻi. Hiki iā ʻoe ke loaʻa nā code a pau i loko hale waihona. ʻO kēlā me kēia commit e pili ana i kahi ʻāpana o kēia ʻatikala.

He Moʻolelo Pōkole o nā Hoʻonui Pūnaewele

Ua lōʻihi ka hoʻonui ʻia ʻana o ka polokalamu kele pūnaewele. Ua ʻike ʻia lākou ma Internet Explorer i ka makahiki 1999, ma Firefox i ka makahiki 2004. Eia naʻe, no ka manawa lōʻihi loa, ʻaʻohe mea maʻamau no ka hoʻonui.

Hiki iā mākou ke ʻōlelo ua ʻike ʻia me nā hoʻonui i ka ʻehā o ka mana o Google Chrome. ʻOiaʻiʻo, ʻaʻohe kikoʻī i kēlā manawa, akā ʻo ia ka Chrome API i lilo i kumu: ua lanakila ʻo ia i ka hapa nui o ka mākeke polokalamu kele pūnaewele a loaʻa kahi hale kūʻai noi i kūkulu ʻia, ua hoʻonohonoho maoli ʻo Chrome i ka maʻamau no ka hoʻonui ʻia ʻana o ka polokalamu.

Loaʻa iā Mozilla kona kūlana ponoʻī, akā i ka ʻike ʻana i ka kaulana o nā hoʻonui Chrome, ua hoʻoholo ka hui e hana i kahi API kūpono. Ma 2015, ma ka hoʻomaka ʻana o Mozilla, ua hana ʻia kahi hui kūikawā i loko o ka World Wide Web Consortium (W3C) e hana i nā kikoʻī hoʻonui cross-browser.

Ua lawe ʻia nā hoʻonui API no Chrome i kumu. Ua hoʻokō ʻia ka hana me ke kākoʻo o Microsoft (hōʻole ʻo Google e komo i ka hoʻomohala ʻana i ka maʻamau), a ma muli o ka puka ʻana o kahi kiʻi. hoakaka.

Ma keʻano maʻamau, kākoʻo ʻia ka kikoʻī e Edge, Firefox a me Opera (e hoʻomaopopo ʻaʻole ʻo Chrome ma kēia papa inoa). Akā ʻo ka ʻoiaʻiʻo, ua kūpono ka maʻamau me Chrome, no ka mea, ua kākau maoli ʻia ma muli o kāna mau hoʻonui. Hiki iā ʻoe ke heluhelu hou aku e pili ana i ka WebExtensions API maanei.

hale hoʻonui

ʻO ka faila wale nō i koi ʻia no ka hoʻonui ʻia ʻo ka hōʻike (manifest.json). ʻO ia hoʻi ka "puka komo" i ka hoʻonui.

Manaipo

Wahi a ka kikoʻī, ʻo ka faila hōʻike he faila JSON kūpono. ʻO kahi wehewehe piha o nā kī hōʻike me ka ʻike e pili ana i nā kī i kākoʻo ʻia i hiki ke ʻike ʻia ka polokalamu kele pūnaewele maanei.

ʻO nā kī ʻaʻole i loko o ka kikoʻī "hiki" ke nānā ʻole ʻia (ua hōʻike ʻia ʻo Chrome a me Firefox i nā hewa, akā hoʻomau ka hoʻonui ʻana).

A makemake wau e huki i ka manaʻo i kekahi mau wahi.

  1. kāʻei kua — he mea i komo i keia mau kahua:
    1. Hō'olālā - kahi ʻano o nā palapala e hoʻokō ʻia ma ka pōʻaiapili hope (e kamaʻilio mākou e pili ana i kēia ma hope iki);
    2. palapala - ma kahi o nā palapala e hoʻokō ʻia ma kahi ʻaoʻao ʻole, hiki iā ʻoe ke kuhikuhi i ka html me ka ʻike. I kēia hihia, e nānā ʻole ʻia ke kahua palapala, a pono e hoʻokomo ʻia nā palapala i loko o ka ʻaoʻao ʻike;
    3. hoomau anei - he hae binary, inā ʻaʻole i kuhikuhi ʻia, e "pepehi" ka polokalamu kele i ke kaʻina hana hope ke manaʻo ʻo ia ʻaʻole ia e hana i kekahi mea, a hoʻomaka hou inā pono. A i ʻole, e wehe wale ʻia ka ʻaoʻao ke pani ʻia ka polokalamu kele pūnaewele. ʻAʻole i kākoʻo ʻia ma Firefox.
  2. content_scripts - he ʻano mea e hiki ai iā ʻoe ke hoʻouka i nā palapala like ʻole i nā ʻaoʻao pūnaewele like ʻole. Loaʻa i kēlā me kēia mea kēia mau kahua koʻikoʻi:
    1. ihoiho - kumu url, ka mea e hoʻoholo ai i ka hoʻokomo ʻia ʻana o kahi palapala kikoʻī a i ʻole.
    2. js - he papa inoa o nā palapala e hoʻouka ʻia i kēia pāʻani;
    3. wehe_matches - kaawale mai ke kahua match Nā URL e pili ana i kēia kahua.
  3. ʻaoʻao_hana - he mea maoli ke kuleana no ka ikona i hōʻike ʻia ma ka ʻaoʻao o ka pahu helu wahi ma ka polokalamu kele pūnaewele a me ka launa pū me ia. Hiki iā ʻoe ke hōʻike i kahi pukaaniani popup, i wehewehe ʻia me kāu HTML, CSS a me JS.
    1. default_popup — ala i ka waihona HTML me ka popup interface, hiki ke loaʻa iā CSS a me JS.
  4. 'ae - he laha no ka mālama ʻana i nā kuleana hoʻonui. Aia nā ʻano kuleana 3, i wehewehe ʻia i nā kikoʻī maanei
  5. punaewele_accessible_resources — nā kumuwaiwai hoʻonui i hiki i kahi ʻaoʻao pūnaewele ke noi, no ka laʻana, nā kiʻi, JS, CSS, nā faila HTML.
  6. externally_connectable - maanei hiki iā ʻoe ke kuhikuhi pololei i nā ID o nā hoʻonui ʻē aʻe a me nā kāʻei o nā ʻaoʻao pūnaewele āu e hoʻopili ai. Hiki ke pae ʻelua a ʻoi aku paha. ʻAʻole hana ma Firefox.

Poʻomanaʻo hoʻokō

ʻO ka hoʻonui ʻia ʻekolu mau ʻano hoʻokō code, ʻo ia hoʻi, ʻekolu ʻāpana o ka noi me nā pae like ʻole o ke komo ʻana i ka API polokalamu.

pōʻaiapili hoʻonui

Loaʻa ka hapa nui o ka API ma aneʻi. Ma kēia pōʻaiapili "ola" lākou:

  1. ʻaoʻao hope - ʻāpana "backend" o ka hoʻonui. Hōʻike ʻia ka faila ma ka hōʻike me ke kī "background".
  2. ʻaoʻao popup - he ʻaoʻao popup e ʻike ʻia ke kaomi ʻana i ka ikona hoʻonui. Ma ka manifesto browser_action -> default_popup.
  3. ʻaoʻao maʻamau — ʻaoʻao hoʻonui, "ola" ma kahi ʻāpana ʻokoʻa o ka ʻike chrome-extension://<id_расширения>/customPage.html.

Aia kēia pōʻaiapili me nā puka makani a me nā tabs. ʻaoʻao hope aia i loko o kahi kope hoʻokahi a hana mau (koe ka ʻaoʻao hanana, ke hoʻomaka ʻia ka palapala hope e kahi hanana a "make" ma hope o kona hoʻokō ʻana). ʻaoʻao popup aia i ka wā e wehe ʻia ai ka pukaaniani popup, a ʻaoʻao maʻamau — oiai e hamama ana ka tab me ia. ʻAʻole hiki ke komo i nā ʻaoʻao ʻē aʻe a me kā lākou ʻike mai kēia pōʻaiapili.

pōʻaiapili palapala maʻiʻo

Hoʻomaka ʻia ka waihona palapala maʻiʻo me kēlā me kēia ʻaoʻao polokalamu kele pūnaewele. Loaʻa iā ia kahi ʻāpana o ka API o ka hoʻonui a i ka lāʻau DOM o ka ʻaoʻao pūnaewele. He mau palapala maʻiʻo ke kuleana no ka launa pū ʻana me ka ʻaoʻao. ʻO nā mea hoʻonui e hoʻopunipuni i ka lāʻau DOM e hana i kēia i loko o nā palapala maʻiʻo - no ka laʻana, nā mea pale hoʻolaha a i ʻole nā ​​mea unuhi. Eia kekahi, hiki i ka palapala maʻiʻo ke kamaʻilio me ka ʻaoʻao ma o ka maʻamau postMessage.

pōʻaiapili palapala pūnaewele

ʻO kēia ka ʻaoʻao pūnaewele ponoʻī. ʻAʻohe mea pili i ka hoʻonui ʻia a ʻaʻole hiki ke komo i laila, koe wale nō i nā hihia kahi i hōʻike ʻole ʻia ai ka domain o kēia ʻaoʻao ma ka hōʻike (ʻoi aku ma lalo o kēia).

Kūʻai leka

Pono nā ʻāpana like ʻole o ka noi e hoʻololi i nā memo me kekahi. Aia kahi API no kēia runtime.sendMessage e hoʻouna i kahi leka background и tabs.sendMessage e hoʻouna i kahi leka i kahi ʻaoʻao (ka palapala maʻiʻo, popup a i ʻole ʻaoʻao pūnaewele inā loaʻa externally_connectable). Aia ma lalo kahi laʻana i ke komo ʻana i ka 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))
    }
)

No ke kamaʻilio piha, hiki iā ʻoe ke hana i nā pilina ma o runtime.connect. Ma ka pane e loaʻa iā mākou runtime.Port, i hiki iā ʻoe ke hoʻouna i kekahi helu o nā memo. Ma ka ʻaoʻao o ka mea kūʻai aku, no ka laʻana, contentscript, ua like ia me keia:

// Опять же 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 a i ʻole ka hope:

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

Aia kekahi hanana onDisconnect a me ke ʻano disconnect.

Kiʻi palapala noi

E hana mākou i kahi hoʻonui polokalamu e mālama i nā kī pilikino, hāʻawi i ka ʻike i ka lehulehu (ka helu wahi, kamaʻilio kī lehulehu me ka ʻaoʻao a hiki i nā noi ʻaoʻao ʻekolu ke noi i kahi pūlima no nā kālepa.

Hoʻomohala noi

Pono kā mākou noi e launa pū me ka mea hoʻohana a hāʻawi i ka ʻaoʻao me kahi API e kāhea ai i nā ala (no ka laʻana, e kau inoa i nā kālepa). E hana me hoʻokahi wale nō contentscript 'a'ole e hana, no ka mea, hiki wale i ka DOM, 'a'ole i ka JS o ka 'ao'ao. Hoʻohui ma o runtime.connect ʻAʻole hiki iā mākou, no ka mea, pono ka API ma nā kikowaena āpau, a hiki ke kuhikuhi ʻia nā mea kikoʻī ma ka hōʻike. ʻO ka hopena, e like ke kiʻi penei:

Ke kākau ʻana i kahi hoʻonui polokalamu kele pūnaewele palekana

Aia kekahi palapala ʻē aʻe - inpage, a mākou e hoʻokomo i loko o ka ʻaoʻao. E holo ia i loko o kona pōʻaiapili a hāʻawi i kahi API no ka hana ʻana me ka hoʻonui.

ʻO ka hoʻomaka

Loaʻa nā code extension browser āpau ma GitHub. Ma ka wehewehe ʻana e loaʻa nā loulou i nā commits.

E hoʻomaka kākou me ka manifesto:

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

E hana i ka background.js, popup.js, inpage.js a me contentscript.js. Hoʻohui mākou i ka popup.html - a hiki ke hoʻouka ʻia kā mākou noi i loko o Google Chrome a hōʻoia i ka hana.

No ka hōʻoia ʻana i kēia, hiki iā ʻoe ke lawe i ke code mai kēia wahi. Ma kahi o nā mea a mākou i hana ai, ua hoʻonohonoho ka loulou i ka hui o ka papahana me ka hoʻohana ʻana i ka webpack. No ka hoʻohui ʻana i kahi noi i ka polokalamu kele, ma chrome://extensions pono ʻoe e koho i ka load unpacked a me ka waihona me ka hoʻonui pili - i kā mākou hihia dist.

Ke kākau ʻana i kahi hoʻonui polokalamu kele pūnaewele palekana

I kēia manawa ua hoʻokomo ʻia kā mākou extension a hana. Hiki iā ʻoe ke holo i nā mea hana hoʻomohala no nā pōʻaiapili like ʻole penei:

popup ->

Ke kākau ʻana i kahi hoʻonui polokalamu kele pūnaewele palekana

Hoʻokomo ʻia ka ʻike i ka console script ma o ka console o ka ʻaoʻao ponoʻī kahi i hoʻomaka ai.Ke kākau ʻana i kahi hoʻonui polokalamu kele pūnaewele palekana

Kūʻai leka

No laila, pono mākou e hoʻokumu i ʻelua mau ala kamaʻilio: inpage <-> background a popup <-> background. Hiki iā ʻoe, ʻoiaʻiʻo, e hoʻouna wale i nā leka i ke awa a hana i kāu protocol ponoʻī, akā makemake wau i ke ala aʻu i ʻike ai ma ka papahana open source metamask.

He hoʻonui polokalamu kēia no ka hana pū me ka pūnaewele Ethereum. I loko o ia mea, kamaʻilio nā ʻāpana like ʻole o ka noi ma o RPC me ka hoʻohana ʻana i ka waihona dnode. Hiki iā ʻoe ke hoʻonohonoho i kahi hoʻololi me ka wikiwiki a me ka maʻalahi inā hāʻawi ʻoe iā ia me kahi kahawai nodejs ma ke ʻano he transport (ʻo ia hoʻi kahi mea e hoʻokō i ka interface like):

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

I kēia manawa e hana mākou i kahi papa noi. E hana ia i nā mea API no ka popup a me ka ʻaoʻao pūnaewele, a hana i kahi dnode no lākou:

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

Ma ʻaneʻi a ma lalo, ma kahi o ka mea Chrome honua, hoʻohana mākou i ka extensionApi, ka mea e komo ai iā Chrome ma ka polokalamu kele pūnaewele a Google a me nā polokalamu ʻē aʻe. Hana ʻia kēia no ka hoʻohālikelike ʻana i nā mea hoʻokele, akā no nā kumu o kēia ʻatikala hiki ke hoʻohana wale i 'chrome.runtime.connect'.

E hana kākou i kahi palapala noi ma ka palapala hope:

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

No ka hana ʻana o dnode me nā kahawai, a loaʻa iā mākou kahi awa, pono kahi papa adapter. Hana ʻia me ka hoʻohana ʻana i ka waihona readable-stream, kahi e hoʻokō ai i nā kahawai nodejs i ka polokalamu kele pūnaewele:

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

I kēia manawa e hana i kahi pilina ma ka 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;
    }
}

A laila hana mākou i ka pilina ma ka ʻatikala maʻiʻo:

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

No ka mea ʻaʻole pono mākou i ka API ma ka ʻatikala kikoʻī, akā pololei ma ka ʻaoʻao, hana mākou i ʻelua mau mea:

  1. Hana mākou i ʻelua kahawai. Hoʻokahi - i ka ʻaoʻao, ma luna o ka postMessage. No kēia, hoʻohana mākou i kēia keia puolo mai nā mea hana o metamask. ʻO ke kahawai ʻelua, ʻo ia ke kiʻi hope ma luna o ke awa i loaʻa mai runtime.connect. E kūʻai kākou iā lākou. I kēia manawa e loaʻa i ka ʻaoʻao kahi kahawai i ke kua.
  2. E hoʻokomo i ka palapala i loko o ka DOM. Hoʻoiho i ka palapala (ua ʻae ʻia ke komo ʻana ma ka hōʻike) a hana i kahi hōʻailona script me kona mau mea i loko:

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

I kēia manawa, hana mākou i kahi mea api ma ka ʻaoʻao a hoʻonoho iā ia i ka honua:

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

Mākaukau mākou Kahea Kaʻina Hana mamao (RPC) me API kaʻawale no ka ʻaoʻao a me ka UI. I ka hoʻohui ʻana i kahi ʻaoʻao hou i ka hope hiki iā mākou ke ʻike i kēia:

Ke kākau ʻana i kahi hoʻonui polokalamu kele pūnaewele palekana

Haʻahaʻa API a me ke kumu. Ma ka ʻaoʻao ʻaoʻao, hiki iā mākou ke kāhea i ka hana hello e like me kēia:

Ke kākau ʻana i kahi hoʻonui polokalamu kele pūnaewele palekana

ʻO ka hana ʻana me nā hana callback i ka JS hou he ʻano maikaʻi ʻole, no laila e kākau i kahi mea kōkua liʻiliʻi e hana i kahi dnode e hiki ai iā ʻoe ke hāʻawi i kahi mea API i nā mea hoʻohana.

E like me kēia nā mea API:

export class SignerApp {

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

...

}

Loaʻa i kahi mea mai kahi mamao e like me kēia:

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

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

A hoʻihoʻi nā hana kelepona i kahi ʻōlelo hoʻohiki:

Ke kākau ʻana i kahi hoʻonui polokalamu kele pūnaewele palekana

Loaʻa ka mana me nā hana asynchronous maanei.

Ma ke ʻano holoʻokoʻa, ʻano maʻalahi ka RPC a me ke kahawai kahawai: hiki iā mākou ke hoʻohana i ka multixing mahu a hana i nā API like ʻole no nā hana like ʻole. Ma ke kumu, hiki ke hoʻohana ʻia ka dnode ma nā wahi āpau, ʻo ka mea nui ke kāʻei i ka halihali ma ke ʻano o kahi kahawai nodejs.

ʻO kahi koho ʻē aʻe ka format JSON, kahi e hoʻokō ai i ka protocol JSON RPC 2. Eia naʻe, hana ia me nā transports kikoʻī (TCP a me HTTP(S)), ʻaʻole pili i kā mākou hihia.

Mokuʻāina kūloko a me ka waihona kūloko

Pono mākou e mālama i ke kūlana kūloko o ka noi - ma ka liʻiliʻi loa i nā kī pūlima. Hiki iā mākou ke hoʻohui maʻalahi i kahi mokuʻāina i ka noi a me nā ala no ka hoʻololi ʻana iā ia i ka 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)
        }
    }

    ...

} 

Ma hope, e hoʻopili mākou i nā mea āpau i kahi hana a kākau i ka mea noi i ka puka makani i hiki iā mākou ke hana pū me ia mai ka console:

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

E hoʻohui i kekahi mau kī mai ka console UI a ʻike i ka mea e hana nei me ka mokuʻāina:

Ke kākau ʻana i kahi hoʻonui polokalamu kele pūnaewele palekana

Pono e hoʻomau ka mokuʻāina i ʻole e nalowale nā ​​kī i ka wā e hoʻomaka hou ai.

E mālama mākou iā ia i loko o ka localStorage, e hoʻololi iā ia me kēlā me kēia hoʻololi. A laila, pono ke komo ʻana iā ia no ka UI, a makemake wau e kākau inoa i nā loli. Ma muli o kēia, e maʻalahi ka hana ʻana i kahi mālama ʻike a kau inoa i kāna mau loli.

E hoʻohana mākou i ka waihona mobx (https://github.com/mobxjs/mobx). Ua hāʻule ka koho no ka mea ʻaʻole pono wau e hana me ia, akā makemake nui wau e aʻo.

E hoʻohui i ka hoʻomaka ʻana o ka mokuʻāina mua a e ʻike ʻia ka hale kūʻai:

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

    ...

}

"Ma lalo o ka puʻupuʻu," ua hoʻololi ʻo mobx i nā kahua hale kūʻai āpau me nā mea koho a hoʻopaʻa i nā kelepona āpau iā lākou. Hiki ke kau inoa i kēia mau memo.

Ma lalo e hoʻohana pinepine wau i ka huaʻōlelo "i ka wā e hoʻololi ai", ʻoiai ʻaʻole pololei kēia. Mālama ʻo Mobx i ke komo ʻana i nā kahua. Hoʻohana ʻia nā kiʻi a me nā mea hoʻonohonoho o nā mea koho i hana ʻia e ka waihona.

Hoʻohana nā mea hoʻolālā hana i ʻelua kumu:

  1. Ma ke ʻano koʻikoʻi me ka hae enforceActions, pāpā ʻo mobx i ka hoʻololi pololei ʻana i ka mokuʻāina. Manaʻo ʻia he ʻano maikaʻi ke hana ma lalo o nā kūlana koʻikoʻi.
  2. ʻOiai inā hoʻololi kekahi hana i ka mokuʻāina i nā manawa he nui - no ka laʻana, hoʻololi mākou i kekahi mau kahua ma nā laina code - e hoʻomaopopo ʻia nā mea nānā i ka wā e pau ai. He mea koʻikoʻi kēia no ka frontend, kahi e alakaʻi ai nā hoʻolaha mokuʻāina pono ʻole i ka hana pono ʻole o nā mea. I kā mākou hihia, ʻaʻole pili ka mua a i ʻole ka lua, akā e hahai mākou i nā hana maikaʻi loa. He mea maʻamau ka hoʻopili ʻana i nā mea hoʻonaninani i nā hana āpau e hoʻololi i ke kūlana o nā māla i ʻike ʻia.

Ma hope e hoʻohui mākou i ka hoʻomaka a mālama i ka mokuʻāina ma 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)
        }
    }
}

He mea hoihoi ka hana ho'ōla maʻaneʻi. He ʻelua kumu hoʻopaʻapaʻa:

  1. Mea koho ʻikepili.
  2. ʻO kahi mea lawelawe e kāhea ʻia me kēia ʻikepili i kēlā me kēia manawa e loli.

ʻAʻole like me redux, kahi e loaʻa ai iā mākou ka mokuʻāina ma ke ʻano he hoʻopaʻapaʻa, hoʻomanaʻo ʻo mobx i nā mea nānā a mākou e komo ai i loko o ka mea koho, a kāhea wale i ka mea hoʻokele ke hoʻololi lākou.

He mea nui e hoʻomaopopo pono i ka hoʻoholo ʻana o mobx i nā mea nānā a mākou e kau inoa ai. Inā wau i kākau i kahi mea koho ma ke code e like me kēia() => app.store, a laila ʻaʻole e kāhea ʻia ka hopena, no ka mea ʻaʻole ʻike ʻia ka waihona ponoʻī, ʻo kāna mau māla wale nō.

Inā wau i kākau penei () => app.store.keys, a laila, ʻaʻohe mea e hiki mai, ʻoiai ke hoʻohui a wehe i nā mea hoʻonohonoho, ʻaʻole e loli ka kuhikuhi ʻana iā ia.

Hana ʻo Mobx ma ke ʻano he mea koho no ka manawa mua a mālama wale i nā mea nānā a mākou i komo ai. Hana ʻia kēia ma o nā mea lawe proxy. No laila, hoʻohana ʻia ka hana i kūkulu ʻia ma ʻaneʻi toJS. Hoʻihoʻi ia i kahi mea hou me nā proxies āpau i pani ʻia me nā kahua kumu. I ka wā o ka hoʻokō ʻana, heluhelu ia i nā kahua āpau o ka mea - no laila ke hoʻomaka ʻia nā getters.

I ka popup console e hoʻohui hou mākou i kekahi mau kī. I kēia manawa ua komo lākou i localStorage:

Ke kākau ʻana i kahi hoʻonui polokalamu kele pūnaewele palekana

Ke hoʻouka hou ʻia ka ʻaoʻao hope, mau ka ʻike.

Hiki ke nānā ʻia nā code noi āpau a hiki i kēia wahi maanei.

Mālama i nā kī pilikino

ʻAʻole palekana ka mālama ʻana i nā kī pilikino ma kahi kikokikona: aia mau ka manawa e hacked ʻia ʻoe, loaʻa ke komo i kāu kamepiula, a pēlā aku. No laila, ma ka localStorage e mālama mākou i nā kī ma kahi ʻano huna huna.

No ka palekana nui aʻe, e hoʻohui mākou i kahi kūlana paʻa i ka noi, kahi e loaʻa ʻole ai ke kiʻi i nā kī. E hoʻoneʻe ʻakomi mākou i ka hoʻonui i ka moku i paʻa ma muli o kahi manawa.

ʻAe ʻo Mobx iā ʻoe e mālama i kahi hoʻonohonoho liʻiliʻi o ka ʻikepili, a ʻo ke koena e helu ʻia ma muli o ia. ʻO kēia nā mea i kapa ʻia nā waiwai helu. Hiki iā lākou ke hoʻohālikelike i nā ʻike ma nā waihona:

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

I kēia manawa mālama mākou i nā kī i hoʻopili ʻia a me ka ʻōlelo huna. Ua helu ʻia nā mea ʻē aʻe. Hana mākou i ka hoʻoili ʻana i kahi mokuʻāina paʻa ma ka wehe ʻana i ka ʻōlelo huna mai ka mokuʻāina. Loaʻa i ka API lehulehu kahi ala no ka hoʻomaka ʻana i ka waiho ʻana.

Kākau ʻia no ka hoʻopunipuni pono e hoʻohana ana i ka 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)
}

Loaʻa i ka polokalamu kele pūnaewele he API idle kahi e hiki ai iā ʻoe ke kau inoa i kahi hanana - hoʻololi mokuʻāina. ʻO ka moku'āina, no laila, hiki paha idle, active и locked. No ka palaualelo hiki iā ʻoe ke hoʻonohonoho i kahi manawa, a hoʻopaʻa ʻia i ka wā i pāpā ʻia ai ka OS ponoʻī. E hoʻololi pū mākou i ka mea koho no ka mālama ʻana 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)
        }
    }
}

ʻO ke code ma mua o kēia kaʻina maanei.

Nā ʻoihana

No laila, hele mākou i ka mea nui loa: ka hana ʻana a me ke kau inoa ʻana i nā hana ma ka blockchain. E hoʻohana mākou i ka WAVES blockchain a me ka waihona nā nalu-transactions.

ʻO ka mea mua, e hoʻohui i ka mokuʻāina i kahi ʻano o nā memo e pono e kau inoa ʻia, a laila e hoʻohui i nā ala no ka hoʻohui ʻana i kahi leka hou, e hōʻoia ana i ka pūlima, a hōʻole:

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

    ...
}

Ke loaʻa iā mākou kahi leka hou, hoʻohui mākou i ka metadata iā ia, hana observable a hoʻohui i store.messages.

Inā ʻaʻole ʻoe observable me ka lima, a laila e hana ʻo mobx iā ia iho i ka hoʻohui ʻana i nā memo i ka array. Eia nō naʻe, e hana ia i kahi mea hou i loaʻa ʻole iā mākou kahi kuhikuhi, akā pono mākou no ka hana aʻe.

A laila, hoʻihoʻi mākou i kahi ʻōlelo hoʻohiki e hoʻoholo i ka wā e loli ai ke kūlana memo. Hoʻopili ʻia ke kūlana e ka hopena, e "pepehi iā ia iho" ke hoʻololi ke kūlana.

Code kiʻina approve и reject maʻalahi loa: hoʻololi wale mākou i ke kūlana o ka leka, ma hope o ke kau inoa ʻana inā pono.

Hoʻokomo mākou i ka ʻae a hōʻole i ka UI API, newMessage ma ka ʻaoʻao 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)
        }
    }

    ...
}

I kēia manawa e ho'āʻo mākou e kau inoa i ke kālepa me ka hoʻonui:

Ke kākau ʻana i kahi hoʻonui polokalamu kele pūnaewele palekana

Ma keʻano laulā, ua mākaukau nā mea a pau,ʻo nā mea i koe hoʻohui i ka UI maʻalahi.

UI

Pono ke kikowaena i ke komo ʻana i ke kūlana noi. Ma ka ʻaoʻao UI e hana mākou observable mokuʻāina a hoʻohui i kahi hana i ka API e hoʻololi i kēia mokuʻāina. E hoʻohui kākou observable i ka mea API i loaʻa mai ka hope:

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

I ka hopena, hoʻomaka mākou e hāʻawi i ka interface noi. ʻO kēia kahi noi react. Hoʻoholo wale ʻia ka mea hope me ka hoʻohana ʻana i nā props. He pololei, ʻoiaʻiʻo, e hana i kahi lawelawe ʻokoʻa no nā ʻano a me kahi hale kūʻai no ka mokuʻāina, akā no nā kumu o kēia ʻatikala ua lawa kēia:

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

Me mobx he mea maʻalahi loa ka hoʻomaka ʻana i ka hoʻololi ʻana i ka ʻikepili. Kau wale mākou i ka mea hoʻonani nānā mai ka pūʻolo mobx-react ma ka ʻāpana, a e kāhea ʻia ka render i ka wā e hoʻololi ʻia nā mea nānā i kuhikuhi ʻia e ka ʻāpana. ʻAʻole pono ʻoe i kahi mapStateToProps a pili paha e like me redux. Hana pono nā mea a pau ma waho o ka pahu:

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

Hiki ke ʻike ʻia nā ʻāpana i koe ma ke code ma ka waihona UI.

I kēia manawa i ka papa noi pono ʻoe e hana i kahi koho mokuʻāina no ka UI a hoʻomaopopo i ka UI i ka wā e loli ai. No ka hana ʻana i kēia, e hoʻohui i kahi ala getState и reactionkahea ana 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())

        })
    }

    ...
}

I ka loaʻa ʻana o kahi mea remote ua hakuʻia reaction e hoʻololi i ka moku'āina e kāhea ana i ka hana ma kaʻaoʻao UI.

ʻO ka pā hope loa e hoʻohui i ka hōʻike ʻana o nā memo hou ma ka ikona hoʻonui:

function setupApp() {
...

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

...
}

No laila, ua mākaukau ka noi. Hiki i nā ʻaoʻao pūnaewele ke noi i pūlima no nā kālepa:

Ke kākau ʻana i kahi hoʻonui polokalamu kele pūnaewele palekana

Ke kākau ʻana i kahi hoʻonui polokalamu kele pūnaewele palekana

Aia ke code maanei loulou.

hopena

Inā ua heluhelu ʻoe i ka ʻatikala a hiki i ka hopena, akā he mau nīnau kāu, hiki iā ʻoe ke nīnau iā lākou ma nā waihona me ka hoʻonui. Ma laila ʻoe e ʻike ai i nā commits no kēlā me kēia pae i koho ʻia.

A inā makemake ʻoe e nānā i ke code no ka hoʻonui maoli, hiki iā ʻoe ke loaʻa i kēia maanei.

Code, waihona a me ka wehewehe hana mai siemarell

Source: www.habr.com

Pākuʻi i ka manaʻo hoʻopuka