Բրաուզերի անվտանգ ընդլայնում գրելը

Բրաուզերի անվտանգ ընդլայնում գրելը

Ի տարբերություն ընդհանուր «հաճախորդ-սերվեր» ճարտարապետության, ապակենտրոնացված հավելվածները բնութագրվում են.

  • Օգտատիրոջ մուտքաբառերով և գաղտնաբառերով տվյալների բազա պահելու կարիք չկա: Մուտքի տեղեկատվությունը պահվում է բացառապես հենց իրենք՝ օգտատերերի կողմից, և դրանց իսկության հաստատումը տեղի է ունենում արձանագրության մակարդակով:
  • Սերվեր օգտագործելու կարիք չկա: Հավելվածի տրամաբանությունը կարող է իրականացվել բլոկչեյն ցանցում, որտեղ հնարավոր է պահպանել անհրաժեշտ քանակությամբ տվյալներ։

Օգտագործողի բանալիների համար կա 2 համեմատաբար անվտանգ պահեստ՝ ապարատային դրամապանակներ և բրաուզերի ընդլայնումներ: Սարքավորումների դրամապանակները հիմնականում չափազանց անվտանգ են, բայց դժվար է օգտագործել և հեռու են անվճար լինելուց, սակայն բրաուզերի ընդլայնումները անվտանգության և օգտագործման հեշտության կատարյալ համադրություն են, ինչպես նաև կարող են լիովին անվճար լինել վերջնական օգտագործողների համար:

Այս ամենը հաշվի առնելով՝ մենք ցանկացանք ստեղծել ամենաապահով ընդլայնումը, որը հեշտացնում է ապակենտրոնացված հավելվածների զարգացումը` ապահովելով գործարքների և ստորագրությունների հետ աշխատելու պարզ API:
Այս փորձառության մասին կպատմենք ստորև։

Հոդվածը պարունակում է քայլ առ քայլ հրահանգներ, թե ինչպես գրել բրաուզերի ընդլայնում, կոդերի օրինակներով և սքրինշոթներով: Դուք կարող եք գտնել ամբողջ կոդը պահոցներ. Յուրաքանչյուր պարտավորություն տրամաբանորեն համապատասխանում է այս հոդվածի մի հատվածին:

Բրաուզերի ընդլայնումների համառոտ պատմություն

Բրաուզերի ընդլայնումները վաղուց են եղել: Internet Explorer-ում դրանք հայտնվել են դեռ 1999 թվականին, Firefox-ում՝ 2004 թվականին։ Այնուամենայնիվ, երկար ժամանակ ընդլայնումների համար մեկ ստանդարտ չկար:

Կարելի է ասել, որ այն ընդլայնումների հետ միասին հայտնվել է Google Chrome-ի չորրորդ տարբերակում։ Իհարկե, այն ժամանակ հստակեցում չկար, բայց դա Chrome API-ն էր, որը դարձավ դրա հիմքը. նվաճելով բրաուզերի շուկայի մեծ մասը և ունենալով ներկառուցված հավելվածների խանութ, Chrome-ն իրականում սահմանեց բրաուզերի ընդլայնումների չափանիշը:

Mozilla-ն ուներ իր ստանդարտը, բայց տեսնելով Chrome-ի ընդլայնումների հանրաճանաչությունը, ընկերությունը որոշեց ստեղծել համատեղելի API: 2015 թվականին Mozilla-ի նախաձեռնությամբ Համաշխարհային ցանցի կոնսորցիումի (W3C) շրջանակներում ստեղծվեց հատուկ խումբ՝ աշխատելու բրաուզերի ընդլայնման առանձնահատկությունների վրա:

Որպես հիմք ընդունվել են Chrome-ի համար գոյություն ունեցող API ընդլայնումները։ Աշխատանքն իրականացվել է Microsoft-ի աջակցությամբ (Google-ը հրաժարվել է մասնակցել ստանդարտի մշակմանը), և արդյունքում հայտնվել է նախագիծ. բնութագրերը.

Ֆորմալ կերպով, ճշգրտումն ապահովվում է Edge-ի, Firefox-ի և Opera-ի կողմից (նկատի ունեցեք, որ Chrome-ն այս ցանկում չէ): Բայց իրականում ստանդարտը հիմնականում համատեղելի է Chrome-ի հետ, քանի որ այն իրականում գրված է իր ընդլայնումների հիման վրա: Դուք կարող եք ավելին կարդալ WebExtensions API-ի մասին այստեղ.

Ընդլայնման կառուցվածքը

Միակ ֆայլը, որը պահանջվում է ընդլայնման համար, մանիֆեստն է (manifest.json): Դա նաև ընդլայնման «մուտքի կետն» է:

Մանիֆեստ

Ըստ ճշգրտման, մանիֆեստի ֆայլը վավեր JSON ֆայլ է: Մանիֆեստի ստեղների ամբողջական նկարագրությունը՝ տեղեկություններով, թե որ ստեղները կարող են դիտվել դիտարկիչում այստեղ.

Բանալիները, որոնք «կարող են» բնութագրում չգտնվեն (ինչպես Chrome-ը, այնպես էլ Firefox-ը հաղորդում են սխալներ, սակայն ընդլայնումները շարունակում են աշխատել):

Եվ ես կցանկանայի ուշադրություն հրավիրել որոշ կետերի վրա.

  1. ֆոն - օբյեկտ, որը ներառում է հետևյալ դաշտերը.
    1. Հաղորդագրություն — սկրիպտների զանգված, որը կկատարվի ֆոնային համատեքստում (այս մասին կխոսենք մի փոքր ուշ);
    2. էջ - սկրիպտների փոխարեն, որոնք կկատարվեն դատարկ էջում, կարող եք նշել html բովանդակությամբ: Այս դեպքում սցենարի դաշտը անտեսվելու է, և սկրիպտները պետք է տեղադրվեն բովանդակության էջում;
    3. դիմանալ — երկուական դրոշակ, եթե նշված չէ, զննարկիչը «կսպանի» ֆոնային գործընթացը, երբ համարի, որ ոչինչ չի անում, և անհրաժեշտության դեպքում կվերագործարկի այն: Հակառակ դեպքում էջը կբեռնաթափվի միայն զննարկիչը փակելու դեպքում: Firefox-ում չի աջակցվում:
  2. բովանդակության_սկրիպտներ — օբյեկտների զանգված, որը թույլ է տալիս բեռնել տարբեր սկրիպտներ տարբեր վեբ էջերում: Յուրաքանչյուր օբյեկտ պարունակում է հետևյալ կարևոր դաշտերը.
    1. հանդիպումները - օրինակի url, որը որոշում է, թե արդյոք որոշակի բովանդակության սցենար կներառվի, թե ոչ:
    2. js — սկրիպտների ցանկ, որոնք կբեռնվեն այս համընկնման մեջ.
    3. exclude_matches - բացառում է դաշտից match URL-ներ, որոնք համապատասխանում են այս դաշտին:
  3. page_action - իրականում օբյեկտ է, որը պատասխանատու է դիտարկիչում հասցեագոտի կողքին ցուցադրվող պատկերակի և դրա հետ փոխազդեցության համար: Այն նաև թույլ է տալիս ցուցադրել թռուցիկ պատուհան, որը սահմանվում է՝ օգտագործելով ձեր սեփական HTML, CSS և JS:
    1. default_popup — ուղին դեպի HTML ֆայլ՝ թռուցիկ ինտերֆեյսով, կարող է պարունակել CSS և JS:
  4. թույլտվությունները — ընդլայնման իրավունքները կառավարելու զանգված: Կան իրավունքների 3 տեսակ, որոնք մանրամասն նկարագրված են այստեղ
  5. web_accessible_resources — ընդլայնման ռեսուրսներ, որոնք կարող է պահանջել վեբ էջը, օրինակ՝ պատկերներ, JS, CSS, HTML ֆայլեր:
  6. արտաքին_միացվող — այստեղ դուք կարող եք հստակորեն նշել վեբ էջերի այլ ընդլայնումների և տիրույթների ID-ները, որոնցից կարող եք միանալ: Դոմենը կարող է լինել երկրորդ կամ ավելի բարձր մակարդակ: Չի աշխատում Firefox-ում:

Կատարման համատեքստ

Ընդլայնումը ունի երեք կոդի կատարման համատեքստ, այսինքն՝ հավելվածը բաղկացած է երեք մասից՝ բրաուզերի API-ի հասանելիության տարբեր մակարդակներով։

Ընդլայնման համատեքստ

API-ի մեծ մասը հասանելի է այստեղ: Այս համատեքստում նրանք «ապրում են».

  1. Ֆոնային էջ — ընդլայնման «backend» մաս: Ֆայլը նշված է մանիֆեստում՝ օգտագործելով «ֆոն» ստեղնը:
  2. Բացվող էջ — թռուցիկ էջ, որը հայտնվում է ընդլայնման պատկերակի վրա սեղմելիս: Մանիֆեստում browser_action -> default_popup.
  3. Պատվերով էջ — ընդլայնման էջ, «ապրել» դիտման առանձին ներդիրում chrome-extension://<id_расширения>/customPage.html.

Այս համատեքստը գոյություն ունի անկախ դիտարկիչի պատուհաններից և ներդիրներից: Ֆոնային էջ գոյություն ունի մեկ օրինակով և միշտ աշխատում է (բացառություն է հանդիսանում իրադարձության էջը, երբ ֆոնային սցենարը գործարկվում է իրադարձության միջոցով և «մեռնում» դրա կատարումից հետո): Բացվող էջ գոյություն ունի, երբ բացվող պատուհանը բաց է, և Պատվերով էջ — մինչ դրա հետ ներդիրը բաց է: Այս համատեքստից այլ ներդիրներին և դրանց բովանդակությանը հասանելիություն չկա:

Բովանդակության սցենարի համատեքստ

Բովանդակության սցենարի ֆայլը գործարկվում է դիտարկիչի յուրաքանչյուր ներդիրի հետ միասին: Այն հասանելի է ընդլայնման API-ի մի մասին և վեբ էջի DOM ծառին: Դա բովանդակության սցենարներ են, որոնք պատասխանատու են էջի հետ փոխգործակցության համար: Ընդլայնումները, որոնք շահարկում են DOM ծառը, դա անում են բովանդակության սկրիպտներում, օրինակ՝ գովազդային արգելափակումներ կամ թարգմանիչներ: Բացի այդ, բովանդակության սցենարը կարող է էջի հետ հաղորդակցվել ստանդարտի միջոցով postMessage.

Վեբ էջի համատեքստ

Սա բուն վեբ էջն է: Այն կապ չունի ընդլայնման հետ և մուտք չունի այնտեղ, բացառությամբ այն դեպքերի, երբ այս էջի տիրույթը հստակ նշված չէ մանիֆեստում (այս մասին ավելին ստորև):

Հաղորդագրություններ

Հավելվածի տարբեր մասերը պետք է հաղորդագրություններ փոխանակեն միմյանց հետ: Դրա համար կա API runtime.sendMessage հաղորդագրություն ուղարկելու համար background и tabs.sendMessage էջին հաղորդագրություն ուղարկելու համար (բովանդակության սցենար, թռուցիկ կամ վեբ էջ, եթե առկա է): externally_connectable) Ստորև բերված է 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))
    }
)

Ամբողջական հաղորդակցության համար կարող եք կապեր ստեղծել միջոցով runtime.connect. Ի պատասխան մենք կստանանք runtime.Port, որին, քանի դեռ այն բաց է, կարող եք ուղարկել ցանկացած քանակությամբ հաղորդագրություն: Հաճախորդի կողմից, օրինակ, contentscript, այն ունի հետևյալ տեսքը.

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

Սերվեր կամ ֆոն.

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

Կա նաև միջոցառում onDisconnect և մեթոդ disconnect.

Կիրառման դիագրամ

Եկեք ստեղծենք զննարկչի ընդլայնում, որը պահում է մասնավոր բանալիներ, ապահովում է հանրային տեղեկատվության հասանելիություն (հասցե, հանրային բանալին հաղորդակցվում է էջի հետ և թույլ է տալիս երրորդ կողմի հավելվածներին ստորագրություն խնդրել գործարքների համար:

Հավելվածի մշակում

Մեր հավելվածը պետք է և՛ փոխազդի օգտագործողի հետ, և՛ էջին տրամադրի API՝ մեթոդներ կանչելու համար (օրինակ՝ գործարքներ ստորագրելու համար): Կատարեք միայն մեկով contentscript չի աշխատի, քանի որ այն մուտք ունի միայն DOM-ին, բայց ոչ էջի JS-ին: Միացեք միջոցով runtime.connect մենք չենք կարող, քանի որ API-ն անհրաժեշտ է բոլոր տիրույթներում, և մանիֆեստում կարող են նշվել միայն կոնկրետները: Արդյունքում դիագրամը կունենա հետևյալ տեսքը.

Բրաուզերի անվտանգ ընդլայնում գրելը

Կլինի մեկ այլ սցենար. inpage, որը մենք կներարկենք էջը։ Այն կաշխատի իր համատեքստում և կտրամադրի API ընդլայնման հետ աշխատելու համար:

Начало

Բրաուզերի ընդլայնման բոլոր ծածկագրերը հասանելի են այստեղ GitHub. Նկարագրության ընթացքում կլինեն հղումներ դեպի պարտավորություններ:

Սկսենք մանիֆեստից.

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

Ստեղծեք դատարկ background.js, popup.js, inpage.js և contentscript.js: Մենք ավելացնում ենք popup.html - և մեր հավելվածն արդեն կարող է բեռնվել Google Chrome-ում և համոզվել, որ այն աշխատում է:

Սա հաստատելու համար կարող եք վերցնել կոդը ուստի. Ի լրումն այն, ինչ մենք արեցինք, հղումը կազմաձևեց նախագծի հավաքումը վեբ փաթեթի միջոցով: Բրաուզերում հավելված ավելացնելու համար chrome://extensions-ում պետք է ընտրել load unpacked և համապատասխան ընդլայնումով թղթապանակը, մեր դեպքում՝ dist:

Բրաուզերի անվտանգ ընդլայնում գրելը

Այժմ մեր ընդլայնումը տեղադրված է և աշխատում է: Դուք կարող եք գործարկել մշակողի գործիքները տարբեր համատեքստերի համար հետևյալ կերպ.

թռուցիկ ->

Բրաուզերի անվտանգ ընդլայնում գրելը

Բովանդակության սցենարի կոնսոլին հասանելիությունն իրականացվում է հենց այն էջի վահանակի միջոցով, որի վրա այն գործարկվել է:Բրաուզերի անվտանգ ընդլայնում գրելը

Հաղորդագրություններ

Այսպիսով, մենք պետք է երկու հաղորդակցման ուղիներ հաստատենք՝ ներէջ <-> ֆոն և թռուցիկ <-> ֆոն: Դուք, իհարկե, կարող եք պարզապես հաղորդագրություններ ուղարկել նավահանգիստ և հորինել ձեր սեփական արձանագրությունը, բայց ես նախընտրում եմ այն ​​մոտեցումը, որը տեսա metamask բաց կոդով նախագծում:

Սա բրաուզերի ընդլայնում է Ethereum ցանցի հետ աշխատելու համար: Դրանում հավելվածի տարբեր մասեր շփվում են RPC-ի միջոցով՝ օգտագործելով dnode գրադարանը։ Այն թույլ է տալիս կազմակերպել փոխանակում բավականին արագ և հարմար, եթե դրան տրամադրում եք nodejs հոսք՝ որպես տրանսպորտ (նշանակում է նույն ինտերֆեյսն իրականացնող օբյեկտ).

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

Այժմ մենք կստեղծենք հավելվածի դաս: Այն կստեղծի API օբյեկտներ թռուցիկ և վեբ էջի համար և կստեղծի dnode նրանց համար.

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

Այստեղ և ներքևում, գլոբալ Chrome օբյեկտի փոխարեն, մենք օգտագործում ենք extensionApi, որը մուտք է գործում Chrome-ը Google-ի բրաուզերում, իսկ բրաուզերը՝ մյուսներում: Սա արվում է բրաուզերների համատեղելիության համար, սակայն այս հոդվածի նպատակների համար կարելի է պարզապես օգտագործել «chrome.runtime.connect»:

Եկեք ստեղծենք հավելվածի օրինակ ֆոնային սցենարում.

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

Քանի որ dnode-ն աշխատում է հոսքերի հետ, և մենք ստանում ենք պորտ, անհրաժեշտ է ադապտերների դաս: Այն պատրաստված է ընթերցվող հոսքային գրադարանի միջոցով, որը բրաուզերում իրականացնում է nodejs հոսքեր.

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

Այժմ եկեք ստեղծենք կապ 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;
    }
}

Այնուհետև մենք ստեղծում ենք կապը բովանդակության սցենարում.

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

Քանի որ մեզ անհրաժեշտ է API-ն ոչ թե բովանդակության սցենարում, այլ ուղղակիորեն էջում, մենք անում ենք երկու բան.

  1. Մենք ստեղծում ենք երկու հոսք: Մեկ - դեպի էջը, գրառման հաղորդագրության վերևում: Դրա համար մենք օգտագործում ենք սա այս փաթեթը metamask-ի ստեղծողներից: Երկրորդ հոսքը պետք է անցնի նավահանգստից ստացված ֆոնին runtime.connect. Եկեք գնենք դրանք։ Այժմ էջը կունենա հոսք դեպի ֆոն:
  2. Ներարկեք սցենարը DOM-ում: Ներբեռնեք սցենարը (դրա մուտքը թույլատրված էր մանիֆեստում) և ստեղծեք պիտակ script ներսում իր պարունակությամբ.

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

Այժմ մենք ստեղծում ենք api օբյեկտ inpage-ում և դնում այն ​​գլոբալ.

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

Մենք պատրաստ ենք Հեռակա ընթացակարգի զանգ (RPC) առանձին API-ով էջի և միջերեսի համար. Նոր էջը ֆոնին միացնելիս կարող ենք տեսնել հետևյալը.

Բրաուզերի անվտանգ ընդլայնում գրելը

Դատարկ API և ծագում: Էջի կողմում մենք կարող ենք անվանել բարևի ֆունկցիան այսպես.

Բրաուզերի անվտանգ ընդլայնում գրելը

Ժամանակակից JS-ում հետադարձ կապի գործառույթների հետ աշխատելը վատ է, ուստի եկեք գրենք մի փոքրիկ օգնական՝ ստեղծելու dnode, որը թույլ է տալիս Ձեզ փոխանցել API օբյեկտը utils-ին:

API-ի օբյեկտներն այժմ այսպիսի տեսք կունենան.

export class SignerApp {

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

...

}

Հեռակառավարման վահանակից օբյեկտ ստանալն այսպես.

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

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

Իսկ ֆունկցիաները կանչելը խոստում է տալիս.

Բրաուզերի անվտանգ ընդլայնում գրելը

Հասանելի է ասինխրոն գործառույթներով տարբերակ այստեղ.

Ընդհանուր առմամբ, RPC-ի և հոսքի մոտեցումը բավականին ճկուն է թվում. մենք կարող ենք օգտագործել գոլորշու մուլտիպլեքսավորում և ստեղծել մի քանի տարբեր API տարբեր առաջադրանքների համար: Սկզբունքորեն, dnode-ը կարող է օգտագործվել ցանկացած վայրում, հիմնականը տրանսպորտը փաթաթելն է nodejs հոսքի տեսքով:

Այլընտրանքը JSON ձևաչափն է, որն իրականացնում է JSON RPC 2 արձանագրությունը: Այնուամենայնիվ, այն աշխատում է հատուկ փոխադրումների հետ (TCP և HTTP(S)), որը կիրառելի չէ մեր դեպքում:

Ներքին պետական ​​և տեղական պահեստավորում

Մեզ անհրաժեշտ կլինի պահպանել հավելվածի ներքին վիճակը՝ առնվազն ստորագրման բանալիները: Մենք կարող ենք բավականին հեշտությամբ հավելվածին վիճակ ավելացնել և այն փոխելու մեթոդները թռուցիկ 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)
        }
    }

    ...

} 

Հետին պլանում մենք ամեն ինչ կփաթաթենք ֆունկցիայի մեջ և կգրենք հավելվածի օբյեկտը պատուհանում, որպեսզի կարողանանք աշխատել դրա հետ վահանակից.

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

Եկեք մի քանի բանալի ավելացնենք UI վահանակից և տեսնենք, թե ինչ է տեղի ունենում պետության հետ.

Բրաուզերի անվտանգ ընդլայնում գրելը

Պետությունը պետք է համառ դարձնել, որպեսզի վերագործարկման ժամանակ բանալիները չկորչեն։

Մենք այն կպահենք localStorage-ում՝ վերագրելով այն յուրաքանչյուր փոփոխության դեպքում: Հետագայում դրա մուտքը նույնպես անհրաժեշտ կլինի UI-ի համար, և ես նույնպես կցանկանայի բաժանորդագրվել փոփոխություններին: Դրա հիման վրա հարմար կլինի ստեղծել դիտարկելի պահեստ և բաժանորդագրվել դրա փոփոխություններին:

Մենք կօգտագործենք mobx գրադարանը (https://github.com/mobxjs/mobx) Ընտրությունն ընկավ դրա վրա, քանի որ ես ստիպված չէի աշխատել դրա հետ, բայց ես իսկապես ուզում էի ուսումնասիրել այն:

Ավելացնենք սկզբնական վիճակի սկզբնավորումը և խանութը դիտելի դարձնենք.

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

    ...

}

«Կապակի տակ», mobx-ը խանութի բոլոր դաշտերը փոխարինել է պրոքսիով և գաղտնալսում է նրանց բոլոր զանգերը: Այս հաղորդագրություններին հնարավոր կլինի բաժանորդագրվել։

Ստորև ես հաճախ կօգտագործեմ «փոխելիս» տերմինը, թեև դա ամբողջովին ճիշտ չէ։ Mobx-ը հետևում է դաշտերի հասանելիությանը: Օգտագործվում են գրադարանի ստեղծած պրոքսի օբյեկտների ստացողներ և կարգավորիչներ:

Գործողությունների դեկորատորները ծառայում են երկու նպատակի.

  1. EnforceActions դրոշով խիստ ռեժիմում mobx-ն արգելում է ուղղակիորեն փոխել վիճակը: Լավ պրակտիկա է համարվում խիստ պայմաններում աշխատելը։
  2. Նույնիսկ եթե ֆունկցիան մի քանի անգամ փոխում է վիճակը, օրինակ, մի քանի դաշտ ենք փոխում կոդի մի քանի տողերում, դիտորդները ծանուցվում են միայն այն ավարտվելուց հետո: Սա հատկապես կարևոր է ճակատային մասի համար, որտեղ վիճակի անհարկի թարմացումները հանգեցնում են տարրերի անհարկի արտապատկերմանը: Մեր դեպքում ոչ առաջինը, ոչ երկրորդն առանձնապես տեղին չեն, բայց մենք կհետևենք լավագույն փորձին։ Ընդունված է դեկորատորներ կցել բոլոր գործառույթներին, որոնք փոխում են դիտարկվող դաշտերի վիճակը։

Հետին պլանում մենք կավելացնենք սկզբնավորումը և կպահենք վիճակը 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)
        }
    }
}

Այստեղ հետաքրքիր է ռեակցիայի ֆունկցիան։ Այն ունի երկու փաստարկ.

  1. Տվյալների ընտրիչ:
  2. Կառավարիչ, որը կկանչվի այս տվյալների հետ ամեն անգամ, երբ դրանք փոխվեն:

Ի տարբերություն redux-ի, որտեղ մենք բացահայտորեն ստանում ենք վիճակը որպես փաստարկ, mobx-ը հիշում է, թե որ դիտելիներին ենք մենք մուտք գործում ընտրիչի ներսում և զանգում է միայն մշակողին, երբ դրանք փոխվում են:

Կարևոր է հստակ հասկանալ, թե ինչպես է mobx-ը որոշում, թե որ դիտելիներին ենք մենք բաժանորդագրվում: Եթե ​​ես սելեկտոր գրեի այսպես կոդով() => app.store, ապա ռեակցիան երբեք չի կոչվի, քանի որ պահեստն ինքնին դիտարկելի չէ, այլ միայն դրա դաշտերը։

Եթե ​​ես այսպես գրեի () => app.store.keys, ապա նորից ոչինչ չէր պատահի, քանի որ զանգվածի տարրեր ավելացնել/հեռացնելիս հղումը դրան չի փոխվի։

Mobx-ը գործում է որպես ընտրիչ առաջին անգամ և հետևում է միայն այն դիտելիներին, որոնց մենք մուտք ենք գործել: Սա արվում է պրոքսի ստացողների միջոցով: Հետեւաբար, այստեղ օգտագործվում է ներկառուցված գործառույթը toJS. Այն վերադարձնում է նոր օբյեկտ՝ բոլոր պրոքսիներով փոխարինված բնօրինակ դաշտերով: Կատարման ընթացքում այն ​​կարդում է օբյեկտի բոլոր դաշտերը, հետևաբար ստացողները գործարկվում են:

Բացվող վահանակում մենք կրկին կավելացնենք մի քանի բանալի: Այս անգամ նրանք նույնպես հայտնվեցին localStorage-ում.

Բրաուզերի անվտանգ ընդլայնում գրելը

Երբ ֆոնային էջը վերաբեռնվում է, տեղեկատվությունը մնում է տեղում:

Մինչև այս կետի բոլոր հայտի կոդը կարելի է դիտել այստեղ.

Անձնական բանալիների անվտանգ պահպանում

Անձնական բանալիները մաքուր տեքստով պահելն անվտանգ չէ. միշտ կա հնարավորություն, որ ձեզ կոտրեն, մուտք գործեն ձեր համակարգիչը և այլն: Հետևաբար, localStorage-ում մենք բանալիները կպահենք գաղտնաբառով ծածկագրված ձևով:

Ավելի մեծ անվտանգության համար հավելվածին կավելացնենք կողպված վիճակ, որում բանալիների մուտքն ընդհանրապես չի լինի։ Մենք ավտոմատ կերպով ընդլայնումը կտեղափոխենք կողպված վիճակի ժամանակի դադարի պատճառով:

Mobx-ը թույլ է տալիս պահպանել միայն տվյալների նվազագույն փաթեթը, իսկ մնացածը ավտոմատ կերպով հաշվարկվում է դրա հիման վրա: Սրանք, այսպես կոչված, հաշվարկված հատկություններ են: Դրանք կարելի է համեմատել տվյալների բազաների դիտումների հետ.

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

Այժմ մենք պահպանում ենք միայն կոդավորված բանալիներն ու գաղտնաբառը: Մնացած ամեն ինչ հաշվարկված է։ Մենք փոխանցումը կողպված վիճակի ենք կատարում՝ գաղտնաբառը վիճակից հեռացնելով։ Հանրային API-ն այժմ ունի պահեստը սկզբնավորելու մեթոդ:

Գրված է կոդավորման համար կոմունալ ծառայություններ՝ օգտագործելով 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)
}

Զննարկիչն ունի անգործուն API, որի միջոցով կարող եք բաժանորդագրվել իրադարձություններին՝ վիճակի փոփոխություններ: Պետությունը, համապատասխանաբար, կարող է լինել idle, active и locked. Անգործության համար դուք կարող եք սահմանել ժամանակի վերջ, իսկ կողպվածը սահմանվում է, երբ ՕՀ-ն ինքնին արգելափակված է: Մենք նաև կփոխենք ընտրիչը՝ պահպանման համար localStorage.

import {reaction, toJS} from 'mobx';
import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {SignerApp} from "./SignerApp";
import {loadState, saveState} from "./utils/localStorage";

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

setupApp();

function setupApp() {
    const initState = loadState();
    const app = new SignerApp(initState);

    if (DEV_MODE) {
        global.app = app;
    }

    // Теперь мы явно узываем поле, которому будет происходить доступ, 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)
        }
    }
}

Այս քայլից առաջ կոդը այստեղ.

Գործարքի

Այսպիսով, մենք հասնում ենք ամենակարևորին. բլոկչեյնով գործարքների ստեղծում և ստորագրում: Մենք կօգտագործենք WAVES բլոկչեյնը և գրադարանը ալիքներ-գործարքներ.

Նախ, եկեք վիճակին ավելացնենք հաղորդագրությունների զանգված, որոնք պետք է ստորագրվեն, ապա ավելացնենք նոր հաղորդագրություն ավելացնելու, ստորագրությունը հաստատելու և մերժելու մեթոդներ.

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

    ...
}

Երբ մենք ստանում ենք նոր հաղորդագրություն, մենք դրան ավելացնում ենք մետատվյալներ, արեք observable և ավելացնել store.messages.

Եթե ​​դուք չեք observable ձեռքով, ապա mobx-ը դա կանի ինքն իրեն զանգվածին հաղորդագրություններ ավելացնելիս: Այնուամենայնիվ, այն կստեղծի նոր օբյեկտ, որին մենք հղում չենք ունենա, բայց այն մեզ անհրաժեշտ կլինի հաջորդ քայլի համար։

Հաջորդը, մենք վերադարձնում ենք խոստում, որը կլուծվի, երբ հաղորդագրության կարգավիճակը փոխվի: Կարգավիճակը վերահսկվում է ռեակցիայի միջոցով, որը «ինքն իրեն կսպանի», երբ կարգավիճակը փոխվի։

Մեթոդի կոդը approve и reject շատ պարզ․ մենք պարզապես փոխում ենք հաղորդագրության կարգավիճակը՝ անհրաժեշտության դեպքում այն ​​ստորագրելուց հետո։

Մենք դնում ենք «Հաստատել և մերժել» UI API-ում, «newMessage»-ը՝ «API» էջի մեջ.

export class SignerApp {
    ...
    popupApi() {
        return {
            addKey: async (key) => this.addKey(key),
            removeKey: async (index) => this.removeKey(index),

            lock: async () => this.lock(),
            unlock: async (password) => this.unlock(password),
            initVault: async (password) => this.initVault(password),

            approve: async (id, keyIndex) => this.approve(id, keyIndex),
            reject: async (id) => this.reject(id)
        }
    }

    pageApi(origin) {
        return {
            signTransaction: async (txParams) => this.newMessage(txParams, origin)
        }
    }

    ...
}

Այժմ փորձենք գործարքը ստորագրել ընդլայնմամբ.

Բրաուզերի անվտանգ ընդլայնում գրելը

Ընդհանուր առմամբ, ամեն ինչ պատրաստ է, մնում է միայն ավելացնել պարզ UI.

UI

Ինտերֆեյսին անհրաժեշտ է մուտք գործել հավելվածի վիճակ: UI-ի կողմից մենք կանենք observable վիճակը և ավելացրեք գործառույթ API-ին, որը կփոխի այս վիճակը: Ավելացնենք observable ֆոնից ստացված API օբյեկտին՝

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

Վերջում մենք սկսում ենք կիրառական ինտերֆեյսի մատուցումը: Սա react հավելված է։ Ֆոնային օբյեկտը պարզապես փոխանցվում է հենարանների միջոցով: Ճիշտ կլինի, իհարկե, առանձին ծառայություն անել մեթոդների համար և խանութ պետության համար, բայց այս հոդվածի նպատակների համար սա բավարար է.

import {render} from 'react-dom'
import App from './App'
import React from "react";

// Инициализируем приложение с background объектом в качест ве props
export async function initApp(background){
    render(
        <App background={background}/>,
        document.getElementById('app-content')
    );
}

Mobx-ի հետ շատ հեշտ է սկսել մատուցումը, երբ տվյալները փոխվում են: Մենք պարզապես փաթեթից կախում ենք դիտորդ դեկորատորին mobx-react բաղադրիչի վրա, և render-ը ավտոմատ կերպով կկանչվի, երբ բաղադրիչի կողմից հիշատակված որևէ դիտելիություն փոխվի: Ձեզ հարկավոր չէ որևէ mapStateToProps կամ միանալ, ինչպես redux-ում: Ամեն ինչ աշխատում է անմիջապես տուփից դուրս.

import React, {Component, Fragment} from 'react'
import {observer} from "mobx-react";
import Init from './components/Initialize'
import Keys from './components/Keys'
import Sign from './components/Sign'
import Unlock from './components/Unlock'

@observer // У Компонета с этим декоратом будет автоматически вызван метод render, если будут изменены observable на которые он ссылается
export default class App extends Component {

    // Правильно конечно вынести логику рендера страниц в роутинг и не использовать вложенные тернарные операторы,
    // и привязывать observable и методы background непосредственно к тем компонентам, которые их используют
    render() {
        const {keys, messages, initialized, locked} = this.props.background.state;
        const {lock, unlock, addKey, removeKey, initVault, deleteVault, approve, reject} = this.props.background;

        return <Fragment>
            {!initialized
                ?
                <Init onInit={initVault}/>
                :
                locked
                    ?
                    <Unlock onUnlock={unlock}/>
                    :
                    messages.length > 0
                        ?
                        <Sign keys={keys} message={messages[messages.length - 1]} onApprove={approve} onReject={reject}/>
                        :
                        <Keys keys={keys} onAdd={addKey} onRemove={removeKey}/>
            }
            <div>
                {!locked && <button onClick={() => lock()}>Lock App</button>}
                {initialized && <button onClick={() => deleteVault()}>Delete all keys and init</button>}
            </div>
        </Fragment>
    }
}

Մնացած բաղադրիչները կարելի է դիտել կոդը UI թղթապանակում.

Այժմ հավելվածների դասում դուք պետք է կազմեք վիճակի ընտրիչ UI-ի համար և տեղեկացնեք UI-ին, երբ այն փոխվի: Դա անելու համար եկեք ավելացնենք մեթոդ getState и reactionզանգահարելով remote.updateState:

import {action, observable, reaction} from 'mobx';
import uuid from 'uuid/v4';
import {signTx} from '@waves/waves-transactions'
import {setupDnode} from "./utils/setupDnode";
import {decrypt, encrypt} from "./utils/cryptoUtils";

export class SignerApp {

    ...

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

        })
    }

    ...
}

Օբյեկտ ստանալիս remote ստեղծվել է reaction փոխել այն վիճակը, որը կանչում է ֆունկցիան UI-ի կողմից:

Վերջին հպումը ընդլայնման պատկերակի վրա նոր հաղորդագրությունների ցուցադրումն ավելացնելն է.

function setupApp() {
...

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

...
}

Այսպիսով, հավելվածը պատրաստ է։ Վեբ էջերը կարող են ստորագրություն պահանջել գործարքների համար.

Բրաուզերի անվտանգ ընդլայնում գրելը

Բրաուզերի անվտանգ ընդլայնում գրելը

Կոդը հասանելի է այստեղ ՈՒղեցույց.

Ամփոփում

Եթե ​​դուք կարդացել եք հոդվածը մինչև վերջ, բայց դեռ ունեք հարցեր, կարող եք դրանք ուղղել հետևյալ հասցեով ընդլայնումով պահեստներ. Այնտեղ կգտնեք նաև պարտավորություններ յուրաքանչյուր նշանակված քայլի համար:

Եվ եթե ձեզ հետաքրքրում է իրական ընդլայնման ծածկագիրը, կարող եք գտնել սա այստեղ.

Կոդը, պահեստը և աշխատանքի նկարագրությունը՝ սկսած siemarell

Source: www.habr.com

Добавить комментарий