Pagsulat ug luwas nga extension sa browser

Pagsulat ug luwas nga extension sa browser

Dili sama sa kasagarang "kliyente-server" nga arkitektura, ang mga desentralisadong aplikasyon gihulagway sa:

  • Dili kinahanglan nga magtipig usa ka database nga adunay mga login ug password sa gumagamit. Ang impormasyon sa pag-access gitipigan lamang sa mga tiggamit mismo, ug ang pagkumpirma sa ilang pagkatinuod mahitabo sa lebel sa protocol.
  • Dili kinahanglan nga mogamit usa ka server. Ang lohika sa aplikasyon mahimong ipatuman sa usa ka blockchain network, diin posible nga tipigan ang gikinahanglan nga gidaghanon sa datos.

Adunay 2 nga medyo luwas nga pagtipig alang sa mga yawe sa gumagamit - mga pitaka sa hardware ug mga extension sa browser. Ang mga pitaka sa hardware kasagaran hilabihan ka luwas, apan lisud gamiton ug layo sa libre, apan ang mga extension sa browser mao ang hingpit nga kombinasyon sa seguridad ug kasayon ​​sa paggamit, ug mahimo usab nga hingpit nga libre alang sa mga end user.

Sa pagkonsiderar niining tanan, gusto namong himoon ang labing luwas nga extension nga nagpayano sa pagpalambo sa mga desentralisadong aplikasyon pinaagi sa paghatag ug yanong API alang sa pagtrabaho sa mga transaksyon ug mga pirma.
Isulti namo kanimo ang bahin niini nga kasinatian sa ubos.

Ang artikulo maglangkob sa mga lakang sa lakang nga mga panudlo kung giunsa pagsulat ang usa ka extension sa browser, nga adunay mga pananglitan sa code ug mga screenshot. Makita nimo ang tanan nga code sa mga tipiganan. Matag commit lohikal nga katumbas sa usa ka seksyon niini nga artikulo.

Usa ka Mubo nga Kasaysayan sa Mga Extension sa Browser

Ang mga extension sa browser dugay na nga naglungtad. Nagpakita sila sa Internet Explorer kaniadtong 1999, sa Firefox kaniadtong 2004. Bisan pa, sa dugay nga panahon wala’y usa nga sukaranan alang sa mga extension.

Mahimo natong isulti nga kini nagpakita uban sa mga extension sa ikaupat nga bersyon sa Google Chrome. Siyempre, wala'y espesipikasyon kaniadto, apan ang Chrome API ang nahimong basehan niini: nga nabuntog ang kadaghanan sa merkado sa browser ug adunay usa ka built-in nga tindahan sa aplikasyon, ang Chrome sa tinuud nagtakda sa sumbanan alang sa mga extension sa browser.

Ang Mozilla adunay kaugalingon nga sumbanan, apan nakita ang pagkapopular sa mga extension sa Chrome, ang kompanya nakahukom nga maghimo usa ka katugbang nga API. Sa 2015, sa inisyatiba sa Mozilla, usa ka espesyal nga grupo ang gihimo sulod sa World Wide Web Consortium (W3C) aron magtrabaho sa mga detalye sa extension sa cross-browser.

Ang kasamtangan nga mga extension sa API alang sa Chrome gikuha isip basehan. Ang trabaho gihimo uban ang suporta sa Microsoft (Ang Google nagdumili sa pag-apil sa pag-uswag sa sumbanan), ug ingon usa ka sangputanan usa ka draft nagpakita mga detalye.

Sa pormal nga paagi, ang detalye gisuportahan sa Edge, Firefox ug Opera (timan-i nga ang Chrome wala sa kini nga lista). Apan sa tinuud, ang sumbanan sa kadaghanan nahiuyon sa Chrome, tungod kay kini sa tinuud gisulat base sa mga extension niini. Makabasa ka ug dugang bahin sa WebExtensions API dinhi.

Istruktura sa extension

Ang bugtong file nga gikinahanglan alang sa extension mao ang manifest (manifest.json). Kini usab ang "entry point" sa pagpalapad.

Makapakita

Sumala sa detalye, ang manifest file usa ka balido nga JSON file. Usa ka bug-os nga paghulagway sa mga dayag nga yawe nga adunay kasayuran bahin sa kung unsang mga yawe ang gisuportahan kung diin makita ang browser dinhi.

Ang mga yawe nga wala sa espesipikasyon "mahimo" nga ibalewala (ang Chrome ug Firefox nagreport sa mga sayup, apan ang mga extension nagpadayon sa pagtrabaho).

Ug gusto nakong ipunting ang atensyon sa pipila ka mga punto.

  1. background — usa ka butang nga naglakip sa mosunod nga mga natad:
    1. skrip — usa ka han-ay sa mga script nga ipatuman sa konteksto sa background (atong hisgutan kini sa ulahi);
    2. panid - imbis nga mga script nga ipatuman sa usa ka walay sulod nga panid, mahimo nimong ipiho ang html nga adunay sulud. Niini nga kaso, ang script field dili tagdon, ug ang mga script kinahanglan nga isulod sa sulod nga panid;
    3. magpadayon - usa ka binary nga bandila, kung wala gipiho, ang browser "mopatay" sa proseso sa background kung gikonsiderar nga wala kini gibuhat, ug i-restart kini kung kinahanglan. Kung dili, ang panid madiskarga lamang kung sirado ang browser. Dili suportado sa Firefox.
  2. content_scripts — usa ka han-ay sa mga butang nga nagtugot kanimo sa pagkarga sa lainlaing mga script sa lainlaing mga panid sa web. Ang matag butang naglangkob sa mosunod nga importante nga mga natad:
    1. posporo - pattern nga url, nga nagtino kung ang usa ka partikular nga script sa sulud iapil o dili.
    2. js — usa ka lista sa mga script nga i-load sa kini nga duwa;
    3. iapil_matches - wala iapil gikan sa uma match Mga URL nga mohaum niini nga field.
  3. page_action - sa tinuud usa ka butang nga responsable sa icon nga gipakita sunod sa address bar sa browser ug pakig-uban niini. Gitugotan ka usab niini nga magpakita sa usa ka popup window, nga gihubit gamit ang imong kaugalingon nga HTML, CSS ug JS.
    1. default_popup — dalan sa HTML file nga adunay popup interface, mahimong adunay CSS ug JS.
  4. Permissions — usa ka han-ay alang sa pagdumala sa mga katungod sa extension. Adunay 3 ka matang sa mga katungod, nga gihulagway sa detalye dinhi
  5. web_accessible_resources — mga kapanguhaan sa extension nga mahimong hangyoon sa usa ka web page, pananglitan, mga imahe, JS, CSS, HTML nga mga file.
  6. externally_connectable — dinhi mahimo nimong klaro nga ipiho ang mga ID sa ubang mga ekstensiyon ug mga dominyo sa mga panid sa web diin mahimo nimong makonektar. Ang usa ka domain mahimong ikaduha nga lebel o mas taas. Dili molihok sa Firefox.

Konteksto sa pagpatuman

Ang extension adunay tulo ka mga konteksto sa pagpatuman sa code, nga mao, ang aplikasyon naglangkob sa tulo ka mga bahin nga adunay lainlaing lebel sa pag-access sa browser API.

Konteksto sa extension

Kadaghanan sa API anaa dinhi. Niini nga konteksto sila "nabuhi":

  1. Panid sa background — “backend” nga bahin sa extension. Ang file gitakda sa manifest gamit ang "background" key.
  2. Popup nga panid - usa ka popup nga panid nga makita kung imong gi-klik ang icon sa extension. Sa manifesto browser_action -> default_popup.
  3. Pasadya nga panid — panid sa extension, "nagpuyo" sa usa ka bulag nga tab sa pagtan-aw chrome-extension://<id_расширения>/customPage.html.

Kini nga konteksto anaa nga independente sa browser windows ug tabs. Panid sa background naglungtad sa usa ka kopya ug kanunay nga molihok (ang eksepsiyon mao ang panid sa panghitabo, kung ang script sa background gilunsad sa usa ka panghitabo ug "mamatay" pagkahuman sa pagpatuman niini). Popup nga panid anaa sa diha nga ang popup nga bintana bukas, ug Pasadya nga panid — samtang ang tab uban niini bukas. Walay access sa ubang mga tab ug sa ilang mga sulod gikan niini nga konteksto.

Konteksto sa script sa sulod

Ang content script file gilunsad uban sa matag tab sa browser. Kini adunay access sa bahin sa extension sa API ug sa DOM tree sa web page. Kini ang mga script sa sulud nga responsable sa interaksyon sa panid. Ang mga extension nga nagmaniobra sa punoan sa DOM naghimo niini sa mga script sa sulud - pananglitan, mga ad blocker o tighubad. Usab, ang sulud nga script mahimong makigkomunikar sa panid pinaagi sa sumbanan postMessage.

Konteksto sa panid sa web

Kini ang aktuwal nga web page mismo. Wala kini'y labot sa extension ug walay access didto, gawas sa mga kaso diin ang domain niini nga panid dili klaro nga gipakita sa manifest (dugang niini sa ubos).

Pagbayloay sa mensahe

Ang lainlaing mga bahin sa aplikasyon kinahanglan magbinayloay og mga mensahe sa usag usa. Adunay usa ka API alang niini runtime.sendMessage para magpadala ug mensahe background и tabs.sendMessage sa pagpadala og mensahe sa usa ka panid (content script, popup o web page kung anaa externally_connectable). Sa ubos usa ka pananglitan kung nag-access sa 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))
    }
)

Para sa bug-os nga komunikasyon, makahimo ka og mga koneksyon pinaagi sa runtime.connect. Agig tubag atong madawat runtime.Port, diin, samtang kini bukas, mahimo nimong ipadala ang bisan unsang gidaghanon sa mga mensahe. Sa bahin sa kliyente, pananglitan, contentscript, murag mao ni:

// Опять же 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 o background:

// Обработчик для подключения 'своих' вкладок. Контент скриптов, popup или страниц расширения
chrome.runtime.onConnect.addListener(function(port) {
    console.assert(port.name === "knockknock");
    port.onMessage.addListener(function(msg) {
        if (msg.joke === "Knock knock")
            port.postMessage({question: "Who's there?"});
        else if (msg.answer === "Madame")
            port.postMessage({question: "Madame who?"});
        else if (msg.answer === "Madame... Bovary")
            port.postMessage({question: "I don't get it."});
    });
});

// Обработчик для подключения внешних вкладок. Других расширений или веб страниц, которым разрешен доступ в манифесте
chrome.runtime.onConnectExternal.addListener(function(port) {
    ...
});

Adunay usab usa ka panghitabo onDisconnect ug pamaagi disconnect.

Diagram sa aplikasyon

Himoon nato ang extension sa browser nga magtipig ug pribado nga mga yawe, maghatag ug access sa publikong impormasyon (adres, public key makigkomunikar sa panid ug motugot sa mga third-party nga aplikasyon sa pagpangayo ug pirma para sa mga transaksyon.

Pagpalambo sa Aplikasyon

Ang among aplikasyon kinahanglan nga makig-uban sa tiggamit ug maghatag sa panid sa usa ka API sa pagtawag sa mga pamaagi (pananglitan, sa pagpirma sa mga transaksyon). Buhata sa usa lang contentscript dili molihok, tungod kay kini adunay access lamang sa DOM, apan dili sa JS sa panid. Sumpaysumpaya pinaagi sa runtime.connect dili namo mahimo, tungod kay ang API gikinahanglan sa tanang mga dominyo, ug ang mga espesipiko lamang ang mahimong espesipiko sa manifest. Ingon nga resulta, ang diagram mahimong sama niini:

Pagsulat ug luwas nga extension sa browser

Adunay laing script - inpage, nga atong i-inject sa panid. Modagan kini sa konteksto niini ug maghatag usa ka API alang sa pagtrabaho sa extension.

Начало

Tanang browser extension code anaa sa GitHub. Atol sa paghulagway adunay mga link sa mga commit.

Magsugod ta sa manifesto:

{
  // Имя и описание, версия. Все это будет видно в браузере в chrome://extensions/?id=<id расширения>
  "name": "Signer",
  "description": "Extension demo",
  "version": "0.0.1",
  "manifest_version": 2,

  // Скрипты, которые будут исполнятся в background, их может быть несколько
  "background": {
    "scripts": ["background.js"]
  },

  // Какой html использовать для popup
  "browser_action": {
    "default_title": "My Extension",
    "default_popup": "popup.html"
  },

  // Контент скрипты.
  // У нас один объект: для всех url начинающихся с http или https мы запускаем
  // contenscript context со скриптом contentscript.js. Запускать сразу по получении документа для всех фреймов
  "content_scripts": [
    {
      "matches": [
        "http://*/*",
        "https://*/*"
      ],
      "js": [
        "contentscript.js"
      ],
      "run_at": "document_start",
      "all_frames": true
    }
  ],
  // Разрешен доступ к localStorage и idle api
  "permissions": [
    "storage",
    // "unlimitedStorage",
    //"clipboardWrite",
    "idle"
    //"activeTab",
    //"webRequest",
    //"notifications",
    //"tabs"
  ],
  // Здесь указываются ресурсы, к которым будет иметь доступ веб страница. Тоесть их можно будет запрашивать fetche'м или просто xhr
  "web_accessible_resources": ["inpage.js"]
}

Paghimo og walay sulod nga background.js, popup.js, inpage.js ug contentscript.js. Among gidugang ang popup.html - ug ang among aplikasyon mahimo nang makarga sa Google Chrome ug siguroha nga kini molihok.

Aron mapamatud-an kini, mahimo nimong kuhaon ang code gikan dinhi. Dugang pa sa among gibuhat, gi-configure sa link ang asembliya sa proyekto gamit ang webpack. Aron makadugang usa ka aplikasyon sa browser, sa chrome://extensions kinahanglan nimo nga pilion ang load unpacked ug ang folder nga adunay katugbang nga extension - sa among kaso dist.

Pagsulat ug luwas nga extension sa browser

Karon ang among extension na-install ug nagtrabaho. Mahimo nimong ipadagan ang mga himan sa developer alang sa lainlaing mga konteksto sama sa mosunod:

popup ->

Pagsulat ug luwas nga extension sa browser

Ang pag-access sa content script console gihimo pinaagi sa console sa panid mismo diin kini gilunsad.Pagsulat ug luwas nga extension sa browser

Pagbayloay sa mensahe

Busa, kinahanglan namong magtukod ug duha ka channel sa komunikasyon: inpage <-> background ug popup <-> background. Mahimo nimo, siyempre, magpadala lang og mga mensahe sa pantalan ug mag-imbento sa imong kaugalingong protocol, apan mas gusto nako ang pamaagi nga akong nakita sa metamask open source project.

Kini usa ka extension sa browser alang sa pagtrabaho kauban ang network sa Ethereum. Niini, lainlain nga bahin sa aplikasyon ang nakigsulti pinaagi sa RPC gamit ang dnode library. Gitugotan ka niini sa pag-organisar sa usa ka pagbinayloay nga dali ug dali kung hatagan nimo kini usa ka sapa sa nodejs ingon usa ka transportasyon (nagpasabut usa ka butang nga nagpatuman sa parehas nga interface):

import Dnode from "dnode/browser";

// В этом примере условимся что клиент удаленно вызывает функции на сервере, хотя ничего нам не мешает сделать это двунаправленным

// Cервер
// API, которое мы хотим предоставить
const dnode = Dnode({
    hello: (cb) => cb(null, "world")
})
// Транспорт, поверх которого будет работать dnode. Любой nodejs стрим. В браузере есть бибилиотека 'readable-stream'
connectionStream.pipe(dnode).pipe(connectionStream)

// Клиент
const dnodeClient = Dnode() // Вызов без агрумента значит что мы не предоставляем API на другой стороне

// Выведет в консоль world
dnodeClient.once('remote', remote => {
    remote.hello(((err, value) => console.log(value)))
})

Karon maghimo kami usa ka klase sa aplikasyon. Maghimo kini og mga butang sa API alang sa popup ug web page, ug maghimo og dnode alang kanila:

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

Dinhi ug sa ubos, imbes sa pangkalibutanon nga butang sa Chrome, among gigamit ang extensionApi, nga nag-access sa Chrome sa browser sa Google ug browser sa uban. Gihimo kini alang sa cross-browser compatibility, apan alang sa mga katuyoan niini nga artikulo, mahimo nimong gamiton ang 'chrome.runtime.connect'.

Maghimo kita usa ka pananglitan sa aplikasyon sa script sa background:

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

Tungod kay ang dnode nagtrabaho sa mga sapa, ug nakadawat kami usa ka pantalan, gikinahanglan ang klase sa adaptor. Gihimo kini gamit ang readable-stream library, nga nagpatuman sa nodejs streams sa browser:

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

Karon maghimo kita og koneksyon sa 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;
    }
}

Dayon naghimo kami og koneksyon sa script sa sulod:

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

Tungod kay kinahanglan namon ang API dili sa script sa sulud, apan direkta sa panid, nagbuhat kami duha ka butang:

  1. Naghimo kami og duha ka sapa. Usa - padulong sa panid, sa ibabaw sa postMessage. Alang niini among gigamit kini kini nga pakete gikan sa mga tiglalang sa metamask. Ang ikaduha nga sapa mao ang background sa pantalan nga nadawat gikan runtime.connect. Palit ta nila. Karon ang panid adunay sapa sa background.
  2. I-inject ang script sa DOM. I-download ang script (gitugotan ang pag-access niini sa manifest) ug paghimo usa ka tag script uban sa sulod niini:

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

Karon naghimo kami usa ka butang nga api sa inpage ug gibutang kini sa global:

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

Andam na mi Remote Procedure Call (RPC) nga adunay bulag nga API para sa panid ug UI. Kung nagkonektar sa usa ka bag-ong panid sa background makita namon kini:

Pagsulat ug luwas nga extension sa browser

Walay sulod nga API ug gigikanan. Sa kilid sa panid, mahimo natong tawagan ang hello function sama niini:

Pagsulat ug luwas nga extension sa browser

Ang pagtrabaho kauban ang mga function sa callback sa modernong JS dili maayo nga pamatasan, mao nga magsulat kita usa ka gamay nga katabang aron makahimo usa ka dnode nga nagtugot kanimo nga ipasa ang usa ka butang sa API sa mga gamit.

Ang mga butang sa API karon tan-awon sama niini:

export class SignerApp {

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

...

}

Pagkuha usa ka butang gikan sa hilit nga sama niini:

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

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

Ug ang mga function sa pagtawag nagbalik usa ka saad:

Pagsulat ug luwas nga extension sa browser

Bersyon nga adunay mga asynchronous nga function nga magamit dinhi.

Sa kinatibuk-an, ang RPC ug stream approach daw medyo flexible: makagamit mi og steam multiplexing ug makamugna og daghang lain-laing mga API para sa lain-laing mga buluhaton. Sa prinsipyo, ang dnode mahimong gamiton bisan asa, ang nag-unang butang mao ang pagputos sa transportasyon sa porma sa usa ka nodejs stream.

Ang usa ka alternatibo mao ang format sa JSON, nga nagpatuman sa protocol sa JSON RPC 2. Bisan pa, kini nagtrabaho uban ang piho nga mga transportasyon (TCP ug HTTP(S)), nga dili magamit sa among kaso.

Internal nga estado ug lokal nga Pagtipig

Kinahanglan namon nga tipigan ang internal nga kahimtang sa aplikasyon - labing menos ang mga yawe sa pagpirma. Dali ra kaayo kami makadugang usa ka estado sa aplikasyon ug mga pamaagi sa pagbag-o niini sa popup API:

import {setupDnode} from "./utils/setupDnode";

export class SignerApp {

    constructor(){
        this.store = {
            keys: [],
        };
    }

    addKey(key){
        this.store.keys.push(key)
    }

    removeKey(index){
        this.store.keys.splice(index,1)
    }

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

    ...

} 

Sa background, ibutang namon ang tanan sa usa ka function ug isulat ang butang nga aplikasyon sa bintana aron magamit namon kini gikan sa console:

import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {SignerApp} from "./SignerApp";

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

setupApp();

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

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

    extensionApi.runtime.onConnect.addListener(connectRemote);

    function connectRemote(remotePort) {
        const processName = remotePort.name;
        const portStream = new PortStream(remotePort);
        if (processName === 'contentscript') {
            const origin = remotePort.sender.url;
            app.connectPage(portStream, origin)
        } else {
            app.connectPopup(portStream)
        }
    }
}

Atong idugang ang pipila ka mga yawe gikan sa UI console ug tan-awa kung unsa ang mahitabo sa estado:

Pagsulat ug luwas nga extension sa browser

Ang estado kinahanglan nga himuon nga makanunayon aron ang mga yawe dili mawala kung mag-restart.

Among tipigan kini sa localStorage, i-overwrite kini sa matag kausaban. Pagkahuman, ang pag-access niini kinahanglan usab alang sa UI, ug gusto ko usab nga mag-subscribe sa mga pagbag-o. Pinasukad niini, mahimong kombenyente ang paghimo usa ka maobserbahan nga pagtipig ug pag-subscribe sa mga pagbag-o niini.

Atong gamiton ang mobx library (https://github.com/mobxjs/mobx). Ang pagpili nahulog niini tungod kay dili ko kinahanglan nga magtrabaho uban niini, apan gusto gyud nako nga magtuon niini.

Atong idugang ang initialization sa inisyal nga estado ug himoon ang tindahan nga maobserbahan:

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

    ...

}

"Ubos sa tabon," gipulihan sa mobx ang tanan nga mga natad sa tindahan nga adunay proxy ug gipugngan ang tanan nga mga tawag sa kanila. Mahimong posible nga mag-subscribe sa kini nga mga mensahe.

Sa ubos kanunay nakong gamiton ang termino nga "kung magbag-o", bisan kung dili kini hingpit nga husto. Gisubay sa Mobx ang pag-access sa mga uma. Ang mga getter ug setter sa proxy nga mga butang nga gihimo sa library gigamit.

Ang mga dekorador sa aksyon adunay duha ka katuyoan:

  1. Sa higpit nga paagi nga adunay bandila sa enforceActions, gidid-an sa mobx ang pagbag-o sa estado nga direkta. Giisip nga maayong praktis ang pagtrabaho ubos sa higpit nga mga kondisyon.
  2. Bisan kung ang usa ka function nagbag-o sa estado sa daghang beses - pananglitan, usbon namon ang daghang mga field sa daghang linya sa code - ang mga tigpaniid gipahibalo lamang kung kini makompleto. Kini labi ka hinungdanon alang sa frontend, diin ang dili kinahanglan nga pag-update sa estado nagdala sa wala kinahanglana nga paghubad sa mga elemento. Sa among kaso, dili ang una o ang ikaduha labi nga may kalabutan, apan among sundon ang labing kaayo nga mga gawi. Naandan nga ilakip ang mga dekorador sa tanan nga mga gimbuhaton nga nagbag-o sa kahimtang sa naobserbahan nga mga uma.

Sa background atong idugang ang initialization ug i-save ang estado sa 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)
        }
    }
}

Ang function sa reaksyon makapaikag dinhi. Kini adunay duha ka argumento:

  1. Tigpili sa datos.
  2. Usa ka handler nga tawgon uban niini nga data sa matag higayon nga kini mausab.

Dili sama sa redux, diin tin-aw natong gidawat ang estado isip argumento, nahinumdom ang mobx kung unsang mga obserbasyon ang atong ma-access sa sulod sa selector, ug motawag lang sa tigdumala kon mag-usab sila.

Importante nga masabtan kung giunsa pagdesisyon sa mobx kung unsang mga obserbasyon ang among gi-subscribe. Kung nagsulat ako usa ka tigpili sa code nga sama niini() => app.store, unya ang reaksyon dili na tawgon, tungod kay ang pagtipig mismo dili maobserbahan, ang mga uma niini ra.

Kung gisulat ko kini nga ingon niini () => app.store.keys, unya wala'y mahitabo pag-usab, tungod kay kung magdugang / magtangtang sa mga elemento sa array, ang paghisgot niini dili mausab.

Ang Mobx naglihok isip usa ka tigpili sa unang higayon ug nagsubay lamang sa mga obserbasyon nga among na-access. Gihimo kini pinaagi sa mga proxy getters. Busa, ang built-in nga function gigamit dinhi toJS. Nagbalik kini sa usa ka bag-ong butang nga ang tanan nga mga proxy gipulihan sa orihinal nga mga uma. Atol sa pagpatuman, gibasa niini ang tanan nga mga natad sa butang - busa ang mga getter na-trigger.

Sa popup console magdugang usab kami daghang mga yawe. Niining higayona nakasulod usab sila sa localStorage:

Pagsulat ug luwas nga extension sa browser

Kung ang panid sa background gi-reload, ang kasayuran nagpabilin sa lugar.

Ang tanan nga code sa aplikasyon hangtod niini nga punto mahimong tan-awon dinhi.

Luwas nga pagtipig sa mga pribadong yawe

Ang pagtipig sa mga pribadong yawe sa tin-aw nga teksto dili luwas: kanunay adunay higayon nga ma-hack ka, maka-access sa imong kompyuter, ug uban pa. Busa, sa localStorage atong tipigan ang mga yawe sa usa ka password-encrypted nga porma.

Alang sa mas dako nga seguridad, magdugang kami usa ka naka-lock nga estado sa aplikasyon, diin wala’y pag-access sa mga yawe. Awtomatiko namong ibalhin ang extension sa naka-lock nga estado tungod sa usa ka timeout.

Gitugotan ka sa Mobx nga magtipig lamang sa usa ka minimum nga set sa datos, ug ang uban awtomatikong kalkulado base niini. Mao ni ang gitawag nga computed properties. Mahimo silang itandi sa mga pagtan-aw sa mga database:

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

Karon gitipigan ra namon ang mga naka-encrypt nga yawe ug password. Ang tanan nga uban gikalkula. Gihimo namo ang pagbalhin ngadto sa usa ka naka-lock nga estado pinaagi sa pagtangtang sa password gikan sa estado. Ang publiko nga API karon adunay usa ka pamaagi alang sa pagsugod sa pagtipig.

Gisulat alang sa pag-encrypt mga utilities gamit ang 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)
}

Ang browser adunay usa ka idle nga API diin mahimo ka mag-subscribe sa usa ka panghitabo - pagbag-o sa estado. Estado, sumala niana, mahimong idle, active и locked. Alang sa idle mahimo nimong itakda ang usa ka timeout, ug gi-lock ang gitakda kung ang OS mismo gibabagan. Usbon usab namo ang tigpili alang sa pagtipig sa 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)
        }
    }
}

Ang code sa wala pa kini nga lakang mao ang dinhi.

Mga Transaksyon

Mao nga, nakaabut kami sa labing hinungdanon nga butang: paghimo ug pagpirma sa mga transaksyon sa blockchain. Atong gamiton ang WAVES blockchain ug library mga balud-transaksyon.

Una, idugang nato sa estado ang usa ka han-ay sa mga mensahe nga kinahanglang pirmahan, dayon idugang ang mga pamaagi sa pagdugang og bag-ong mensahe, pagkumpirma sa pirma, ug pagdumili:

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

    ...
}

Kung makadawat kami usa ka bag-ong mensahe, gidugang namon ang metadata niini, buhata observable ug idugang sa store.messages.

Kung dili ka observable mano-mano, unya ang mobx mobuhat niini sa iyang kaugalingon kung magdugang og mga mensahe sa laray. Bisan pa, maghimo kini usa ka bag-ong butang diin wala kami usa ka pakisayran, apan kinahanglan namon kini alang sa sunod nga lakang.

Sunod, ibalik namon ang usa ka saad nga masulbad kung ang kahimtang sa mensahe mausab. Ang kahimtang gibantayan pinaagi sa reaksyon, nga "mopatay sa kaugalingon" kung mabag-o ang kahimtang.

Code sa pamaagi approve и reject yano ra kaayo: usbon lang namo ang kahimtang sa mensahe, pagkahuman sa pagpirma niini kung gikinahanglan.

Among gibutang ang Approve ug reject sa UI API, newMessage sa page 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)
        }
    }

    ...
}

Karon atong sulayan nga pirmahan ang transaksyon gamit ang extension:

Pagsulat ug luwas nga extension sa browser

Sa kinatibuk-an, andam na ang tanan, ang nahabilin mao na idugang ang yano nga UI.

UI

Ang interface nanginahanglan pag-access sa kahimtang sa aplikasyon. Sa bahin sa UI atong buhaton observable estado ug pagdugang usa ka function sa API nga magbag-o niini nga estado. Atong idugang observable sa API object nga nadawat gikan sa background:

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

Sa katapusan magsugod kami sa paghubad sa interface sa aplikasyon. Kini usa ka reaksyon nga aplikasyon. Ang background nga butang gipasa lang gamit ang props. Husto, siyempre, ang paghimo sa usa ka lahi nga serbisyo alang sa mga pamaagi ug usa ka tindahan alang sa estado, apan alang sa mga katuyoan sa kini nga artikulo igo na kini:

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

Uban sa mobx dali ra kaayo magsugod sa paghubad kung magbag-o ang datos. Gibitay ra namo ang tigdayandayan sa tigpaniid gikan sa pakete mobx-react sa component, ug ang render awtomatik nga tawgon kung adunay mga obserbasyon nga gi-refer sa component nga mausab. Dili nimo kinahanglan ang bisan unsang mapStateToProps o magkonektar sama sa redux. Ang tanan nagtrabaho sa gawas sa kahon:

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

Ang nahabilin nga mga sangkap mahimong makita sa code sa UI folder.

Karon sa klase sa aplikasyon kinahanglan nimo nga maghimo usa ka tigpili sa estado alang sa UI ug ipahibalo ang UI kung kini magbag-o. Aron mahimo kini, magdugang kita usa ka pamaagi getState и reactiongitawag 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())

        })
    }

    ...
}

Sa dihang nakadawat ug butang remote gibuhat reaction aron usbon ang estado nga nagtawag sa function sa kilid sa UI.

Ang katapusan nga paghikap mao ang pagdugang sa pagpakita sa bag-ong mga mensahe sa extension icon:

function setupApp() {
...

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

...
}

Busa, ang aplikasyon andam na. Ang mga panid sa web mahimong mangayo og pirma alang sa mga transaksyon:

Pagsulat ug luwas nga extension sa browser

Pagsulat ug luwas nga extension sa browser

Ang code anaa dinhi link.

konklusyon

Kung nabasa na nimo ang artikulo hangtod sa katapusan, apan adunay mga pangutana, mahimo nimo silang pangutan-on sa mga repository nga adunay extension. Didto makit-an usab nimo ang mga pasalig alang sa matag gitudlo nga lakang.

Ug kung interesado ka sa pagtan-aw sa code alang sa aktwal nga extension, makit-an nimo kini dinhi.

Code, repository ug job description gikan sa siemarell

Source: www.habr.com

Idugang sa usa ka comment