Ի տարբերություն ընդհանուր «հաճախորդ-սերվեր» ճարտարապետության, ապակենտրոնացված հավելվածները բնութագրվում են.
Օգտատիրոջ մուտքաբառերով և գաղտնաբառերով տվյալների բազա պահելու կարիք չկա: Մուտքի տեղեկատվությունը պահվում է բացառապես հենց իրենք՝ օգտատերերի կողմից, և դրանց իսկության հաստատումը տեղի է ունենում արձանագրության մակարդակով:
Սերվեր օգտագործելու կարիք չկա: Հավելվածի տրամաբանությունը կարող է իրականացվել բլոկչեյն ցանցում, որտեղ հնարավոր է պահպանել անհրաժեշտ քանակությամբ տվյալներ։
Օգտագործողի բանալիների համար կա 2 համեմատաբար անվտանգ պահեստ՝ ապարատային դրամապանակներ և բրաուզերի ընդլայնումներ: Սարքավորումների դրամապանակները հիմնականում չափազանց անվտանգ են, բայց դժվար է օգտագործել և հեռու են անվճար լինելուց, սակայն բրաուզերի ընդլայնումները անվտանգության և օգտագործման հեշտության կատարյալ համադրություն են, ինչպես նաև կարող են լիովին անվճար լինել վերջնական օգտագործողների համար:
Այս ամենը հաշվի առնելով՝ մենք ցանկացանք ստեղծել ամենաապահով ընդլայնումը, որը հեշտացնում է ապակենտրոնացված հավելվածների զարգացումը` ապահովելով գործարքների և ստորագրությունների հետ աշխատելու պարզ API:
Այս փորձառության մասին կպատմենք ստորև։
Հոդվածը պարունակում է քայլ առ քայլ հրահանգներ, թե ինչպես գրել բրաուզերի ընդլայնում, կոդերի օրինակներով և սքրինշոթներով: Դուք կարող եք գտնել ամբողջ կոդը պահոցներ. Յուրաքանչյուր պարտավորություն տրամաբանորեն համապատասխանում է այս հոդվածի մի հատվածին:
Բրաուզերի ընդլայնումների համառոտ պատմություն
Բրաուզերի ընդլայնումները վաղուց են եղել: Internet Explorer-ում դրանք հայտնվել են դեռ 1999 թվականին, Firefox-ում՝ 2004 թվականին։ Այնուամենայնիվ, երկար ժամանակ ընդլայնումների համար մեկ ստանդարտ չկար:
Կարելի է ասել, որ այն ընդլայնումների հետ միասին հայտնվել է Google Chrome-ի չորրորդ տարբերակում։ Իհարկե, այն ժամանակ հստակեցում չկար, բայց դա Chrome API-ն էր, որը դարձավ դրա հիմքը. նվաճելով բրաուզերի շուկայի մեծ մասը և ունենալով ներկառուցված հավելվածների խանութ, Chrome-ն իրականում սահմանեց բրաուզերի ընդլայնումների չափանիշը:
Mozilla-ն ուներ իր ստանդարտը, բայց տեսնելով Chrome-ի ընդլայնումների հանրաճանաչությունը, ընկերությունը որոշեց ստեղծել համատեղելի API: 2015 թվականին Mozilla-ի նախաձեռնությամբ Համաշխարհային ցանցի կոնսորցիումի (W3C) շրջանակներում ստեղծվեց հատուկ խումբ՝ աշխատելու բրաուզերի ընդլայնման առանձնահատկությունների վրա:
Որպես հիմք ընդունվել են Chrome-ի համար գոյություն ունեցող API ընդլայնումները։ Աշխատանքն իրականացվել է Microsoft-ի աջակցությամբ (Google-ը հրաժարվել է մասնակցել ստանդարտի մշակմանը), և արդյունքում հայտնվել է նախագիծ. բնութագրերը.
Ֆորմալ կերպով, ճշգրտումն ապահովվում է Edge-ի, Firefox-ի և Opera-ի կողմից (նկատի ունեցեք, որ Chrome-ն այս ցանկում չէ): Բայց իրականում ստանդարտը հիմնականում համատեղելի է Chrome-ի հետ, քանի որ այն իրականում գրված է իր ընդլայնումների հիման վրա: Դուք կարող եք ավելին կարդալ WebExtensions API-ի մասին այստեղ.
Ընդլայնման կառուցվածքը
Միակ ֆայլը, որը պահանջվում է ընդլայնման համար, մանիֆեստն է (manifest.json): Դա նաև ընդլայնման «մուտքի կետն» է:
Մանիֆեստ
Ըստ ճշգրտման, մանիֆեստի ֆայլը վավեր JSON ֆայլ է: Մանիֆեստի ստեղների ամբողջական նկարագրությունը՝ տեղեկություններով, թե որ ստեղները կարող են դիտվել դիտարկիչում այստեղ.
Բանալիները, որոնք «կարող են» բնութագրում չգտնվեն (ինչպես Chrome-ը, այնպես էլ Firefox-ը հաղորդում են սխալներ, սակայն ընդլայնումները շարունակում են աշխատել):
Եվ ես կցանկանայի ուշադրություն հրավիրել որոշ կետերի վրա.
ֆոն - օբյեկտ, որը ներառում է հետևյալ դաշտերը.
Հաղորդագրություն — սկրիպտների զանգված, որը կկատարվի ֆոնային համատեքստում (այս մասին կխոսենք մի փոքր ուշ);
էջ - սկրիպտների փոխարեն, որոնք կկատարվեն դատարկ էջում, կարող եք նշել html բովանդակությամբ: Այս դեպքում սցենարի դաշտը անտեսվելու է, և սկրիպտները պետք է տեղադրվեն բովանդակության էջում;
դիմանալ — երկուական դրոշակ, եթե նշված չէ, զննարկիչը «կսպանի» ֆոնային գործընթացը, երբ համարի, որ ոչինչ չի անում, և անհրաժեշտության դեպքում կվերագործարկի այն: Հակառակ դեպքում էջը կբեռնաթափվի միայն զննարկիչը փակելու դեպքում: Firefox-ում չի աջակցվում:
բովանդակության_սկրիպտներ — օբյեկտների զանգված, որը թույլ է տալիս բեռնել տարբեր սկրիպտներ տարբեր վեբ էջերում: Յուրաքանչյուր օբյեկտ պարունակում է հետևյալ կարևոր դաշտերը.
հանդիպումները - օրինակի url, որը որոշում է, թե արդյոք որոշակի բովանդակության սցենար կներառվի, թե ոչ:
js — սկրիպտների ցանկ, որոնք կբեռնվեն այս համընկնման մեջ.
exclude_matches - բացառում է դաշտից match URL-ներ, որոնք համապատասխանում են այս դաշտին:
page_action - իրականում օբյեկտ է, որը պատասխանատու է դիտարկիչում հասցեագոտի կողքին ցուցադրվող պատկերակի և դրա հետ փոխազդեցության համար: Այն նաև թույլ է տալիս ցուցադրել թռուցիկ պատուհան, որը սահմանվում է՝ օգտագործելով ձեր սեփական HTML, CSS և JS:
default_popup — ուղին դեպի HTML ֆայլ՝ թռուցիկ ինտերֆեյսով, կարող է պարունակել CSS և JS:
թույլտվությունները — ընդլայնման իրավունքները կառավարելու զանգված: Կան իրավունքների 3 տեսակ, որոնք մանրամասն նկարագրված են այստեղ
web_accessible_resources — ընդլայնման ռեսուրսներ, որոնք կարող է պահանջել վեբ էջը, օրինակ՝ պատկերներ, JS, CSS, HTML ֆայլեր:
արտաքին_միացվող — այստեղ դուք կարող եք հստակորեն նշել վեբ էջերի այլ ընդլայնումների և տիրույթների ID-ները, որոնցից կարող եք միանալ: Դոմենը կարող է լինել երկրորդ կամ ավելի բարձր մակարդակ: Չի աշխատում Firefox-ում:
Կատարման համատեքստ
Ընդլայնումը ունի երեք կոդի կատարման համատեքստ, այսինքն՝ հավելվածը բաղկացած է երեք մասից՝ բրաուզերի API-ի հասանելիության տարբեր մակարդակներով։
Ընդլայնման համատեքստ
API-ի մեծ մասը հասանելի է այստեղ: Այս համատեքստում նրանք «ապրում են».
Ֆոնային էջ — ընդլայնման «backend» մաս: Ֆայլը նշված է մանիֆեստում՝ օգտագործելով «ֆոն» ստեղնը:
Բացվող էջ — թռուցիկ էջ, որը հայտնվում է ընդլայնման պատկերակի վրա սեղմելիս: Մանիֆեստում browser_action -> default_popup.
Պատվերով էջ — ընդլայնման էջ, «ապրել» դիտման առանձին ներդիրում chrome-extension://<id_расширения>/customPage.html.
Այս համատեքստը գոյություն ունի անկախ դիտարկիչի պատուհաններից և ներդիրներից: Ֆոնային էջ գոյություն ունի մեկ օրինակով և միշտ աշխատում է (բացառություն է հանդիսանում իրադարձության էջը, երբ ֆոնային սցենարը գործարկվում է իրադարձության միջոցով և «մեռնում» դրա կատարումից հետո): Բացվող էջ գոյություն ունի, երբ բացվող պատուհանը բաց է, և Պատվերով էջ — մինչ դրա հետ ներդիրը բաց է: Այս համատեքստից այլ ներդիրներին և դրանց բովանդակությանը հասանելիություն չկա:
Բովանդակության սցենարի համատեքստ
Բովանդակության սցենարի ֆայլը գործարկվում է դիտարկիչի յուրաքանչյուր ներդիրի հետ միասին: Այն հասանելի է ընդլայնման API-ի մի մասին և վեբ էջի DOM ծառին: Դա բովանդակության սցենարներ են, որոնք պատասխանատու են էջի հետ փոխգործակցության համար: Ընդլայնումները, որոնք շահարկում են DOM ծառը, դա անում են բովանդակության սկրիպտներում, օրինակ՝ գովազդային արգելափակումներ կամ թարգմանիչներ: Բացի այդ, բովանդակության սցենարը կարող է էջի հետ հաղորդակցվել ստանդարտի միջոցով postMessage.
Վեբ էջի համատեքստ
Սա բուն վեբ էջն է: Այն կապ չունի ընդլայնման հետ և մուտք չունի այնտեղ, բացառությամբ այն դեպքերի, երբ այս էջի տիրույթը հստակ նշված չէ մանիֆեստում (այս մասին ավելին ստորև):
Հաղորդագրություններ
Հավելվածի տարբեր մասերը պետք է հաղորդագրություններ փոխանակեն միմյանց հետ: Դրա համար կա API runtime.sendMessage հաղորդագրություն ուղարկելու համար background и tabs.sendMessage էջին հաղորդագրություն ուղարկելու համար (բովանդակության սցենար, թռուցիկ կամ վեբ էջ, եթե առկա է): externally_connectable) Ստորև բերված է Chrome API-ին մուտք գործելու օրինակ:
// Сообщением может быть любой JSON сериализуемый объект
const msg = {a: 'foo', b: 'bar'};
// extensionId можно не указывать, если мы хотим послать сообщение 'своему' расширению (из ui или контент скрипта)
chrome.runtime.sendMessage(extensionId, msg);
// Так выглядит обработчик
chrome.runtime.onMessage.addListener((msg) => console.log(msg))
// Можно слать сообщения вкладкам зная их id
chrome.tabs.sendMessage(tabId, msg)
// Получить к вкладкам и их id можно, например, вот так
chrome.tabs.query(
{currentWindow: true, active : true},
function(tabArray){
tabArray.forEach(tab => console.log(tab.id))
}
)
Ամբողջական հաղորդակցության համար կարող եք կապեր ստեղծել միջոցով runtime.connect. Ի պատասխան մենք կստանանք runtime.Port, որին, քանի դեռ այն բաց է, կարող եք ուղարկել ցանկացած քանակությամբ հաղորդագրություն: Հաճախորդի կողմից, օրինակ, contentscript, այն ունի հետևյալ տեսքը.
// Опять же extensionId можно не указывать при коммуникации внутри одного расширения. Подключение можно именовать
const port = chrome.runtime.connect({name: "knockknock"});
port.postMessage({joke: "Knock knock"});
port.onMessage.addListener(function(msg) {
if (msg.question === "Who's there?")
port.postMessage({answer: "Madame"});
else if (msg.question === "Madame who?")
port.postMessage({answer: "Madame... Bovary"});
Սերվեր կամ ֆոն.
// Обработчик для подключения 'своих' вкладок. Контент скриптов, popup или страниц расширения
chrome.runtime.onConnect.addListener(function(port) {
console.assert(port.name === "knockknock");
port.onMessage.addListener(function(msg) {
if (msg.joke === "Knock knock")
port.postMessage({question: "Who's there?"});
else if (msg.answer === "Madame")
port.postMessage({question: "Madame who?"});
else if (msg.answer === "Madame... Bovary")
port.postMessage({question: "I don't get it."});
});
});
// Обработчик для подключения внешних вкладок. Других расширений или веб страниц, которым разрешен доступ в манифесте
chrome.runtime.onConnectExternal.addListener(function(port) {
...
});
Կա նաև միջոցառում onDisconnect և մեթոդ disconnect.
Կիրառման դիագրամ
Եկեք ստեղծենք զննարկչի ընդլայնում, որը պահում է մասնավոր բանալիներ, ապահովում է հանրային տեղեկատվության հասանելիություն (հասցե, հանրային բանալին հաղորդակցվում է էջի հետ և թույլ է տալիս երրորդ կողմի հավելվածներին ստորագրություն խնդրել գործարքների համար:
Հավելվածի մշակում
Մեր հավելվածը պետք է և՛ փոխազդի օգտագործողի հետ, և՛ էջին տրամադրի API՝ մեթոդներ կանչելու համար (օրինակ՝ գործարքներ ստորագրելու համար): Կատարեք միայն մեկով contentscript չի աշխատի, քանի որ այն մուտք ունի միայն DOM-ին, բայց ոչ էջի JS-ին: Միացեք միջոցով runtime.connect մենք չենք կարող, քանի որ API-ն անհրաժեշտ է բոլոր տիրույթներում, և մանիֆեստում կարող են նշվել միայն կոնկրետները: Արդյունքում դիագրամը կունենա հետևյալ տեսքը.
Կլինի մեկ այլ սցենար. inpage, որը մենք կներարկենք էջը։ Այն կաշխատի իր համատեքստում և կտրամադրի API ընդլայնման հետ աշխատելու համար:
Начало
Բրաուզերի ընդլայնման բոլոր ծածկագրերը հասանելի են այստեղ GitHub. Նկարագրության ընթացքում կլինեն հղումներ դեպի պարտավորություններ:
Սկսենք մանիֆեստից.
{
// Имя и описание, версия. Все это будет видно в браузере в chrome://extensions/?id=<id расширения>
"name": "Signer",
"description": "Extension demo",
"version": "0.0.1",
"manifest_version": 2,
// Скрипты, которые будут исполнятся в background, их может быть несколько
"background": {
"scripts": ["background.js"]
},
// Какой html использовать для popup
"browser_action": {
"default_title": "My Extension",
"default_popup": "popup.html"
},
// Контент скрипты.
// У нас один объект: для всех url начинающихся с http или https мы запускаем
// contenscript context со скриптом contentscript.js. Запускать сразу по получении документа для всех фреймов
"content_scripts": [
{
"matches": [
"http://*/*",
"https://*/*"
],
"js": [
"contentscript.js"
],
"run_at": "document_start",
"all_frames": true
}
],
// Разрешен доступ к localStorage и idle api
"permissions": [
"storage",
// "unlimitedStorage",
//"clipboardWrite",
"idle"
//"activeTab",
//"webRequest",
//"notifications",
//"tabs"
],
// Здесь указываются ресурсы, к которым будет иметь доступ веб страница. Тоесть их можно будет запрашивать fetche'м или просто xhr
"web_accessible_resources": ["inpage.js"]
}
Ստեղծեք դատարկ background.js, popup.js, inpage.js և contentscript.js: Մենք ավելացնում ենք popup.html - և մեր հավելվածն արդեն կարող է բեռնվել Google Chrome-ում և համոզվել, որ այն աշխատում է:
Սա հաստատելու համար կարող եք վերցնել կոդը ուստի. Ի լրումն այն, ինչ մենք արեցինք, հղումը կազմաձևեց նախագծի հավաքումը վեբ փաթեթի միջոցով: Բրաուզերում հավելված ավելացնելու համար chrome://extensions-ում պետք է ընտրել load unpacked և համապատասխան ընդլայնումով թղթապանակը, մեր դեպքում՝ dist:
Այժմ մեր ընդլայնումը տեղադրված է և աշխատում է: Դուք կարող եք գործարկել մշակողի գործիքները տարբեր համատեքստերի համար հետևյալ կերպ.
թռուցիկ ->
Բովանդակության սցենարի կոնսոլին հասանելիությունն իրականացվում է հենց այն էջի վահանակի միջոցով, որի վրա այն գործարկվել է:
Հաղորդագրություններ
Այսպիսով, մենք պետք է երկու հաղորդակցման ուղիներ հաստատենք՝ ներէջ <-> ֆոն և թռուցիկ <-> ֆոն: Դուք, իհարկե, կարող եք պարզապես հաղորդագրություններ ուղարկել նավահանգիստ և հորինել ձեր սեփական արձանագրությունը, բայց ես նախընտրում եմ այն մոտեցումը, որը տեսա metamask բաց կոդով նախագծում:
Սա բրաուզերի ընդլայնում է Ethereum ցանցի հետ աշխատելու համար: Դրանում հավելվածի տարբեր մասեր շփվում են RPC-ի միջոցով՝ օգտագործելով dnode գրադարանը։ Այն թույլ է տալիս կազմակերպել փոխանակում բավականին արագ և հարմար, եթե դրան տրամադրում եք nodejs հոսք՝ որպես տրանսպորտ (նշանակում է նույն ինտերֆեյսն իրականացնող օբյեկտ).
import Dnode from "dnode/browser";
// В этом примере условимся что клиент удаленно вызывает функции на сервере, хотя ничего нам не мешает сделать это двунаправленным
// Cервер
// API, которое мы хотим предоставить
const dnode = Dnode({
hello: (cb) => cb(null, "world")
})
// Транспорт, поверх которого будет работать dnode. Любой nodejs стрим. В браузере есть бибилиотека 'readable-stream'
connectionStream.pipe(dnode).pipe(connectionStream)
// Клиент
const dnodeClient = Dnode() // Вызов без агрумента значит что мы не предоставляем API на другой стороне
// Выведет в консоль world
dnodeClient.once('remote', remote => {
remote.hello(((err, value) => console.log(value)))
})
Այժմ մենք կստեղծենք հավելվածի դաս: Այն կստեղծի API օբյեկտներ թռուցիկ և վեբ էջի համար և կստեղծի dnode նրանց համար.
import Dnode from 'dnode/browser';
export class SignerApp {
// Возвращает объект API для ui
popupApi(){
return {
hello: cb => cb(null, 'world')
}
}
// Возвращает объет API для страницы
pageApi(){
return {
hello: cb => cb(null, 'world')
}
}
// Подключает popup ui
connectPopup(connectionStream){
const api = this.popupApi();
const dnode = Dnode(api);
connectionStream.pipe(dnode).pipe(connectionStream);
dnode.on('remote', (remote) => {
console.log(remote)
})
}
// Подключает страницу
connectPage(connectionStream, origin){
const api = this.popupApi();
const dnode = Dnode(api);
connectionStream.pipe(dnode).pipe(connectionStream);
dnode.on('remote', (remote) => {
console.log(origin);
console.log(remote)
})
}
}
Այստեղ և ներքևում, գլոբալ Chrome օբյեկտի փոխարեն, մենք օգտագործում ենք extensionApi, որը մուտք է գործում Chrome-ը Google-ի բրաուզերում, իսկ բրաուզերը՝ մյուսներում: Սա արվում է բրաուզերների համատեղելիության համար, սակայն այս հոդվածի նպատակների համար կարելի է պարզապես օգտագործել «chrome.runtime.connect»:
Եկեք ստեղծենք հավելվածի օրինակ ֆոնային սցենարում.
import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {SignerApp} from "./SignerApp";
const app = new SignerApp();
// onConnect срабатывает при подключении 'процессов' (contentscript, popup, или страница расширения)
extensionApi.runtime.onConnect.addListener(connectRemote);
function connectRemote(remotePort) {
const processName = remotePort.name;
const portStream = new PortStream(remotePort);
// При установке соединения можно указывать имя, по этому имени мы и оппределяем кто к нам подлючился, контентскрипт или ui
if (processName === 'contentscript'){
const origin = remotePort.sender.url
app.connectPage(portStream, origin)
}else{
app.connectPopup(portStream)
}
}
Քանի որ dnode-ն աշխատում է հոսքերի հետ, և մենք ստանում ենք պորտ, անհրաժեշտ է ադապտերների դաս: Այն պատրաստված է ընթերցվող հոսքային գրադարանի միջոցով, որը բրաուզերում իրականացնում է nodejs հոսքեր.
import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import Dnode from 'dnode/browser';
const DEV_MODE = process.env.NODE_ENV !== 'production';
setupUi().catch(console.error);
async function setupUi(){
// Также, как и в классе приложения создаем порт, оборачиваем в stream, делаем dnode
const backgroundPort = extensionApi.runtime.connect({name: 'popup'});
const connectionStream = new PortStream(backgroundPort);
const dnode = Dnode();
connectionStream.pipe(dnode).pipe(connectionStream);
const background = await new Promise(resolve => {
dnode.once('remote', api => {
resolve(api)
})
});
// Делаем объект API доступным из консоли
if (DEV_MODE){
global.background = background;
}
}
Այնուհետև մենք ստեղծում ենք կապը բովանդակության սցենարում.
import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import PostMessageStream from 'post-message-stream';
setupConnection();
injectScript();
function setupConnection(){
const backgroundPort = extensionApi.runtime.connect({name: 'contentscript'});
const backgroundStream = new PortStream(backgroundPort);
const pageStream = new PostMessageStream({
name: 'content',
target: 'page',
});
pageStream.pipe(backgroundStream).pipe(pageStream);
}
function injectScript(){
try {
// inject in-page script
let script = document.createElement('script');
script.src = extensionApi.extension.getURL('inpage.js');
const container = document.head || document.documentElement;
container.insertBefore(script, container.children[0]);
script.onload = () => script.remove();
} catch (e) {
console.error('Injection failed.', e);
}
}
Քանի որ մեզ անհրաժեշտ է API-ն ոչ թե բովանդակության սցենարում, այլ ուղղակիորեն էջում, մենք անում ենք երկու բան.
Մենք ստեղծում ենք երկու հոսք: Մեկ - դեպի էջը, գրառման հաղորդագրության վերևում: Դրա համար մենք օգտագործում ենք սա այս փաթեթը metamask-ի ստեղծողներից: Երկրորդ հոսքը պետք է անցնի նավահանգստից ստացված ֆոնին runtime.connect. Եկեք գնենք դրանք։ Այժմ էջը կունենա հոսք դեպի ֆոն:
Ներարկեք սցենարը DOM-ում: Ներբեռնեք սցենարը (դրա մուտքը թույլատրված էր մանիֆեստում) և ստեղծեք պիտակ script ներսում իր պարունակությամբ.
import PostMessageStream from 'post-message-stream';
import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
setupConnection();
injectScript();
function setupConnection(){
// Стрим к бекграунду
const backgroundPort = extensionApi.runtime.connect({name: 'contentscript'});
const backgroundStream = new PortStream(backgroundPort);
// Стрим к странице
const pageStream = new PostMessageStream({
name: 'content',
target: 'page',
});
pageStream.pipe(backgroundStream).pipe(pageStream);
}
function injectScript(){
try {
// inject in-page script
let script = document.createElement('script');
script.src = extensionApi.extension.getURL('inpage.js');
const container = document.head || document.documentElement;
container.insertBefore(script, container.children[0]);
script.onload = () => script.remove();
} catch (e) {
console.error('Injection failed.', e);
}
}
Այժմ մենք ստեղծում ենք api օբյեկտ inpage-ում և դնում այն գլոբալ.
import PostMessageStream from 'post-message-stream';
import Dnode from 'dnode/browser';
setupInpageApi().catch(console.error);
async function setupInpageApi() {
// Стрим к контентскрипту
const connectionStream = new PostMessageStream({
name: 'page',
target: 'content',
});
const dnode = Dnode();
connectionStream.pipe(dnode).pipe(connectionStream);
// Получаем объект API
const pageApi = await new Promise(resolve => {
dnode.once('remote', api => {
resolve(api)
})
});
// Доступ через window
global.SignerApp = pageApi;
}
Դատարկ API և ծագում: Էջի կողմում մենք կարող ենք անվանել բարևի ֆունկցիան այսպես.
Ժամանակակից JS-ում հետադարձ կապի գործառույթների հետ աշխատելը վատ է, ուստի եկեք գրենք մի փոքրիկ օգնական՝ ստեղծելու dnode, որը թույլ է տալիս Ձեզ փոխանցել API օբյեկտը utils-ին:
import {cbToPromise, transformMethods} from "../../src/utils/setupDnode";
const pageApi = await new Promise(resolve => {
dnode.once('remote', remoteApi => {
// С помощью утилит меняем все callback на promise
resolve(transformMethods(cbToPromise, remoteApi))
})
});
Իսկ ֆունկցիաները կանչելը խոստում է տալիս.
Հասանելի է ասինխրոն գործառույթներով տարբերակ այստեղ.
Ընդհանուր առմամբ, RPC-ի և հոսքի մոտեցումը բավականին ճկուն է թվում. մենք կարող ենք օգտագործել գոլորշու մուլտիպլեքսավորում և ստեղծել մի քանի տարբեր API տարբեր առաջադրանքների համար: Սկզբունքորեն, dnode-ը կարող է օգտագործվել ցանկացած վայրում, հիմնականը տրանսպորտը փաթաթելն է nodejs հոսքի տեսքով:
Այլընտրանքը JSON ձևաչափն է, որն իրականացնում է JSON RPC 2 արձանագրությունը: Այնուամենայնիվ, այն աշխատում է հատուկ փոխադրումների հետ (TCP և HTTP(S)), որը կիրառելի չէ մեր դեպքում:
Ներքին պետական և տեղական պահեստավորում
Մեզ անհրաժեշտ կլինի պահպանել հավելվածի ներքին վիճակը՝ առնվազն ստորագրման բանալիները: Մենք կարող ենք բավականին հեշտությամբ հավելվածին վիճակ ավելացնել և այն փոխելու մեթոդները թռուցիկ API-ում.
Հետին պլանում մենք ամեն ինչ կփաթաթենք ֆունկցիայի մեջ և կգրենք հավելվածի օբյեկտը պատուհանում, որպեսզի կարողանանք աշխատել դրա հետ վահանակից.
import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {SignerApp} from "./SignerApp";
const DEV_MODE = process.env.NODE_ENV !== 'production';
setupApp();
function setupApp() {
const app = new SignerApp();
if (DEV_MODE) {
global.app = app;
}
extensionApi.runtime.onConnect.addListener(connectRemote);
function connectRemote(remotePort) {
const processName = remotePort.name;
const portStream = new PortStream(remotePort);
if (processName === 'contentscript') {
const origin = remotePort.sender.url;
app.connectPage(portStream, origin)
} else {
app.connectPopup(portStream)
}
}
}
Եկեք մի քանի բանալի ավելացնենք UI վահանակից և տեսնենք, թե ինչ է տեղի ունենում պետության հետ.
Պետությունը պետք է համառ դարձնել, որպեսզի վերագործարկման ժամանակ բանալիները չկորչեն։
Մենք այն կպահենք localStorage-ում՝ վերագրելով այն յուրաքանչյուր փոփոխության դեպքում: Հետագայում դրա մուտքը նույնպես անհրաժեշտ կլինի UI-ի համար, և ես նույնպես կցանկանայի բաժանորդագրվել փոփոխություններին: Դրա հիման վրա հարմար կլինի ստեղծել դիտարկելի պահեստ և բաժանորդագրվել դրա փոփոխություններին:
Մենք կօգտագործենք mobx գրադարանը (https://github.com/mobxjs/mobx) Ընտրությունն ընկավ դրա վրա, քանի որ ես ստիպված չէի աշխատել դրա հետ, բայց ես իսկապես ուզում էի ուսումնասիրել այն:
Ավելացնենք սկզբնական վիճակի սկզբնավորումը և խանութը դիտելի դարձնենք.
import {observable, action} from 'mobx';
import {setupDnode} from "./utils/setupDnode";
export class SignerApp {
constructor(initState = {}) {
// Внешне store так и останется тем же объектом, только теперь все его поля стали proxy, которые отслеживают доступ к ним
this.store = observable.object({
keys: initState.keys || [],
});
}
// Методы, которые меняют observable принято оборачивать декоратором
@action
addKey(key) {
this.store.keys.push(key)
}
@action
removeKey(index) {
this.store.keys.splice(index, 1)
}
...
}
«Կապակի տակ», mobx-ը խանութի բոլոր դաշտերը փոխարինել է պրոքսիով և գաղտնալսում է նրանց բոլոր զանգերը: Այս հաղորդագրություններին հնարավոր կլինի բաժանորդագրվել։
Ստորև ես հաճախ կօգտագործեմ «փոխելիս» տերմինը, թեև դա ամբողջովին ճիշտ չէ։ Mobx-ը հետևում է դաշտերի հասանելիությանը: Օգտագործվում են գրադարանի ստեղծած պրոքսի օբյեկտների ստացողներ և կարգավորիչներ:
Գործողությունների դեկորատորները ծառայում են երկու նպատակի.
EnforceActions դրոշով խիստ ռեժիմում mobx-ն արգելում է ուղղակիորեն փոխել վիճակը: Լավ պրակտիկա է համարվում խիստ պայմաններում աշխատելը։
Նույնիսկ եթե ֆունկցիան մի քանի անգամ փոխում է վիճակը, օրինակ, մի քանի դաշտ ենք փոխում կոդի մի քանի տողերում, դիտորդները ծանուցվում են միայն այն ավարտվելուց հետո: Սա հատկապես կարևոր է ճակատային մասի համար, որտեղ վիճակի անհարկի թարմացումները հանգեցնում են տարրերի անհարկի արտապատկերմանը: Մեր դեպքում ոչ առաջինը, ոչ երկրորդն առանձնապես տեղին չեն, բայց մենք կհետևենք լավագույն փորձին։ Ընդունված է դեկորատորներ կցել բոլոր գործառույթներին, որոնք փոխում են դիտարկվող դաշտերի վիճակը։
Հետին պլանում մենք կավելացնենք սկզբնավորումը և կպահենք վիճակը 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)
}
}
}
Այստեղ հետաքրքիր է ռեակցիայի ֆունկցիան։ Այն ունի երկու փաստարկ.
Տվյալների ընտրիչ:
Կառավարիչ, որը կկանչվի այս տվյալների հետ ամեն անգամ, երբ դրանք փոխվեն:
Ի տարբերություն redux-ի, որտեղ մենք բացահայտորեն ստանում ենք վիճակը որպես փաստարկ, mobx-ը հիշում է, թե որ դիտելիներին ենք մենք մուտք գործում ընտրիչի ներսում և զանգում է միայն մշակողին, երբ դրանք փոխվում են:
Կարևոր է հստակ հասկանալ, թե ինչպես է mobx-ը որոշում, թե որ դիտելիներին ենք մենք բաժանորդագրվում: Եթե ես սելեկտոր գրեի այսպես կոդով() => app.store, ապա ռեակցիան երբեք չի կոչվի, քանի որ պահեստն ինքնին դիտարկելի չէ, այլ միայն դրա դաշտերը։
Եթե ես այսպես գրեի () => app.store.keys, ապա նորից ոչինչ չէր պատահի, քանի որ զանգվածի տարրեր ավելացնել/հեռացնելիս հղումը դրան չի փոխվի։
Mobx-ը գործում է որպես ընտրիչ առաջին անգամ և հետևում է միայն այն դիտելիներին, որոնց մենք մուտք ենք գործել: Սա արվում է պրոքսի ստացողների միջոցով: Հետեւաբար, այստեղ օգտագործվում է ներկառուցված գործառույթը toJS. Այն վերադարձնում է նոր օբյեկտ՝ բոլոր պրոքսիներով փոխարինված բնօրինակ դաշտերով: Կատարման ընթացքում այն կարդում է օբյեկտի բոլոր դաշտերը, հետևաբար ստացողները գործարկվում են:
Բացվող վահանակում մենք կրկին կավելացնենք մի քանի բանալի: Այս անգամ նրանք նույնպես հայտնվեցին localStorage-ում.
Երբ ֆոնային էջը վերաբեռնվում է, տեղեկատվությունը մնում է տեղում:
Մինչև այս կետի բոլոր հայտի կոդը կարելի է դիտել այստեղ.
Անձնական բանալիների անվտանգ պահպանում
Անձնական բանալիները մաքուր տեքստով պահելն անվտանգ չէ. միշտ կա հնարավորություն, որ ձեզ կոտրեն, մուտք գործեն ձեր համակարգիչը և այլն: Հետևաբար, localStorage-ում մենք բանալիները կպահենք գաղտնաբառով ծածկագրված ձևով:
Ավելի մեծ անվտանգության համար հավելվածին կավելացնենք կողպված վիճակ, որում բանալիների մուտքն ընդհանրապես չի լինի։ Մենք ավտոմատ կերպով ընդլայնումը կտեղափոխենք կողպված վիճակի ժամանակի դադարի պատճառով:
Mobx-ը թույլ է տալիս պահպանել միայն տվյալների նվազագույն փաթեթը, իսկ մնացածը ավտոմատ կերպով հաշվարկվում է դրա հիման վրա: Սրանք, այսպես կոչված, հաշվարկված հատկություններ են: Դրանք կարելի է համեմատել տվյալների բազաների դիտումների հետ.
import {observable, action} from 'mobx';
import {setupDnode} from "./utils/setupDnode";
// Утилиты для безопасного шифрования строк. Используют crypto-js
import {encrypt, decrypt} from "./utils/cryptoUtils";
export class SignerApp {
constructor(initState = {}) {
this.store = observable.object({
// Храним пароль и зашифрованные ключи. Если пароль null - приложение locked
password: null,
vault: initState.vault,
// Геттеры для вычислимых полей. Можно провести аналогию с view в бд.
get locked(){
return this.password == null
},
get keys(){
return this.locked ?
undefined :
SignerApp._decryptVault(this.vault, this.password)
},
get initialized(){
return this.vault !== undefined
}
})
}
// Инициализация пустого хранилища новым паролем
@action
initVault(password){
this.store.vault = SignerApp._encryptVault([], password)
}
@action
lock() {
this.store.password = null
}
@action
unlock(password) {
this._checkPassword(password);
this.store.password = password
}
@action
addKey(key) {
this._checkLocked();
this.store.vault = SignerApp._encryptVault(this.store.keys.concat(key), this.store.password)
}
@action
removeKey(index) {
this._checkLocked();
this.store.vault = SignerApp._encryptVault([
...this.store.keys.slice(0, index),
...this.store.keys.slice(index + 1)
],
this.store.password
)
}
... // код подключения и api
// private
_checkPassword(password) {
SignerApp._decryptVault(this.store.vault, password);
}
_checkLocked() {
if (this.store.locked){
throw new Error('App is locked')
}
}
// Методы для шифровки/дешифровки хранилища
static _encryptVault(obj, pass){
const jsonString = JSON.stringify(obj)
return encrypt(jsonString, pass)
}
static _decryptVault(str, pass){
if (str === undefined){
throw new Error('Vault not initialized')
}
try {
const jsonString = decrypt(str, pass)
return JSON.parse(jsonString)
}catch (e) {
throw new Error('Wrong password')
}
}
}
Այժմ մենք պահպանում ենք միայն կոդավորված բանալիներն ու գաղտնաբառը: Մնացած ամեն ինչ հաշվարկված է։ Մենք փոխանցումը կողպված վիճակի ենք կատարում՝ գաղտնաբառը վիճակից հեռացնելով։ Հանրային API-ն այժմ ունի պահեստը սկզբնավորելու մեթոդ:
import CryptoJS from 'crypto-js'
// Используется для осложнения подбора пароля перебором. На каждый вариант пароля злоумышленнику придется сделать 5000 хешей
function strengthenPassword(pass, rounds = 5000) {
while (rounds-- > 0){
pass = CryptoJS.SHA256(pass).toString()
}
return pass
}
export function encrypt(str, pass){
const strongPass = strengthenPassword(pass);
return CryptoJS.AES.encrypt(str, strongPass).toString()
}
export function decrypt(str, pass){
const strongPass = strengthenPassword(pass)
const decrypted = CryptoJS.AES.decrypt(str, strongPass);
return decrypted.toString(CryptoJS.enc.Utf8)
}
Զննարկիչն ունի անգործուն API, որի միջոցով կարող եք բաժանորդագրվել իրադարձություններին՝ վիճակի փոփոխություններ: Պետությունը, համապատասխանաբար, կարող է լինել idle, active и locked. Անգործության համար դուք կարող եք սահմանել ժամանակի վերջ, իսկ կողպվածը սահմանվում է, երբ ՕՀ-ն ինքնին արգելափակված է: Մենք նաև կփոխենք ընտրիչը՝ պահպանման համար localStorage.
import {reaction, toJS} from 'mobx';
import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {SignerApp} from "./SignerApp";
import {loadState, saveState} from "./utils/localStorage";
const DEV_MODE = process.env.NODE_ENV !== 'production';
const IDLE_INTERVAL = 30;
setupApp();
function setupApp() {
const initState = loadState();
const app = new SignerApp(initState);
if (DEV_MODE) {
global.app = app;
}
// Теперь мы явно узываем поле, которому будет происходить доступ, reaction отработает нормально
reaction(
() => ({
vault: app.store.vault
}),
saveState
);
// Таймаут бездействия, когда сработает событие
extensionApi.idle.setDetectionInterval(IDLE_INTERVAL);
// Если пользователь залочил экран или бездействовал в течение указанного интервала лочим приложение
extensionApi.idle.onStateChanged.addListener(state => {
if (['locked', 'idle'].indexOf(state) > -1) {
app.lock()
}
});
// Connect to other contexts
extensionApi.runtime.onConnect.addListener(connectRemote);
function connectRemote(remotePort) {
const processName = remotePort.name;
const portStream = new PortStream(remotePort);
if (processName === 'contentscript') {
const origin = remotePort.sender.url
app.connectPage(portStream, origin)
} else {
app.connectPopup(portStream)
}
}
}
Այսպիսով, մենք հասնում ենք ամենակարևորին. բլոկչեյնով գործարքների ստեղծում և ստորագրում: Մենք կօգտագործենք WAVES բլոկչեյնը և գրադարանը ալիքներ-գործարքներ.
Նախ, եկեք վիճակին ավելացնենք հաղորդագրությունների զանգված, որոնք պետք է ստորագրվեն, ապա ավելացնենք նոր հաղորդագրություն ավելացնելու, ստորագրությունը հաստատելու և մերժելու մեթոդներ.
import {action, observable, reaction} from 'mobx';
import uuid from 'uuid/v4';
import {signTx} from '@waves/waves-transactions'
import {setupDnode} from "./utils/setupDnode";
import {decrypt, encrypt} from "./utils/cryptoUtils";
export class SignerApp {
...
@action
newMessage(data, origin) {
// Для каждого сообщения создаем метаданные с id, статусом, выременем создания и тд.
const message = observable.object({
id: uuid(), // Идентификатор, используюю uuid
origin, // Origin будем впоследствии показывать в интерфейсе
data, //
status: 'new', // Статусов будет четыре: new, signed, rejected и failed
timestamp: Date.now()
});
console.log(`new message: ${JSON.stringify(message, null, 2)}`);
this.store.messages.push(message);
// Возвращаем промис внутри которого mobx мониторит изменения сообщения. Как только статус поменяется мы зарезолвим его
return new Promise((resolve, reject) => {
reaction(
() => message.status, //Будем обсервить статус сообщеня
(status, reaction) => { // второй аргумент это ссылка на сам reaction, чтобы его можно было уничтожть внутри вызова
switch (status) {
case 'signed':
resolve(message.data);
break;
case 'rejected':
reject(new Error('User rejected message'));
break;
case 'failed':
reject(new Error(message.err.message));
break;
default:
return
}
reaction.dispose()
}
)
})
}
@action
approve(id, keyIndex = 0) {
const message = this.store.messages.find(msg => msg.id === id);
if (message == null) throw new Error(`No msg with id:${id}`);
try {
message.data = signTx(message.data, this.store.keys[keyIndex]);
message.status = 'signed'
} catch (e) {
message.err = {
stack: e.stack,
message: e.message
};
message.status = 'failed'
throw e
}
}
@action
reject(id) {
const message = this.store.messages.find(msg => msg.id === id);
if (message == null) throw new Error(`No msg with id:${id}`);
message.status = 'rejected'
}
...
}
Երբ մենք ստանում ենք նոր հաղորդագրություն, մենք դրան ավելացնում ենք մետատվյալներ, արեք observable և ավելացնել store.messages.
Եթե դուք չեք observable ձեռքով, ապա mobx-ը դա կանի ինքն իրեն զանգվածին հաղորդագրություններ ավելացնելիս: Այնուամենայնիվ, այն կստեղծի նոր օբյեկտ, որին մենք հղում չենք ունենա, բայց այն մեզ անհրաժեշտ կլինի հաջորդ քայլի համար։
Հաջորդը, մենք վերադարձնում ենք խոստում, որը կլուծվի, երբ հաղորդագրության կարգավիճակը փոխվի: Կարգավիճակը վերահսկվում է ռեակցիայի միջոցով, որը «ինքն իրեն կսպանի», երբ կարգավիճակը փոխվի։
Մեթոդի կոդը approve и reject շատ պարզ․ մենք պարզապես փոխում ենք հաղորդագրության կարգավիճակը՝ անհրաժեշտության դեպքում այն ստորագրելուց հետո։
Մենք դնում ենք «Հաստատել և մերժել» UI API-ում, «newMessage»-ը՝ «API» էջի մեջ.
Ընդհանուր առմամբ, ամեն ինչ պատրաստ է, մնում է միայն ավելացնել պարզ UI.
UI
Ինտերֆեյսին անհրաժեշտ է մուտք գործել հավելվածի վիճակ: UI-ի կողմից մենք կանենք observable վիճակը և ավելացրեք գործառույթ API-ին, որը կփոխի այս վիճակը: Ավելացնենք observable ֆոնից ստացված API օբյեկտին՝
import {observable} from 'mobx'
import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {cbToPromise, setupDnode, transformMethods} from "./utils/setupDnode";
import {initApp} from "./ui/index";
const DEV_MODE = process.env.NODE_ENV !== 'production';
setupUi().catch(console.error);
async function setupUi() {
// Подключаемся к порту, создаем из него стрим
const backgroundPort = extensionApi.runtime.connect({name: 'popup'});
const connectionStream = new PortStream(backgroundPort);
// Создаем пустой observable для состояния background'a
let backgroundState = observable.object({});
const api = {
//Отдаем бекграунду функцию, которая будет обновлять observable
updateState: async state => {
Object.assign(backgroundState, state)
}
};
// Делаем RPC объект
const dnode = setupDnode(connectionStream, api);
const background = await new Promise(resolve => {
dnode.once('remote', remoteApi => {
resolve(transformMethods(cbToPromise, remoteApi))
})
});
// Добавляем в background observable со стейтом
background.state = backgroundState;
if (DEV_MODE) {
global.background = background;
}
// Запуск интерфейса
await initApp(background)
}
Վերջում մենք սկսում ենք կիրառական ինտերֆեյսի մատուցումը: Սա react հավելված է։ Ֆոնային օբյեկտը պարզապես փոխանցվում է հենարանների միջոցով: Ճիշտ կլինի, իհարկե, առանձին ծառայություն անել մեթոդների համար և խանութ պետության համար, բայց այս հոդվածի նպատակների համար սա բավարար է.
import {render} from 'react-dom'
import App from './App'
import React from "react";
// Инициализируем приложение с background объектом в качест ве props
export async function initApp(background){
render(
<App background={background}/>,
document.getElementById('app-content')
);
}
Mobx-ի հետ շատ հեշտ է սկսել մատուցումը, երբ տվյալները փոխվում են: Մենք պարզապես փաթեթից կախում ենք դիտորդ դեկորատորին mobx-react բաղադրիչի վրա, և render-ը ավտոմատ կերպով կկանչվի, երբ բաղադրիչի կողմից հիշատակված որևէ դիտելիություն փոխվի: Ձեզ հարկավոր չէ որևէ mapStateToProps կամ միանալ, ինչպես redux-ում: Ամեն ինչ աշխատում է անմիջապես տուփից դուրս.
import React, {Component, Fragment} from 'react'
import {observer} from "mobx-react";
import Init from './components/Initialize'
import Keys from './components/Keys'
import Sign from './components/Sign'
import Unlock from './components/Unlock'
@observer // У Компонета с этим декоратом будет автоматически вызван метод render, если будут изменены observable на которые он ссылается
export default class App extends Component {
// Правильно конечно вынести логику рендера страниц в роутинг и не использовать вложенные тернарные операторы,
// и привязывать observable и методы background непосредственно к тем компонентам, которые их используют
render() {
const {keys, messages, initialized, locked} = this.props.background.state;
const {lock, unlock, addKey, removeKey, initVault, deleteVault, approve, reject} = this.props.background;
return <Fragment>
{!initialized
?
<Init onInit={initVault}/>
:
locked
?
<Unlock onUnlock={unlock}/>
:
messages.length > 0
?
<Sign keys={keys} message={messages[messages.length - 1]} onApprove={approve} onReject={reject}/>
:
<Keys keys={keys} onAdd={addKey} onRemove={removeKey}/>
}
<div>
{!locked && <button onClick={() => lock()}>Lock App</button>}
{initialized && <button onClick={() => deleteVault()}>Delete all keys and init</button>}
</div>
</Fragment>
}
}
Այժմ հավելվածների դասում դուք պետք է կազմեք վիճակի ընտրիչ UI-ի համար և տեղեկացնեք UI-ին, երբ այն փոխվի: Դա անելու համար եկեք ավելացնենք մեթոդ getState и reactionզանգահարելով remote.updateState:
import {action, observable, reaction} from 'mobx';
import uuid from 'uuid/v4';
import {signTx} from '@waves/waves-transactions'
import {setupDnode} from "./utils/setupDnode";
import {decrypt, encrypt} from "./utils/cryptoUtils";
export class SignerApp {
...
// public
getState() {
return {
keys: this.store.keys,
messages: this.store.newMessages,
initialized: this.store.initialized,
locked: this.store.locked
}
}
...
//
connectPopup(connectionStream) {
const api = this.popupApi();
const dnode = setupDnode(connectionStream, api);
dnode.once('remote', (remote) => {
// Создаем reaction на изменения стейта, который сделает вызовет удаленну процедуру и обновит стейт в ui процессе
const updateStateReaction = reaction(
() => this.getState(),
(state) => remote.updateState(state),
// Третьим аргументом можно передавать параметры. fireImmediatly значит что reaction выполниться первый раз сразу.
// Это необходимо, чтобы получить начальное состояние. Delay позволяет установить debounce
{fireImmediately: true, delay: 500}
);
// Удалим подписку при отключении клиента
dnode.once('end', () => updateStateReaction.dispose())
})
}
...
}
Օբյեկտ ստանալիս remote ստեղծվել է reaction փոխել այն վիճակը, որը կանչում է ֆունկցիան UI-ի կողմից:
Վերջին հպումը ընդլայնման պատկերակի վրա նոր հաղորդագրությունների ցուցադրումն ավելացնելն է.
function setupApp() {
...
// Reaction на выставление текста беджа.
reaction(
() => app.store.newMessages.length > 0 ? app.store.newMessages.length.toString() : '',
text => extensionApi.browserAction.setBadgeText({text}),
{fireImmediately: true}
);
...
}
Այսպիսով, հավելվածը պատրաստ է։ Վեբ էջերը կարող են ստորագրություն պահանջել գործարքների համար.
Եթե դուք կարդացել եք հոդվածը մինչև վերջ, բայց դեռ ունեք հարցեր, կարող եք դրանք ուղղել հետևյալ հասցեով ընդլայնումով պահեստներ. Այնտեղ կգտնեք նաև պարտավորություններ յուրաքանչյուր նշանակված քայլի համար:
Եվ եթե ձեզ հետաքրքրում է իրական ընդլայնման ծածկագիրը, կարող եք գտնել սա այստեղ.
Կոդը, պահեստը և աշխատանքի նկարագրությունը՝ սկսած siemarell