Xavfsiz brauzer kengaytmasini yozish

Xavfsiz brauzer kengaytmasini yozish

Umumiy "mijoz-server" arxitekturasidan farqli o'laroq, markazlashtirilmagan ilovalar quyidagilar bilan tavsiflanadi:

  • Foydalanuvchi loginlari va parollari bilan ma'lumotlar bazasini saqlashga hojat yo'q. Kirish ma'lumotlari faqat foydalanuvchilarning o'zlari tomonidan saqlanadi va ularning haqiqiyligini tasdiqlash protokol darajasida amalga oshiriladi.
  • Serverdan foydalanish shart emas. Ilova mantig'i blokcheyn tarmog'ida bajarilishi mumkin, bu erda kerakli miqdordagi ma'lumotlarni saqlash mumkin.

Foydalanuvchi kalitlari uchun ikkita nisbatan xavfsiz saqlash joyi mavjud - apparat hamyonlari va brauzer kengaytmalari. Uskuna hamyonlari asosan juda xavfsiz, ammo ulardan foydalanish qiyin va bepul emas, lekin brauzer kengaytmalari xavfsizlik va foydalanish qulayligining mukammal kombinatsiyasi bo'lib, oxirgi foydalanuvchilar uchun mutlaqo bepul bo'lishi mumkin.

Bularning barchasini hisobga olgan holda, biz tranzaktsiyalar va imzolar bilan ishlash uchun oddiy API taqdim etish orqali markazlashtirilmagan ilovalarni ishlab chiqishni soddalashtiradigan eng xavfsiz kengaytmani yaratmoqchi edik.
Quyida ushbu tajriba haqida aytib beramiz.

Maqolada brauzer kengaytmasini qanday yozish bo'yicha bosqichma-bosqich ko'rsatmalar, kod misollari va skrinshotlar mavjud. Siz barcha kodlarni topishingiz mumkin omborlar. Har bir topshiriq mantiqan ushbu maqolaning bir qismiga mos keladi.

Brauzer kengaytmalarining qisqacha tarixi

Brauzer kengaytmalari uzoq vaqtdan beri mavjud. Ular 1999 yilda Internet Explorer-da, 2004 yilda Firefox-da paydo bo'lgan. Biroq, juda uzoq vaqt davomida kengaytmalar uchun yagona standart yo'q edi.

Aytishimiz mumkinki, u Google Chrome-ning to'rtinchi versiyasida kengaytmalar bilan birga paydo bo'ldi. Albatta, o'sha paytda hech qanday spetsifikatsiya yo'q edi, lekin uning asosi bo'lgan Chrome API edi: brauzer bozorining ko'p qismini zabt etgan va o'rnatilgan ilovalar do'koniga ega bo'lgan Chrome aslida brauzer kengaytmalari uchun standartni o'rnatdi.

Mozilla o'z standartiga ega edi, ammo Chrome kengaytmalarining mashhurligini ko'rib, kompaniya mos API yaratishga qaror qildi. 2015-yilda Mozilla tashabbusi bilan World Wide Web Consortium (W3C) tarkibida brauzerlararo kengaytmalar spetsifikatsiyalari ustida ishlash uchun maxsus guruh yaratildi.

Chrome uchun mavjud API kengaytmalari asos sifatida olindi. Ish Microsoft ko'magida amalga oshirildi (Google standartni ishlab chiqishda ishtirok etishdan bosh tortdi) va natijada qoralama paydo bo'ldi. texnik xususiyatlar.

Rasmiy ravishda, spetsifikatsiya Edge, Firefox va Opera tomonidan qo'llab-quvvatlanadi (esda tutingki, Chrome ushbu ro'yxatda yo'q). Lekin, aslida, standart asosan Chrome bilan mos keladi, chunki u aslida kengaytmalari asosida yozilgan. WebExtensions API haqida ko'proq o'qishingiz mumkin shu yerda.

Kengaytma tuzilishi

Kengaytma uchun talab qilinadigan yagona fayl manifest (manifest.json). Bu, shuningdek, kengaytirish uchun "kirish nuqtasi".

Manifesti

Spetsifikatsiyaga ko'ra, manifest fayli haqiqiy JSON faylidir. Qaysi brauzerda qaysi tugmalar qo'llab-quvvatlanishi haqida ma'lumot bilan manifest kalitlarning to'liq tavsifi shu yerda.

Spetsifikatsiyada bo'lmagan kalitlarga e'tibor berilmasligi mumkin (Chrome va Firefox ham xatolar haqida xabar beradi, ammo kengaytmalar ishlashda davom etadi).

Va men ba'zi fikrlarga e'tibor qaratmoqchiman.

  1. fon - quyidagi maydonlarni o'z ichiga olgan ob'ekt:
    1. skriptlar — fon kontekstida bajariladigan skriptlar majmuasi (bu haqda biroz keyinroq gaplashamiz);
    2. bet - bo'sh sahifada bajariladigan skriptlar o'rniga kontent bilan html ni belgilashingiz mumkin. Bunday holda, skript maydoni e'tiborga olinmaydi va skriptlarni kontent sahifasiga kiritish kerak bo'ladi;
    3. saqlamoq — ikkilik bayroq, agar ko'rsatilmagan bo'lsa, brauzer hech narsa qilmayotgan deb hisoblasa, fon jarayonini "o'ldiradi" va kerak bo'lganda uni qayta ishga tushiradi. Aks holda, sahifa faqat brauzer yopilganda o'chiriladi. Firefox-da qo'llab-quvvatlanmaydi.
  2. kontent_skriptlar — turli veb-sahifalarga turli skriptlarni yuklash imkonini beruvchi obyektlar massivi. Har bir ob'ekt quyidagi muhim maydonlarni o'z ichiga oladi:
    1. o'yinlar - naqsh url, bu ma'lum bir kontent skripti qo'shiladimi yoki yo'qligini aniqlaydi.
    2. js — ushbu oʻyinga yuklanadigan skriptlar roʻyxati;
    3. exclude_matches - maydondan chiqarib tashlaydi match Ushbu maydonga mos keladigan URL manzillari.
  3. sahifa_harakati - aslida brauzerda manzil satrining yonida ko'rsatilgan belgi va u bilan o'zaro ta'sir qilish uchun javobgar bo'lgan ob'ekt. Shuningdek, u o'z HTML, CSS va JS-dan foydalangan holda aniqlangan qalqib chiquvchi oynani ko'rsatishga imkon beradi.
    1. default_qalqib chiquvchi oyna — qalqib chiquvchi interfeysga ega HTML faylga yo'l, CSS va JS ni o'z ichiga olishi mumkin.
  4. ruxsatlar — kengaytma huquqlarini boshqarish uchun massiv. Huquqlarning 3 turi mavjud bo'lib, ular batafsil tavsiflanadi shu yerda
  5. web_accessible_sources — veb-sahifa so'rashi mumkin bo'lgan kengaytma resurslari, masalan, rasmlar, JS, CSS, HTML fayllari.
  6. tashqi_ulanish mumkin — bu yerda siz boshqa kengaytmalarning identifikatorlarini va ulanishingiz mumkin bo'lgan veb-sahifalar domenlarini aniq belgilashingiz mumkin. Domen ikkinchi darajali yoki undan yuqori bo'lishi mumkin. Firefox-da ishlamaydi.

Amalga oshirish konteksti

Kengaytma uchta kodni bajarish kontekstiga ega, ya'ni dastur brauzer API-ga kirishning turli darajalariga ega uchta qismdan iborat.

Kengaytma konteksti

API-ning aksariyati bu erda mavjud. Shu nuqtai nazardan ular "yashaydilar":

  1. Fon sahifasi — kengaytmaning “backend” qismi. Fayl manifestda "fon" tugmachasi yordamida ko'rsatilgan.
  2. Qalqib chiquvchi sahifa — kengaytma belgisini bosganingizda paydo bo'ladigan qalqib chiquvchi sahifa. Manifestda browser_action -> default_popup.
  3. maxsus sahifa — kengaytma sahifasi, ko'rinishning alohida yorlig'ida "yashash" chrome-extension://<id_расширения>/customPage.html.

Ushbu kontekst brauzer oynalari va yorliqlaridan mustaqil ravishda mavjud. Fon sahifasi bitta nusxada mavjud va har doim ishlaydi (istisno - bu hodisa sahifasi, fon skripti voqea tomonidan ishga tushirilganda va u bajarilgandan keyin "o'ladi"). Qalqib chiquvchi sahifa qalqib chiquvchi oyna ochiq bo'lganda mavjud va maxsus sahifa — u bilan yorliq ochiq bo'lganda. Ushbu kontekstdan boshqa varaqlar va ularning mazmuniga kirish imkoni yo'q.

Kontent skripti konteksti

Kontent skript fayli har bir brauzer yorlig'i bilan birga ishga tushiriladi. U kengaytmaning API qismiga va veb-sahifaning DOM daraxtiga kirish huquqiga ega. Bu sahifa bilan o'zaro ta'sir qilish uchun javobgar bo'lgan kontent skriptlari. DOM daraxtini boshqaradigan kengaytmalar buni kontent skriptlarida bajaradi - masalan, reklama blokerlari yoki tarjimonlar. Bundan tashqari, kontent skripti standart orqali sahifa bilan bog'lanishi mumkin postMessage.

Veb-sahifa konteksti

Bu haqiqiy veb-sahifaning o'zi. Bu kengaytma bilan hech qanday aloqasi yo'q va u erga kirish huquqiga ega emas, manifestda ushbu sahifaning domeni aniq ko'rsatilmagan hollar bundan mustasno (quyida bu haqda batafsilroq).

Xabarlar

Ilovaning turli qismlari bir-biri bilan xabar almashishi kerak. Buning uchun API mavjud runtime.sendMessage xabar yuborish uchun background и tabs.sendMessage sahifaga xabar yuborish uchun (agar mavjud bo'lsa, kontent skripti, qalqib chiquvchi oyna yoki veb-sahifa). externally_connectable). Quyida Chrome API-ga kirish misoli keltirilgan.

// Сообщением может быть любой 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))
    }
)

To'liq muloqot qilish uchun siz orqali ulanishlarni yaratishingiz mumkin runtime.connect. Bunga javoban biz qabul qilamiz runtime.Port, unga ochiq bo'lganda, istalgan sonli xabarlarni yuborishingiz mumkin. Mijoz tomonida, masalan, contentscript, bu shunday ko'rinadi:

// Опять же extensionId можно не указывать при коммуникации внутри одного расширения. Подключение можно именовать
const port = chrome.runtime.connect({name: "knockknock"});
port.postMessage({joke: "Knock knock"});
port.onMessage.addListener(function(msg) {
    if (msg.question === "Who's there?")
        port.postMessage({answer: "Madame"});
    else if (msg.question === "Madame who?")
        port.postMessage({answer: "Madame... Bovary"});

Server yoki fon:

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

Tadbir ham bor onDisconnect va usul disconnect.

Ilova diagrammasi

Keling, shaxsiy kalitlarni saqlaydigan, umumiy ma'lumotlarga kirishni ta'minlaydigan brauzer kengaytmasini yarataylik (manzil, ochiq kalit sahifa bilan bog'lanadi va uchinchi tomon ilovalariga tranzaktsiyalar uchun imzo so'rash imkonini beradi.

Ilovalarni ishlab chiqish

Bizning ilovamiz foydalanuvchi bilan o'zaro aloqada bo'lishi va sahifani qo'ng'iroq qilish usullari (masalan, tranzaktsiyalarni imzolash) uchun API bilan ta'minlashi kerak. Faqat bittasi bilan shug'ullaning contentscript ishlamaydi, chunki u faqat DOM-ga kirish huquqiga ega, lekin sahifaning JS-ga emas. orqali ulanish runtime.connect qila olmaymiz, chunki API barcha domenlarda kerak va manifestda faqat aniqlarini ko'rsatish mumkin. Natijada, diagramma quyidagicha ko'rinadi:

Xavfsiz brauzer kengaytmasini yozish

Boshqa skript bo'ladi - inpage, biz uni sahifaga kiritamiz. U o'z kontekstida ishlaydi va kengaytma bilan ishlash uchun API taqdim etadi.

start

Barcha brauzer kengaytmalari kodi quyidagi manzilda mavjud GitHub. Tavsif davomida majburiyatlarga havolalar bo'ladi.

Manifestdan boshlaylik:

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

Bo'sh background.js, popup.js, inpage.js va contentscript.js yarating. Biz popup.html ni qo'shamiz - va bizning ilovamiz allaqachon Google Chrome-ga yuklangan bo'lishi mumkin va uning ishlashiga ishonch hosil qiling.

Buni tekshirish uchun siz kodni olishingiz mumkin shu yerda. Biz qilgan ishimizga qo'shimcha ravishda, havola veb-paket yordamida loyihani yig'ishni sozladi. Brauzerga ilova qo'shish uchun chrome://extensions-da siz yukni ochilmagan va tegishli kengaytmali papkani tanlashingiz kerak - bizning holatlarimizda dist.

Xavfsiz brauzer kengaytmasini yozish

Endi kengaytmamiz o'rnatildi va ishlamoqda. Turli kontekstlar uchun ishlab chiquvchi vositalarini quyidagicha ishga tushirishingiz mumkin:

qalqib chiquvchi oyna ->

Xavfsiz brauzer kengaytmasini yozish

Kontent skript konsoliga kirish u ishga tushirilgan sahifaning konsoli orqali amalga oshiriladi.Xavfsiz brauzer kengaytmasini yozish

Xabarlar

Shunday qilib, biz ikkita aloqa kanalini o'rnatishimiz kerak: sahifada <-> fon va qalqib chiquvchi <-> fon. Albatta, siz shunchaki portga xabarlar yuborishingiz va o'z protokolingizni ixtiro qilishingiz mumkin, ammo men ochiq kodli metamask loyihasida ko'rgan yondashuvni afzal ko'raman.

Bu Ethereum tarmog'i bilan ishlash uchun brauzer kengaytmasi. Unda dasturning turli qismlari dnode kutubxonasi yordamida RPC orqali muloqot qiladi. Agar siz uni transport sifatida nodejs oqimi bilan ta'minlasangiz (bir xil interfeysni amalga oshiradigan ob'ektni nazarda tutsangiz) almashinuvni juda tez va qulay tashkil qilish imkonini beradi:

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

Endi biz ilovalar sinfini yaratamiz. U qalqib chiquvchi oyna va veb-sahifa uchun API obyektlarini yaratadi va ular uchun dnode yaratadi:

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

Bu erda va pastda global Chrome ob'ekti o'rniga biz Google brauzerida Chrome brauzeriga va boshqalarda brauzerga kiruvchi extensionApi dan foydalanamiz. Bu brauzerlar o'rtasida muvofiqligi uchun qilingan, ammo ushbu maqola maqsadlari uchun oddiygina "chrome.runtime.connect" dan foydalanish mumkin.

Keling, fon skriptida dastur namunasini yarataylik:

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 oqimlar bilan ishlayotgani va biz portni olganimiz uchun adapter sinfi kerak. U brauzerda nodejs oqimlarini amalga oshiradigan o'qiladigan oqim kutubxonasi yordamida amalga oshiriladi:

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

Endi UIda ulanishni yaratamiz:

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

Keyin kontent skriptida ulanishni yaratamiz:

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

Bizga API kontent skriptida emas, balki to'g'ridan-to'g'ri sahifada kerak bo'lgani uchun biz ikkita narsani qilamiz:

  1. Biz ikkita oqim hosil qilamiz. Biri - sahifa tomon, postMessage tepasida. Buning uchun biz buni ishlatamiz bu paket metamask yaratuvchilardan. Ikkinchi oqim - olingan portning orqa fonida runtime.connect. Keling, ularni sotib olaylik. Endi sahifada fonga oqim bo'ladi.
  2. Skriptni DOMga kiriting. Skriptni yuklab oling (manifestda unga kirishga ruxsat berilgan) va teg yarating script ichidagi tarkibi bilan:

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

Endi biz inpageda api ob'ektini yaratamiz va uni global ga o'rnatamiz:

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

Biz tayyormiz Sahifa va UI uchun alohida API bilan masofaviy protsedura chaqiruvi (RPC).. Yangi sahifani fonga ulashda biz buni ko'rishimiz mumkin:

Xavfsiz brauzer kengaytmasini yozish

Bo'sh API va kelib chiqishi. Sahifa tomonida biz salom funksiyasini quyidagicha chaqirishimiz mumkin:

Xavfsiz brauzer kengaytmasini yozish

Zamonaviy JS-da qayta qo'ng'iroq qilish funktsiyalari bilan ishlash yomon odobdir, shuning uchun API ob'ektini utils-ga o'tkazish imkonini beruvchi dnode yaratish uchun kichik yordamchi yozaylik.

API ob'ektlari endi quyidagicha ko'rinadi:

export class SignerApp {

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

...

}

Ob'ektni masofadan turib olish quyidagicha:

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

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

Va funksiyalarni chaqirish va'dani qaytaradi:

Xavfsiz brauzer kengaytmasini yozish

Asinxron funksiyalarga ega versiya mavjud shu yerda.

Umuman olganda, RPC va oqim yondashuvi juda moslashuvchan ko'rinadi: biz bug 'multipleksiyasidan foydalanishimiz va turli vazifalar uchun bir nechta turli API yaratishimiz mumkin. Asosan, dnode har qanday joyda ishlatilishi mumkin, asosiysi transportni nodejs oqimi shaklida o'rashdir.

Muqobil variant JSON RPC 2 protokolini amalga oshiradigan JSON formatidir.Biroq u maxsus transportlar (TCP va HTTP(S)) bilan ishlaydi, bu bizning holatlarimizda qo‘llanilmaydi.

Ichki davlat va mahalliy saqlash

Biz ilovaning ichki holatini - hech bo'lmaganda imzolash kalitlarini saqlashimiz kerak. Biz dasturga holatni va uni qalqib chiquvchi APIda o'zgartirish usullarini osongina qo'shishimiz mumkin:

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

    ...

} 

Orqa fonda biz hamma narsani funksiyaga aylantiramiz va dastur ob'ektini oynaga yozamiz, shunda biz u bilan konsoldan ishlay olamiz:

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

Keling, UI konsolidan bir nechta kalitlarni qo'shamiz va holat bilan nima sodir bo'lishini ko'ramiz:

Xavfsiz brauzer kengaytmasini yozish

Qayta ishga tushirishda kalitlarni yo'qotmaslik uchun holatni doimiy qilish kerak.

Biz uni localStorage-da saqlaymiz, har bir o'zgarishda ustiga yozamiz. Keyinchalik, unga kirish UI uchun ham zarur bo'ladi va men ham o'zgarishlarga obuna bo'lishni xohlayman. Bunga asoslanib, kuzatiladigan xotirani yaratish va uning o'zgarishlariga obuna bo'lish qulay bo'ladi.

Biz mobx kutubxonasidan foydalanamiz (https://github.com/mobxjs/mobx). Tanlov unga to'g'ri keldi, chunki u bilan ishlashim shart emas edi, lekin men uni o'rganishni juda xohlardim.

Keling, boshlang'ich holatni ishga tushirishni qo'shamiz va do'konni kuzatiladigan holga keltiramiz:

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

    ...

}

"Kaput ostida" mobx barcha do'kon maydonlarini proksi-server bilan almashtirdi va ularga qilingan barcha qo'ng'iroqlarni to'xtatdi. Ushbu xabarlarga obuna bo'lish mumkin bo'ladi.

Quyida men "o'zgartirilganda" atamasini tez-tez ishlataman, ammo bu mutlaqo to'g'ri emas. Mobx maydonlarga kirishni kuzatib boradi. Kutubxona yaratadigan proksi-server ob'yektlarining qabul qiluvchilari va sozlagichlari ishlatiladi.

Harakat dekoratorlari ikkita maqsadga xizmat qiladi:

  1. EnforceActions bayrog'i bilan qat'iy rejimda mobx holatni bevosita o'zgartirishni taqiqlaydi. Qattiq sharoitlarda ishlash yaxshi amaliyot hisoblanadi.
  2. Agar funktsiya holatni bir necha marta o'zgartirsa ham - masalan, biz kodning bir nechta satridagi bir nechta maydonlarni o'zgartiramiz - kuzatuvchilarga faqat u tugagandan so'ng xabar beriladi. Bu, ayniqsa, keraksiz holat yangilanishlari elementlarning keraksiz ko'rsatilishiga olib keladigan frontend uchun juda muhimdir. Bizning holatda, na birinchi, na ikkinchisi ayniqsa ahamiyatli emas, lekin biz eng yaxshi amaliyotlarga amal qilamiz. Kuzatilgan maydonlarning holatini o'zgartiradigan barcha funktsiyalarga dekorativlarni biriktirish odatiy holdir.

Orqa fonda biz localStorage-da ishga tushirish va holatni saqlashni qo'shamiz:

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

Bu erda reaksiya funktsiyasi qiziq. Uning ikkita argumenti bor:

  1. Ma'lumotlar selektori.
  2. Har safar o'zgarganda ushbu ma'lumotlar bilan chaqiriladigan ishlov beruvchi.

Biz holatni argument sifatida aniq qabul qiladigan reduxdan farqli o'laroq, mobx biz selektor ichida qaysi kuzatuvchiga kirishimizni eslab qoladi va faqat ular o'zgarganda ishlov beruvchini chaqiradi.

Mobx biz obuna bo'lgan obuna bo'lishni qanday hal qilishini tushunish muhimdir. Agar men shunday kodda selektor yozgan bo'lsam() => app.store, keyin reaktsiya hech qachon chaqirilmaydi, chunki saqlashning o'zi kuzatilmaydi, faqat uning maydonlari.

Agar shunday yozgan bo'lsam () => app.store.keys, keyin yana hech narsa bo'lmaydi, chunki massiv elementlarini qo'shish/o'chirishda unga havola o'zgarmaydi.

Mobx birinchi marta selektor vazifasini bajaradi va faqat biz kirgan kuzatilishi mumkin bo'lgan narsalarni kuzatib boradi. Bu proksi oluvchilar orqali amalga oshiriladi. Shuning uchun bu erda o'rnatilgan funksiya ishlatiladi toJS. Asl maydonlar bilan almashtirilgan barcha proksi-serverlar bilan yangi ob'ektni qaytaradi. Amalga oshirish jarayonida u ob'ektning barcha maydonlarini o'qiydi - shuning uchun oluvchilar ishga tushiriladi.

Qalqib chiquvchi konsolda biz yana bir nechta kalitlarni qo'shamiz. Bu safar ular ham localStorage-da tugadi:

Xavfsiz brauzer kengaytmasini yozish

Fon sahifasi qayta yuklanganda, ma'lumotlar joyida qoladi.

Shu nuqtaga qadar barcha dastur kodlarini ko'rish mumkin shu yerda.

Shaxsiy kalitlarni xavfsiz saqlash

Shaxsiy kalitlarni aniq matnda saqlash xavflidir: sizni buzish, kompyuteringizga kirish va hokazolar ehtimoli har doim mavjud. Shuning uchun, localStorage-da biz kalitlarni parol bilan shifrlangan shaklda saqlaymiz.

Kattaroq xavfsizlik uchun biz ilovaga qulflangan holatni qo'shamiz, unda kalitlarga umuman kirish imkoni bo'lmaydi. Vaqt tugashi sababli kengaytmani avtomatik ravishda qulflangan holatga o'tkazamiz.

Mobx sizga faqat minimal ma'lumotlar to'plamini saqlashga imkon beradi, qolganlari esa unga asoslanib avtomatik ravishda hisoblanadi. Bular hisoblangan xususiyatlar deb ataladi. Ularni ma'lumotlar bazalaridagi ko'rinishlar bilan solishtirish mumkin:

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

Endi biz faqat shifrlangan kalitlarni va parolni saqlaymiz. Qolganlarning hammasi hisoblab chiqiladi. Biz parolni davlatdan olib tashlash orqali qulflangan holatga o'tkazamiz. Umumiy API endi saqlashni ishga tushirish usuliga ega.

Shifrlash uchun yozilgan crypto-js-dan foydalanadigan yordam dasturlari:

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

Brauzerda bo'sh API mavjud, u orqali siz hodisaga obuna bo'lishingiz mumkin - holat o'zgarishlari. Davlat, shunga ko'ra, bo'lishi mumkin idle, active и locked. Bo'sh turish uchun siz kutish vaqtini belgilashingiz mumkin va OS bloklanganda qulflangan bo'ladi. Shuningdek, biz localStorage-ga saqlash uchun selektorni o'zgartiramiz:

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

Ushbu bosqichdan oldingi kod shu yerda.

Jurnallar

Shunday qilib, biz eng muhim narsaga keldik: blokcheynda tranzaktsiyalarni yaratish va imzolash. Biz WAVES blokcheyn va kutubxonasidan foydalanamiz to'lqinlar - operatsiyalar.

Birinchidan, shtatga imzolanishi kerak bo'lgan xabarlar qatorini qo'shamiz, keyin yangi xabar qo'shish, imzoni tasdiqlash va rad etish usullarini qo'shamiz:

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

    ...
}

Biz yangi xabar olganimizda, biz unga metadata qo'shamiz, bajaring observable va qo'shing store.messages.

Agar qilmasangiz observable qo'lda, keyin massivga xabarlar qo'shganda mobx buni o'zi bajaradi. Biroq, u bizda havolaga ega bo'lmagan yangi ob'ektni yaratadi, ammo keyingi qadam uchun bizga kerak bo'ladi.

Keyinchalik, xabar holati o'zgarganda hal qilinadigan va'dani qaytaramiz. Vaziyat reaktsiya orqali kuzatiladi, bu holat o'zgarganda "o'zini o'ldiradi".

Usul kodi approve и reject juda oddiy: agar kerak bo'lsa, imzolangandan so'ng biz xabarning holatini o'zgartiramiz.

Biz UI API-ga tasdiqlash va rad etishni, API sahifasiga newMessage-ni qo'yamiz:

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

    ...
}

Endi kengaytma bilan tranzaktsiyani imzolashga harakat qilaylik:

Xavfsiz brauzer kengaytmasini yozish

Umuman olganda, hamma narsa tayyor, qolgan narsa oddiy UI qo'shing.

UI

Interfeys dastur holatiga kirishni talab qiladi. UI tomonida biz qilamiz observable holatini kiriting va APIga ushbu holatni o'zgartiradigan funksiya qo'shing. Qo'shamiz observable fondan olingan API ob'ektiga:

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

Oxirida biz dastur interfeysini ko'rsatishni boshlaymiz. Bu reaksiya ilovasi. Fon ob'ekti oddiygina rekvizitlar yordamida uzatiladi. Albatta, usullar uchun alohida xizmat va davlat uchun do'kon qilish to'g'ri bo'lar edi, ammo ushbu maqolaning maqsadlari uchun bu etarli:

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 bilan ma'lumotlar o'zgarganda renderlashni boshlash juda oson. Biz shunchaki kuzatuvchi dekoratorni paketdan osib qo'yamiz mobx-reaktsiya Komponentda ko'rsatiladi va komponent tomonidan havola qilingan har qanday kuzatuvchi o'zgarganda avtomatik ravishda chaqiriladi. Sizga mapStateToProps kerak emas yoki reduxdagi kabi ulanish kerak emas. Hamma narsa qutidan tashqarida ishlaydi:

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

Qolgan komponentlarni kodda ko'rish mumkin UI papkasida.

Endi dastur sinfida siz UI uchun holat selektorini qilishingiz va u o'zgarganda UIni xabardor qilishingiz kerak. Buning uchun usulni qo'shamiz getState и reactionqo'ng'iroq qilish 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())

        })
    }

    ...
}

Ob'ektni qabul qilishda remote yaratiladi reaction UI tomonidagi funktsiyani chaqiradigan holatni o'zgartirish uchun.

Yakuniy teginish - kengaytma belgisiga yangi xabarlar ko'rinishini qo'shish:

function setupApp() {
...

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

...
}

Shunday qilib, ariza tayyor. Veb-sahifalar tranzaktsiyalar uchun imzo talab qilishi mumkin:

Xavfsiz brauzer kengaytmasini yozish

Xavfsiz brauzer kengaytmasini yozish

Kod bu yerda mavjud aloqa.

xulosa

Agar siz maqolani oxirigacha o'qib chiqqan bo'lsangiz, lekin hali ham savollaringiz bo'lsa, ularni quyidagi manzilda so'rashingiz mumkin kengaytmali omborlar. U erda siz har bir belgilangan qadam uchun majburiyatlarni topasiz.

Va agar siz haqiqiy kengaytma kodini ko'rishni xohlasangiz, buni topishingiz mumkin shu yerda.

Kimdan kod, ombor va ish tavsifi Siemarell

Manba: www.habr.com

a Izoh qo'shish