Kunyora yakachengeteka browser yekuwedzera

Kunyora yakachengeteka browser yekuwedzera

Kusiyana neyakajairwa "mutengi-server" architecture, decentralized applications inoratidzwa ne:

  • Iko hakuna chikonzero chekuchengeta dhatabhesi ine mushandisi logins uye mapassword. Ruzivo rwekuwana runochengetwa chete nevashandisi pachavo, uye kusimbiswa kwehuchokwadi hwavo kunoitika padanho reprotocol.
  • Hapana chikonzero chekushandisa sevha. Iyo yekushandisa logic inogona kuurayiwa pane blockchain network, pazvinogoneka kuchengetedza huwandu hunodiwa hwe data.

Kune 2 zvakati chengetedzo zvekuchengetedza makiyi emushandisi - hardware wallet uye browser ekuwedzera. Hardware wallets anonyanya kuchengetedzeka, asi akaoma kushandisa uye kure nemahara, asi browser yekuwedzera ndiyo yakakwana musanganiswa wekuchengetedza uye nyore kushandisa, uye inogona zvakare kusununguka zvachose kune vashandisi vekupedzisira.

Tichifunga zvese izvi, isu taida kuita iyo yakachengeteka yekuwedzera iyo inorerutsa kusimudzira kwezvikumbiro zvakaiswa nekupa iri nyore API yekushanda nekutengeserana nemasaini.
Tichakuudza nezve chiitiko ichi pazasi.

Chinyorwa chacho chichange chine nhanho-ne-nhanho mirairo yekuti unganyora sei browser yekuwedzera, ine kodhi mienzaniso uye skrini. Unogona kuwana ese kodhi mukati repositories. Kuzvipira kwega kwega kunoenderana nechikamu chechinyorwa ichi.

Nhoroondo Pfupi yebrowser Extensions

Mabrowser ekuwedzera ave aripo kwenguva yakareba. Vakaonekwa muInternet Explorer kumashure muna 1999, muFirefox muna 2004. Nekudaro, kwenguva yakareba kwazvo pakanga pasina mwero mumwe chete wekuwedzera.

Tinogona kutaura kuti yakaonekwa pamwe nekuwedzera mune yechina vhezheni yeGoogle Chrome. Ehe, pakanga pasina kutsanangurwa ipapo, asi yaive Chrome API yakave hwaro hwayo: yakunda yakawanda yemusika webrowser uye kuve neyakavakwa-mukati application chitoro, Chrome yakatoisa mwero wekuwedzera browser.

Mozilla yaive neyayo chiyero, asi ichiona kufarirwa kwekuwedzera kweChrome, kambani yakafunga kugadzira API inowirirana. Muna 2015, pakutanga kweMozilla, boka rakakosha rakagadzirwa mukati meWorld Wide Web Consortium (W3C) kuti rishande pane zvakachinjika-browser yekuwedzera zvirevo.

Iyo iripo API yekuwedzera yeChrome yakatorwa sehwaro. Basa racho rakaitwa nerutsigiro rweMicrosoft (Google yakaramba kutora chikamu mukuvandudzwa kweiyo chiyero), uye nekudaro gwaro rakaonekwa. kutaurwa.

Pakare, iyo yakatarwa inotsigirwa neEdge, Firefox uye Opera (ona kuti Chrome haisi pane iyi runyorwa). Asi kutaura zvazviri, chiyero chinonyanya kuenderana neChrome, sezvo chakanyatso kunyorwa zvichienderana nekuwedzera kwayo. Unogona kuverenga zvakawanda nezveWebExtensions API pano.

Kuwedzera chimiro

Iyo chete faira inodiwa pakuwedzera ndeye manifest (manifest.json). Iri zvakare "nzvimbo yekupinda" yekuwedzera.

Ratidza

Zvinoenderana nezvakatsanangurwa, iyo manifest faira ndeye JSON faira. Tsanangudzo izere yemanifest kiyi ine ruzivo rwekuti ndeapi makiyi anotsigirwa mubrowser inogona kutariswa pano.

Makiyi asiri mune yakatarwa "anogona" kufuratirwa (zvese zviri zviviri Chrome neFirefox zvinoshuma kukanganisa, asi mawedzero anoramba achishanda).

Uye ndinoda kukwevera pfungwa kune dzimwe pfungwa.

  1. kumashure - chinhu chinosanganisira minda inotevera:
    1. scripts - rondedzero yezvinyorwa zvinozoitwa mukati memamiriro ekunze (tichataura pamusoro peizvi zvishoma gare gare);
    2. peji - pachinzvimbo chezvinyorwa zvichaitwa mune peji isina chinhu, unogona kutsanangura html ine zvirimo. Muchiitiko ichi, script munda icharegererwa, uye zvinyorwa zvinoda kuiswa mukati peji yezvinyorwa;
    3. zvinoramba - mureza webhanari, kana usina kutaurwa, bhurawuza "inouraya" maitiro ekumashure kana ichifunga kuti haisi kuita chero chinhu, uye itangezve kana zvichidikanwa. Zvikasadaro, peji inongoburitswa chete kana browser yakavharwa. Haitsigirwi muFirefox.
  2. content_scripts - hurongwa hwezvinhu zvinokutendera kuti utore zvinyorwa zvakasiyana kumapeji ewebhu akasiyana. Chinhu chimwe nechimwe chine minda yakakosha inotevera:
    1. mamiriro - url muenzaniso, iyo inosarudza kana chinyorwa chemukati chichabatanidzwa kana kuti kwete.
    2. js - rondedzero yezvinyorwa zvichaiswa mumutambo uyu;
    3. kusasanganisa_matches - isingabatanidzi kubva kumunda match Ma URL anoenderana nenzvimbo iyi.
  3. peji_chiito -Chaizvoizvo chinhu chine chekuita neiyo icon inoratidzwa padivi pekero bar mubrowser uye kudyidzana nayo. Inokubvumirawo kuti uratidze hwindo rinobuda, iro rinotsanangurwa uchishandisa yako HTML, CSS uye JS.
    1. default_popup - nzira inoenda kuHTML faira ine popup interface, inogona kunge iine CSS uye JS.
  4. Permissions - hurongwa hwekutarisira kodzero dzekuwedzera. Kune marudzi matatu ekodzero, ayo anotsanangurwa zvakadzama pano
  5. web_accessible_resources - zviwanikwa zvekuwedzera izvo peji rewebhu rinogona kukumbira, semuenzaniso, mifananidzo, JS, CSS, HTML mafaera.
  6. externally_connectable - pano unogona kutsanangura zvakajeka maID emamwe mawedzero uye madomasi emapeji ewebhu kwaunogona kubatanidza. A domain inogona kuva yechipiri level kana kupfuura. Haishande muFirefox.

Execution context

Iyo yekuwedzera ine matatu kodhi execution mamiriro, ndiko kuti, iyo application ine zvikamu zvitatu zvine mazinga akasiyana ekuwana kune browser API.

Mamiriro ekuwedzera

Yakawanda yeAPI inowanikwa pano. Muchirevo chechinyorwa ichi "vanorarama":

  1. Peji yekumashure - "backend" chikamu chekuwedzera. Iyo faira inotsanangurwa mumanifest uchishandisa "background" kiyi.
  2. Popup peji - peji inobuda inooneka paunodzvanya pane yekuwedzera icon. Mumanifesto browser_action -> default_popup.
  3. Custom peji - peji rekuwedzera, "kurarama" mune imwe tebhu yekuona chrome-extension://<id_расширения>/customPage.html.

Ichi chinyorwa chiripo chakazvimiririra chebrowser windows uye tabo. Peji yekumashure iripo mune imwe kopi uye inogara ichishanda (kunze kweiyo peji rechiitiko, kana iyo yekumashure script inotangwa nechiitiko uye "inofa" mushure mekuitwa kwayo). Popup peji iripo kana iyo popup hwindo yakavhurwa, uye Custom peji - nepo tebhu nayo yakavhurika. Iko hakuna mukana kune mamwe ma tabo uye zvirimo kubva pane ino mamiriro.

Content script context

Iyo yemukati script faira inotangwa pamwe nechero browser tab. Iyo inokwanisa kuwana chikamu chekuwedzera API uye kune iyo DOM muti wewebhu peji. Izvo zvinyorwa zvemukati zvine basa rekudyidzana nepeji. Mawedzero anonyengedza muti weDOM anoita izvi muzvinyorwa zvemukati - semuenzaniso, ad blockers kana vaturikiri. Zvakare, iyo yemukati script inogona kutaurirana nepeji kuburikidza neyakajairwa postMessage.

Webhu peji mamiriro

Iri ndiro peji rewebhu chairo. Izvo hazvinei nekuwedzera uye hazvigone kuwana ipapo, kunze kwekunge iyo nzvimbo yepeji ino isina kunyatso kuratidzwa mumanifesiti (zvimwe pane izvi pazasi).

Meseji kuchinjana

Zvikamu zvakasiyana zvekushandisa zvinofanirwa kuchinjana mameseji nemumwe. Pane API yeizvi runtime.sendMessage kutumira meseji background и tabs.sendMessage kutumira meseji kune peji (yemukati script, popup kana webhusaiti peji kana iripo externally_connectable) Pazasi pane muenzaniso kana uchiwana iyo 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))
    }
)

Kuti uwane kutaurirana kwakazara, unogona kugadzira zvisungo kuburikidza runtime.connect. Mukupindura tichagamuchira runtime.Port, iyo, iyo yakavhurika, unogona kutumira chero nhamba yemeseji. Padivi revatengi, semuenzaniso, contentscript, zvinoita seizvi:

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

Sevha kana kumashure:

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

Panewo chiitiko onDisconnect uye nzira disconnect.

Dhiyagiramu yekushandisa

Ngatigadzirei browser yekuwedzera inochengeta makiyi akavanzika, inopa ruzivo rweruzhinji (kero, kiyi yeruzhinji inotaurirana nepeji uye inobvumira vechitatu-bato maapplication kukumbira siginecha yekutengeserana.

Kuvandudza application

Yedu application inofanirwa kudyidzana nemushandisi uye nekupa iyo peji ine API yekufonera nzira (semuenzaniso, kusaina kutengeserana). Ita chimwe chete contentscript haishande, sezvo ichingokwanisa kuwana iyo DOM, asi kwete kuJS yepeji. Batanidza kuburikidza runtime.connect isu hatigone, nekuti iyo API inodiwa pane ese madomasi, uye iwo chaiwo chaiwo anogona kutsanangurwa mune manifest. Nekuda kweizvozvo, iyo dhizaini ichaita seizvi:

Kunyora yakachengeteka browser yekuwedzera

Pachave neimwe script - inpage, yatichapinza mupeji. Ichamhanya mumamiriro ayo uye inopa API yekushanda pamwe nekuwedzera.

Musha

Yese browser yekuwedzera kodhi inowanikwa pa GitHub. Munguva yetsanangudzo pachave nemalink ekuita.

Ngatitangei nemanifesto:

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

Gadzira isina chinhu background.js, popup.js, inpage.js uye contentscript.js. Isu tinowedzera popup.html - uye application yedu inogona kutoiswa muGoogle Chrome uye tive nechokwadi chekuti inoshanda.

Kuti uone izvi, unogona kutora kodhi kubva pano. Pamusoro pezvatakaita, chinongedzo chakagadzirisa kuungana kweprojekiti uchishandisa webpack. Kuti uwedzere application kubrowser, mu chrome: // mawedzero iwe unofanirwa kusarudza kurodha isina kurongedzerwa uye folda ine inoenderana nekuwedzera - mune yedu dist.

Kunyora yakachengeteka browser yekuwedzera

Iye zvino kuwedzera kwedu kwakaiswa uye kushanda. Iwe unogona kumhanyisa maturusi ekuvandudza mamiriro akasiyana seanotevera:

popup ->

Kunyora yakachengeteka browser yekuwedzera

Kuwana kune zvemukati script koni kunoitwa kuburikidza nekoni yepeji pachayo painotangwa.Kunyora yakachengeteka browser yekuwedzera

Meseji kuchinjana

Saka, isu tinofanirwa kumisa nzira mbiri dzekutaurirana: inpage <-> kumashure uye popup <-> kumashure. Iwe unogona, hongu, kungotumira mameseji kuchiteshi uye kugadzira yako protocol, asi ini ndinosarudza nzira yandakaona mune metamask yakavhurika sosi purojekiti.

Iyi ibrowser yekuwedzera yekushanda neEthereum network. Mariri, zvikamu zvakasiyana zvekushandisa zvinotaurirana kuburikidza neRPC uchishandisa dnode raibhurari. Zvinokutendera kuti uronge shanduko nekukurumidza uye zviri nyore kana iwe ukapa iyo nodejs rwizi sekutakura (zvinoreva chinhu chinoshandisa iyo yakafanana interface):

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

Iye zvino tichagadzira kirasi yekushandisa. Ichagadzira zvinhu zveAPI zvepopup uye peji rewebhu, uye kugadzira dnode yavo:

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

Pano nepasi, pachinzvimbo chechinhu chepasi rose cheChrome, tinoshandisa extensionApi, inowana Chrome mubrowser yeGoogle uye browser mune vamwe. Izvi zvinoitirwa kufambirana nebrowser, asi nezvinangwa zvechinyorwa chino, unogona kungoshandisa 'chrome.runtime.connect'.

Ngatigadzirei muenzaniso wekushandisa mune yekumashure script:

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

Sezvo dnode inoshanda nehova, uye isu tinogamuchira chiteshi, kirasi yeadapter inodiwa. Iyo inogadzirwa uchishandisa inoverengeka-rukova raibhurari, iyo inoshandisa nodejs hova mubrowser:

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

Zvino ngatigadzire chinongedzo muUI:

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

Zvadaro tinogadzira kubatana mune script yemukati:

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

Sezvo isu tichida iyo API kwete mune yemukati script, asi zvakananga pane peji, tinoita zvinhu zviviri:

  1. Isu tinogadzira hova mbiri. Imwe - yakananga peji, pamusoro peiyo postMessage. Nokuda kweizvi tinoshandisa izvi pasuru iyi kubva kune vanogadzira metamask. Rukova rwechipiri nderwekumashure pamusoro pechiteshi chakagamuchirwa kubva runtime.connect. Ngatitengei. Iye zvino peji yacho ichava nerukova rwekumashure.
  2. Baira script muDOM. Dhawunirodha script (kusvika kwairi kwakabvumidzwa mumanifesiti) uye gadzira tag script nezviri mukati maro:

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

Iye zvino tinogadzira chinhu che api mu inpage uye tochiisa kune yepasirese:

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

Takagadzirira Remote Procedure Call (RPC) ine yakaparadzana API yepeji uye UI. Kana uchibatanidza peji idzva kune kumashure tinogona kuona izvi:

Kunyora yakachengeteka browser yekuwedzera

Empty API uye kwakabva. Padivi repeji, tinogona kudaidza iyo hello basa seizvi:

Kunyora yakachengeteka browser yekuwedzera

Kushanda necallback mabasa muJS yemazuva ano itsika dzakaipa, saka ngatinyore mubatsiri mudiki kugadzira dnode inobvumidza iwe kupfuudza chinhu cheAPI kune utils.

Izvo API zvinhu zvino zvinotaridzika seizvi:

export class SignerApp {

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

...

}

Kutora chinhu kubva kure seizvi:

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

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

Uye kufona mabasa kunodzosera vimbiso:

Kunyora yakachengeteka browser yekuwedzera

Shanduro ine asynchronous mabasa aripo pano.

Pakazere, iyo RPC uye nzira yekuyerera inoita seinochinjika: isu tinogona kushandisa tsinga yekuwedzeredza uye kugadzira akati wandei APIs emabasa akasiyana. Muchidimbu, dnode inogona kushandiswa chero kupi zvako, chinhu chikuru ndechokuputira kutakura nenzira ye nodejs stream.

Imwe nzira ndeye JSON fomati, iyo inoshandisa iyo JSON RPC protocol 2. Zvisinei, inoshanda nemafambisirwo chaiwo (TCP neHTTP(S)), izvo zvisingashande kwatiri.

Yemukati nyika uye YenzvimboKuchengeta

Isu tichada kuchengetedza iyo yemukati mamiriro ekushandisa - angangoita makiyi ekusaina. Isu tinokwanisa kuwedzera zviri nyore nyika kune application uye nzira dzekuchishandura mupopup 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)
        }
    }

    ...

} 

Kumashure, isu tinoputira zvese muchiitiko uye tonyora chinhu chekunyorera pahwindo kuti isu tishande nacho kubva kuconsole:

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

Ngatiwedzere makiyi mashoma kubva kuUI koni uye tione zvinoitika nenyika:

Kunyora yakachengeteka browser yekuwedzera

Iyo nyika inoda kuitwa inoshingirira kuitira kuti makiyi asarasikirwe kana achitangazve.

Tichazvichengeta muNzvimbo yekuchengetedza, tichiinyora neshanduko yese. Mushure meizvozvo, kupinda kwairi kuchave kuri madikanwa kune iyo UI, uye ini ndodawo kunyorera kune shanduko. Zvichienderana neizvi, zvichave nyore kugadzira chengetedzo inooneka uye kunyorera kune shanduko dzayo.

Isu tichashandisa iyo mobx raibhurari (https://github.com/mobxjs/mobx) Sarudzo yakawira pairi nekuti ini ndaisafanira kushanda nayo, asi ndaida chaizvo kuidzidza.

Ngatiwedzerei kutanga kwekutanga mamiriro uye kuita kuti chitoro chionekwe:

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

    ...

}

"Pasi pehodhi," mobx yakatsiva minda yese yechitoro neproxy uye inotambira mafoni ese kwavari. Zvichakwanisika kunyoresa kune aya mameseji.

Pazasi ini ndichawanzo shandisa izwi rekuti "kana uchichinja", kunyangwe izvi zvisiri izvo zvachose. Mobx inoteedzera kupinda muminda. Mageta nemaseta ezvinhu zvinogadzirwa neraibhurari zvinoshandiswa.

Vagadziri vezviito vanoita zvinangwa zviviri:

  1. Mune yakasimba mode ine enforceActions mureza, mobx inorambidza kuchinja nyika zvakananga. Inonzi tsika yakanaka kushanda pasi pemamiriro ezvinhu akaoma.
  2. Kunyangwe kana basa richichinja nyika kakawanda - semuenzaniso, isu tinoshandura akati wandei mumitsara yakati wandei yekodhi - vanocherekedza vanoziviswa chete kana yapedza. Izvi zvinonyanya kukosha kune yekumberi, uko kusingakoshi kugadziridzwa kwenyika kunotungamira kune kusingaite kupa zvinhu. Muchiitiko chedu, kwete yekutanga kana yechipiri inonyanya kukosha, asi isu tichatevera maitiro akanaka. Iro tsika yekubatanidza vagadziri kune ese mabasa anoshandura mamiriro enzvimbo dzakacherechedzwa.

Kumashure isu ticha wedzera kutanga uye kuchengetedza iyo nyika munzvimbo yekuchengetedza:

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

The reaction function inonakidza pano. Iine nharo mbiri:

  1. Data selector.
  2. Mubati anozodaidzwa neiyi data pese painochinja.

Kusiyana nereux, kwatinogashira nyika pachena senharo, mobx inorangarira kuti ndezvipi zvinoonekwa zvatinowana mukati memusarudzo, uye zvinongofonera mubati kana achinja.

Izvo zvakakosha kuti unzwisise chaizvo kuti mobx inosarudza zvipi zvinoonekwa zvatinonyorera kwazviri. Kana ini ndakanyora sarudzo mune kodhi seizvi() => app.store, ipapo reaction haizombodanwa, sezvo chengetedzo pachayo isingaonekwe, minda yayo chete ndiyo.

Kana ndakanyora kudai () => app.store.keys, saka zvakare hapana chaizoitika, sezvo pakuwedzera/kubvisa array elements, chirevo pazviri hachizochinji.

Mobx inoita senge yekusarudzika kekutanga uye inongochengeta zvinoonekwa zvatakawana. Izvi zvinoitwa kuburikidza nevanotora proxy. Naizvozvo, iyo yakavakirwa-mukati basa rinoshandiswa pano toJS. Inodzosa chinhu chitsva nema proxies ese akatsiviwa neminda yekutanga. Panguva yekuurayiwa, inoverenga minda yese yechinhu - saka maitirwo anotangwa.

Mune popup console isu tichawedzera zvakare akati wandei makiyi. Panguva ino ivo vakapindawo munzvimbo yekuchengetedza:

Kunyora yakachengeteka browser yekuwedzera

Kana iyo yekumashure peji ichiiswazve, ruzivo runoramba rwuripo.

Yese kodhi yekushandisa kusvika panguva ino inogona kutariswa pano.

Chengetedza kuchengetedza makiyi akavanzika

Kuchengeta makiyi epachivande mumavara akajeka hakuna kuchengetedzeka: pane nguva dzose mukana wekuti iwe uchabiwa, kuwana mukana kune komputa yako, zvichingodaro. Naizvozvo, munzvimbo yekuchengetedza isu tichachengeta makiyi mune password-encrypted fomu.

Kuti chengetedzo yakakura, isu tichawedzera yakavharwa kune application, umo musingazove nekuwana makiyi zvachose. Isu tichaendesa otomatiki iyo yekuwedzera kune yakakiyiwa mamiriro nekuda kwekufamba kwenguva.

Mobx inokutendera iwe kuti uchengetedze chete shoma seti yedata, uye imwe yese inoverengerwa otomatiki zvichibva pairi. Aya ndiwo anonzi ma computed properties. Ivo vanogona kufananidzwa nekuona mune dhatabhesi:

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

Iye zvino isu tinongochengeta makiyi akavharidzirwa uye password. Zvimwe zvese zvinoverengerwa. Isu tinoita kuendesa kune yakavharwa nyika nekubvisa password kubva mudunhu. Iyo yeruzhinji API ikozvino ine nzira yekutanga kuchengetedza.

Yakanyorerwa encryption zvishandiso zvinoshandisa 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)
}

Iyo bhurawuza ine isina basa API iyo iwe yaunogona kunyorera kune chiitiko - shanduko yenyika. State, maererano, inogona kuva idle, active и locked. Nekusina basa unogona kuseta nguva yekubuda, uye kukiyiwa kunoiswa kana iyo OS pachayo yakavharwa. Isu tichashandurawo iyo inosarudzika yekuchengetedza kune yemunoStorage:

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

Kodhi pamberi danho iri pano.

Transactions

Saka, isu tinouya kune chinonyanya kukosha: kugadzira uye kusaina kutengeserana pa blockchain. Isu tichashandisa iyo WAVES blockchain uye raibhurari masaisai-kutengesa.

Chekutanga, ngatiwedzerei kudunhu mameseji akawanda anoda kusainwa, tobva tawedzera nzira dzekuwedzera meseji nyowani, kusimbisa siginecha, uye kuramba:

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

    ...
}

Kana tagamuchira meseji nyowani, tinowedzera metadata kwairi, ita observable uye kuwedzera store.messages.

Kana usingadaro observable nemaoko, ipapo mobx inozviita pachayo kana ichiwedzera mameseji kune array. Nekudaro, ichagadzira chinhu chitsva chatisingazove nereferensi, asi isu tichachida padanho rinotevera.

Tevere, tinodzosera vimbiso inogadzirisa kana meseji mamiriro achinja. Chimiro chinotariswa nekuita, icho "chichazviuraya" kana chimiro chachinja.

Nzira kodhi approve и reject zviri nyore: isu tinongoshandura chimiro chemeseji, mushure mekusaina kana zvichidikanwa.

Isu tinoisa Bvumira uye kuramba muUI API, newMessage mune peji 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)
        }
    }

    ...
}

Zvino ngatiedze kusaina kutengeserana nekuwedzera:

Kunyora yakachengeteka browser yekuwedzera

Kazhinji, zvinhu zvose zvakagadzirira, zvose zvinosara ndezve wedzera UI iri nyore.

UI

Iyo interface inoda kuwana kune iyo application state. Kudivi reUI tichaita observable state uye wedzera basa kune API inozoshandura iyi mamiriro. Ngatiwedzere observable kune API chinhu chakagamuchirwa kubva kumashure:

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

Pakupedzisira tinotanga kupa iyo interface yekushandisa. Ichi chishandiso chinobatika. Chinhu chekumashure chinongopfuudzwa pachishandiswa props. Zvingave zvakanaka, hongu, kuita sevhisi yakaparadzana kune nzira uye chitoro chenyika, asi nezvinangwa zvechinyorwa ichi zvakakwana:

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

Ne mobx zviri nyore kwazvo kutanga kupa kana data rachinja. Isu tinongorembera mucherechedzi wekushongedza kubva pasuru mobx-react pachikamu, uye render ichadaidzwa otomatiki kana chero chinocherechedzwa chinoratidzwa nechikamu chachinja. Iwe haudi chero mepuStateToProps kana kubatanidza senge mune redux. Zvese zvinoshanda kunze kwebhokisi:

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

Izvo zvikamu zvakasara zvinogona kuonekwa mukodhi muUI folda.

Iye zvino mukirasi yekushandisa iwe unofanirwa kuita sarudzo yenyika yeUI uye zivisa iyo UI kana yachinja. Kuti uite izvi, ngatiwedzere nzira getState и reactionkufona 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())

        })
    }

    ...
}

Pakugamuchira chinhu remote created reaction kushandura iyo inodaidza basa padivi reUI.

Kubata kwekupedzisira ndiko kuwedzera kuratidzwa kwemameseji matsva pane icon yekuwedzera:

function setupApp() {
...

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

...
}

Saka, application yakagadzirira. Mapeji ewebhu anogona kukumbira siginicha yekutengeserana:

Kunyora yakachengeteka browser yekuwedzera

Kunyora yakachengeteka browser yekuwedzera

Iyo kodhi inowanikwa pano batanidzo.

mhedziso

Kana iwe wakaverenga chinyorwa kusvika kumagumo, asi uchine mibvunzo, unogona kuvabvunza pa repositories pamwe nekuwedzera. Ikoko iwe unowana zvakare kuzvipira kune yega yega nhanho yakatarwa.

Uye kana iwe uchifarira kutarisa iyo kodhi yekuwedzera chaiyo, unogona kuwana izvi pano.

Code, repository uye tsananguro yebasa kubva siemarell

Source: www.habr.com

Voeg