שרייבן אַ זיכער בלעטערער געשפּרייט

שרייבן אַ זיכער בלעטערער געשפּרייט

ניט ענלעך די פּראָסט "קליענט-סערווער" אַרקאַטעקטשער, דיסענטראַלייזד אַפּלאַקיישאַנז זענען קעראַקטערייזד דורך:

  • עס איז ניט דאַרפֿן צו קראָם אַ דאַטאַבייס מיט באַניצער לאָגינס און פּאַסווערדז. אַקסעס אינפֿאָרמאַציע איז סטאָרד אויסשליסלעך דורך די ניצערס זיך, און באַשטעטיקונג פון זייער אָטאַנטיסיטי אַקערז אויף די פּראָטאָקאָל מדרגה.
  • ניט דאַרפֿן צו נוצן אַ סערווער. די אַפּלאַקיישאַן לאָגיק קענען זיין עקסאַקיוטאַד אויף אַ בלאָקטשיין נעץ, ווו עס איז מעגלעך צו קראָם די פארלאנגט סומע פון ​​דאַטן.

עס זענען 2 לעפיערעך זיכער סטאָרידזש פֿאַר באַניצער שליסלען - ייַזנוואַרג וואָלאַץ און בלעטערער יקסטענשאַנז. האַרדוואַרע וואָלאַץ זענען מערסטנס גאָר זיכער, אָבער שווער צו נוצן און ווייַט פון פריי, אָבער בלעטערער יקסטענשאַנז זענען די שליימעסדיק קאָמבינאַציע פון ​​זיכערהייט און יז פון נוצן, און קענען אויך זיין גאָר פריי פֿאַר סוף ניצערס.

גענומען אַלע דעם אין חשבון, מיר געוואלט צו מאַכן די מערסט זיכער פאַרלענגערונג וואָס סימפּלאַפייז די אַנטוויקלונג פון דיסענטראַלייזד אַפּלאַקיישאַנז דורך צושטעלן אַ פּשוט אַפּי פֿאַר ארבעטן מיט טראַנזאַקשאַנז און סיגנאַטשערז.
מיר וועלן דערציילן איר וועגן דעם דערפאַרונג אונטן.

דער אַרטיקל וועט אַנטהאַלטן שריט-דורך-שריט ינסטראַקשאַנז אויף ווי צו שרייַבן אַ בלעטערער פאַרלענגערונג, מיט קאָד ביישפילן און סקרעענשאָץ. איר קענען געפֿינען אַלע די קאָד אין ריפּאַזאַטאָריז. יעדער יבערגעבן לאַדזשיקלי קאָראַספּאַנדז צו אַ אָפּטיילונג פון דעם אַרטיקל.

א קורץ געשיכטע פון ​​בלעטערער יקסטענשאַנז

בלעטערער יקסטענשאַנז האָבן שוין אַרום פֿאַר אַ לאַנג צייַט. זיי ארויס אין Internet Explorer צוריק אין 1999, אין פירעפאָקס אין 2004. אָבער, פֿאַר אַ זייער לאַנג צייַט עס איז געווען קיין איין נאָרמאַל פֿאַר יקסטענשאַנז.

מיר קענען זאָגן אַז עס איז ארויס צוזאמען מיט יקסטענשאַנז אין דער פערט ווערסיע פון ​​Google קראָום. פון קורס, עס איז געווען קיין ספּעסיפיקאַטיאָן דעמאָלט, אָבער עס איז געווען די קראָום אַפּי וואָס איז געווארן זיין יקער: מיט קאַנגקערד רובֿ פון די בלעטערער מאַרק און מיט אַ געבויט-אין אַפּלאַקיישאַן קראָם, קראָום אַקשלי שטעלן די נאָרמאַל פֿאַר בלעטערער יקסטענשאַנז.

מאָזיללאַ האט זיין אייגענע נאָרמאַל, אָבער זינט די פּאָפּולאַריטעט פון קראָום יקסטענשאַנז, די פירמע באַשלאָסן צו מאַכן אַ קאַמפּאַטאַבאַל אַפּי. אין 2015, אין דער איניציאטיוו פון מאָזיללאַ, אַ ספּעציעל גרופּע איז באשאפן אין די World Wide Web Consortium (W3C) צו אַרבעטן אויף די ספּעסאַפאַקיישאַנז פון קרייַז בלעטערער פאַרלענגערונג.

די יגזיסטינג אַפּי יקסטענשאַנז פֿאַר קראָום זענען גענומען ווי אַ יקער. די אַרבעט איז דורכגעקאָכט מיט די שטיצן פון מייקראָסאָפֿט (גוגל אפגעזאגט צו אָנטייל נעמען אין דער אַנטוויקלונג פון דער נאָרמאַל), און ווי אַ רעזולטאַט, אַ פּלאַן ארויס ספּעסאַפאַקיישאַנז.

פאָרמאַלי, די באַשרייַבונג איז געשטיצט דורך עדזש, פירעפאָקס און אָפּעראַ (טאָן אַז קראָום איז נישט אויף דער רשימה). אָבער אין פאַקט, דער נאָרמאַל איז לאַרגעלי קאַמפּאַטאַבאַל מיט קראָום, ווייַל עס איז אַקשלי געשריבן באזירט אויף זיין יקסטענשאַנז. איר קענט לייענען מער וועגן די WebExtensions API דאָ.

געשפּרייט סטרוקטור

דער בלויז טעקע וואָס איז פארלאנגט פֿאַר די פאַרלענגערונג איז די מאַנאַפעסטיישאַן (manifest.json). עס איז אויך די "פּאָזיציע פונט" צו די יקספּאַנשאַן.

Manifesto

לויט די ספּעסאַפאַקיישאַנז, די באַשייַמפּערלעך טעקע איז אַ גילטיק JSON טעקע. א פול באַשרייַבונג פון באַשייַמפּערלעך קיז מיט אינפֿאָרמאַציע וועגן וואָס שליסלען זענען געשטיצט אין וואָס בלעטערער קענען זיין וויוד דאָ.

קיז וואָס זענען נישט אין די באַשרייַבונג "קען" זיין איגנאָרירט (ביידע קראָום און פירעפאָקס באַריכט ערראָרס, אָבער די יקסטענשאַנז פאָרזעצן צו אַרבעטן).

און איך וואָלט ווי צו ציען ופמערקזאַמקייַט צו עטלעכע פונקטן.

  1. הינטערגרונט - אַ כייפעץ וואָס כולל די פאלגענדע פעלדער:
    1. סקריפּס - אַ מענגע פון ​​סקריפּס וואָס וועט זיין עקסאַקיוטאַד אין די הינטערגרונט קאָנטעקסט (מיר וועלן רעדן וועגן דעם אַ ביסל שפּעטער);
    2. בלאַט - אַנשטאָט פון סקריפּס וואָס וועט זיין עקסאַקיוטאַד אין אַ ליידיק בלאַט, איר קענען ספּעציפיצירן HTML מיט אינהאַלט. אין דעם פאַל, די שריפט פעלד וועט זיין איגנאָרירט, און די סקריפּס וועט דאַרפֿן צו זיין ינסערטאַד אין די אינהאַלט בלאַט;
    3. אָנהאַלטן - אַ ביינערי פאָן, אויב ניט ספּעסיפיעד, דער בלעטערער וועט "טייטן" דעם הינטערגרונט פּראָצעס ווען עס האלט אַז עס טוט נישט טאָן עפּעס, און ריסטאַרט עס אויב נייטיק. אַנדערש, דער בלאַט וועט זיין אַנלאָודיד בלויז ווען דער בלעטערער איז פארמאכט. ניט געשטיצט אין פירעפאָקס.
  2. content_scripts - אַ מענגע פון ​​​​אַבדזשעקץ וואָס אַלאַוז איר צו לאָדן פאַרשידענע סקריפּס צו פאַרשידענע וועב זייַטלעך. יעדער כייפעץ כּולל די פאלגענדע וויכטיק פעלדער:
    1. שוועבעלעך - מוסטער URL, וואָס באַשטימט צי אַ באַזונדער אינהאַלט שריפט וועט זיין אַרייַנגערעכנט אָדער נישט.
    2. js - אַ רשימה פון סקריפּס וואָס וועט זיין לאָודיד אין דעם גלייַכן;
    3. ויסשליסן_מאַטשעס - יקסקלודז פון די פעלד match URL ס וואָס גלייַכן דעם פעלד.
  3. בלאַט_אַקשאַן - איז פאקטיש אַ כייפעץ וואָס איז פאַראַנטוואָרטלעך פֿאַר די ייקאַן וואָס איז געוויזן ווייַטער צו די אַדרעס באַר אין דעם בלעטערער און ינטעראַקשאַן מיט אים. עס אויך אַלאַוז איר צו ווייַזן אַ אויפֿשפּרינג פֿענצטער, וואָס איז דיפיינד מיט דיין אייגענע HTML, CSS און JS.
    1. default_popup - דרך צו די HTML טעקע מיט די אויפֿשפּרינג צובינד, קען אַנטהאַלטן CSS און JS.
  4. פּערמישאַנז - אַ מענגע פֿאַר אָנפירונג פאַרלענגערונג רעכט. עס זענען 3 טייפּס פון רעכט, וואָס זענען דיסקרייבד אין דעטאַל דאָ
  5. web_accessible_resources - פאַרלענגערונג רעסורסן אַז אַ וועב בלאַט קענען בעטן, למשל בילדער, JS, CSS, HTML טעקעס.
  6. עקסטערנעלי_קאַנעקטאַבאַל - דאָ איר קענען בפירוש ספּעציפיצירן די IDs פון אנדערע יקסטענשאַנז און דאָומיינז פון וועב זייַטלעך פֿון וואָס איר קענען פאַרבינדן. א פעלד קענען זיין צווייטע מדרגה אָדער העכער. עס אַרבעט נישט אין Firefox.

דורכפירונג קאָנטעקסט

די פאַרלענגערונג האט דריי קאַנטעקסץ פון קאָד דורכפירונג, דאָס איז, די אַפּלאַקיישאַן באשטייט פון דריי פּאַרץ מיט פאַרשידענע לעוועלס פון אַקסעס צו די בלעטערער אַפּי.

פאַרלענגערונג קאָנטעקסט

רובֿ פון די API איז בנימצא דאָ. אין דעם קאָנטעקסט זיי "לעבן":

  1. הינטערגרונט בלאַט - "באַקקענד" טייל פון די פאַרלענגערונג. דער טעקע איז ספּעסיפיעד אין די מאַנאַפעסטיישאַן מיט די "הינטערגרונט" שליסל.
  2. אויפֿשפּרינג בלאַט - אַ אויפֿשפּרינג בלאַט וואָס איז ארויס ווען איר דריקט אויף די פאַרלענגערונג ייקאַן. אין די מאַניפעסטאָ browser_action -> default_popup.
  3. מנהג בלאַט - פאַרלענגערונג בלאַט, "לעבן" אין אַ באַזונדער קוויטל פון די מיינונג chrome-extension://<id_расширения>/customPage.html.

דעם קאָנטעקסט יגזיסץ ינדיפּענדאַנטלי פון בלעטערער פֿענצטער און טאַבס. הינטערגרונט בלאַט עקסיסטירט אין אַ איין קאָפּיע און שטענדיק אַרבעט (די ויסנעם איז דער געשעעניש בלאַט, ווען דער הינטערגרונט שריפט איז לאָנטשט דורך אַ געשעעניש און "שטאַרבט" נאָך זיין דורכפירונג). אויפֿשפּרינג בלאַט יגזיסץ ווען די אויפֿשפּרינג פֿענצטער איז אָפן, און מנהג בלאַט — בשעת דער קוויטל מיט אים איז אָפן. עס איז קיין אַקסעס צו אנדערע טאַבס און זייער אינהאַלט פֿון דעם קאָנטעקסט.

אינהאַלט שריפט קאָנטעקסט

דער אינהאַלט שריפט טעקע איז לאָנטשט צוזאמען מיט יעדער בלעטערער קוויטל. עס האט אַקסעס צו אַ טייל פון די פאַרלענגערונג ס אַפּי און צו די 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.

אַפּפּליקאַטיאָן דיאַגראַמע

לאָמיר מאַכן אַ בלעטערער פאַרלענגערונג וואָס סטאָרז פּריוואַט שליסלען, גיט אַקסעס צו ציבור אינפֿאָרמאַציע (אַדרעס, ציבור שליסל קאַמיוניקייץ מיט די בלאַט און אַלאַוז דריט-פּאַרטיי אַפּלאַקיישאַנז צו בעטן אַ כסימע פֿאַר טראַנזאַקשאַנז.

אַפּפּליקאַטיאָן אַנטוויקלונג

אונדזער אַפּלאַקיישאַן מוזן ביידע ינטעראַקט מיט די באַניצער און צושטעלן די בלאַט מיט אַ אַפּי צו רופן מעטהאָדס (למשל, צו צייכן טראַנזאַקשאַנז). מאַכן זיך מיט בלויז איין 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"]
}

שאַפֿן ליידיק background.js, popup.js, inpage.js און contentscript.js. מיר לייגן popup.html - און אונדזער אַפּלאַקיישאַן קענען שוין זיין לאָודיד אין Google קראָום און מאַכן זיכער אַז עס אַרבעט.

צו באַשטעטיקן דעם, איר קענען נעמען די קאָד פונ דאַנעט. אין אַדישאַן צו וואָס מיר האָבן געטאן, די לינק קאַנפיגיערד די פֿאַרזאַמלונג פון די פּרויעקט מיט וועבפּאַק. צו לייגן אַ אַפּלאַקיישאַן צו דעם בלעטערער, ​​אין קראָום: // יקסטענשאַנז איר דאַרפֿן צו סעלעקטירן "לאָוד אַנפּאַקט" און די טעקע מיט די קאָראַספּאַנדינג פאַרלענגערונג - אין אונדזער פאַל דיסט.

שרייבן אַ זיכער בלעטערער געשפּרייט

איצט אונדזער פאַרלענגערונג איז אינסטאַלירן און אַרבעט. איר קענען לויפן די דעוועלאָפּער מכשירים פֿאַר פאַרשידענע קאַנטעקסץ ווי גייט:

אויפֿשפּרינג ->

שרייבן אַ זיכער בלעטערער געשפּרייט

אַקסעס צו די אינהאַלט שריפט קאַנסאָול איז דורכגעקאָכט דורך די קאַנסאָול פון די בלאַט זיך אויף וואָס עס איז לאָנטשט.שרייבן אַ זיכער בלעטערער געשפּרייט

אָנזאָג וועקסל

אַזוי, מיר דאַרפֿן צו פאַרלייגן צוויי קאָמוניקאַציע טשאַנאַלז: ינפּאַגע הינטערגרונט און אויפֿשפּרינג הינטערגרונט. איר קענט, פון קורס, נאָר שיקן אַרטיקלען צו די פּאָרט און אויסטראַכטן דיין אייגענע פּראָטאָקאָל, אָבער איך בעסער וועלן די צוגאַנג וואָס איך געזען אין די מעטאַמאַסק עפֿענען מקור פּרויעקט.

דאָס איז אַ בלעטערער געשפּרייט פֿאַר ארבעטן מיט די עטהערעום נעץ. אין עס, פאַרשידענע פּאַרץ פון די אַפּלאַקיישאַן יבערגעבן דורך רפּק ניצן די דנאָדע ביבליאָטעק. עס אַלאַוז איר צו אָרגאַניזירן אַ וועקסל גאַנץ געשווינד און קאַנוויניאַנטלי אויב איר צושטעלן עס מיט אַ נאָדעדזשס טייַך ווי אַ אַריבערפירן (טייַטש אַ כייפעץ וואָס ימפּלאַמאַנץ די זעלבע צובינד):

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

איצט מיר וועלן מאַכן אַ אַפּלאַקיישאַן קלאַס. עס וועט מאַכן אַפּי אַבדזשעקץ פֿאַר די אויפֿשפּרינג און וועב בלאַט, און שאַפֿן אַ דנאָדע פֿאַר זיי:

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

דאָ און אונטן, אַנשטאָט פון די גלאבאלע קראָום כייפעץ, מיר נוצן extensionApi, וואָס אַקסעס קראָום אין 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)
    }
}

זינט דנאָדע אַרבעט מיט סטרימז, און מיר באַקומען אַ פּאָרט, אַ אַדאַפּטער קלאַס איז דארף. עס איז געמאכט מיט די ליינעוודיק טייַך ביבליאָטעק, וואָס ימפּלאַמאַנץ נאָדעדזשס סטרימז אין דעם בלעטערער:

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

זינט מיר דאַרפֿן די 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);
    }
}

איצט מיר מאַכן אַן אַפּי כייפעץ אין ינפּאַגע און שטעלן עס צו גלאבאלע:

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

מיר זענען גרייט רימאָוט פּראַסידזשער רופן (רפּק) מיט באַזונדער אַפּי פֿאַר בלאַט און וי. ווען קאַנעקטינג אַ נייַע בלאַט צו הינטערגרונט מיר קענען זען דעם:

שרייבן אַ זיכער בלעטערער געשפּרייט

ליידיק אַפּי און אָנהייב. אויף די בלאַט זייַט, מיר קענען רופן די העלא פונקציע ווי דאָס:

שרייבן אַ זיכער בלעטערער געשפּרייט

ארבעטן מיט קאַללבאַקק פאַנגקשאַנז אין מאָדערן JS איז שלעכט מאַנירן, אַזוי לאָזן אונדז שרייַבן אַ קליין העלפּער צו שאַפֿן אַ דנאָדע וואָס אַלאַוז איר צו פאָרן אַן אַפּי כייפעץ צו יוטילאַטיז.

די 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 און טייַך צוגאַנג איז גאַנץ פלעקסאַבאַל: מיר קענען נוצן פּאַרע מולטיפּלעקסינג און שאַפֿן עטלעכע פאַרשידענע אַפּיס פֿאַר פאַרשידענע טאַסקס. אין פּרינציפּ, דנאָדע קענען זיין געוויינט ערגעץ, די הויפּט זאַך איז צו ייַנוויקלען די אַריבערפירן אין די פאָרעם פון אַ נאָדעדזשס טייַך.

אַן אנדער ברירה איז די JSON פֿאָרמאַט, וואָס ימפּלאַמאַנץ די JSON RPC 2. אָבער, עס אַרבעט מיט ספּעציפיש טראַנספּאָרץ (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)
        }
    }
}

לאָמיר לייגן אַ ביסל שליסלען פון די וי קאַנסאָול און זען וואָס כאַפּאַנז מיט די שטאַט:

שרייבן אַ זיכער בלעטערער געשפּרייט

די שטאַט דאַרף זיין פּערסיסטענט אַזוי אַז די שליסלען זענען נישט פאַרפאַלן ווען ריסטאַרטינג.

מיר וועלן קראָם עס אין לאָקאַל סטאָרידזש, אָווועררייטינג עס מיט יעדער ענדערונג. דערנאָך, אַקסעס צו עס וועט אויך זיין נויטיק פֿאַר די וי, און איך וואָלט אויך ווי צו אַבאָנירן צו ענדערונגען. באַזירט אויף דעם, עס וועט זיין באַקוועם צו שאַפֿן אַ אַבזערוואַבאַל סטאָרידזש און אַבאָנירן צו די ענדערונגען.

מיר וועלן נוצן די מאָבקס ביבליאָטעק (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)
    }

    ...

}

"אונטער די קאַפּטער," מאָבקס ריפּלייסט אַלע קראָם פעלדער מיט פּראַקסי און ינטערסעפּט אַלע קאַללס צו זיי. עס וועט זיין מעגלעך צו אַבאָנירן צו די אַרטיקלען.

ונטער איך וועל אָפט נוצן דעם טערמין "ווען טשאַנגינג", כאָטש דאָס איז נישט לעגאַמרע ריכטיק. מאָבקס טראַקס אַקסעס צו פעלדער. געטערס און סעטערז פון פּראַקסי אַבדזשעקץ וואָס די ביבליאָטעק קריייץ זענען געניצט.

קאַמף דעקערייטערז דינען צוויי צוועקן:

  1. אין שטרענג מאָדע מיט די ענפאָרסע אַקטיאָנס פאָן, מאָבקס פּראָוכיבאַץ גלייך טשאַנגינג די שטאַט. עס איז געהאלטן גוט פיר צו אַרבעטן אונטער שטרענג טנאָים.
  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 געדענקט וואָס אָבסערוואַבלעס מיר אַקסעס אין די סעלעקטאָר, און נאָר רופט די האַנדלער ווען זיי טוישן.

עס איז וויכטיק צו פֿאַרשטיין פּונקט ווי מאָבקס דיסיידז וואָס אַבזערוואַבאַלז מיר אַבאָנירן צו. אויב איך געשריבן אַ סעלעקטאָר אין קאָד ווי דאָס() => 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')
        }
    }
}

איצט מיר נאָר קראָם די ינקריפּטיד שליסלען און פּאַראָל. אַלץ אַנדערש איז קאַלקיאַלייטיד. מיר טאָן די אַריבערפירן צו אַ פארשפארט שטאַט דורך רימוווינג די פּאַראָל פון די שטאַט. דער ציבור אַפּי איצט האט אַ אופֿן פֿאַר ינישאַליזינג די סטאָרידזש.

געשריבן פֿאַר ענקריפּשאַן יוטילאַטיז ניצן קריפּטאָ-דזשס:

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

דער קאָד איידער דעם שריט איז דאָ.

טראַנזאַקשאַנז

אַזוי, מיר קומען צו די מערסט וויכטיק זאַך: קריייטינג און סיינינג טראַנזאַקשאַנז אויף די בלאָקטשיין. מיר וועלן נוצן די 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 זייער פּשוט: מיר פשוט טוישן די סטאַטוס פון דעם אָנזאָג, נאָך סיינינג עס אויב נייטיק.

מיר שטעלן אַפּרווו און אָפּוואַרפן אין די וי 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

די צובינד דאַרף אַקסעס צו די אַפּלאַקיישאַן שטאַט. אויף די וי זייַט מיר וועלן טאָן 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-react אויף דער קאָמפּאָנענט, און ופפירן וועט ווערן אויטאָמאַטיש גערופן ווען קיין אָבסערוואַבלעס רעפערענסט דורך די קאָמפּאָנענט טוישן. איר טאָן ניט דאַרפֿן קיין MapStateToProps אָדער פאַרבינדן ווי אין רעדוקס. אַלץ אַרבעט רעכט אויס פון די קעסטל:

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

די רוען קאַמפּאָונאַנץ קענען זיין וויוד אין די קאָד אין די וי טעקע.

איצט אין די אַפּלאַקיישאַן קלאַס איר דאַרפֿן צו מאַכן אַ שטאַט סעלעקטאָר פֿאַר די וי און געבנ צו וויסן די וי ווען עס ענדערונגען. צו טאָן דאָס, לאָזן אונדז לייגן אַ אופֿן 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 צו טוישן די שטאַט וואָס רופט די פונקציע אויף די וי זייַט.

די לעצט פאַרבינדן איז צו לייגן די אַרויסווייַזן פון נייַע אַרטיקלען אויף די פאַרלענגערונג בילדל:

function setupApp() {
...

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

...
}

אַזוי, די אַפּלאַקיישאַן איז גרייט. וועב בלעטער קען בעטן אַ כסימע פֿאַר טראַנזאַקשאַנז:

שרייבן אַ זיכער בלעטערער געשפּרייט

שרייבן אַ זיכער בלעטערער געשפּרייט

דער קאָד איז בנימצא דאָ רונג.

סאָף

אויב איר האָט לייענען דעם אַרטיקל ביז דעם סוף, אָבער נאָך האָבן פֿראגן, איר קענט פרעגן זיי ביי ריפּאַזאַטאָריז מיט פאַרלענגערונג. דאָרט איר וועט אויך געפֿינען קאַמיץ פֿאַר יעדער דעזיגנייטיד שריט.

און אויב איר זענט אינטערעסירט צו קוקן אין די קאָד פֿאַר די פאַקטיש פאַרלענגערונג, איר קענען געפֿינען דאָס דאָ.

קאָד, ריפּאַזאַטאָרי און אַרבעט באַשרייַבונג פון siemarell

מקור: www.habr.com

לייגן אַ באַמערקונג