
على عكس بنية "العميل - الخادم" الشائعة ، تتميز التطبيقات اللامركزية بما يلي:
- لا حاجة لتخزين قاعدة بيانات مع تسجيلات دخول المستخدم وكلمات المرور. يتم تخزين معلومات الوصول بشكل حصري من قبل المستخدمين أنفسهم ، ويحدث تأكيد صحتها على مستوى البروتوكول.
- لا حاجة لاستخدام الخادم. يمكن تنفيذ منطق التطبيق على شبكة blockchain ، حيث يمكن تخزين الكمية المطلوبة من البيانات.
يوجد مستودعتان آمنتان نسبيًا لمفاتيح المستخدم - محافظ الأجهزة وملحقات المستعرض. معظم محافظ الأجهزة آمنة بقدر الإمكان ، لكنها صعبة الاستخدام وبعيدة عن الاستخدام المجاني ، ولكن ملحقات المستعرضات هي مزيج مثالي من الأمان وسهولة الاستخدام ، ويمكن أن تكون أيضًا مجانية تمامًا للمستخدمين النهائيين.
بالنظر إلى كل هذا ، أردنا أن نجعل الإضافة الأكثر أمانًا ، والتي تبسط تطوير التطبيقات اللامركزية ، وتوفر واجهة برمجة تطبيقات بسيطة للتعامل مع المعاملات والتوقيعات.
سنخبرك عن هذه التجربة أدناه.
ستوفر المقالة إرشادات خطوة بخطوة حول كيفية كتابة ملحق المستعرض ، مع أمثلة التعليمات البرمجية ولقطات الشاشة. يمكنك أن تجد كل الكود في المستودع . يتوافق كل التزام منطقيًا مع قسم من هذه المقالة.
تاريخ موجز لملحقات المتصفح
امتدادات المتصفح موجودة منذ بعض الوقت. في Internet Explorer ، ظهرت في عام 1999 ، في Firefox - في 2004. ومع ذلك ، لم يكن هناك معيار واحد للإضافات لفترة طويلة جدًا.
يمكننا القول أنه ظهر مع الإضافات في الإصدار الرابع من Google Chrome. بالطبع ، لم تكن هناك مواصفات بعد ، ولكن واجهة برمجة تطبيقات Chrome هي أساسها: بعد أن احتلت جزءًا كبيرًا من سوق المتصفح وامتلكت متجر تطبيقات مدمجًا ، حدد Chrome في الواقع المعيار لإضافات المتصفح.
كان لدى Mozilla معيارها الخاص ، ولكن بعد رؤية شعبية الإضافات لمتصفح Chrome ، قررت الشركة إنشاء واجهة برمجة تطبيقات متوافقة. في عام 2015 ، بمبادرة من Mozilla ، تم إنشاء مجموعة خاصة ضمن اتحاد شبكة الويب العالمية (W3C) للعمل على مواصفات ملحقات المستعرضات المتقاطعة.
استنادًا إلى إضافات API الموجودة بالفعل لـ Chrome. تم دعم العمل من قِبل Microsoft (رفضت Google المشاركة في تطوير المعيار) ، ونتيجة لذلك ، ظهرت مسودة للمواصفات .
بشكل رسمي ، يتم دعم المواصفات بواسطة Edge و Firefox و Opera (لاحظ أن Chrome ليس في هذه القائمة). لكن في الواقع ، يتوافق المعيار إلى حد كبير مع Chrome ، لأنه مكتوب بالفعل بناءً على امتداداته. اقرأ المزيد حول WebExtensions API هنا .
هيكل التمديد
الملف الوحيد المطلوب للملحق هو البيان (manifest.json). هو "نقطة الدخول" إلى التمديد.
بيان رسمي
حسب المواصفات ، ملف البيان هو ملف JSON صالح. وصف كامل لمفاتيح البيان مع معلومات حول المفاتيح التي يتم دعم المتصفح بها هنا .
"يمكن" تجاهل المفاتيح غير الموجودة في المواصفات (أخطاء تقرير Chrome و Firefox ، ولكن الإضافات تستمر في العمل).
وأود أن ألفت الانتباه إلى بعض النقاط.
- الخلفية - كائن يتضمن الحقول التالية:
- البرامج النصية - مجموعة من النصوص التي سيتم تنفيذها في سياق الخلفية (سنتحدث عن هذا لاحقًا) ؛
- صفحة - بدلاً من البرامج النصية التي سيتم تنفيذها على صفحة فارغة ، يمكنك تحديد html مع المحتوى. في هذه الحالة ، سيتم تجاهل حقل البرنامج النصي ، وسوف تحتاج إلى إدخال البرامج النصية في الصفحة مع المحتوى ؛
- مستمر - علامة ثنائية ، إن لم تكن محددة ، سيقوم المتصفح "بقتل" عملية الخلفية عندما يعتبر أنه لا يفعل أي شيء ، وإعادة التشغيل إذا لزم الأمر. وإلا ، سيتم إلغاء تحميل الصفحة فقط عند إغلاق المتصفح. غير مدعوم في Firefox.
- content_scripts - مجموعة من الكائنات التي تسمح لك بتحميل نصوص مختلفة إلى صفحات الويب المختلفة. يحتوي كل كائن على الحقول المهمة التالية:
- التطابقات - نمط عنوان url الذي يتم به تحديد ما إذا كان سيتم تضمين نص محتوى معين أم لا.
- شبيبة - قائمة البرامج النصية التي سيتم تحميلها في هذه المباراة ؛
- exclude_matches - يستبعد عناوين URL
match
من حقل المطابقة الذي يطابق هذا الحقل.
- page_action - في الواقع ، هو الكائن المسؤول عن الأيقونة التي تظهر بجانب شريط العنوان في المتصفح ، والتفاعل معها. كما يسمح لك بإظهار نافذة منبثقة ، تم ضبطها باستخدام HTML و CSS و JS.
- default_popup - المسار إلى ملف HTML مع واجهة منبثقة ، قد يحتوي على CSS و JS.
- أذونات - مجموعة لإدارة حقوق التمديد. هناك 3 أنواع من الحقوق الموضحة بالتفصيل هنا.
- web_accessible_resources - موارد الامتداد التي يمكن أن تطلبها صفحة الويب ، على سبيل المثال ، الصور ، ملفات JS ، CSS ، HTML.
- externally_connectable - هنا يمكنك تحديد معرفات الإضافات الأخرى ونطاقات صفحات الويب التي يمكنك الاتصال منها صراحة. مجال يمكن أن يكون المستوى الثاني أو أعلى. لا يعمل في فايرفوكس.
سياق التنفيذ
يحتوي الملحق على ثلاثة سياقات لتنفيذ التعليمات البرمجية ، أي أن التطبيق يتكون من ثلاثة أجزاء بمستويات مختلفة من الوصول إلى واجهة برمجة تطبيقات المتصفح.
سياق التمديد
معظم واجهات برمجة التطبيقات متوفرة هنا. في هذا السياق ، "مباشر":
- صفحة الخلفية - "الخلفية" جزء من التمديد. يشار إلى الملف في البيان بمفتاح "الخلفية".
- صفحة منبثقة - صفحة منبثقة تظهر عند النقر على أيقونة الامتداد. في البيان ،
browser_action
-> default_popup
. - صفحة مخصصة - صفحة ملحق ، "معيشة" في علامة تبويب منفصلة للنموذج
chrome-extension://<id_>/customPage.html
.
هذا السياق موجود بشكل مستقل عن نوافذ المتصفح وعلامات التبويب. توجد صفحة الخلفية في نسخة واحدة وتعمل دائمًا (الاستثناء هو صفحة الحدث ، عندما يتم تشغيل البرنامج النصي للخلفية في حدث ويموت بعد تنفيذه). توجد الصفحة المنبثقة عندما تكون النافذة المنبثقة مفتوحة ، والصفحة المخصصة - أثناء فتح علامة التبويب. لا يمكن الوصول إلى علامات التبويب الأخرى ومحتوياتها من هذا السياق.
سياق البرنامج النصي المحتوى
يتم تشغيل ملف البرنامج النصي المحتوى مع كل علامة تبويب المتصفح. لديه حق الوصول إلى جزء من واجهة برمجة تطبيقات الامتداد وإلى شجرة DOM بصفحة الويب. البرامج النصية للمحتوى هي المسؤولة عن التفاعل مع الصفحة. تقوم الإضافات التي تتعامل مع شجرة DOM بهذا الأمر في البرامج النصية للمحتوى - على سبيل المثال ، برامج حظر الإعلانات أو المترجمين. أيضًا ، يمكن للبرنامج النصي للمحتوى التواصل مع الصفحة من خلال postMessage
القياسي.
سياق صفحة الويب
هذه هي في الواقع صفحة الويب نفسها. لا علاقة له بالامتداد ولا يمكنه الوصول إلى هناك ، ما لم يتم تحديد نطاق هذه الصفحة بشكل صريح في البيان (مزيد من المعلومات حول هذا أدناه).
الرسائل
يجب أن تتبادل الأجزاء المختلفة من التطبيق الرسائل مع بعضها البعض. للقيام بذلك ، هناك API runtime.sendMessage
لإرسال رسالة في background
و tabs.sendMessage
لإرسال رسالة إلى صفحة (نص برمجي أو منبثق أو صفحة ويب في حالة وجود externally_connectable
). فيما يلي مثال عند الوصول إلى Chrome API.
للاتصال الكامل ، يمكنك إنشاء اتصالات من خلال runtime.connect
. رداً على ذلك ، نحصل على runtime.Port
. runtime.Port
، والذي يمكنك من خلاله فتح أي عدد من الرسائل. من جانب العميل ، على سبيل المثال ، contentscript
، يبدو كما يلي:
الخادم أو الخلفية:
هناك أيضا حدث onDisconnect
وطريقة disconnect
.
مخطط التطبيق
دعنا نجعل امتداد المتصفح الذي يخزن المفاتيح الخاصة ، ويوفر الوصول إلى المعلومات العامة (العنوان ، يتصل المفتاح العام بالصفحة ويسمح لتطبيقات الطرف الثالث بطلب توقيع معاملة.
تطوير التطبيق
يجب أن يتفاعل تطبيقنا مع المستخدم ويوفر صفحة API لطرق الاتصال (على سبيل المثال ، لتوقيع المعاملات). لن ينجح الأمر مع وجود contentscript
وحده ، لأنه لا يمكنه الوصول إلا إلى DOM ، ولكن ليس إلى صفحة JS. لا يمكننا الاتصال من خلال runtime.connect
، لأن واجهة برمجة التطبيقات مطلوبة في جميع المجالات ، ويمكن تحديد مجالات محددة فقط في البيان. نتيجة لذلك ، سيبدو المخطط كما يلي:

سيكون هناك نص آخر في الصفحة ، والذي سنحقنه في الصفحة. سيتم تشغيله في سياقه ويوفر واجهة برمجة تطبيقات للعمل مع الامتداد.
بداية
كل رمز تمديد المتصفح متاح على جيثب . في عملية الوصف ، سيكون هناك روابط لالتزامات.
لنبدأ مع البيان:
{
قم بإنشاء background.js و popup.js و inpage.js و contentcript.js الفارغة. أضف popup.html - ويمكن تنزيل تطبيقنا بالفعل في Google Chrome وتأكد من أنه يعمل.
للتحقق من ذلك ، يمكنك الحصول على الكود من هنا . بالإضافة إلى ما فعلناه ، تم تكوين الرابط لإنشاء المشروع باستخدام webpack. لإضافة تطبيق إلى المتصفح ، في chrome: // extensions ، تحتاج إلى تحديد تحميل وتفريغ المجلد مع الملحق المقابل - في حالتنا ، dist.

الآن تم تثبيت ملحق لدينا وتعمل. يمكنك تشغيل أدوات المطور لسياقات مختلفة كما يلي:
المنبثقة ->

يتم الوصول إلى وحدة التحكم في النص البرمجي للمحتوى من خلال وحدة التحكم في الصفحة نفسها التي يتم تشغيلها عليه. 
الرسائل
لذلك ، نحن بحاجة إلى إنشاء قناتين للاتصال: الخلفية في الصفحة <-> والخلفية المنبثقة <->. يمكنك بالطبع إرسال رسائل إلى المنفذ وابتكار البروتوكول الخاص بك ، لكنني أفضل الطريقة التي كنت أتجسس عليها في مشروع metamask مفتوح المصدر.
هذا امتداد للمستعرض للعمل مع شبكة Ethereum. في ذلك ، تتواصل أجزاء مختلفة من التطبيق من خلال RPC باستخدام مكتبة dnode. يسمح لك بتنظيم عملية تبادل بسرعة وسهولة إذا قمت بتوفير دفق nodejs كوسيلة نقل (بمعنى كائن يقوم بتنفيذ نفس الواجهة):
import Dnode from "dnode/browser";
الآن سنقوم بإنشاء فئة التطبيق. سيقوم بإنشاء كائنات واجهة برمجة التطبيقات للصفحة المنبثقة وصفحة الويب ، وأيضًا إنشاء dnode لهم:
import Dnode from 'dnode/browser'; export class SignerApp {
فيما يلي ، بدلاً من كائن Chrome العام ، نستخدم extentionApi ، والذي يشير إلى Chrome في المستعرض من Google وإلى المستعرض في الآخرين. يتم ذلك من أجل التوافق عبر المستعرض ، ولكن ببساطة يمكن استخدام chrome.runtime.connect في إطار هذه المقالة.
إنشاء مثيل التطبيق في البرنامج النصي الخلفية:
import {extensionApi} from "./utils/extensionApi"; import {PortStream} from "./utils/PortStream"; import {SignerApp} from "./SignerApp"; const app = new SignerApp();
نظرًا لأن 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() } }
الآن قم بإنشاء اتصال في واجهة المستخدم:
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(){
ثم نقوم بإنشاء اتصال في البرنامج النصي المحتوى:
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 {
نظرًا لأننا لا نحتاج إلى واجهة برمجة التطبيقات في البرنامج النصي للمحتوى ، ولكن على الصفحة مباشرةً ، فإننا نقوم بعمل شيئين:
- نخلق تيارات اثنين. واحد هو نحو الصفحة ، في أعلى postMessage. لهذا نستخدم هذه الحزمة من المبدعين من metamask. الدفق الثاني هو الخلفية الموجودة أعلى المنفذ المستلم من
runtime.connect
. runtime.connect
. ضعهم الآن سوف تحتوي الصفحة على دفق إلى الخلفية. - حقن البرنامج النصي في DOM. نقوم بضخ البرنامج النصي (تم السماح بالوصول إليه في البيان) وإنشاء علامة
script
مع محتوياته في الداخل:
import PostMessageStream from 'post-message-stream'; import {extensionApi} from "./utils/extensionApi"; import {PortStream} from "./utils/PortStream"; setupConnection(); injectScript(); function setupConnection(){
أنشئ الآن كائن api في الصفحة وابدأ تشغيله عالميًا:
import PostMessageStream from 'post-message-stream'; import Dnode from 'dnode/browser'; setupInpageApi().catch(console.error); async function setupInpageApi() {
نحن على استعداد لاستدعاء إجراء عن بُعد (RPC) مع واجهة برمجة تطبيقات منفصلة للصفحة وواجهة المستخدم . عند توصيل صفحة جديدة بالخلفية ، يمكننا أن نرى هذا:

API فارغة والأصل. على جانب الصفحة ، يمكننا استدعاء وظيفة الترحيب مثل هذا:

يعد العمل مع وظائف رد الاتصال في JS الحديثة فكرة سيئة ، لذلك سنكتب مساعدًا صغيرًا لإنشاء 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 => {
استدعاء دالة بإرجاع وعد:

هناك نسخة مع وظائف غير متزامنة متاحة هنا .
بشكل عام ، يبدو النهج مع RPC والتدفقات مرنة للغاية: يمكننا استخدام البخار المضاعف وإنشاء عدة واجهات برمجة تطبيقات مختلفة لمختلف المهام. من حيث المبدأ ، يمكن استخدام dnode في أي مكان ، والشيء الرئيسي هو التفاف النقل في شكل تيار nodejs.
البديل هو تنسيق 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) } } }
أضف بعض المفاتيح من وحدة تحكم واجهة المستخدم وشاهد ما حدث مع الحالة:

يجب أن تكون الحالة ثابتة حتى لا تضيع المفاتيح عند إعادة تشغيل الجهاز.
سنقوم بتخزينها في localStorage ، الكتابة مع كل تغيير. وبالتالي ، سيكون الوصول إليها ضروريًا لواجهة المستخدم ، وأريد أيضًا الاشتراك في التغييرات. بناءً على ذلك ، سيكون من المناسب إجراء تخزين يمكن ملاحظته والاشتراك في تغييراته.
سنستخدم مكتبة mobx ( https://github.com/mobxjs/mobx ). وقع الاختيار عليها ، حيث أنني لم أضطر إلى العمل معها ، لكنني أردت حقًا دراستها.
أضف تهيئة الحالة الأولية وجعل المتجر يمكن ملاحظته:
import {observable, action} from 'mobx'; import {setupDnode} from "./utils/setupDnode"; export class SignerApp { constructor(initState = {}) {
استبدل mobx "تحت الغطاء" جميع حقول المتجر بالبروكسي واعترض كل المكالمات إليهم. يمكنك الاشتراك في هذه الطعون.
علاوة على ذلك ، سأستخدم مصطلح "عند التغيير" غالبًا ، رغم أن هذا غير صحيح تمامًا. Mobx يتتبع الوصول إلى الحقول. يتم استخدام getters وكائنات الكائنات الوكيل التي تنشئها المكتبة.
يخدم ديكور الحركة غرضين:
- في الوضع الصارم مع العلم ، يمنع mobx تغيير الحالة مباشرةً. تعتبر ممارسة جيدة للعمل في وضع صارم.
- حتى إذا غيرت الدالة الحالة عدة مرات - على سبيل المثال ، قمنا بتغيير عدة حقول إلى عدة أسطر من التعليمات البرمجية - يتم إخطار المراقبين فقط عند اكتمالها. هذا مهم بشكل خاص للواجهة الأمامية ، حيث تؤدي تحديثات الحالة غير الضرورية إلى عرض غير ضروري للعناصر. في حالتنا ، ليست الأولى أو الثانية ذات صلة بشكل خاص ، ومع ذلك ، سوف نتبع أفضل الممارسات. قرر القائمون على الديكور تعليق جميع الوظائف التي تغير حالة الحقول الملاحظة.
في الخلفية ، أضف التهيئة وحفظ الحالة في localStorage:
import {reaction, toJS} from 'mobx'; import {extensionApi} from "./utils/extensionApi"; import {PortStream} from "./utils/PortStream"; import {SignerApp} from "./SignerApp";
وظيفة رد الفعل مثيرة للاهتمام هنا. لديها حجتان:
- محدد البيانات.
- معالج سيتم استدعاؤه بهذه البيانات في كل مرة يتغير فيها.
على عكس الإعادة ، حيث نحصل صراحة على الحالة كوسيطة ، يتذكر mobx ما يمكن ملاحظته والذي نشير إليه داخل المحدد ، وفقط عند تغييرهم يستدعي المعالج.
من المهم أن نفهم بالضبط كيف تقرر mobx أي شيء يمكن ملاحظته نشترك فيه. إذا كتبت محددًا في الكود مثل هذا () => app.store
، فلن يتم استدعاء رد الفعل أبدًا ، نظرًا لأن المستودع نفسه لا يمكن ملاحظته ، فهناك حقوله فقط.
إذا كتبت مثل هذا () => app.store.keys
، فلن يحدث شيء مرة أخرى ، لأنه عند إضافة / إزالة عناصر المصفوفة ، لن يتغير الرابط إليها.
لأول مرة ، تعمل Mobx كمحدد وتراقب فقط تلك التي يمكن ملاحظتها. ويتم ذلك من خلال وكيل الوكيل. toJS
. , . – , .
popup . localStorage:

background- .
.
: , , . localStorage .
locked, . locked .
Mobx , . — computed properties. view :
import {observable, action} from 'mobx'; import {setupDnode} from "./utils/setupDnode";
. . locked . API .
rypto-js :
import CryptoJS from 'crypto-js'
idle API, — . , , idle
, active
locked
. idle , 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; }
.
المعاملات
, : . WAVES waves-transactions .
, , — , :
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) {
, observable
store.messages
.
observable
, mobx messages. , , .
, . reaction, "" .
approve
reject
: , , .
Approve reject API UI, newMessage — 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
. UI observable
API , . observable
API, background:
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() {
. react-. Background- props. , , store , :
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 . observer mobx-react , observable, . mapStateToProps connect, 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 { ...
remote
reaction
, UI.
— :
function setupApp() { ...
, . - :


.
استنتاج
, , . .
, .
, siemarell