نوشتن پسوند مرورگر امن

نوشتن پسوند مرورگر امن

بر خلاف معماری رایج "مشتری-سرور"، برنامه های غیرمتمرکز با موارد زیر مشخص می شوند:

  • نیازی به ذخیره یک پایگاه داده با لاگین و رمز عبور کاربر نیست. اطلاعات دسترسی منحصراً توسط خود کاربران ذخیره می شود و تأیید صحت آنها در سطح پروتکل انجام می شود.
  • نیازی به استفاده از سرور نیست. منطق برنامه را می توان در یک شبکه بلاک چین اجرا کرد، جایی که امکان ذخیره مقدار مورد نیاز داده وجود دارد.

2 فضای ذخیره سازی نسبتاً ایمن برای کلیدهای کاربر وجود دارد - کیف پول های سخت افزاری و افزونه های مرورگر. کیف پول‌های سخت‌افزاری عمدتاً بسیار امن هستند، اما استفاده از آنها دشوار است و رایگان نیستند، اما افزونه‌های مرورگر ترکیبی عالی از امنیت و سهولت استفاده هستند و همچنین می‌توانند برای کاربران نهایی کاملاً رایگان باشند.

با در نظر گرفتن همه این موارد، ما می‌خواستیم امن‌ترین افزونه را ایجاد کنیم که توسعه برنامه‌های غیرمتمرکز را با ارائه یک API ساده برای کار با تراکنش‌ها و امضاها ساده می‌کند.
در ادامه در مورد این تجربه به شما خواهیم گفت.

این مقاله حاوی دستورالعمل های گام به گام در مورد نحوه نوشتن افزونه مرورگر، همراه با نمونه کد و اسکرین شات است. می توانید تمام کدها را در آن پیدا کنید مخازن. هر commit از نظر منطقی با بخشی از این مقاله مطابقت دارد.

تاریخچه مختصری از برنامه های افزودنی مرورگر

افزونه های مرورگر برای مدت طولانی در دسترس بوده اند. آنها در اینترنت اکسپلورر در سال 1999 و در فایرفاکس در سال 2004 ظاهر شدند. با این حال، برای مدت طولانی هیچ استاندارد واحدی برای برنامه های افزودنی وجود نداشت.

می توان گفت که همراه با افزونه ها در نسخه چهارم گوگل کروم ظاهر شد. البته در آن زمان هیچ مشخصاتی وجود نداشت، اما کروم API بود که اساس آن شد: کروم با تسخیر بیشتر بازار مرورگرها و داشتن یک فروشگاه برنامه داخلی، در واقع استانداردهای برنامه افزودنی مرورگر را تعیین کرد.

موزیلا استاندارد خاص خود را داشت، اما با دیدن محبوبیت افزونه‌های کروم، این شرکت تصمیم گرفت یک API سازگار بسازد. در سال 2015، به ابتکار موزیلا، یک گروه ویژه در کنسرسیوم وب جهانی (W3C) ایجاد شد تا روی مشخصات افزونه بین مرورگرها کار کند.

پسوندهای API موجود برای Chrome به عنوان پایه در نظر گرفته شد. این کار با پشتیبانی مایکروسافت انجام شد (گوگل از مشارکت در توسعه استاندارد خودداری کرد) و در نتیجه یک پیش نویس ظاهر شد مشخصات فنی.

به طور رسمی، این مشخصات توسط Edge، Firefox و Opera پشتیبانی می شود (توجه داشته باشید که کروم در این لیست نیست). اما در واقع، این استاندارد تا حد زیادی با کروم سازگار است، زیرا در واقع بر اساس پسوندهای آن نوشته شده است. می‌توانید درباره WebExtensions API بیشتر بخوانید اینجا.

ساختار پسوند

تنها فایلی که برای پسوند مورد نیاز است مانیفست (manifest.json) است. همچنین "نقطه ورود" به گسترش است.

مانیفست

با توجه به مشخصات، فایل مانیفست یک فایل JSON معتبر است. شرح کامل کلیدهای مانیفست با اطلاعاتی درباره اینکه کدام کلیدها در کدام مرورگر قابل مشاهده هستند پشتیبانی می شوند اینجا.

کلیدهایی که در مشخصات "ممکن است" نادیده گرفته شوند (هر دو کروم و فایرفاکس خطاها را گزارش می کنند، اما برنامه های افزودنی به کار خود ادامه می دهند).

و توجه را به چند نکته جلب می کنم.

  1. زمینه - یک شی که شامل فیلدهای زیر است:
    1. اسکریپت - آرایه ای از اسکریپت ها که در زمینه پس زمینه اجرا می شوند (در این مورد کمی بعد صحبت خواهیم کرد).
    2. با ما - به جای اسکریپت هایی که در یک صفحه خالی اجرا می شوند، می توانید html را با محتوا مشخص کنید. در این حالت، قسمت اسکریپت نادیده گرفته می شود و اسکریپت ها باید در صفحه محتوا درج شوند.
    3. ادامه - یک پرچم باینری، اگر مشخص نشده باشد، مرورگر وقتی فکر کند که کاری انجام نمی دهد، فرآیند پس زمینه را "کشته" می کند و در صورت لزوم آن را مجددا راه اندازی می کند. در غیر این صورت، صفحه تنها زمانی که مرورگر بسته باشد، بارگیری می شود. در فایرفاکس پشتیبانی نمی شود.
  2. محتوا_اسکریپت ها - آرایه ای از اشیاء که به شما امکان می دهد اسکریپت های مختلف را در صفحات وب مختلف بارگذاری کنید. هر شی شامل فیلدهای مهم زیر است:
    1. کبریت - نشانی وب الگو، که تعیین می کند آیا یک اسکریپت محتوای خاص گنجانده می شود یا خیر.
    2. js - لیستی از اسکریپت هایی که در این مسابقه بارگذاری می شوند.
    3. exclude_match - از میدان خارج می شود match URL هایی که با این فیلد مطابقت دارند.
  3. page_action - در واقع یک شی است که مسئول آیکونی است که در کنار نوار آدرس در مرورگر و تعامل با آن نمایش داده می شود. همچنین به شما این امکان را می دهد که یک پنجره بازشو نمایش دهید که با استفاده از HTML، CSS و JS خودتان تعریف می شود.
    1. default_popup - مسیر فایل HTML با رابط بازشو، ممکن است حاوی CSS و JS باشد.
  4. مجوز - آرایه ای برای مدیریت حقوق پسوند. 3 نوع حقوق وجود دارد که به تفصیل توضیح داده شده است اینجا
  5. web_accessible_resources - منابع افزونه ای که یک صفحه وب می تواند درخواست کند، به عنوان مثال، تصاویر، JS، CSS، فایل های HTML.
  6. خارجی_قابل اتصال — در اینجا می‌توانید به صراحت شناسه‌های دیگر پسوندها و دامنه‌های صفحات وب را که می‌توانید از آن‌ها متصل شوید، مشخص کنید. دامنه می تواند سطح دوم یا بالاتر باشد. در فایرفاکس کار نمی کند.

زمینه اجرا

برنامه افزودنی دارای سه زمینه اجرای کد است، یعنی برنامه از سه بخش با سطوح مختلف دسترسی به API مرورگر تشکیل شده است.

زمینه پسوند

بیشتر API در اینجا موجود است. در این زمینه آنها "زندگی می کنند":

  1. صفحه پس زمینه - بخش "باطن" از برنامه افزودنی. فایل در مانیفست با استفاده از کلید "بک گراند" مشخص می شود.
  2. صفحه پاپ آپ - یک صفحه بازشو که با کلیک بر روی نماد برنامه افزودنی ظاهر می شود. در مانیفست browser_action -> default_popup.
  3. صفحه سفارشی - صفحه پسوند، "زندگی" در یک برگه جداگانه از نمای chrome-extension://<id_расширения>/customPage.html.

این زمینه مستقل از پنجره ها و برگه های مرورگر وجود دارد. صفحه پس زمینه در یک کپی وجود دارد و همیشه کار می‌کند (استثنا صفحه رویداد است، زمانی که اسکریپت پس‌زمینه توسط یک رویداد راه‌اندازی می‌شود و پس از اجرای آن می‌میرد). صفحه پاپ آپ زمانی وجود دارد که پنجره بازشو باز است، و صفحه سفارشی - در حالی که برگه آن باز است. از این متن به سایر برگه ها و محتویات آنها دسترسی ندارید.

زمینه اسکریپت محتوا

فایل اسکریپت محتوا همراه با هر برگه مرورگر راه اندازی می شود. به بخشی از API برنامه افزودنی و درخت DOM صفحه وب دسترسی دارد. این اسکریپت های محتوا هستند که مسئول تعامل با صفحه هستند. برنامه‌های افزودنی که درخت DOM را دستکاری می‌کنند این کار را در اسکریپت‌های محتوا انجام می‌دهند - به عنوان مثال، مسدودکننده‌های تبلیغات یا مترجم‌ها. همچنین اسکریپت محتوا می تواند از طریق استاندارد با صفحه ارتباط برقرار کند postMessage.

زمینه صفحه وب

این خود صفحه وب واقعی است. هیچ ربطی به پسوند ندارد و به آنجا دسترسی ندارد، مگر در مواردی که دامنه این صفحه به صراحت در مانیفست نشان داده نشده باشد (در ادامه در این مورد بیشتر توضیح داده شده است).

تبادل پیام

بخش های مختلف برنامه باید با یکدیگر پیام رد و بدل کنند. یک API برای این وجود دارد runtime.sendMessage برای ارسال پیام background и tabs.sendMessage برای ارسال پیام به یک صفحه (اسکریپت محتوا، پنجره بازشو یا صفحه وب در صورت وجود externally_connectable). در زیر مثالی هنگام دسترسی به Chrome API آورده شده است.

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

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

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

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

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

برای ارتباط کامل، می‌توانید از طریق ارتباط ایجاد کنید runtime.connect. در پاسخ دریافت خواهیم کرد runtime.Port، که در حالی که باز است، می توانید هر تعداد پیام به آن ارسال کنید. برای مثال، در سمت مشتری، contentscript، به نظر می رسد این است:

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

سرور یا پس زمینه:

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

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

یک رویداد نیز وجود دارد onDisconnect و روش disconnect.

نمودار کاربرد

بیایید یک افزونه مرورگر بسازیم که کلیدهای خصوصی را ذخیره می کند، دسترسی به اطلاعات عمومی را فراهم می کند (آدرس، کلید عمومی با صفحه ارتباط برقرار می کند و به برنامه های شخص ثالث اجازه می دهد تا برای تراکنش ها درخواست امضا کنند.

توسعه اپلیکیشن

برنامه ما باید هم با کاربر تعامل داشته باشد و هم یک API برای فراخوانی روش ها (مثلاً برای امضای تراکنش ها) به صفحه ارائه دهد. فقط به یکی بسنده کنید contentscript کار نخواهد کرد، زیرا فقط به DOM دسترسی دارد، اما به JS صفحه دسترسی ندارد. اتصال از طریق runtime.connect ما نمی توانیم، زیرا API در همه دامنه ها مورد نیاز است و فقط موارد خاص را می توان در مانیفست مشخص کرد. در نتیجه، نمودار به شکل زیر خواهد بود:

نوشتن پسوند مرورگر امن

اسکریپت دیگری وجود خواهد داشت - inpage، که به صفحه تزریق می کنیم. در زمینه خود اجرا می شود و یک API برای کار با برنامه افزودنی ارائه می دهد.

شروع

همه کد برنامه افزودنی مرورگر در دسترس است GitHub. در طول توضیحات، پیوندهایی به commit ها وجود خواهد داشت.

بیایید با مانیفست شروع کنیم:

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

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

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

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

background.js، popup.js، inpage.js و contentscript.js خالی ایجاد کنید. ما popup.html را اضافه می کنیم - و برنامه ما می تواند از قبل در Google Chrome بارگیری شود و مطمئن شویم که کار می کند.

برای تأیید این موضوع، می توانید کد را دریافت کنید از این رو. علاوه بر کاری که انجام دادیم، پیوند مونتاژ پروژه را با استفاده از وب پک پیکربندی کرد. برای افزودن یک برنامه به مرورگر، در chrome://extensions باید load unpacked و پوشه را با پسوند مربوطه انتخاب کنید - در مورد ما dist.

نوشتن پسوند مرورگر امن

اکنون افزونه ما نصب شده و کار می کند. شما می توانید ابزارهای توسعه دهنده را برای زمینه های مختلف به صورت زیر اجرا کنید:

پنجره بازشو ->

نوشتن پسوند مرورگر امن

دسترسی به کنسول اسکریپت محتوا از طریق کنسول خود صفحه ای که در آن راه اندازی شده است انجام می شود.نوشتن پسوند مرورگر امن

تبادل پیام

بنابراین، ما باید دو کانال ارتباطی ایجاد کنیم: پس‌زمینه درون صفحه <-> و پس‌زمینه پاپ آپ <->. البته می توانید فقط به پورت پیام بفرستید و پروتکل خود را اختراع کنید، اما من رویکردی را که در پروژه متن باز متاماسک دیدم ترجیح می دهم.

این یک افزونه مرورگر برای کار با شبکه اتریوم است. در آن، بخش‌های مختلف برنامه از طریق RPC با استفاده از کتابخانه dnode ارتباط برقرار می‌کنند. اگر یک جریان nodejs را به‌عنوان یک انتقال (به معنای شی‌ای که همان رابط را پیاده‌سازی می‌کند)، به شما اجازه می‌دهد تا یک تبادل را خیلی سریع و راحت سازماندهی کنید:

import Dnode from "dnode/browser";

// В этом примере условимся что клиент удаленно вызывает функции на сервере, хотя ничего нам не мешает сделать это двунаправленным

// Cервер
// API, которое мы хотим предоставить
const dnode = Dnode({
    hello: (cb) => cb(null, "world")
})
// Транспорт, поверх которого будет работать dnode. Любой nodejs стрим. В браузере есть бибилиотека 'readable-stream'
connectionStream.pipe(dnode).pipe(connectionStream)

// Клиент
const dnodeClient = Dnode() // Вызов без агрумента значит что мы не предоставляем API на другой стороне

// Выведет в консоль world
dnodeClient.once('remote', remote => {
    remote.hello(((err, value) => console.log(value)))
})

حالا یک کلاس اپلیکیشن ایجاد می کنیم. اشیاء API را برای پاپ آپ و صفحه وب ایجاد می کند و یک dnode برای آنها ایجاد می کند:

import Dnode from 'dnode/browser';

export class SignerApp {

    // Возвращает объект API для ui
    popupApi(){
        return {
            hello: cb => cb(null, 'world')
        }
    }

    // Возвращает объет API для страницы
    pageApi(){
        return {
            hello: cb => cb(null, 'world')
        }
    }

    // Подключает popup ui
    connectPopup(connectionStream){
        const api = this.popupApi();
        const dnode = Dnode(api);

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

        dnode.on('remote', (remote) => {
            console.log(remote)
        })
    }

    // Подключает страницу
    connectPage(connectionStream, origin){
        const api = this.popupApi();
        const dnode = Dnode(api);

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

        dnode.on('remote', (remote) => {
            console.log(origin);
            console.log(remote)
        })
    }
}

در اینجا و پایین، به جای شی جهانی کروم، از extensionApi استفاده می کنیم که به کروم در مرورگر گوگل و مرورگر در سایرین دسترسی دارد. این کار برای سازگاری بین مرورگرها انجام می شود، اما برای اهداف این مقاله می توان به سادگی از «chrome.runtime.connect» استفاده کرد.

بیایید یک نمونه برنامه در اسکریپت پس زمینه ایجاد کنیم:

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

const app = new SignerApp();

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

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

از آنجایی که dnode با استریم ها کار می کند و ما یک پورت دریافت می کنیم، یک کلاس آداپتور مورد نیاز است. این با استفاده از کتابخانه readable-stream ساخته شده است که جریان‌های nodejs را در مرورگر پیاده‌سازی می‌کند:

import {Duplex} from 'readable-stream';

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

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

    _onDisconnect() {
        this.destroy()
    }

    _read(){}

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

حالا بیایید یک اتصال در UI ایجاد کنیم:

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

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

setupUi().catch(console.error);

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

    const dnode = Dnode();

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

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

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

سپس اتصال را در اسکریپت محتوا ایجاد می کنیم:

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

setupConnection();
injectScript();

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

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

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

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

از آنجایی که ما به API نه در اسکریپت محتوا، بلکه مستقیماً در صفحه نیاز داریم، دو کار انجام می دهیم:

  1. ما دو جریان ایجاد می کنیم. یک - به سمت صفحه، در بالای postMessage. برای این ما از این استفاده می کنیم این بسته از سازندگان متاماسک. جریان دوم برای پس‌زمینه پورت دریافت شده از آن است 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);
    }
}

حالا یک شی api در inpage ایجاد می کنیم و آن را روی global قرار می دهیم:

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) با API جداگانه برای صفحه و UI. هنگام اتصال یک صفحه جدید به پس زمینه، می توانیم این را ببینیم:

نوشتن پسوند مرورگر امن

API و مبدا خالی است. در سمت صفحه، می توانیم تابع hello را به این صورت فراخوانی کنیم:

نوشتن پسوند مرورگر امن

کار با توابع callback در JS مدرن رفتار بدی است، بنابراین بیایید یک کمک کننده کوچک برای ایجاد یک dnode بنویسیم که به شما امکان می دهد یک شی API را به utils ارسال کنید.

اشیاء API اکنون به شکل زیر خواهند بود:

export class SignerApp {

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

...

}

گرفتن یک شی از راه دور مانند این:

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

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

و فراخوانی توابع یک وعده را برمی گرداند:

نوشتن پسوند مرورگر امن

نسخه با توابع ناهمزمان در دسترس است اینجا.

به طور کلی، رویکرد RPC و استریم کاملاً منعطف به نظر می‌رسد: ما می‌توانیم از مالتی پلکسینگ بخار استفاده کنیم و چندین API مختلف برای وظایف مختلف ایجاد کنیم. در اصل ، dnode را می توان در هر جایی استفاده کرد ، نکته اصلی این است که حمل و نقل را به شکل یک جریان nodejs بپیچید.

یک جایگزین فرمت JSON است که پروتکل JSON RPC 2 را پیاده‌سازی می‌کند. با این حال، با انتقال‌های خاص (TCP و HTTP(S)) کار می‌کند که در مورد ما قابل اجرا نیست.

ایالت داخلی و ذخیره سازی محلی

ما باید وضعیت داخلی برنامه را ذخیره کنیم - حداقل کلیدهای امضا. ما به راحتی می توانیم یک حالت به برنامه و روش های تغییر آن در API پاپ آپ اضافه کنیم:

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

export class SignerApp {

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

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

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

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

    ...

} 

در پس زمینه، همه چیز را در یک تابع قرار می دهیم و شی برنامه را در پنجره می نویسیم تا بتوانیم با آن از کنسول کار کنیم:

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

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

setupApp();

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

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

    extensionApi.runtime.onConnect.addListener(connectRemote);

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

بیایید چند کلید از کنسول UI اضافه کنیم و ببینیم با وضعیت چه اتفاقی می‌افتد:

نوشتن پسوند مرورگر امن

وضعیت باید پایدار باشد تا کلیدها هنگام راه اندازی مجدد گم نشوند.

ما آن را در localStorage ذخیره می کنیم و با هر تغییری آن را بازنویسی می کنیم. متعاقباً دسترسی به آن برای UI نیز ضروری خواهد بود و من همچنین می خواهم در تغییرات مشترک شوم. بر این اساس، ایجاد یک ذخیره سازی قابل مشاهده و اشتراک در تغییرات آن راحت خواهد بود.

ما از کتابخانه mobx استفاده خواهیم کرد (https://github.com/mobxjs/mobx). انتخاب روی آن افتاد زیرا مجبور نبودم با آن کار کنم، اما واقعاً می خواستم آن را مطالعه کنم.

بیایید مقداردهی اولیه حالت اولیه را اضافه کنیم و فروشگاه را قابل مشاهده کنیم:

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

export class SignerApp {

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

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

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

    ...

}

mobx تمام فیلدهای فروشگاه را با پروکسی جایگزین کرده است و تمام تماس‌های آنها را رهگیری می‌کند. اشتراک در این پیام ها امکان پذیر خواهد بود.

در زیر اغلب از اصطلاح "هنگام تغییر" استفاده می کنم، اگرچه این کاملاً صحیح نیست. Mobx دسترسی به فیلدها را ردیابی می کند. دریافت کننده ها و تنظیم کننده های اشیاء پراکسی که کتابخانه ایجاد می کند استفاده می شود.

دکوراتورهای اکشن دو هدف را دنبال می کنند:

  1. در حالت سخت با پرچم enforceActions، mobx تغییر وضعیت را به طور مستقیم ممنوع می کند. کار کردن در شرایط سخت عمل خوبی محسوب می شود.
  2. حتی اگر یک تابع چندین بار وضعیت را تغییر دهد - برای مثال، چندین فیلد را در چندین خط کد تغییر دهیم - ناظران تنها پس از تکمیل آن مطلع می شوند. این امر به ویژه برای فرانت‌اند مهم است، جایی که به‌روزرسانی‌های غیرضروری وضعیت منجر به رندر غیرضروری عناصر می‌شود. در مورد ما، نه اولی و نه دومی ارتباط خاصی ندارد، اما ما بهترین شیوه ها را دنبال خواهیم کرد. مرسوم است که دکوراتورها را به تمام عملکردهایی که وضعیت فیلدهای مشاهده شده را تغییر می دهند متصل می کنند.

در پس‌زمینه، مقداردهی اولیه و ذخیره وضعیت را در localStorage اضافه می‌کنیم:

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

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

setupApp();

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

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

    // Setup state persistence

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

    extensionApi.runtime.onConnect.addListener(connectRemote);

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

تابع واکنش در اینجا جالب است. دو استدلال دارد:

  1. انتخابگر داده
  2. کنترل کننده ای که با این داده ها هر بار که تغییر می کند فراخوانی می شود.

برخلاف redux، که در آن وضعیت را به صراحت به عنوان یک آرگومان دریافت می‌کنیم، mobx به یاد می‌آورد که به کدام قابل مشاهده‌ها در انتخابگر دسترسی داریم، و تنها زمانی که کنترل کننده تغییر می‌کنند، آنها را فراخوانی می‌کند.

این مهم است که دقیقاً بفهمیم 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')
        }
    }
}

اکنون فقط کلیدهای رمزگذاری شده و رمز عبور را ذخیره می کنیم. بقیه چیزا حساب شده با حذف رمز عبور از حالت، انتقال را به حالت قفل انجام می دهیم. API عمومی اکنون روشی برای مقداردهی اولیه ذخیره سازی دارد.

برای رمزگذاری نوشته شده است برنامه های کاربردی با استفاده از crypto-js:

import CryptoJS from 'crypto-js'

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

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

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

مرورگر دارای یک API غیرفعال است که از طریق آن می توانید در یک رویداد مشترک شوید - تغییرات وضعیت. دولت، بر این اساس، ممکن است idle, active и locked. برای حالت بی‌حرکت می‌توانید یک بازه زمانی تعیین کنید، و قفل زمانی تنظیم می‌شود که خود سیستم‌عامل مسدود باشد. ما همچنین انتخابگر ذخیره را به localStorage تغییر خواهیم داد:

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

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

setupApp();

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

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

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

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

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

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

کد قبل از این مرحله است اینجا.

معاملات

بنابراین، به مهمترین چیز می رسیم: ایجاد و امضای تراکنش ها در بلاک چین. ما از بلاک چین و کتابخانه WAVES استفاده خواهیم کرد امواج - معاملات.

ابتدا، بیایید آرایه‌ای از پیام‌هایی را که باید امضا شوند به حالت اضافه می‌کنیم، سپس روش‌هایی را برای افزودن یک پیام جدید، تأیید امضا و رد کردن اضافه می‌کنیم:

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

export class SignerApp {

    ...

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

        this.store.messages.push(message);

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

    ...
}

وقتی پیام جدیدی دریافت می کنیم، متادیتا را به آن اضافه می کنیم، انجام دهید observable و به store.messages.

اگر این کار را نکنید observable به صورت دستی، سپس mobx هنگام اضافه کردن پیام به آرایه، این کار را خودش انجام می دهد. با این حال، یک شی جدید ایجاد می کند که ما مرجعی به آن نخواهیم داشت، اما برای مرحله بعدی به آن نیاز خواهیم داشت.

در مرحله بعد، وعده ای را برمی گردانیم که با تغییر وضعیت پیام برطرف می شود. وضعیت با واکنش کنترل می شود، که با تغییر وضعیت "خود را می کشد".

کد روش approve и reject بسیار ساده: ما به سادگی وضعیت پیام را پس از امضا در صورت لزوم تغییر می دهیم.

تأیید و رد را در 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 دریافت شده از پس زمینه:

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

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

setupUi().catch(console.error);

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

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

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

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

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

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

در پایان ما شروع به رندر کردن رابط برنامه می کنیم. این یک برنامه React است. شی پس زمینه به سادگی با استفاده از props ارسال می شود. البته درست است که یک سرویس جداگانه برای روش ها و یک فروشگاه برای ایالت ایجاد کنیم، اما برای اهداف این مقاله این کافی است:

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

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

با mobx شروع رندر هنگام تغییر داده ها بسیار آسان است. ما به سادگی دکوراتور مشاهده گر را از بسته آویزان می کنیم mobx-react بر روی کامپوننت، و رندر به طور خودکار فراخوانی می شود، زمانی که هر مشاهده پذیری که توسط کامپوننت به آن ارجاع می شود تغییر کند. شما نیازی به mapStateToProps ندارید یا مانند redux متصل شوید. همه چیز درست خارج از جعبه کار می کند:

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

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

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

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

اجزای باقی مانده را می توان در کد مشاهده کرد در پوشه UI.

اکنون در کلاس برنامه باید یک انتخابگر حالت برای رابط کاربری ایجاد کنید و در صورت تغییر به UI اطلاع دهید. برای این کار، بیایید یک روش اضافه کنیم getState и reactionصدا زدن remote.updateState:

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

export class SignerApp {

    ...

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

    ...

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

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

        })
    }

    ...
}

هنگام دریافت یک شی remote ایجاد شده است reaction برای تغییر حالتی که تابع را در سمت UI فراخوانی می کند.

آخرین لمس اضافه کردن نمایش پیام های جدید در نماد برنامه افزودنی است:

function setupApp() {
...

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

...
}

بنابراین، برنامه آماده است. صفحات وب ممکن است برای تراکنش ها درخواست امضا کنند:

نوشتن پسوند مرورگر امن

نوشتن پسوند مرورگر امن

کد در اینجا موجود است پیوند.

نتیجه

اگر مقاله را تا انتها خوانده‌اید، اما هنوز سؤالی دارید، می‌توانید از آنها بپرسید مخازن با پسوند. در آنجا نیز تعهدات مربوط به هر مرحله تعیین شده را خواهید یافت.

و اگر علاقه مند به دیدن کد برای پسوند واقعی هستید، می توانید این را پیدا کنید اینجا.

کد، مخزن و شرح وظایف از سیمارل

منبع: www.habr.com

اضافه کردن نظر