ደህንነቱ የተጠበቀ የአሳሽ ቅጥያ በመጻፍ ላይ

ደህንነቱ የተጠበቀ የአሳሽ ቅጥያ በመጻፍ ላይ

ከተለመደው “ደንበኛ-አገልጋይ” አርክቴክቸር በተለየ ያልተማከለ ትግበራዎች በሚከተሉት ተለይተው ይታወቃሉ፡

  • በተጠቃሚ መግቢያዎች እና የይለፍ ቃሎች የውሂብ ጎታ ማከማቸት አያስፈልግም. የመዳረሻ መረጃ የሚቀመጠው በተጠቃሚዎች ብቻ ነው፣ እና የእነሱ ትክክለኛነት ማረጋገጫ በፕሮቶኮል ደረጃ ላይ ይከሰታል።
  • አገልጋይ መጠቀም አያስፈልግም። የመተግበሪያው አመክንዮ የሚፈለገውን የውሂብ መጠን ማከማቸት በሚቻልበት በ blockchain አውታረመረብ ላይ ሊተገበር ይችላል.

ለተጠቃሚ ቁልፎች 2 በአንጻራዊነት ደህና ማከማቻዎች አሉ - የሃርድዌር ቦርሳዎች እና የአሳሽ ቅጥያዎች። የሃርድዌር የኪስ ቦርሳዎች በአብዛኛው እጅግ በጣም ደህንነታቸው የተጠበቀ ነው፣ ግን ለመጠቀም አስቸጋሪ እና ከነጻ የራቁ ናቸው፣ ነገር ግን የአሳሽ ማራዘሚያዎች ፍጹም የደህንነት እና የአጠቃቀም ቅንጅት ናቸው፣ እና ለዋና ተጠቃሚዎችም ሙሉ በሙሉ ነፃ ሊሆኑ ይችላሉ።

ይህንን ሁሉ ግምት ውስጥ በማስገባት ከግብይቶች እና ፊርማዎች ጋር ለመስራት ቀላል ኤፒአይ በማቅረብ ያልተማከለ አፕሊኬሽኖችን እድገትን የሚያቃልል በጣም ደህንነቱ የተጠበቀ ቅጥያ ለማድረግ እንፈልጋለን።
ከዚህ በታች ስለዚህ ተሞክሮ እንነግርዎታለን.

ጽሑፉ የአሳሽ ቅጥያ እንዴት እንደሚጻፍ የደረጃ በደረጃ መመሪያዎችን ከኮድ ምሳሌዎች እና ቅጽበታዊ ገጽ እይታዎች ጋር ይይዛል። ሁሉንም ኮድ በ ውስጥ ማግኘት ይችላሉ። ማከማቻዎች. እያንዳንዱ ቃል በምክንያታዊነት ከዚህ ጽሑፍ ክፍል ጋር ይዛመዳል።

የአሳሽ ቅጥያዎች አጭር ታሪክ

የአሳሽ ማራዘሚያዎች ለረጅም ጊዜ ነበሩ. እ.ኤ.አ. በ1999 በፋየርፎክስ እ.ኤ.አ. በ2004 በኢንተርኔት ኤክስፕሎረር ታይተዋል። ሆኖም ግን, ለረጅም ጊዜ ማራዘሚያዎች አንድ ነጠላ መስፈርት አልነበረም.

በአራተኛው የ Google Chrome ስሪት ውስጥ ከቅጥያዎች ጋር አብሮ ታየ ማለት እንችላለን። በእርግጥ በዚያን ጊዜ ምንም ዝርዝር መግለጫ አልነበረም፣ ግን መሰረቱ የሆነው Chrome API ነበር፡ አብዛኛው የአሳሽ ገበያን ድል በማድረግ እና አብሮ የተሰራ የመተግበሪያ መደብር ያለው፣ Chrome በእውነቱ የአሳሽ ቅጥያዎችን መስፈርት አዘጋጅቷል።

ሞዚላ የራሱ መስፈርት ነበረው ነገር ግን የ Chrome ቅጥያዎችን ታዋቂነት በማየቱ ኩባንያው ተኳሃኝ ኤፒአይ ለመስራት ወሰነ። እ.ኤ.አ. በ 2015 ፣ በሞዚላ ተነሳሽነት ፣ በአሳሽ ማራዘሚያ ዝርዝሮች ላይ ለመስራት በአለም አቀፍ ድር ኮንሰርቲየም (W3C) ውስጥ ልዩ ቡድን ተፈጠረ።

የ Chrome ነባር የኤፒአይ ቅጥያዎች እንደ መሰረት ተወስደዋል። ስራው የተካሄደው በማይክሮሶፍት ድጋፍ ነው (ጉግል በደረጃው ልማት ውስጥ ለመሳተፍ ፈቃደኛ አልሆነም) እና በዚህ ምክንያት ረቂቅ ታየ ዝርዝር መግለጫዎች.

በመደበኛነት መግለጫው በ Edge ፣ Firefox እና Opera ይደገፋል (Chrome በዚህ ዝርዝር ውስጥ እንደሌለ ልብ ይበሉ)። ነገር ግን እንደ እውነቱ ከሆነ, ደረጃው በአብዛኛው ከ Chrome ጋር ተኳሃኝ ነው, ምክንያቱም በእውነቱ ቅጥያዎቹ ላይ ተመስርቷል. ስለ WebExtensions API የበለጠ ማንበብ ትችላለህ እዚህ.

የኤክስቴንሽን መዋቅር

ለቅጥያው የሚያስፈልገው ብቸኛው ፋይል አንጸባራቂ (manifest.json) ነው። እንዲሁም የማስፋፊያው "የመግቢያ ነጥብ" ነው.

አንጸባራቂ።

በመግለጫው መሰረት፣ የሰነድ ሰነዱ ትክክለኛ የJSON ፋይል ነው። የትኛዎቹ ቁልፎች በየትኛው አሳሽ ውስጥ እንደሚታዩ መረጃ ያለው ዝርዝር መግለጫ ቁልፎች ሙሉ መግለጫ እዚህ.

በዝርዝሩ ውስጥ የሌሉ ቁልፎች ችላ ሊባሉ ይችላሉ (ሁለቱም Chrome እና Firefox ስህተቶችን ሪፖርት ያደርጋሉ, ግን ቅጥያዎቹ መስራታቸውን ይቀጥላሉ).

እና ወደ አንዳንድ ነጥቦች ትኩረት መስጠት እፈልጋለሁ.

  1. ዳራ - የሚከተሉትን መስኮች የሚያካትት ዕቃ
    1. ስክሪፕቶች - ከበስተጀርባ አውድ ውስጥ የሚፈጸሙ የስክሪፕቶች ስብስብ (ስለዚህ ትንሽ ቆይቶ እንነጋገራለን);
    2. ገጽ - በባዶ ገጽ ውስጥ ከሚፈጸሙ ስክሪፕቶች ይልቅ ኤችቲኤምኤል ከይዘት ጋር መግለጽ ይችላሉ። በዚህ ሁኔታ, የስክሪፕት መስኩ ችላ ይባላል, እና ስክሪፕቶቹ ወደ የይዘት ገጽ ውስጥ ማስገባት አለባቸው;
    3. በጥንካሬ ሠራ — ሁለትዮሽ ባንዲራ፣ ካልተገለጸ፣ አሳሹ ምንም ነገር እየሰራ እንዳልሆነ ሲቆጥር የጀርባ ሂደቱን “ይገድላል” እና አስፈላጊ ከሆነ እንደገና ያስጀምረዋል። ያለበለዚያ ገጹ የሚወርደው አሳሹ ሲዘጋ ብቻ ነው። በፋየርፎክስ ውስጥ አይደገፍም።
  2. የይዘት_ስክሪፕቶች - የተለያዩ ስክሪፕቶችን ወደተለያዩ ድረ-ገጾች እንዲጭኑ የሚያስችልዎ የነገሮች ስብስብ። እያንዳንዱ ነገር የሚከተሉትን አስፈላጊ መስኮች ይይዛል-
    1. ግጥሚያዎች - ስርዓተ ጥለት urlአንድ የተወሰነ የይዘት ስክሪፕት መካተት ወይም አለመካተቱን የሚወስነው።
    2. js - በዚህ ግጥሚያ ላይ የሚጫኑ የስክሪፕቶች ዝርዝር;
    3. ግጥሚያዎችን_አግልል። - ከሜዳ አያካትትም match ከዚህ መስክ ጋር የሚዛመዱ ዩአርኤሎች።
  3. ገጽ_ድርጊት - በእውነቱ በአሳሹ ውስጥ ካለው የአድራሻ አሞሌ ቀጥሎ ለሚታየው አዶ እና ከእሱ ጋር መስተጋብር ኃላፊነት ያለው ነገር ነው። እንዲሁም ብቅ ባይ መስኮት እንዲያሳዩ ይፈቅድልዎታል፣ ይህም የእራስዎን HTML፣ CSS እና JS በመጠቀም ይገለጻል።
    1. ነባሪ_ብቅ-ባይ - በብቅ ባይ በይነገጽ ወደ HTML ፋይል የሚወስደው መንገድ CSS እና JS ሊይዝ ይችላል።
  4. ፍቃዶች - የቅጥያ መብቶችን ለማስተዳደር ድርድር። 3 የመብቶች ዓይነቶች አሉ, እነሱም በዝርዝር ተገልጸዋል እዚህ
  5. ድር_ተደራሽ_ሃብቶች — አንድ ድረ-ገጽ የሚጠይቃቸው የኤክስቴንሽን መርጃዎች፣ ለምሳሌ ምስሎች፣ JS፣ CSS፣ HTML ፋይሎች።
  6. በውጪ_ሊገናኝ የሚችል — እዚህ የሌሎች ቅጥያዎችን እና ሊገናኙባቸው የሚችሉባቸው የድረ-ገጾች ጎራዎች መታወቂያዎችን በግልፅ መግለጽ ይችላሉ። አንድ ጎራ ሁለተኛ ደረጃ ወይም ከዚያ በላይ ሊሆን ይችላል። በፋየርፎክስ ውስጥ አይሰራም።

የአፈጻጸም አውድ

ቅጥያው ሶስት የኮድ ማስፈጸሚያ አውዶች አሉት፣ ማለትም፣ አፕሊኬሽኑ የተለያዩ የአሳሽ ኤፒአይ መዳረሻ ያላቸው ሶስት ክፍሎችን ያቀፈ ነው።

የኤክስቴንሽን አውድ

አብዛኛው ኤፒአይ እዚህ አለ። በዚህ አውድ ውስጥ “ይኖራሉ”፡-

  1. የበስተጀርባ ገጽ - የቅጥያው "የጀርባ" ክፍል. ፋይሉ የ "ዳራ" ቁልፍን በመጠቀም በማንፀባረቂያው ውስጥ ተገልጿል.
  2. ብቅ ባይ ገጽ - በቅጥያው አዶ ላይ ጠቅ ሲያደርጉ ብቅ ባይ ገጽ። በማኒፌስቶው ውስጥ browser_action -> default_popup.
  3. ብጁ ገጽ - የኤክስቴንሽን ገጽ ፣ “መኖር” በተለየ የእይታ ትር ውስጥ chrome-extension://<id_расширения>/customPage.html.

ይህ አውድ ከአሳሽ መስኮቶች እና ትሮች ነጻ አለ። የበስተጀርባ ገጽ በአንድ ቅጂ ውስጥ አለ እና ሁልጊዜም ይሰራል (ልዩ ሁኔታ የክስተት ገጽ ነው ፣ የበስተጀርባ ስክሪፕት በአንድ ክስተት ሲጀመር እና ከተፈፀመ በኋላ “ይሞታል”)። ብቅ ባይ ገጽ ብቅ ባይ መስኮቱ ሲከፈት ይኖራል, እና ብጁ ገጽ - ከእሱ ጋር ያለው ትር ክፍት ሆኖ ሳለ. ከዚህ አውድ ወደ ሌሎች ትሮች እና ይዘቶቻቸው ምንም መዳረሻ የለም።

የይዘት ስክሪፕት አውድ

የይዘት ስክሪፕት ፋይል ከእያንዳንዱ አሳሽ ትር ጋር ተጀምሯል። የቅጥያው ኤፒአይ ከፊል እና የድረ-ገጹ የDOM ዛፍ መዳረሻ አለው። ከገጹ ጋር መስተጋብር ተጠያቂ የሆኑት የይዘት ስክሪፕቶች ናቸው። የ DOM ዛፉን የሚቆጣጠሩ ቅጥያዎች ይህንን በይዘት ስክሪፕቶች ውስጥ ያደርጉታል - ለምሳሌ የማስታወቂያ አጋጆች ወይም ተርጓሚዎች። እንዲሁም የይዘት ስክሪፕቱ ከገጹ ጋር በመደበኛነት መገናኘት ይችላል። postMessage.

የድረ-ገጽ አውድ

ይህ ትክክለኛው ድረ-ገጽ ራሱ ነው። የዚህ ገጽ ጎራ በአንጸባራቂው ውስጥ በግልጽ ካልተጠቀሰ በስተቀር (ከዚህ በታች በዚህ ላይ ተጨማሪ) ካልሆነ በስተቀር ከቅጥያው ጋር ምንም ግንኙነት የለውም እና ወደዚያ መዳረሻ የለውም።

የመልዕክት ልውውጥ

የመተግበሪያው የተለያዩ ክፍሎች እርስ በርሳቸው መልእክት መለዋወጥ አለባቸው። ለዚህ ኤፒአይ አለ። runtime.sendMessage መልእክት ለመላክ background и tabs.sendMessage ወደ ገጽ መልእክት ለመላክ (የይዘት ስክሪፕት ፣ ብቅ ባይ ወይም ድረ-ገጽ ካለ externally_connectable). የChrome ኤፒአይን ሲደርሱ ምሳሌ ከዚህ በታች አለ።

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

የመተግበሪያ ንድፍ

የግል ቁልፎችን የሚያከማች፣የሕዝብ መረጃ መዳረሻ የሚሰጥ (አድራሻ፣የሕዝብ ቁልፍ ከገጹ ጋር ይገናኛል እና የሶስተኛ ወገን መተግበሪያዎች ለግብይቶች ፊርማ እንዲጠይቁ የሚያስችል) አሳሽ ቅጥያ እንሥራ።

የመተግበሪያ ልማት

የእኛ መተግበሪያ ከተጠቃሚው ጋር መስተጋብር መፍጠር እና ገጹን ለመደወል ዘዴዎችን (ለምሳሌ ግብይቶችን ለመፈረም) ከኤፒአይ ጋር ማቅረብ አለበት። በአንድ ብቻ ያድርጉት contentscript ወደ DOM ብቻ መዳረሻ ስላለው፣ ግን የገጹ JS ስላልሆነ አይሰራም። በ በኩል ይገናኙ runtime.connect አንችልም፣ ምክንያቱም ኤፒአይ በሁሉም ጎራዎች ላይ ያስፈልጋል፣ እና የተወሰኑት ብቻ በአንጸባራቂው ውስጥ ሊገለጹ ይችላሉ። በውጤቱም, ስዕሉ ይህን ይመስላል.

ደህንነቱ የተጠበቀ የአሳሽ ቅጥያ በመጻፍ ላይ

ሌላ ስክሪፕት ይኖራል - inpage, ይህም ወደ ገጹ ውስጥ እናስገባዋለን. በእሱ አውድ ውስጥ ይሰራል እና ከቅጥያው ጋር ለመስራት ኤፒአይ ያቀርባል።

የመጀመሪያው

ሁሉም የአሳሽ ቅጥያ ኮድ በ ላይ ይገኛል። የፊልሙ. በማብራሪያው ጊዜ ለድርጊቶች አገናኞች ይኖራሉ።

በማኒፌስቶው እንጀምር፡-

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

ባዶ ዳራ.js፣ popup.js፣ inpage.js እና contentscript.js ይፍጠሩ። popup.html እንጨምራለን - እና የእኛ መተግበሪያ ቀድሞውኑ ወደ ጎግል ክሮም ሊጫን እና መስራቱን ያረጋግጡ።

ይህንን ለማረጋገጥ, ኮዱን መውሰድ ይችላሉ እዚህ. እኛ ካደረግነው በተጨማሪ አገናኙ የዌብፓክን በመጠቀም የፕሮጀክቱን ስብሰባ አዋቅሯል. መተግበሪያን ወደ አሳሹ ለመጨመር በ chrome://extensions ውስጥ ያልታሸገ ጭነት እና አቃፊውን ከተዛማጅ ቅጥያ ጋር መምረጥ ያስፈልግዎታል - በእኛ ሁኔታ dist.

ደህንነቱ የተጠበቀ የአሳሽ ቅጥያ በመጻፍ ላይ

አሁን የእኛ ቅጥያ ተጭኗል እና እየሰራ ነው። የገንቢ መሳሪያዎችን ለተለያዩ ሁኔታዎች እንደሚከተለው ማሄድ ይችላሉ፡

ብቅ ባይ ->

ደህንነቱ የተጠበቀ የአሳሽ ቅጥያ በመጻፍ ላይ

የይዘት ስክሪፕት ኮንሶል መዳረሻ በራሱ በተከፈተበት የገጹ ኮንሶል በኩል ይከናወናል።ደህንነቱ የተጠበቀ የአሳሽ ቅጥያ በመጻፍ ላይ

የመልዕክት ልውውጥ

ስለዚህ፣ ሁለት የመገናኛ መንገዶችን መመስረት አለብን፡ inገጽ <-> ዳራ እና ብቅ ባይ <-> ዳራ። በእርግጥ መልዕክቶችን ወደ ወደብ መላክ እና የራስዎን ፕሮቶኮል መፍጠር ይችላሉ ፣ ግን በሜታማስክ ክፍት ምንጭ ፕሮጀክት ውስጥ ያየሁትን አካሄድ እመርጣለሁ።

ይህ ከ 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)))
})

አሁን የመተግበሪያ ክፍል እንፈጥራለን. ለ ብቅ-ባይ እና ድረ-ገጽ የኤፒአይ ነገሮችን ይፈጥራል እና ለእነሱ 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 ነገር ይልቅ፣ 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)
    }
}

ዲኖድ ከጅረቶች ጋር ስለሚሰራ እና ወደብ ስለምንቀበል አስማሚ ክፍል ያስፈልጋል። በአሳሹ ውስጥ የ 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()
    }
}

አሁን በዩአይዩ ውስጥ ግንኙነት እንፍጠር፡-

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

ኤፒአይ የምንፈልገው በይዘት ስክሪፕት ውስጥ ሳይሆን በቀጥታ በገጹ ላይ ስለሆነ ሁለት ነገሮችን እናደርጋለን፡-

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

አሁን በገጽ ውስጥ የኤፒአይ ነገር ፈጠርን እና ወደ ዓለምአቀፋዊ አቀናብረነዋል፡-

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

ዝግጁ ነን የርቀት ሂደት ጥሪ (RPC) ከገጽ እና UI የተለየ ኤፒአይ ያለው. አዲስ ገጽ ከበስተጀርባ ስናገናኝ ይህንን ማየት እንችላለን፡-

ደህንነቱ የተጠበቀ የአሳሽ ቅጥያ በመጻፍ ላይ

ባዶ ኤፒአይ እና መነሻ። በገጹ በኩል የሄሎ ተግባርን እንደሚከተለው ልንለው እንችላለን፡-

ደህንነቱ የተጠበቀ የአሳሽ ቅጥያ በመጻፍ ላይ

በዘመናዊው JS ውስጥ የመልሶ መደወል ተግባራትን መስራት መጥፎ ምግባር ነው፣ስለዚህ የኤፒአይ ነገርን ወደ መገልገያዎች እንዲያልፉ የሚያስችልዎትን ዲኖድ ለመፍጠር ትንሽ ረዳት እንፃፍ።

የኤፒአይ ነገሮች አሁን ይህን ይመስላል፡-

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 እና የዥረት አቀራረብ በጣም ተለዋዋጭ ይመስላል፡ የእንፋሎት ማባዛትን መጠቀም እና ለተለያዩ ስራዎች የተለያዩ ኤፒአይዎችን መፍጠር እንችላለን። በመርህ ደረጃ, dnode በየትኛውም ቦታ መጠቀም ይቻላል, ዋናው ነገር መጓጓዣውን በ nodejs ዥረት መልክ መጠቅለል ነው.

የJSON RPC 2 ፕሮቶኮልን የሚተገበረው አማራጭ የJSON ቅርጸት ነው።ነገር ግን፣ ከተወሰኑ መጓጓዣዎች (TCP እና HTTP(S)) ጋር ይሰራል፣ይህም በእኛ ጉዳይ ላይ የማይተገበር ነው።

የውስጥ ግዛት እና የአካባቢ ማከማቻ

የመተግበሪያውን ውስጣዊ ሁኔታ ማከማቸት ያስፈልገናል - ቢያንስ የመፈረሚያ ቁልፎች. በብቅ ባዩ ኤፒአይ ውስጥ ወደ አፕሊኬሽኑ እና ለመለወጥ ዘዴዎች በቀላሉ ሁኔታን ማከል እንችላለን።

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 አስፈላጊ ይሆናል፣ እና ለለውጦች መመዝገብም እፈልጋለሁ። በዚህ መሠረት, የሚታይ ማከማቻ ለመፍጠር እና ለለውጦቹ መመዝገብ አመቺ ይሆናል.

የ 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. ጥብቅ በሆነ ሁነታ በአስፈፃሚ ድርጊቶች ባንዲራ፣ mobx ግዛቱን በቀጥታ መቀየር ይከለክላል። ጥብቅ በሆኑ ሁኔታዎች ውስጥ መሥራት ጥሩ አሠራር ተደርጎ ይቆጠራል.
  2. ምንም እንኳን አንድ ተግባር ግዛቱን ብዙ ጊዜ ቢቀይርም - ለምሳሌ በበርካታ የኮድ መስመሮች ውስጥ ብዙ መስኮችን እንለውጣለን - ታዛቢዎቹ ሲጠናቀቅ ብቻ ይነገራቸዋል. ይህ በተለይ ለግንባር ግንባሩ አስፈላጊ ነው፣ አላስፈላጊ የግዛት ዝመናዎች ወደ አላስፈላጊ የንጥረ ነገሮች አፈጣጠር ይመራሉ ። በእኛ ሁኔታ, የመጀመሪያውም ሆነ ሁለተኛው በተለይ ጠቃሚ አይደለም, ነገር ግን ምርጥ ልምዶችን እንከተላለን. የተመለከቱትን መስኮች ሁኔታ የሚቀይሩትን ሁሉንም ተግባራት ማስጌጫዎችን ማያያዝ የተለመደ ነው.

ከበስተጀርባ ማስጀመር እና ግዛቱን በአካባቢ ማከማቻ ውስጥ እናስቀምጣለን፡-

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. በዚህ ውሂብ በተለወጠ ቁጥር የሚጠራ ተቆጣጣሪ።

እንደ ሬዱክስ ሳይሆን፣ መንግስትን እንደ መከራከሪያ በግልፅ ከምንቀበልበት፣ mobx የትኞቹን ታዛቢዎች በመራጩ ውስጥ እንደምናገኝ ያስታውሳል፣ እና ተቆጣጣሪው ሲቀየሩ ብቻ ይደውላል።

mobx የትኛዎቹ ታዛቢዎችን እንደምንመዘግብ በትክክል እንዴት እንደሚወስን መረዳት በጣም አስፈላጊ ነው። እንደዚህ አይነት ኮድ ውስጥ መራጭ ከጻፍኩ() => app.store, ከዚያ ምላሽ ፈጽሞ አይጠራም, ምክንያቱም ማከማቻው ራሱ የማይታይ ስለሆነ, መስኮቹ ብቻ ናቸው.

እንደዚህ ከጻፍኩት () => app.store.keys, ከዚያ እንደገና ምንም ነገር አይከሰትም, ምክንያቱም የድርድር ክፍሎችን ሲጨምሩ / ሲያስወግዱ, የእሱ ማመሳከሪያ አይለወጥም.

Mobx ለመጀመሪያ ጊዜ እንደ መራጭ ይሰራል እና እኛ የደረስናቸውን ታዛቢዎችን ብቻ ይከታተላል። ይህ የሚከናወነው በፕሮክሲ ጌተርስ በኩል ነው። ስለዚህ, አብሮ የተሰራው ተግባር እዚህ ጥቅም ላይ ይውላል toJS. ሁሉም ፕሮክሲዎች በዋናው መስኮች ተተክተው አዲስ ነገር ይመልሳል። በአፈፃፀም ወቅት የነገሩን ሁሉንም መስኮች ያነባል - ስለዚህ ጌቶች ይነሳሉ.

በብቅ ባዩ ኮንሶል ውስጥ እንደገና ብዙ ቁልፎችን እንጨምራለን. በዚህ ጊዜ እንዲሁም በአካባቢ ማከማቻ ውስጥ አብቅተዋል፡

ደህንነቱ የተጠበቀ የአሳሽ ቅጥያ በመጻፍ ላይ

የበስተጀርባ ገጹ እንደገና ሲጫን መረጃው እንዳለ ይቆያል።

እስከዚህ ነጥብ ድረስ ሁሉም የመተግበሪያ ኮድ ሊታዩ ይችላሉ እዚህ.

የግል ቁልፎችን ደህንነቱ የተጠበቀ ማከማቻ

ግልጽ በሆነ ጽሑፍ ውስጥ የግል ቁልፎችን ማከማቸት ደህንነቱ ያልተጠበቀ ነው፡ ሁል ጊዜ የመጥለፍ፣ የኮምፒውተራችንን መዳረሻ እና የመሳሰሉትን እድል አለ:: ስለዚህ, በ 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')
        }
    }
}

አሁን የተመሰጠሩትን ቁልፎች እና የይለፍ ቃል ብቻ እናከማቻለን. የተቀረው ሁሉ ይሰላል። የይለፍ ቃሉን ከስቴቱ በማስወገድ ወደ ተቆለፈ ሁኔታ ማስተላለፍን እናደርጋለን. ይፋዊው ኤፒአይ አሁን ማከማቻውን የማስጀመር ዘዴ አለው።

ለማመስጠር የተጻፈ 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)
}

አሳሹ ለክስተት መመዝገብ የምትችልበት የስራ ፈት ኤፒአይ አለው - ሁኔታ ለውጦች። ግዛት, በዚህ መሠረት, ሊሆን ይችላል idle, active и locked. ለስራ ፈት ጊዜ ማብቂያ ማቀናበር ይችላሉ እና የተቆለፈው ስርዓተ ክወናው ራሱ ሲታገድ ነው። እንዲሁም ለማስቀመጥ መራጩን ወደ አካባቢያዊ ማከማቻ እንለውጣለን፡-

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

ከዚህ እርምጃ በፊት ያለው ኮድ ነው። እዚህ.

ግብይቶች

ስለዚህ, ወደ በጣም አስፈላጊው ነገር ደርሰናል-በ blockchain ላይ ግብይቶችን መፍጠር እና መፈረም. WAVES blockchain እና ላይብረሪ እንጠቀማለን። ሞገዶች-ግብይቶች.

በመጀመሪያ፣ መፈረም ያለባቸውን የተለያዩ መልዕክቶችን ወደ ስቴቱ እንጨምር፣ በመቀጠል አዲስ መልእክት ለማከል፣ ፊርማውን የምናረጋግጥበት እና እምቢ ለማለት ዘዴዎችን እንጨምር፡-

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፡

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

በይነገጹ የመተግበሪያው ሁኔታ መዳረሻ ያስፈልገዋል። በዩአይ በኩል እኛ እናደርጋለን observable ይህንን ሁኔታ የሚቀይር ተግባር ወደ ኤፒአይ ይግለጹ እና ያክሉ። እንጨምር observable ከበስተጀርባ ለተቀበለው የኤፒአይ ነገር፡-

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 - ምላሽ በክፍሉ ላይ፣ እና ቀረጻው በቀጥታ የሚጠራው በክፍለ አህጉሩ የተገለጹ ማንኛቸውም ታዛቢዎች ሲቀየሩ ነው። ምንም 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}
    );

...
}

ስለዚህ, ማመልከቻው ዝግጁ ነው. ድረ-ገጾች ለግብይቶች ፊርማ ሊጠይቁ ይችላሉ፡-

ደህንነቱ የተጠበቀ የአሳሽ ቅጥያ በመጻፍ ላይ

ደህንነቱ የተጠበቀ የአሳሽ ቅጥያ በመጻፍ ላይ

ኮዱ እዚህ አለ። ማያያዣ.

መደምደሚያ

ጽሑፉን እስከ መጨረሻው አንብበው፣ ግን አሁንም ጥያቄዎች ካሉዎት፣ በ ላይ ሊጠይቋቸው ይችላሉ። ማከማቻዎች ከቅጥያ ጋር. እዚያም ለእያንዳንዱ የተወሰነ ደረጃ ቁርጠኝነት ያገኛሉ።

እና ለትክክለኛው ቅጥያ ኮዱን ለመመልከት ፍላጎት ካሎት, ይህንን ማግኘት ይችላሉ እዚህ.

ኮድ, ማከማቻ እና የስራ መግለጫ ከ ሲማሬል

ምንጭ: hab.com

አስተያየት ያክሉ