Təhlükəsiz brauzer uzantısının yazılması

Təhlükəsiz brauzer uzantısının yazılması

Ümumi "müştəri-server" arxitekturasından fərqli olaraq, mərkəzləşdirilməmiş tətbiqlər aşağıdakılarla xarakterizə olunur:

  • İstifadəçi girişləri və parolları ilə verilənlər bazasını saxlamağa ehtiyac yoxdur. Giriş məlumatı yalnız istifadəçilərin özləri tərəfindən saxlanılır və onların həqiqiliyinin təsdiqi protokol səviyyəsində baş verir.
  • Serverdən istifadə etməyə ehtiyac yoxdur. Tətbiq məntiqi blokçeyn şəbəkəsində icra oluna bilər, burada lazımi miqdarda məlumat saxlamaq mümkündür.

İstifadəçi açarları üçün 2 nisbətən təhlükəsiz anbar var - aparat pul kisələri və brauzer uzantıları. Avadanlıq pul kisələri əsasən son dərəcə təhlükəsizdir, lakin istifadəsi çətin və pulsuz deyil, lakin brauzer genişləndirmələri təhlükəsizlik və istifadə rahatlığının mükəmməl birləşməsidir və son istifadəçilər üçün tamamilə pulsuz ola bilər.

Bütün bunları nəzərə alaraq, biz əməliyyatlar və imzalarla işləmək üçün sadə API təmin etməklə, mərkəzləşdirilməmiş proqramların işlənib hazırlanmasını asanlaşdıran ən təhlükəsiz genişləndirmə yaratmaq istədik.
Aşağıda bu təcrübə haqqında sizə məlumat verəcəyik.

Məqalədə kod nümunələri və ekran görüntüləri ilə brauzer genişlənməsini necə yazmaq barədə addım-addım təlimatlar olacaq. Bütün kodu tapa bilərsiniz depolar. Hər bir törətmə məntiqi olaraq bu maddənin bir hissəsinə uyğun gəlir.

Brauzer Genişləndirmələrinin Qısa Tarixi

Brauzer uzantıları uzun müddətdir ki, mövcuddur. Onlar 1999-cu ildə Internet Explorer-də, 2004-cü ildə Firefox-da göründülər. Bununla belə, çox uzun müddətdir ki, genişləndirmələr üçün vahid standart yox idi.

Deyə bilərik ki, Google Chrome-un dördüncü versiyasında uzantılarla birlikdə ortaya çıxdı. Əlbəttə ki, o zaman heç bir spesifikasiya yox idi, lakin onun əsası olan Chrome API idi: brauzer bazarının çox hissəsini fəth edərək və quraşdırılmış proqram mağazasına sahib olan Chrome, əslində brauzer genişləndirmələri üçün standart təyin etdi.

Mozilla'nın öz standartı var idi, lakin Chrome genişləndirmələrinin populyarlığını görən şirkət uyğun API yaratmağa qərar verdi. 2015-ci ildə Mozilla-nın təşəbbüsü ilə Brauzerlər arası genişləndirmə spesifikasiyaları üzərində işləmək üçün World Wide Web Consortium (W3C) daxilində xüsusi qrup yaradıldı.

Chrome üçün mövcud API genişləndirmələri əsas götürülüb. İş Microsoft-un dəstəyi ilə həyata keçirildi (Google standartın hazırlanmasında iştirak etməkdən imtina etdi) və nəticədə bir layihə ortaya çıxdı. spesifikasiyalar.

Formal olaraq, spesifikasiya Edge, Firefox və Opera tərəfindən dəstəklənir (qeyd edək ki, Chrome bu siyahıda deyil). Ancaq əslində standart Chrome ilə əsasən uyğun gəlir, çünki o, əslində uzantıları əsasında yazılmışdır. WebExtensions API haqqında ətraflı oxuya bilərsiniz burada.

Uzatma quruluşu

Genişlənmə üçün tələb olunan yeganə fayl manifestdir (manifest.json). Bu, həm də genişlənməyə “giriş nöqtəsidir”.

Manifest

Spesifikasiyaya əsasən, manifest faylı etibarlı JSON faylıdır. Hansı brauzerə baxıla bilən düymələrin dəstəkləndiyi barədə məlumat ilə manifest açarların tam təsviri burada.

Spesifikasiyada olmayan düymələr "ola bilər" (həm Chrome, həm də Firefox səhvləri bildirir, lakin genişləndirmələr işləməyə davam edir).

Və bəzi məqamlara diqqət çəkmək istərdim.

  1. fon — aşağıdakı sahələri ehtiva edən obyekt:
    1. scripts — fon kontekstində yerinə yetiriləcək bir sıra skriptlər (bu barədə bir az sonra danışacağıq);
    2. səhifə - boş səhifədə yerinə yetiriləcək skriptlərin yerinə, məzmunu olan html təyin edə bilərsiniz. Bu halda, skript sahəsi nəzərə alınmayacaq və skriptləri məzmun səhifəsinə daxil etmək lazımdır;
    3. davam — ikili bayraq, göstərilmədiyi təqdirdə, brauzer heç bir şey etmədiyini düşündüyü zaman fon prosesini "öldürəcək" və lazım olduqda onu yenidən işə salacaq. Əks halda, səhifə yalnız brauzer bağlandıqda yüklənəcək. Firefox-da dəstəklənmir.
  2. məzmun_skriptləri — müxtəlif veb səhifələrə müxtəlif skriptləri yükləməyə imkan verən obyektlər massivi. Hər bir obyekt aşağıdakı mühüm sahələri ehtiva edir:
    1. Uyğun olan - nümunə url, bu, müəyyən məzmun skriptinin daxil edilib-edilməyəcəyini müəyyən edir.
    2. js — bu matça yüklənəcək skriptlərin siyahısı;
    3. uyğunluqları istisna edin - sahədən xaric edir match Bu sahəyə uyğun olan URL-lər.
  3. səhifə_fəaliyyəti - əslində brauzerdə ünvan çubuğunun yanında göstərilən işarəyə və onunla qarşılıqlı əlaqəyə cavabdeh olan obyektdir. O, həmçinin öz HTML, CSS və JS-dən istifadə etməklə müəyyən edilən popup pəncərəni göstərməyə imkan verir.
    1. default_popup — popup interfeysi ilə HTML faylına gedən yol, CSS və JS-dən ibarət ola bilər.
  4. icazələrin — uzadılma hüquqlarını idarə etmək üçün massiv. Təfərrüatlı şəkildə təsvir olunan 3 hüquq növü var burada
  5. web_accessible_resources — veb səhifənin tələb edə biləcəyi genişləndirmə resursları, məsələn, şəkillər, JS, CSS, HTML faylları.
  6. xaricdən_bağlana bilər — burada siz qoşula biləcəyiniz veb səhifələrin digər genişləndirmələrinin və domenlərinin identifikatorlarını açıq şəkildə göstərə bilərsiniz. Domen ikinci və ya daha yüksək səviyyəli ola bilər. Firefox-da işləmir.

İcra konteksti

Genişlənmə üç kod icra kontekstinə malikdir, yəni proqram brauzer API-yə müxtəlif səviyyələrdə çıxışı olan üç hissədən ibarətdir.

Genişləndirmə konteksti

API-nin əksəriyyəti burada mövcuddur. Bu kontekstdə onlar “yaşayırlar”:

  1. Fon səhifəsi — uzantının “arxa uç” hissəsi. Fayl manifestdə “fon” düyməsi ilə müəyyən edilir.
  2. Popup səhifə — genişləndirmə ikonasına kliklədiyiniz zaman görünən popup səhifə. Manifestdə browser_action -> default_popup.
  3. Xüsusi səhifə — uzadılma səhifəsi, görünüşün ayrıca nişanında "yaşayan" chrome-extension://<id_расширения>/customPage.html.

Bu kontekst brauzer pəncərələrindən və nişanlardan asılı olmayaraq mövcuddur. Fon səhifəsi bir nüsxədə mövcuddur və həmişə işləyir (istisna hadisə səhifəsidir, fon skripti hadisə tərəfindən işə salındıqda və icra edildikdən sonra "ölür"). Popup səhifə popup pəncərə açıq olduqda mövcuddur və Xüsusi səhifə — onunla nişan açıq olduqda. Bu kontekstdən digər tablara və onların məzmununa giriş yoxdur.

Məzmun skripti konteksti

Məzmun skripti faylı hər bir brauzer nişanı ilə birlikdə işə salınır. O, genişləndirmə API-sinin bir hissəsinə və veb səhifənin DOM ağacına çıxışı var. Səhifə ilə qarşılıqlı əlaqə üçün cavabdeh olan məzmun skriptləridir. DOM ağacını manipulyasiya edən genişləndirmələr bunu məzmun skriptlərində edir - məsələn, reklam blokerləri və ya tərcüməçilər. Həmçinin, məzmun skripti standart vasitəsilə səhifə ilə əlaqə saxlaya bilər postMessage.

Veb səhifə konteksti

Bu faktiki veb səhifənin özüdür. Bunun genişləndirmə ilə heç bir əlaqəsi yoxdur və bu səhifənin domeni manifestdə açıq şəkildə göstərilmədiyi hallar istisna olmaqla, ora daxil ola bilməz (aşağıda daha ətraflı).

Mesajlaşma

Tətbiqin müxtəlif hissələri bir-biri ilə mesaj mübadiləsi etməlidir. Bunun üçün API var runtime.sendMessage mesaj göndərmək üçün background и tabs.sendMessage səhifəyə mesaj göndərmək üçün (əgər varsa məzmun skripti, popup və ya veb səhifə externally_connectable). Aşağıda Chrome API-yə daxil olmaq üçün bir nümunə verilmişdir.

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

Tam ünsiyyət üçün siz vasitəsilə əlaqələr yarada bilərsiniz runtime.connect. Cavab olaraq alacağıq runtime.Port, ona açıq olan zaman istənilən sayda mesaj göndərə bilərsiniz. Müştəri tərəfində, məsələn, contentscript, belə görünür:

// Опять же 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 və ya 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) {
    ...
});

Tədbir də var onDisconnect və üsul disconnect.

Tətbiq diaqramı

Şəxsi açarları saxlayan, ictimai məlumatlara çıxışı təmin edən (ünvan, açıq açar səhifə ilə əlaqə saxlayır və üçüncü tərəf proqramlarına əməliyyatlar üçün imza tələb etməyə imkan verən) brauzer genişləndirilməsi edək.

Tətbiq inkişafı

Tətbiqimiz həm istifadəçi ilə qarşılıqlı əlaqədə olmalı, həm də səhifəni metodlara zəng etmək üçün (məsələn, əməliyyatları imzalamaq üçün) API ilə təmin etməlidir. Yalnız biri ilə məşğul olun contentscript işləməyəcək, çünki onun yalnız DOM-a girişi var, lakin səhifənin JS-ə daxil deyil. vasitəsilə əlaqə saxlayın runtime.connect edə bilmərik, çünki API bütün domenlərdə lazımdır və manifestdə yalnız konkret olanlar göstərilə bilər. Nəticədə diaqram bu kimi görünəcək:

Təhlükəsiz brauzer uzantısının yazılması

Başqa bir ssenari olacaq - inpage, biz səhifəyə yeridəcəyik. O, öz kontekstində işləyəcək və genişləndirmə ilə işləmək üçün API təmin edəcək.

Start

Bütün brauzer genişləndirmə kodu burada mövcuddur Github. Təsvir zamanı öhdəliklərə keçidlər olacaq.

Manifestdən başlayaq:

{
  // Имя и описание, версия. Все это будет видно в браузере в 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ş background.js, popup.js, inpage.js və contentscript.js yaradın. Biz popup.html əlavə edirik - və tətbiqimiz artıq Google Chrome-a yüklənə bilər və onun işlədiyinə əmin olun.

Bunu yoxlamaq üçün kodu götürə bilərsiniz buradan. Etdiklərimizə əlavə olaraq, link veb paketindən istifadə edərək layihənin montajını konfiqurasiya etdi. Brauzerə bir proqram əlavə etmək üçün, chrome://extensions-da yüklənməmiş yükləməni və müvafiq uzantılı qovluğu seçməlisiniz - bizim vəziyyətimizdə dist.

Təhlükəsiz brauzer uzantısının yazılması

İndi genişləndirməmiz quraşdırılıb və işləyir. Müxtəlif kontekstlər üçün tərtibatçı alətlərini aşağıdakı kimi işlədə bilərsiniz:

popup ->

Təhlükəsiz brauzer uzantısının yazılması

Məzmun skript konsoluna giriş onun işə salındığı səhifənin özünün konsolu vasitəsilə həyata keçirilir.Təhlükəsiz brauzer uzantısının yazılması

Mesajlaşma

Beləliklə, iki kommunikasiya kanalı qurmalıyıq: səhifədə <-> fon və popup <-> fon. Siz əlbəttə ki, sadəcə limana mesaj göndərə və öz protokolunuzu icad edə bilərsiniz, lakin mən metamask açıq mənbə layihəsində gördüyüm yanaşmaya üstünlük verirəm.

Bu Ethereum şəbəkəsi ilə işləmək üçün brauzer uzantısıdır. Orada proqramın müxtəlif hissələri dnode kitabxanasından istifadə edərək RPC vasitəsilə əlaqə qurur. Nəqliyyat kimi nodejs axını (eyni interfeysi həyata keçirən obyekt deməkdir) ilə təmin etsəniz, mübadiləni olduqca tez və rahat şəkildə təşkil etməyə imkan verir:

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

İndi bir tətbiq sinfi yaradacağıq. O, pop-up və veb səhifə üçün API obyektləri yaradacaq və onlar üçün dnode yaradacaq:

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

Burada və aşağıda qlobal Chrome obyekti əvəzinə Google brauzerində Chrome-a, digərlərində isə brauzerə daxil olan extensionApi-dən istifadə edirik. Bu, cross-brauzer uyğunluğu üçün edilir, lakin bu məqalənin məqsədləri üçün siz sadəcə olaraq 'chrome.runtime.connect' istifadə edə bilərsiniz.

Arxa fon skriptində tətbiq nümunəsi yaradaq:

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 axınlarla işlədiyindən və biz port aldığımız üçün adapter sinfinə ehtiyac var. Brauzerdə nodejs axınlarını həyata keçirən oxuna bilən axın kitabxanasından istifadə etməklə hazırlanır:

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

İndi UI-də bir əlaqə yaradaq:

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

Sonra məzmun skriptində əlaqə yaradırıq:

import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import PostMessageStream from 'post-message-stream';

setupConnection();
injectScript();

function setupConnection(){
    const backgroundPort = extensionApi.runtime.connect({name: 'contentscript'});
    const backgroundStream = new PortStream(backgroundPort);

    const pageStream = new PostMessageStream({
        name: 'content',
        target: 'page',
    });

    pageStream.pipe(backgroundStream).pipe(pageStream);
}

function injectScript(){
    try {
        // inject in-page script
        let script = document.createElement('script');
        script.src = extensionApi.extension.getURL('inpage.js');
        const container = document.head || document.documentElement;
        container.insertBefore(script, container.children[0]);
        script.onload = () => script.remove();
    } catch (e) {
        console.error('Injection failed.', e);
    }
}

API-yə məzmun skriptində deyil, birbaşa səhifədə ehtiyacımız olduğundan, biz iki şeyi edirik:

  1. İki axın yaradırıq. Biri - səhifəyə doğru, mesajın üstündə. Bunun üçün biz bundan istifadə edirik bu paket metamask yaradıcılarından. İkinci axın, alınan portun fonundadır runtime.connect. Gəlin onları alaq. İndi səhifənin arxa plana keçməsi olacaq.
  2. Skripti DOM-a daxil edin. Skripti yükləyin (manifestdə ona girişə icazə verilib) və teq yaradın script içərisindəki məzmunu ilə:

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

İndi səhifədə api obyekti yaradırıq və onu qlobal olaraq təyin edirik:

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 hazırıq Səhifə və UI üçün ayrıca API ilə Uzaqdan Prosedur Zəngi (RPC).. Yeni səhifəni fona bağlayarkən bunu görə bilərik:

Təhlükəsiz brauzer uzantısının yazılması

Boş API və mənşə. Səhifə tərəfində biz salam funksiyasını belə çağıra bilərik:

Təhlükəsiz brauzer uzantısının yazılması

Müasir JS-də geri çağırış funksiyaları ilə işləmək pis davranışdır, ona görə də API obyektini utils-ə ötürməyə imkan verən dnode yaratmaq üçün kiçik köməkçi yazaq.

API obyektləri indi belə görünəcək:

export class SignerApp {

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

...

}

Uzaqdan belə bir obyekt əldə etmək:

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

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

Və funksiyaları çağırmaq vəd qaytarır:

Təhlükəsiz brauzer uzantısının yazılması

Asinxron funksiyaları olan versiya mövcuddur burada.

Ümumilikdə, RPC və axın yanaşması olduqca çevik görünür: biz buxar multipleksindən istifadə edə və müxtəlif tapşırıqlar üçün bir neçə fərqli API yarada bilərik. Prinsipcə, dnode hər yerdə istifadə edilə bilər, əsas odur ki, nəqliyyatı nodejs axını şəklində bükün.

Alternativ olaraq JSON RPC 2 protokolunu həyata keçirən JSON formatıdır.Lakin o, xüsusi nəqliyyatlarla (TCP və HTTP(S)) işləyir ki, bu da bizim halda tətbiq olunmur.

Daxili dövlət və yerli Storage

Tətbiqin daxili vəziyyətini saxlamalıyıq - ən azı imza açarları. Açılan API-də tətbiqə vəziyyəti və onu dəyişdirmək üsullarını asanlıqla əlavə edə bilərik:

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

    ...

} 

Arxa planda biz hər şeyi bir funksiyaya bükəcəyik və proqram obyektini pəncərəyə yazacağıq ki, onunla konsoldan işləyə bilək:

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

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

setupApp();

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

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

    extensionApi.runtime.onConnect.addListener(connectRemote);

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

UI konsolundan bir neçə açar əlavə edək və vəziyyətlə nə baş verdiyini görək:

Təhlükəsiz brauzer uzantısının yazılması

Yenidən başladıqda açarların itirilməməsi üçün vəziyyəti davamlı etmək lazımdır.

Biz onu localStorage-də saxlayacağıq, hər dəyişiklikdə onun üzərinə yazacağıq. Sonradan ona giriş UI üçün də lazım olacaq və mən də dəyişikliklərə abunə olmaq istərdim. Buna əsaslanaraq, müşahidə olunan yaddaş yaratmaq və onun dəyişikliklərinə abunə olmaq rahat olacaq.

mobx kitabxanasından istifadə edəcəyik (https://github.com/mobxjs/mobx). Seçim onun üzərinə düşdü, çünki onunla işləmək məcburiyyətində deyildim, amma həqiqətən öyrənmək istəyirdim.

Gəlin ilkin vəziyyətin başlanğıcını əlavə edək və mağazanı müşahidə edilə bilən edək:

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

    ...

}

"Başlıq altında" mobx bütün mağaza sahələrini proksi ilə əvəz etdi və onlara edilən bütün zəngləri kəsdi. Bu mesajlara abunə olmaq mümkün olacaq.

Aşağıda mən tez-tez "dəyişən zaman" ifadəsindən istifadə edəcəyəm, baxmayaraq ki, bu tamamilə düzgün deyil. Mobx sahələrə girişi izləyir. Kitabxananın yaratdığı proxy obyektlərin alıcıları və təyinçiləri istifadə olunur.

Fəaliyyət dekoratorları iki məqsədə xidmət edir:

  1. EnforceActions bayrağı ilə ciddi rejimdə mobx vəziyyəti birbaşa dəyişməyi qadağan edir. Ciddi şərtlər altında işləmək yaxşı təcrübə hesab olunur.
  2. Bir funksiya vəziyyəti bir neçə dəfə dəyişdirsə belə - məsələn, bir neçə kod sətirində bir neçə sahəni dəyişdiririk - müşahidəçilərə yalnız tamamlandıqda xəbər verilir. Bu, lazımsız vəziyyət yeniləmələrinin elementlərin lazımsız göstərilməsinə səbəb olduğu frontend üçün xüsusilə vacibdir. Bizim vəziyyətimizdə nə birinci, nə də ikinci xüsusilə aktual deyil, lakin biz ən yaxşı təcrübələrə əməl edəcəyik. Müşahidə olunan sahələrin vəziyyətini dəyişdirən bütün funksiyalara dekorativləri əlavə etmək adətdir.

Arxa planda biz inisializasiya əlavə edəcəyik və vəziyyəti localStorage-də saxlayacağıq:

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

Burada reaksiya funksiyası maraqlıdır. Bunun iki arqumenti var:

  1. Məlumat seçicisi.
  2. Hər dəfə dəyişdikdə bu məlumatla çağırılacaq işləyici.

Vəziyyəti arqument kimi açıq şəkildə qəbul etdiyimiz reduxdan fərqli olaraq, mobx selektor daxilində hansı müşahidə olunanlara daxil olduğumuzu xatırlayır və yalnız onlar dəyişdikdə işləyicini çağırır.

Mobx-in hansı müşahidə olunanlara abunə olduğumuza necə qərar verdiyini dəqiq başa düşmək vacibdir. Kodda belə bir selektor yazsaydım() => app.store, onda reaksiya heç vaxt çağırılmayacaq, çünki saxlama özü müşahidə olunmur, yalnız onun sahələridir.

Mən belə yazsaydım () => app.store.keys, sonra yenə heç nə baş verməyəcək, çünki massiv elementlərini əlavə edərkən/çıxararkən ona istinad dəyişməyəcək.

Mobx ilk dəfə seçici kimi çıxış edir və yalnız bizim əldə etdiyimiz müşahidə olunanları izləyir. Bu, proxy alıcıları vasitəsilə edilir. Buna görə də burada daxili funksiyadan istifadə olunur toJS. O, orijinal sahələrlə əvəz edilmiş bütün proksilərlə yeni obyekti qaytarır. İcra zamanı o, obyektin bütün sahələrini oxuyur - buna görə də alıcılar işə salınır.

Açılan konsolda yenidən bir neçə açar əlavə edəcəyik. Bu dəfə onlar da localStorage-də sona çatdılar:

Təhlükəsiz brauzer uzantısının yazılması

Fon səhifəsi yenidən yükləndikdə məlumat yerində qalır.

Bu nöqtəyə qədər bütün proqram koduna baxmaq olar burada.

Şəxsi açarların təhlükəsiz saxlanması

Şəxsi açarların aydın mətndə saxlanması təhlükəlidir: sındırılma, kompüterinizə giriş əldə etmək və s. şansınız həmişə var. Buna görə də, localStorage-də açarları parolla şifrələnmiş formada saxlayacağıq.

Daha çox təhlükəsizlik üçün proqrama kilidli bir vəziyyət əlavə edəcəyik, burada açarlara ümumiyyətlə giriş olmayacaqdır. Vaxt aşımı səbəbindən uzadmanı avtomatik olaraq kilidli vəziyyətə keçirəcəyik.

Mobx yalnız minimum məlumat dəstini saxlamağa imkan verir, qalanları isə ona əsasən avtomatik olaraq hesablanır. Bunlar sözdə hesablanmış xüsusiyyətlərdir. Onları verilənlər bazasındakı baxışlarla müqayisə etmək olar:

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

İndi biz yalnız şifrələnmiş açarları və parolu saxlayırıq. Qalan hər şey hesablanır. Şifrəni dövlətdən silməklə kilidli vəziyyətə köçürməni həyata keçiririk. İctimai API indi yaddaşı işə salmaq üçün bir üsula malikdir.

Şifrələmə üçün yazılmışdır kripto-js istifadə edən kommunal xidmətlər:

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

Brauzerdə bir hadisəyə - vəziyyət dəyişikliklərinə abunə ola biləcəyiniz boş API var. Dövlət buna görə ola bilər idle, active и locked. Boş rejimdə siz vaxt aşımı təyin edə bilərsiniz və OS özü bloklandıqda kilidlənir. Biz həmçinin localStorage-da saxlamaq üçün selektoru dəyişəcəyik:

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

Bu addımdan əvvəl koddur burada.

Əməliyyatlar

Beləliklə, biz ən vacib şeyə gəlirik: blockchain-də əməliyyatlar yaratmaq və imzalamaq. WAVES blokçeynindən və kitabxanasından istifadə edəcəyik dalğalar-əməliyyatlar.

Əvvəlcə dövlətə imzalanması lazım olan bir sıra mesajlar əlavə edək, sonra yeni mesaj əlavə etmək, imzanı təsdiqləmək və imtina etmək üsullarını əlavə edək:

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

    ...
}

Yeni mesaj aldıqda ona metadata əlavə edirik, edirik observable və əlavə edin store.messages.

Əgər yoxsa observable Əllə, sonra seriala mesajlar əlavə edərkən mobx bunu özü edəcək. Bununla belə, o, istinadımız olmayan yeni bir obyekt yaradacaq, lakin növbəti addım üçün bizə lazım olacaq.

Sonra, mesaj statusu dəyişdikdə həll olunan vədi qaytarırıq. Status reaksiya ilə izlənilir, status dəyişdikdə "özünü öldürəcək".

Metod kodu approve и reject çox sadə: biz sadəcə mesajın statusunu, lazım gələrsə, imzaladıqdan sonra dəyişirik.

UI API-də Təsdiq et və rədd et, API səhifəsinə newMessage qoyuruq:

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

    ...
}

İndi genişlənmə ilə əməliyyatı imzalamağa çalışaq:

Təhlükəsiz brauzer uzantısının yazılması

Ümumiyyətlə, hər şey hazırdır, qalan hər şeydir sadə UI əlavə edin.

UI

İnterfeys proqram vəziyyətinə giriş tələb edir. UI tərəfində biz edəcəyik observable vəziyyəti və API-yə bu vəziyyəti dəyişdirəcək bir funksiya əlavə edin. əlavə edək observable fondan alınan API obyektinə:

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

Sonda proqram interfeysini göstərməyə başlayırıq. Bu reaksiya tətbiqidir. Fon obyekti sadəcə rekvizitlərdən istifadə etməklə ötürülür. Əlbəttə ki, üsullar üçün ayrıca bir xidmət və dövlət üçün bir mağaza etmək düzgün olardı, lakin bu məqalənin məqsədləri üçün bu kifayətdir:

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 ilə data dəyişdikdə göstərməyə başlamaq çox asandır. Biz sadəcə olaraq paketdən müşahidəçi dekoratorunu asırıq mobx-reaksiya komponentdə istinad edilir və komponent dəyişikliyi ilə istinad edilən hər hansı müşahidə olunanlar avtomatik olaraq çağırılacaq. Sizə heç bir mapStateToProps lazım deyil və ya redux-da olduğu kimi qoşulun. Hər şey qutudan dərhal işləyir:

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

Qalan komponentlərə kodda baxmaq olar UI qovluğunda.

İndi tətbiq sinfində UI üçün dövlət seçicisi etməli və dəyişdikdə UI-ni xəbərdar etməlisiniz. Bunun üçün bir üsul əlavə edək getState и reactionzəng edir 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())

        })
    }

    ...
}

Bir obyekti qəbul edərkən remote yaradılır reaction UI tərəfində funksiyanı çağıran vəziyyəti dəyişdirmək üçün.

Son toxunuş, genişləndirmə simvoluna yeni mesajların ekranını əlavə etməkdir:

function setupApp() {
...

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

...
}

Beləliklə, ərizə hazırdır. Veb səhifələr əməliyyatlar üçün imza tələb edə bilər:

Təhlükəsiz brauzer uzantısının yazılması

Təhlükəsiz brauzer uzantısının yazılması

Kod burada mövcuddur əlaqə.

Nəticə

Məqaləni sona qədər oxumusunuzsa, lakin hələ də suallarınız varsa, onlara ünvanından soruşa bilərsiniz uzadılması ilə depolar. Orada, həmçinin hər bir təyin olunmuş addım üçün öhdəliklər tapa bilərsiniz.

Əgər siz faktiki genişləndirmə koduna baxmaqla maraqlanırsınızsa, bunu tapa bilərsiniz burada.

Kod, anbar və iş təsviri siemarell

Mənbə: www.habr.com

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