Аюулгүй хөтчийн өргөтгөл бичиж байна

Аюулгүй хөтчийн өргөтгөл бичиж байна

Түгээмэл "клиент-сервер" архитектураас ялгаатай нь төвлөрсөн бус програмууд нь дараахь шинж чанартай байдаг.

  • Хэрэглэгчийн нэвтрэх нэр, нууц үг бүхий мэдээллийн санг хадгалах шаардлагагүй. Хандалтын мэдээллийг зөвхөн хэрэглэгчид өөрсдөө хадгалдаг бөгөөд тэдний жинхэнэ эсэхийг протоколын түвшинд баталгаажуулдаг.
  • Сервер ашиглах шаардлагагүй. Хэрэглээний логик нь шаардлагатай хэмжээний өгөгдлийг хадгалах боломжтой блокчейн сүлжээнд ажиллах боломжтой.

Хэрэглэгчийн түлхүүрүүдийн хувьд харьцангуй аюулгүй 2 хадгалалт байдаг - тоног төхөөрөмжийн түрийвч болон хөтчийн өргөтгөлүүд. Техник хангамжийн түрийвч нь ихэвчлэн маш найдвартай, гэхдээ ашиглахад хэцүү, үнэ төлбөргүй байдаг ч хөтчийн өргөтгөлүүд нь аюулгүй байдал, ашиглахад хялбар байдлын төгс хослол бөгөөд эцсийн хэрэглэгчдэд бүрэн үнэ төлбөргүй байдаг.

Энэ бүхнийг харгалзан бид гүйлгээ, гарын үсэгтэй ажиллахад хялбар API-ээр хангаснаар төвлөрсөн бус програмуудыг хөгжүүлэхэд хялбаршуулсан хамгийн найдвартай өргөтгөлийг хийхийг хүссэн.
Энэ туршлагын талаар бид доор ярих болно.

Уг нийтлэлд хөтчийн өргөтгөлийг хэрхэн бичих талаар алхам алхмаар зааварчилгаа, кодын жишээ болон дэлгэцийн агшинг агуулсан болно. Та бүх кодыг эндээс олох боломжтой агуулахууд. Үйлдэл бүр нь логикийн хувьд энэ зүйлийн аль нэг хэсэгтэй тохирч байна.

Хөтөчийн өргөтгөлийн товч түүх

Хөтөчийн өргөтгөлүүд удаан хугацааны туршид бий болсон. Тэд 1999 онд Internet Explorer дээр, 2004 онд Firefox дээр гарч ирэв. Гэсэн хэдий ч маш удаан хугацаанд өргөтгөлийн нэгдсэн стандарт байхгүй байсан.

Энэ нь Google Chrome-ийн дөрөв дэх хувилбарт өргөтгөлүүдийн хамт гарч ирсэн гэж бид хэлж чадна. Мэдээжийн хэрэг, тэр үед ямар ч тодорхойлолт байгаагүй, гэхдээ энэ нь Chrome API нь түүний үндэс болсон: хөтчийн зах зээлийн ихэнх хэсгийг эзэлж, суурилуулсан програмын дэлгүүртэй болсноор Chrome нь хөтөчийн өргөтгөлүүдийн стандартыг тогтоосон.

Mozilla өөрийн гэсэн стандарттай байсан ч Chrome-ын өргөтгөлүүдийн алдар нэрийг олж хараад компани тохирох API хийхээр шийджээ. 2015 онд Mozilla-ийн санаачилгаар World Wide Web Consortium (W3C) дотор хөтөч хоорондын өргөтгөлийн техникийн үзүүлэлтүүд дээр ажиллах тусгай бүлгийг байгуулсан.

Chrome-д зориулсан одоо байгаа API өргөтгөлүүдийг үндэс болгон авсан. Энэ ажлыг Microsoft-ын дэмжлэгтэйгээр гүйцэтгэсэн (Google стандартыг боловсруулахад оролцохоос татгалзсан) үр дүнд нь төсөл гарч ирэв. техникийн үзүүлэлтүүд.

Албан ёсоор техникийн үзүүлэлтийг Edge, Firefox болон Opera дэмждэг (Chrome энэ жагсаалтад байхгүй гэдгийг анхаарна уу). Гэвч үнэн хэрэгтээ энэ стандарт нь Chrome-той ихээхэн нийцдэг, учир нь энэ нь өргөтгөл дээр үндэслэн бичигдсэн байдаг. Та WebExtensions API-ийн талаар илүү ихийг уншиж болно энд.

Өргөтгөлийн бүтэц

Өргөтгөл хийхэд шаардлагатай цорын ганц файл бол манифест (manifest.json) юм. Энэ нь мөн өргөтгөлийн "орох цэг" юм.

Манифест

Тодорхойлолтын дагуу манифест файл нь хүчинтэй JSON файл юм. Аль хөтөч дээр ямар товчлуурыг дэмждэг тухай мэдээлэл бүхий манифест түлхүүрүүдийн бүрэн тайлбар энд.

Тодорхойлолтод ороогүй түлхүүрүүдийг үл тоомсорлож болно (Chrome болон Firefox аль аль нь алдаа мэдээлдэг боловч өргөтгөлүүд үргэлжлүүлэн ажилласаар байна).

Мөн би зарим зүйлд анхаарлаа хандуулахыг хүсч байна.

  1. суурь - дараах талбаруудыг агуулсан объект:
    1. скриптүүд - арын контекст дээр гүйцэтгэх скриптүүдийн массив (бид энэ талаар бага зэрэг ярих болно);
    2. Page - хоосон хуудсанд хийгдэх скриптүүдийн оронд та контент бүхий html-г зааж өгч болно. Энэ тохиолдолд скриптийн талбарыг үл тоомсорлож, скриптүүдийг агуулгын хуудсанд оруулах шаардлагатай болно;
    3. үлддэггүй - хоёртын туг, хэрэв заагаагүй бол хөтөч юу ч хийхгүй байна гэж үзвэл арын процессыг "үхэх" бөгөөд шаардлагатай бол дахин эхлүүлэх болно. Үгүй бол хөтөч хаагдсан үед л хуудсыг буулгах болно. Firefox дээр дэмжигдээгүй.
  2. контент_скриптүүд — өөр өөр вэб хуудсанд өөр өөр скриптүүдийг ачаалах боломжийг олгодог объектуудын массив. Объект бүр дараах чухал талбаруудыг агуулна.
    1. тоглолт - загварын url, энэ нь тухайн агуулгын скриптийг оруулах эсэхийг тодорхойлдог.
    2. js - энэ тоглолтонд ачаалагдах скриптүүдийн жагсаалт;
    3. тохирохыг_хасах - талбайгаас хасна match Энэ талбарт тохирох URL-ууд.
  3. хуудас_үйлдэл - үнэндээ бол хөтөч дээрх хаягийн талбарын хажууд харагдах дүрс, түүнтэй харилцах харилцааг хариуцдаг объект юм. Энэ нь танд өөрийн HTML, CSS болон JS ашиглан тодорхойлогдсон попап цонхыг харуулах боломжийг олгодог.
    1. анхдагч_попап — гарч ирэх интерфэйс бүхий HTML файлын зам, CSS болон JS агуулж болно.
  4. зөвшөөрөл — өргөтгөлийн эрхийг удирдах массив. 3 төрлийн эрх байдаг бөгөөд тэдгээрийг нарийвчлан тайлбарласан болно энд
  5. вэб_хүртээмжтэй_нөөцүүд — зураг, JS, CSS, HTML файл гэх мэт вэб хуудасны хүсэлт гаргаж болох өргөтгөлийн нөөцүүд.
  6. гаднаас_холбох боломжтой — энд та холбогдох боломжтой вэб хуудасны бусад өргөтгөл болон домайнуудын ID-г тодорхой зааж өгч болно. Домэйн нь хоёр дахь буюу түүнээс дээш түвшний байж болно. Firefox дээр ажиллахгүй.

Гүйцэтгэлийн контекст

Өргөтгөл нь гурван кодын гүйцэтгэх контексттэй, өөрөөр хэлбэл програм нь хөтчийн API-д хандах янз бүрийн түвшний гурван хэсгээс бүрдэнэ.

Өргөтгөлийн контекст

API-ийн ихэнх хэсгийг эндээс авах боломжтой. Энэ нөхцөлд тэд "амьдардаг":

  1. Арын дэвсгэр хуудас - өргөтгөлийн "арын" хэсэг. Файлыг "арын дэвсгэр" товчлуурыг ашиглан манифестт зааж өгсөн болно.
  2. Попап хуудас — өргөтгөлийн дүрс дээр дарахад гарч ирэх цонхон хуудас. Манифестод browser_action -> default_popup.
  3. захиалгат хуудас - өргөтгөл хуудас, "амьдрах" харах тусдаа таб chrome-extension://<id_расширения>/customPage.html.

Энэ контекст нь хөтчийн цонх, табаас хамааралгүй байдаг. Арын дэвсгэр хуудас нэг хуулбарт байдаг бөгөөд үргэлж ажилладаг (үл хамаарах зүйл бол үйл явдлын арын скриптийг үйл явдал эхлүүлж, гүйцэтгэсний дараа "үхэх" тохиолдолд үйл явдлын хуудас юм). Попап хуудас гарч ирэх цонх нээлттэй үед байдаг ба захиалгат хуудас - түүнтэй хамт таб нээлттэй байх үед. Энэ контекстээс бусад таб болон тэдгээрийн агуулгад хандах эрх байхгүй.

Агуулгын скриптийн контекст

Агуулгын скрипт файлыг хөтчийн таб бүрийн хамт эхлүүлнэ. Энэ нь өргөтгөлийн API-ийн хэсэг болон вэб хуудасны DOM мод руу хандах эрхтэй. Энэ нь хуудастай харилцах үүрэгтэй агуулгын скриптүүд юм. DOM модыг удирддаг өргөтгөлүүд нь үүнийг агуулгын скрипт дээр хийдэг - жишээлбэл, зар хориглогч эсвэл орчуулагч. Мөн агуулгын скрипт нь стандартаар хуудастай холбогдох боломжтой postMessage.

Вэб хуудасны контекст

Энэ бол жинхэнэ вэб хуудас өөрөө юм. Энэ нь өргөтгөлтэй ямар ч холбоогүй бөгөөд энэ хуудасны домэйныг манифестэд тодорхой заагаагүйгээс бусад тохиолдолд тэнд хандах эрхгүй (энэ талаар доор дэлгэрэнгүй үзнэ үү).

Мессеж солилцох

Програмын өөр өөр хэсгүүд хоорондоо мессеж солилцох ёстой. Үүнд зориулсан API байдаг runtime.sendMessage зурвас илгээх background и tabs.sendMessage хуудас руу мессеж илгээх (агуулгын скрипт, гарч ирэх цонх эсвэл боломжтой бол вэб хуудас externally_connectable). Chrome API-д хандах жишээг доор харуулав.

// Сообщением может быть любой JSON сериализуемый объект
const msg = {a: 'foo', b: 'bar'};

// extensionId можно не указывать, если мы хотим послать сообщение 'своему' расширению (из ui или контент скрипта)
chrome.runtime.sendMessage(extensionId, msg);

// Так выглядит обработчик
chrome.runtime.onMessage.addListener((msg) => console.log(msg))

// Можно слать сообщения вкладкам зная их id
chrome.tabs.sendMessage(tabId, msg)

// Получить к вкладкам и их id можно, например, вот так
chrome.tabs.query(
    {currentWindow: true, active : true},
    function(tabArray){
      tabArray.forEach(tab => console.log(tab.id))
    }
)

Бүрэн харилцахын тулд та дамжуулан холболт үүсгэж болно runtime.connect. Хариуд нь бид хүлээн авах болно runtime.Port, үүн рүү нээлттэй байхад та хэдэн ч мессеж илгээх боломжтой. Үйлчлүүлэгч тал дээр, жишээлбэл, contentscript, энэ нь иймэрхүү харагдаж байна:

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

Сервер эсвэл дэвсгэр:

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

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

Мөн арга хэмжээ болж байна onDisconnect ба арга disconnect.

Хэрэглээний диаграм

Хувийн түлхүүрүүдийг хадгалдаг, нийтийн мэдээлэлд (хаяг, нийтийн түлхүүр нь хуудастай харилцаж, гуравдагч талын програмууд гүйлгээнд гарын үсэг зурах хүсэлт гаргах боломжийг олгодог) хөтөчийн өргөтгөл хийцгээе.

Хэрэглээний хөгжүүлэлт

Манай аппликейшн нь хэрэглэгчтэй харилцаж, дуудлагын аргууд (жишээ нь, гүйлгээнд гарын үсэг зурах) API-ээр хуудсыг хангах ёстой. Ганцаараа л хий contentscript Энэ нь зөвхөн DOM-д хандах боломжтой, гэхдээ хуудасны JS-д хандах боломжгүй тул ажиллахгүй. -ээр холбогдоно runtime.connect Бид чадахгүй, учир нь API нь бүх домэйнд хэрэгтэй бөгөөд зөвхөн тодорхой нэгийг нь манифестэд зааж өгөх боломжтой. Үүний үр дүнд диаграмм дараах байдлаар харагдах болно.

Аюулгүй хөтчийн өргөтгөл бичиж байна

Өөр скрипт байх болно - inpage, үүнийг бид хуудсанд оруулах болно. Энэ нь контекст дээрээ ажиллаж, өргөтгөлтэй ажиллах API-г өгөх болно.

Начало

Бүх хөтөчийн өргөтгөлийн кодыг эндээс авах боломжтой GitHub. Тайлбарын үеэр үүрэг хариуцлагатай холбоотой холбоосууд байх болно.

Манифестоос эхэлье:

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

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

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

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

Хоосон background.js, popup.js, inpage.js болон contentscript.js үүсгэх. Бид popup.html-г нэмдэг бөгөөд манай програмыг Google Chrome-д аль хэдийн ачаалж, ажиллаж байгаа эсэхийг шалгаарай.

Үүнийг баталгаажуулахын тулд та кодыг авч болно Эндээс. Бидний хийсэн зүйлээс гадна холбоос нь webpack ашиглан төслийн угсралтыг тохируулсан. Хөтөч рүү програм нэмэхийн тулд chrome://extensions дотроос та load unpacked болон холбогдох өргөтгөлтэй хавтсыг сонгох хэрэгтэй - манай тохиолдолд dist.

Аюулгүй хөтчийн өргөтгөл бичиж байна

Одоо манай өргөтгөл суулгаад ажиллаж байна. Та хөгжүүлэгчийн хэрэгслийг янз бүрийн контекстэд дараах байдлаар ажиллуулж болно:

попап ->

Аюулгүй хөтчийн өргөтгөл бичиж байна

Агуулгын скриптийн консол руу нэвтрэх нь тухайн хуудасны консолоор дамжуулан хийгддэг.Аюулгүй хөтчийн өргөтгөл бичиж байна

Мессеж солилцох

Тиймээс бид хоёр харилцааны сувгийг бий болгох хэрэгтэй: хуудасны <-> дэвсгэр болон гарч ирэх <-> дэвсгэр. Мэдээжийн хэрэг та порт руу мессеж илгээж, өөрийн протоколыг зохион бүтээж болно, гэхдээ би метамаск нээлттэй эхийн төсөл дээр харсан арга барилыг илүүд үздэг.

Энэ бол Ethereum сүлжээтэй ажиллах хөтчийн өргөтгөл юм. Үүнд програмын өөр өөр хэсгүүд dnode номын санг ашиглан RPC-ээр холбогддог. Хэрэв та 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 объектын оронд Google-ийн хөтөч болон бусад хөтөч дээр Chrome-д ханддаг extensionApi-г ашигладаг. Энэ нь хөтчүүд хоорондын нийцтэй байдлын үүднээс хийгдсэн боловч энэ нийтлэлийн зорилгоор "chrome.runtime.connect"-г ашиглаж болно.

Арын скрипт дээр програмын жишээ үүсгэцгээе:

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

const app = new SignerApp();

// onConnect срабатывает при подключении 'процессов' (contentscript, popup, или страница расширения)
extensionApi.runtime.onConnect.addListener(connectRemote);

function connectRemote(remotePort) {
    const processName = remotePort.name;
    const portStream = new PortStream(remotePort);
    // При установке соединения можно указывать имя, по этому имени мы и оппределяем кто к нам подлючился, контентскрипт или ui
    if (processName === 'contentscript'){
        const origin = remotePort.sender.url
        app.connectPage(portStream, origin)
    }else{
        app.connectPopup(portStream)
    }
}

Dnode урсгалтай ажилладаг бөгөөд бид порт хүлээн авдаг тул адаптерийн анги хэрэгтэй. Энэ нь хөтөч дээр nodejs урсгалуудыг хэрэгжүүлдэг уншигдах урсгал номын санг ашиглан хийгдсэн:

import {Duplex} from 'readable-stream';

export class PortStream extends Duplex{
    constructor(port){
        super({objectMode: true});
        this._port = port;
        port.onMessage.addListener(this._onMessage.bind(this));
        port.onDisconnect.addListener(this._onDisconnect.bind(this))
    }

    _onMessage(msg) {
        if (Buffer.isBuffer(msg)) {
            delete msg._isBuffer;
            const data = new Buffer(msg);
            this.push(data)
        } else {
            this.push(msg)
        }
    }

    _onDisconnect() {
        this.destroy()
    }

    _read(){}

    _write(msg, encoding, cb) {
        try {
            if (Buffer.isBuffer(msg)) {
                const data = msg.toJSON();
                data._isBuffer = true;
                this._port.postMessage(data)
            } else {
                this._port.postMessage(msg)
            }
        } catch (err) {
            return cb(new Error('PortStream - disconnected'))
        }
        cb()
    }
}

Одоо UI дээр холболт үүсгэцгээе:

import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import Dnode from 'dnode/browser';

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

setupUi().catch(console.error);

async function setupUi(){
    // Также, как и в классе приложения создаем порт, оборачиваем в stream, делаем  dnode
    const backgroundPort = extensionApi.runtime.connect({name: 'popup'});
    const connectionStream = new PortStream(backgroundPort);

    const dnode = Dnode();

    connectionStream.pipe(dnode).pipe(connectionStream);

    const background = await new Promise(resolve => {
        dnode.once('remote', api => {
            resolve(api)
        })
    });

    // Делаем объект API доступным из консоли
    if (DEV_MODE){
        global.background = background;
    }
}

Дараа нь бид агуулгын скрипт дэх холболтыг үүсгэнэ:

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

setupConnection();
injectScript();

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

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

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

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

API нь агуулгын скрипт дээр биш, харин шууд хуудсан дээр хэрэгтэй тул бид хоёр зүйлийг хийдэг:

  1. Бид хоёр урсгал үүсгэдэг. Нэг - хуудас руу, мессежийн дээд талд. Үүний тулд бид үүнийг ашигладаг энэ багц метамаск бүтээгчдээс. Хоёрдахь урсгал нь хүлээн авсан портын дэвсгэр дээр байна runtime.connect. Тэднийг худалдаж авцгаая. Одоо хуудас нь арын дэвсгэр рүү чиглэсэн урсгалтай болно.
  2. Скриптийг DOM руу оруулна уу. Скриптийг татаж аваад (манифестт хандахыг зөвшөөрсөн) шошго үүсгэ script доторх агуулгатай нь:

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

setupConnection();
injectScript();

function setupConnection(){
    // Стрим к бекграунду
    const backgroundPort = extensionApi.runtime.connect({name: 'contentscript'});
    const backgroundStream = new PortStream(backgroundPort);

    // Стрим к странице
    const pageStream = new PostMessageStream({
        name: 'content',
        target: 'page',
    });

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

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

Одоо бид inpage-д api объект үүсгэж, үүнийг глобал болгож тохируулна уу:

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

Бид бэлэн байна Хуудас болон UI-д зориулсан тусдаа API-тай Remote Procedure Call (RPC).. Шинэ хуудсыг дэвсгэртэй холбоход бид дараахыг харж болно:

Аюулгүй хөтчийн өргөтгөл бичиж байна

Хоосон API болон гарал үүсэл. Хуудасны талд бид hello функцийг дараах байдлаар дуудаж болно.

Аюулгүй хөтчийн өргөтгөл бичиж байна

Орчин үеийн JS дээр дуудлагын функцүүдтэй ажиллах нь ёс суртахуунгүй тул API объектыг utils руу дамжуулах боломжийг олгодог dnode үүсгэх жижиг туслах бичье.

API объектууд одоо иймэрхүү харагдах болно:

export class SignerApp {

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

...

}

Алсын удирдлагаас ийм объект авах:

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

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

Мөн функцуудыг дуудах нь амлалтыг буцаана:

Аюулгүй хөтчийн өргөтгөл бичиж байна

Асинхрон функц бүхий хувилбар боломжтой энд.

Ерөнхийдөө RPC болон урсгалын арга нь нэлээд уян хатан юм шиг санагддаг: бид уурын мультиплексийг ашиглаж, янз бүрийн даалгаварт хэд хэдэн өөр API үүсгэж болно. Зарчмын хувьд dnode-ийг хаана ч ашиглаж болно, гол зүйл бол тээвэрлэлтийг nodejs урсгал хэлбэрээр боох явдал юм.

Альтернатив хувилбар бол JSON RPC 2 протоколыг хэрэгжүүлдэг JSON формат юм. Гэсэн хэдий ч энэ нь тодорхой тээвэрлэлтүүдтэй (TCP болон HTTP(S)) ажилладаг бөгөөд энэ нь манай тохиолдолд хамаарахгүй.

Дотоод улсын болон орон нутгийн хадгалалт

Бид програмын дотоод төлөвийг хадгалах хэрэгтэй болно - наад зах нь гарын үсэг зурах түлхүүрүүд. Бид програмын төлөвийг хялбархан нэмэх боломжтой бөгөөд үүнийг попап API-д өөрчлөх аргууд:

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

export class SignerApp {

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

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

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

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

    ...

} 

Ар талд нь бид бүх зүйлийг функцэд багтааж, програмын объектыг цонхонд бичих бөгөөд ингэснээр консолоос түүнтэй ажиллах боломжтой болно.

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

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

setupApp();

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

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

    extensionApi.runtime.onConnect.addListener(connectRemote);

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

UI консолоос цөөн хэдэн түлхүүр нэмж, төлөвт юу тохиолдохыг харцгаая:

Аюулгүй хөтчийн өргөтгөл бичиж байна

Дахин эхлүүлэх үед түлхүүрүүд алдагдахгүй байхын тулд төлөвийг тууштай болгох хэрэгтэй.

Бид үүнийг localStorage-д хадгалж, өөрчлөлт болгонд дарж бичнэ. Дараа нь UI-д хандах хандалт бас шаардлагатай болно, мөн би өөрчлөлтөд бүртгүүлэхийг хүсч байна. Үүн дээр үндэслэн ажиглах боломжтой хадгалах сан үүсгэж, түүний өөрчлөлтөд бүртгүүлэхэд тохиромжтой байх болно.

Бид mobx номын санг ашиглах болно (https://github.com/mobxjs/mobx). Би түүнтэй ажиллах шаардлагагүй байсан ч би үүнийг судлахыг үнэхээр хүсч байсан тул сонголт үүн дээр унасан.

Анхны төлөвийн эхлэлийг нэмж, дэлгүүрийг ажиглагдах боломжтой болгоё:

import {observable, action} from 'mobx';
import {setupDnode} from "./utils/setupDnode";

export class SignerApp {

    constructor(initState = {}) {
        // Внешне store так и останется тем же объектом, только теперь все его поля стали proxy, которые отслеживают доступ к ним
        this.store =  observable.object({
            keys: initState.keys || [],
        });
    }

    // Методы, которые меняют observable принято оборачивать декоратором
    @action
    addKey(key) {
        this.store.keys.push(key)
    }

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

    ...

}

“Нүдний дор” mobx дэлгүүрийн бүх талбарыг проксигоор сольж, тэдгээрт ирсэн бүх дуудлагыг таслан зогсоодог. Эдгээр мессежүүдэд бүртгүүлэх боломжтой болно.

Доор би "өөрчлөх үед" гэсэн нэр томъёог ихэвчлэн ашиглах болно, гэхдээ энэ нь бүрэн зөв биш юм. Mobx талбарт хандах хандалтыг хянадаг. Номын сангийн үүсгэсэн прокси объектуудын хүлээн авагч, тохируулагчийг ашигладаг.

Үйлдлийн засал чимэглэл нь хоёр зорилготой:

  1. EnforceActions тугтай хатуу горимд mobx нь төлөвийг шууд өөрчлөхийг хориглодог. Хатуу нөхцөлд ажиллах нь сайн туршлага гэж үздэг.
  2. Функц нь төлөвийг хэд хэдэн удаа өөрчилсөн ч - жишээлбэл, бид хэд хэдэн мөр кодын хэд хэдэн талбарыг өөрчилдөг - энэ нь дууссаны дараа ажиглагчид мэдэгдэнэ. Энэ нь ялангуяа шаардлагагүй төлөвийн шинэчлэлтүүд нь элементүүдийг шаардлагагүй буулгахад хүргэдэг урд талын хувьд чухал юм. Манай тохиолдолд эхний болон хоёр дахь нь онцгой ач холбогдолтой биш боловч бид хамгийн сайн туршлагыг дагаж мөрдөх болно. Ажиглагдсан талбаруудын төлөвийг өөрчилдөг бүх функцэд чимэглэгчийг хавсаргах нь заншилтай байдаг.

Цаана нь бид localStorage-д эхлүүлж, төлөвийг хадгалахыг нэмнэ:

import {reaction, toJS} from 'mobx';
import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {SignerApp} from "./SignerApp";
// Вспомогательные методы. Записывают/читают объект в/из localStorage виде JSON строки по ключу 'store'
import {loadState, saveState} from "./utils/localStorage";

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

setupApp();

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

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

    // Setup state persistence

    // Результат reaction присваивается переменной, чтобы подписку можно было отменить. Нам это не нужно, оставлено для примера
    const localStorageReaction = reaction(
        () => toJS(app.store), // Функция-селектор данных
        saveState // Функция, которая будет вызвана при изменении данных, которые возвращает селектор
    );

    extensionApi.runtime.onConnect.addListener(connectRemote);

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

Урвалын функц нь энд сонирхолтой юм. Үүнд хоёр аргумент байна:

  1. Өгөгдөл сонгогч.
  2. Энэ өгөгдөл өөрчлөгдөх бүрт дуудагдах зохицуулагч.

Бид аргумент болгон төлөвийг тодорхой хүлээн авдаг redux-аас ялгаатай нь mobx сонгогч дотор бид ямар ажиглагдах боломжтойг санадаг бөгөөд тэдгээрийг өөрчлөх үед л зохицуулагчийг дууддаг.

Мобкс нь биднийг ямар ажиглалтын жагсаалтад бүртгүүлэхийг яг яаж шийддэгийг ойлгох нь чухал юм. Хэрэв би ийм кодонд сонгогч бичсэн бол() => app.store, дараа нь урвал хэзээ ч дуудагдахгүй, учир нь хадгалалт нь өөрөө ажиглагддаггүй, зөвхөн түүний талбарууд байдаг.

Би ингэж бичсэн бол () => app.store.keys, дараа нь дахин юу ч болохгүй, учир нь массивын элементүүдийг нэмэх/хасах үед түүний ишлэл өөрчлөгдөхгүй.

Mobx нь анх удаа сонгогчоор ажиллаж байгаа бөгөөд зөвхөн бидний хандсан ажиглалтын мэдээллийг хөтөлдөг. Үүнийг прокси хүлээн авагчаар дамжуулан хийдэг. Тиймээс энд суулгасан функцийг ашигладаг toJS. Энэ нь бүх проксиг анхны талбаруудаар сольсон шинэ объектыг буцаана. Гүйцэтгэх явцад энэ нь объектын бүх талбарыг уншдаг тул хүлээн авагчдыг идэвхжүүлдэг.

Попап консол дээр бид дахин хэд хэдэн түлхүүр нэмэх болно. Энэ удаад тэд мөн localStorage-д орлоо:

Аюулгүй хөтчийн өргөтгөл бичиж байна

Арын дэвсгэр хуудсыг дахин ачаалах үед мэдээлэл хэвээр үлдэнэ.

Энэ хүртэлх бүх програмын кодыг үзэх боломжтой энд.

Хувийн түлхүүрүүдийг найдвартай хадгалах

Хувийн түлхүүрүүдийг тодорхой текстээр хадгалах нь аюултай: таныг хакердах, компьютертээ нэвтрэх гэх мэт боломж үргэлж байдаг. Тиймээс localStorage-д бид түлхүүрүүдийг нууц үгээр шифрлэгдсэн хэлбэрээр хадгалах болно.

Аюулгүй байдлыг нэмэгдүүлэхийн тулд бид програмд ​​түгжигдсэн төлөвийг нэмэх бөгөөд энэ нь түлхүүрүүдэд огт хандах боломжгүй болно. Хугацаа хэтэрсэн тул бид өргөтгөлийг автоматаар түгжигдсэн төлөвт шилжүүлэх болно.

Mobx нь зөвхөн хамгийн бага багц өгөгдлийг хадгалах боломжийг олгодог бөгөөд үлдсэнийг нь түүн дээр үндэслэн автоматаар тооцдог. Эдгээр нь тооцоолсон шинж чанарууд юм. Тэдгээрийг мэдээллийн сан дахь үзэл бодолтой харьцуулж болно:

import {observable, action} from 'mobx';
import {setupDnode} from "./utils/setupDnode";
// Утилиты для безопасного шифрования строк. Используют crypto-js
import {encrypt, decrypt} from "./utils/cryptoUtils";

export class SignerApp {
    constructor(initState = {}) {
        this.store = observable.object({
            // Храним пароль и зашифрованные ключи. Если пароль null - приложение locked
            password: null,
            vault: initState.vault,

            // Геттеры для вычислимых полей. Можно провести аналогию с view в бд.
            get locked(){
                return this.password == null
            },
            get keys(){
                return this.locked ?
                    undefined :
                    SignerApp._decryptVault(this.vault, this.password)
            },
            get initialized(){
                return this.vault !== undefined
            }
        })
    }
    // Инициализация пустого хранилища новым паролем
    @action
    initVault(password){
        this.store.vault = SignerApp._encryptVault([], password)
    }
    @action
    lock() {
        this.store.password = null
    }
    @action
    unlock(password) {
        this._checkPassword(password);
        this.store.password = password
    }
    @action
    addKey(key) {
        this._checkLocked();
        this.store.vault = SignerApp._encryptVault(this.store.keys.concat(key), this.store.password)
    }
    @action
    removeKey(index) {
        this._checkLocked();
        this.store.vault = SignerApp._encryptVault([
                ...this.store.keys.slice(0, index),
                ...this.store.keys.slice(index + 1)
            ],
            this.store.password
        )
    }

    ... // код подключения и api

    // private
    _checkPassword(password) {
        SignerApp._decryptVault(this.store.vault, password);
    }

    _checkLocked() {
        if (this.store.locked){
            throw new Error('App is locked')
        }
    }

    // Методы для шифровки/дешифровки хранилища
    static _encryptVault(obj, pass){
        const jsonString = JSON.stringify(obj)
        return encrypt(jsonString, pass)
    }

    static _decryptVault(str, pass){
        if (str === undefined){
            throw new Error('Vault not initialized')
        }
        try {
            const jsonString = decrypt(str, pass)
            return JSON.parse(jsonString)
        }catch (e) {
            throw new Error('Wrong password')
        }
    }
}

Одоо бид зөвхөн шифрлэгдсэн түлхүүр, нууц үгээ хадгалдаг. Бусад бүх зүйлийг тооцоолсон. Бид нууц үгээ мужаас устгаснаар түгжигдсэн төлөв рүү шилжүүлдэг. Нийтийн API нь одоо хадгалах санг эхлүүлэх аргатай болсон.

Шифрлэлтэнд зориулж бичсэн crypto-js ашигладаг хэрэгслүүд:

import CryptoJS from 'crypto-js'

// Используется для осложнения подбора пароля перебором. На каждый вариант пароля злоумышленнику придется сделать 5000 хешей
function strengthenPassword(pass, rounds = 5000) {
    while (rounds-- > 0){
        pass = CryptoJS.SHA256(pass).toString()
    }
    return pass
}

export function encrypt(str, pass){
    const strongPass = strengthenPassword(pass);
    return CryptoJS.AES.encrypt(str, strongPass).toString()
}

export function decrypt(str, pass){
    const strongPass = strengthenPassword(pass)
    const decrypted = CryptoJS.AES.decrypt(str, strongPass);
    return decrypted.toString(CryptoJS.enc.Utf8)
}

Хөтөч нь идэвхгүй API-тай бөгөөд үүгээр дамжуулан та үйл явдалд бүртгүүлэх боломжтой - төлөвийн өөрчлөлт. Үүний дагуу муж улс байж болно idle, active и locked. Сул зогсолтын хувьд та завсарлага тохируулж болох бөгөөд үйлдлийн систем өөрөө хаагдсан үед түгжигдсэн байна. Бид мөн localStorage-д хадгалах сонгогчийг өөрчилнө:

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

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

setupApp();

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

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

    // Теперь мы явно узываем поле, которому будет происходить доступ, reaction отработает нормально
    reaction(
        () => ({
            vault: app.store.vault
        }),
        saveState
    );

    // Таймаут бездействия, когда сработает событие
    extensionApi.idle.setDetectionInterval(IDLE_INTERVAL);
    // Если пользователь залочил экран или бездействовал в течение указанного интервала лочим приложение
    extensionApi.idle.onStateChanged.addListener(state => {
        if (['locked', 'idle'].indexOf(state) > -1) {
            app.lock()
        }
    });

    // Connect to other contexts
    extensionApi.runtime.onConnect.addListener(connectRemote);

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

Энэ алхамын өмнөх код нь энд.

Ажил гүйлгээ

Тиймээс бид хамгийн чухал зүйл бол блокчейн дээр гүйлгээ үүсгэх, гарын үсэг зурах явдал юм. Бид WAVES блокчейн болон номын санг ашиглах болно долгион-гүйлгээ.

Эхлээд мужид гарын үсэг зурах шаардлагатай мессежийн массив нэмж, дараа нь шинэ мессеж нэмэх, гарын үсгийг баталгаажуулах, татгалзах аргуудыг нэмье:

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

export class SignerApp {

    ...

    @action
    newMessage(data, origin) {
        // Для каждого сообщения создаем метаданные с id, статусом, выременем создания и тд.
        const message = observable.object({
            id: uuid(), // Идентификатор, используюю uuid
            origin, // Origin будем впоследствии показывать в интерфейсе
            data, //
            status: 'new', // Статусов будет четыре: new, signed, rejected и failed
            timestamp: Date.now()
        });
        console.log(`new message: ${JSON.stringify(message, null, 2)}`);

        this.store.messages.push(message);

        // Возвращаем промис внутри которого mobx мониторит изменения сообщения. Как только статус поменяется мы зарезолвим его
        return new Promise((resolve, reject) => {
            reaction(
                () => message.status, //Будем обсервить статус сообщеня
                (status, reaction) => { // второй аргумент это ссылка на сам reaction, чтобы его можно было уничтожть внутри вызова
                    switch (status) {
                        case 'signed':
                            resolve(message.data);
                            break;
                        case 'rejected':
                            reject(new Error('User rejected message'));
                            break;
                        case 'failed':
                            reject(new Error(message.err.message));
                            break;
                        default:
                            return
                    }
                    reaction.dispose()
                }
            )
        })
    }
    @action
    approve(id, keyIndex = 0) {
        const message = this.store.messages.find(msg => msg.id === id);
        if (message == null) throw new Error(`No msg with id:${id}`);
        try {
            message.data = signTx(message.data, this.store.keys[keyIndex]);
            message.status = 'signed'
        } catch (e) {
            message.err = {
                stack: e.stack,
                message: e.message
            };
            message.status = 'failed'
            throw e
        }
    }
    @action
    reject(id) {
        const message = this.store.messages.find(msg => msg.id === id);
        if (message == null) throw new Error(`No msg with id:${id}`);
        message.status = 'rejected'
    }

    ...
}

Бид шинэ мессеж хүлээн авахдаа түүнд мета өгөгдөл нэмдэг observable болон нэмэх store.messages.

Үгүй бол observable гараар, дараа нь массив руу мессеж нэмэх үед mobx өөрөө хийх болно. Гэсэн хэдий ч, энэ нь бидэнд лавлагаа байхгүй шинэ объект үүсгэх боловч дараагийн алхамд хэрэгтэй болно.

Дараа нь бид мессежийн төлөв өөрчлөгдөхөд шийдэгдэх амлалтыг буцаана. Статус нь хариу урвалаар хянагддаг бөгөөд энэ нь статус өөрчлөгдөхөд "өөрийгөө хороох" болно.

Аргын код approve и reject маш энгийн: шаардлагатай бол гарын үсэг зурсны дараа бид мессежийн статусыг өөрчилдөг.

Бид UI API-д Зөвшөөрөх, татгалзах, API хуудасны newMessage-г оруулав:

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

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

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

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

    ...
}

Одоо өргөтгөлтэй гүйлгээнд гарын үсэг зурахыг оролдъё:

Аюулгүй хөтчийн өргөтгөл бичиж байна

Ерөнхийдөө бүх зүйл бэлэн болсон, бүх зүйл үлдсэн энгийн UI нэмнэ.

UI

Интерфэйс нь програмын төлөвт хандах шаардлагатай. UI тал дээр бид хийх болно observable төлөв болгож, энэ төлөвийг өөрчлөх функцийг API-д нэмнэ үү. Нэмье observable Араас хүлээн авсан API объект руу:

import {observable} from 'mobx'
import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {cbToPromise, setupDnode, transformMethods} from "./utils/setupDnode";
import {initApp} from "./ui/index";

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

setupUi().catch(console.error);

async function setupUi() {
    // Подключаемся к порту, создаем из него стрим
    const backgroundPort = extensionApi.runtime.connect({name: 'popup'});
    const connectionStream = new PortStream(backgroundPort);

    // Создаем пустой observable для состояния background'a
    let backgroundState = observable.object({});
    const api = {
        //Отдаем бекграунду функцию, которая будет обновлять observable
        updateState: async state => {
            Object.assign(backgroundState, state)
        }
    };

    // Делаем RPC объект
    const dnode = setupDnode(connectionStream, api);
    const background = await new Promise(resolve => {
        dnode.once('remote', remoteApi => {
            resolve(transformMethods(cbToPromise, remoteApi))
        })
    });

    // Добавляем в background observable со стейтом
    background.state = backgroundState;

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

    // Запуск интерфейса
    await initApp(background)
}

Төгсгөлд нь бид програмын интерфейсийг үзүүлж эхэлнэ. Энэ бол хариу үйлдэл үзүүлэх програм юм. Арын дэвсгэр объектыг зүгээр л тулгуур ашиглан дамжуулдаг. Мэдээжийн хэрэг, аргын хувьд тусдаа үйлчилгээ, улсын дэлгүүр хийх нь зөв байх болно, гэхдээ энэ зүйлийн зорилгын хувьд энэ нь хангалттай юм.

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

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

mobx-ийн тусламжтайгаар өгөгдөл өөрчлөгдөхөд рэндэрлэж эхлэхэд маш хялбар байдаг. Бид зүгээр л ажиглагч чимэглэгчийг багцаас өлгөдөг mobx-react Бүрэлдэхүүн хэсэг дээр дурьдсан аливаа ажиглагдах зүйл өөрчлөгдөхөд render автоматаар дуудагдах болно. Танд mapStateToProps хэрэггүй эсвэл redux шиг холбогдох шаардлагагүй. Бүх зүйл хайрцагнаас шууд ажилладаг:

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

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

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

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

Үлдсэн бүрэлдэхүүн хэсгүүдийг кодоос харж болно UI хавтсанд.

Одоо програмын ангилалд та UI-д төлөв сонгогч хийж, өөрчлөгдөх үед UI-д мэдэгдэх хэрэгтэй. Үүнийг хийхийн тулд нэг аргыг нэмье getState и reactionдуудаж байна remote.updateState:

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

export class SignerApp {

    ...

    // public
    getState() {
        return {
            keys: this.store.keys,
            messages: this.store.newMessages,
            initialized: this.store.initialized,
            locked: this.store.locked
        }
    }

    ...

    //
    connectPopup(connectionStream) {
        const api = this.popupApi();
        const dnode = setupDnode(connectionStream, api);

        dnode.once('remote', (remote) => {
            // Создаем reaction на изменения стейта, который сделает вызовет удаленну процедуру и обновит стейт в ui процессе
            const updateStateReaction = reaction(
                () => this.getState(),
                (state) => remote.updateState(state),
                // Третьим аргументом можно передавать параметры. fireImmediatly значит что reaction выполниться первый раз сразу.
                // Это необходимо, чтобы получить начальное состояние. Delay позволяет установить debounce
                {fireImmediately: true, delay: 500}
            );
            // Удалим подписку при отключении клиента
            dnode.once('end', () => updateStateReaction.dispose())

        })
    }

    ...
}

Объектыг хүлээн авах үед remote үүсгэгдсэн reaction UI талд функцийг дууддаг төлөвийг өөрчлөх.

Эцсийн мэдрэгч нь өргөтгөлийн дүрс дээр шинэ мессежийн дэлгэцийг нэмэх явдал юм:

function setupApp() {
...

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

...
}

Тиймээс өргөдөл бэлэн боллоо. Вэб хуудсууд гүйлгээнд гарын үсэг зурах хүсэлт гаргаж болно:

Аюулгүй хөтчийн өргөтгөл бичиж байна

Аюулгүй хөтчийн өргөтгөл бичиж байна

Кодыг эндээс авах боломжтой холбоос.

дүгнэлт

Хэрэв та нийтлэлийг дуустал нь уншсан ч асуулт байгаа бол эндээс асууж болно өргөтгөл бүхий агуулахууд. Тэнд та мөн заасан алхам бүрийн үүрэг хариуцлагыг олох болно.

Хэрэв та бодит өргөтгөлийн кодыг сонирхож байвал үүнийг олж болно энд.

Код, хадгалах газар, ажлын байрны тодорхойлолт сиемарелл

Эх сурвалж: www.habr.com

сэтгэгдэл нэмэх