Viết phần mở rộng trình duyệt an toàn

Viết phần mở rộng trình duyệt an toàn

Không giống như kiến ​​trúc “máy khách-máy chủ” thông thường, các ứng dụng phi tập trung được đặc trưng bởi:

  • Không cần phải lưu trữ cơ sở dữ liệu với thông tin đăng nhập và mật khẩu của người dùng. Thông tin truy cập được lưu trữ độc quyền bởi chính người dùng và việc xác nhận tính xác thực của chúng xảy ra ở cấp độ giao thức.
  • Không cần sử dụng máy chủ. Logic ứng dụng có thể được thực thi trên mạng blockchain, nơi có thể lưu trữ lượng dữ liệu cần thiết.

Có 2 kho lưu trữ tương đối an toàn cho khóa người dùng - ví phần cứng và tiện ích mở rộng trình duyệt. Ví phần cứng hầu hết cực kỳ an toàn, nhưng khó sử dụng và không miễn phí, nhưng tiện ích mở rộng trình duyệt là sự kết hợp hoàn hảo giữa bảo mật và tính dễ sử dụng, đồng thời cũng có thể hoàn toàn miễn phí cho người dùng cuối.

Khi tính đến tất cả những điều này, chúng tôi muốn tạo ra tiện ích mở rộng an toàn nhất giúp đơn giản hóa việc phát triển các ứng dụng phi tập trung bằng cách cung cấp một API đơn giản để làm việc với các giao dịch và chữ ký.
Chúng tôi sẽ cho bạn biết về trải nghiệm này dưới đây.

Bài viết sẽ có hướng dẫn từng bước về cách viết tiện ích mở rộng trình duyệt, kèm theo các ví dụ về mã và ảnh chụp màn hình. Bạn có thể tìm thấy tất cả mã trong kho lưu trữ. Mỗi cam kết tương ứng một cách hợp lý với một phần của bài viết này.

Tóm tắt lịch sử của tiện ích mở rộng trình duyệt

Tiện ích mở rộng trình duyệt đã có từ lâu. Chúng xuất hiện trong Internet Explorer vào năm 1999, trong Firefox năm 2004. Tuy nhiên, trong một thời gian rất dài không có một tiêu chuẩn duy nhất nào cho việc mở rộng.

Có thể nói rằng nó xuất hiện cùng với các tiện ích mở rộng trong phiên bản thứ tư của Google Chrome. Tất nhiên, khi đó không có thông số kỹ thuật nào, nhưng chính API Chrome đã trở thành nền tảng của nó: sau khi chinh phục hầu hết thị trường trình duyệt và có kho ứng dụng tích hợp sẵn, Chrome thực sự đã đặt ra tiêu chuẩn cho các tiện ích mở rộng của trình duyệt.

Mozilla có tiêu chuẩn riêng, nhưng nhận thấy sự phổ biến của các tiện ích mở rộng của Chrome, công ty đã quyết định tạo một API tương thích. Vào năm 2015, theo sáng kiến ​​của Mozilla, một nhóm đặc biệt đã được thành lập trong World Wide Web Consortium (W3C) để nghiên cứu các thông số kỹ thuật mở rộng trên nhiều trình duyệt.

Các tiện ích mở rộng API hiện có dành cho Chrome được lấy làm cơ sở. Công việc được thực hiện với sự hỗ trợ của Microsoft (Google từ chối tham gia phát triển tiêu chuẩn) và kết quả là một bản dự thảo đã xuất hiện thông số kỹ thuật.

Về mặt chính thức, thông số kỹ thuật được hỗ trợ bởi Edge, Firefox và Opera (lưu ý rằng Chrome không có trong danh sách này). Nhưng trên thực tế, tiêu chuẩn này phần lớn tương thích với Chrome vì nó thực sự được viết dựa trên các tiện ích mở rộng của nó. Bạn có thể đọc thêm về API WebExtensions đây.

Cấu trúc mở rộng

Tệp duy nhất cần thiết cho tiện ích mở rộng là tệp kê khai (manifest.json). Nó cũng là “điểm vào” cho việc mở rộng.

Biểu hiện

Theo đặc điểm kỹ thuật, tệp kê khai là tệp JSON hợp lệ. Mô tả đầy đủ về các khóa kê khai cùng với thông tin về những khóa nào được hỗ trợ trong trình duyệt nào có thể xem được đây.

Các khóa không có trong thông số kỹ thuật “có thể” bị bỏ qua (cả Chrome và Firefox đều báo lỗi nhưng các tiện ích mở rộng vẫn tiếp tục hoạt động).

Và tôi muốn thu hút sự chú ý đến một số điểm.

  1. lý lịch - một đối tượng bao gồm các trường sau:
    1. kịch bản — một mảng các tập lệnh sẽ được thực thi trong bối cảnh nền (chúng ta sẽ nói về vấn đề này sau);
    2. trang - thay vì các tập lệnh sẽ được thực thi trên một trang trống, bạn có thể chỉ định nội dung html. Trong trường hợp này, trường tập lệnh sẽ bị bỏ qua và tập lệnh sẽ cần được chèn vào trang nội dung;
    3. cố chấp — cờ nhị phân, nếu không được chỉ định, trình duyệt sẽ “giết” tiến trình nền khi nó cho rằng nó không làm gì cả và khởi động lại nó nếu cần. Nếu không, trang sẽ chỉ được tải khi đóng trình duyệt. Không được hỗ trợ trong Firefox.
  2. nội dung_script — một mảng các đối tượng cho phép bạn tải các tập lệnh khác nhau vào các trang web khác nhau. Mỗi đối tượng chứa các trường quan trọng sau:
    1. diêm - url mẫu, xác định liệu một tập lệnh nội dung cụ thể có được đưa vào hay không.
    2. js — danh sách các tập lệnh sẽ được tải vào trận đấu này;
    3. loại trừ_matches - loại trừ khỏi trường match URL phù hợp với trường này.
  3. trang_action - thực chất là một đối tượng chịu trách nhiệm về biểu tượng hiển thị bên cạnh thanh địa chỉ trong trình duyệt và tương tác với nó. Nó cũng cho phép bạn hiển thị một cửa sổ bật lên, được xác định bằng HTML, CSS và JS của riêng bạn.
    1. mặc định_popup — đường dẫn đến tệp HTML có giao diện bật lên, có thể chứa CSS và JS.
  4. quyền — một mảng để quản lý quyền mở rộng. Có 3 loại quyền được mô tả chi tiết đây
  5. web_accessible_resource — tài nguyên tiện ích mở rộng mà một trang web có thể yêu cầu, ví dụ: hình ảnh, tệp JS, CSS, HTML.
  6. bên ngoài_có thể kết nối được — tại đây bạn có thể chỉ định rõ ràng ID của các tiện ích mở rộng và miền khác của trang web mà bạn có thể kết nối. Một tên miền có thể ở cấp độ thứ hai hoặc cao hơn. Không hoạt động trong Firefox.

Bối cảnh thực thi

Tiện ích mở rộng có ba bối cảnh thực thi mã, tức là ứng dụng bao gồm ba phần với các cấp độ truy cập khác nhau vào API trình duyệt.

Bối cảnh mở rộng

Hầu hết API đều có sẵn ở đây. Trong bối cảnh này họ “sống”:

  1. Trang nền - phần “phụ trợ” của tiện ích mở rộng. Tệp được chỉ định trong tệp kê khai bằng phím “nền”.
  2. Trang bật lên — một trang bật lên xuất hiện khi bạn nhấp vào biểu tượng tiện ích mở rộng. Trong bản tuyên ngôn browser_action -> default_popup.
  3. Trang tùy chỉnh - trang tiện ích mở rộng, “sống” trong một tab riêng của chế độ xem chrome-extension://<id_расширения>/customPage.html.

Ngữ cảnh này tồn tại độc lập với các cửa sổ và tab trình duyệt. Trang nền tồn tại trong một bản sao duy nhất và luôn hoạt động (ngoại lệ là trang sự kiện, khi tập lệnh nền được khởi chạy bởi một sự kiện và “chết” sau khi thực thi). Trang bật lên tồn tại khi cửa sổ bật lên được mở và Trang tùy chỉnh — trong khi tab có nó đang mở. Không có quyền truy cập vào các tab khác và nội dung của chúng từ ngữ cảnh này.

Bối cảnh tập lệnh nội dung

Tệp tập lệnh nội dung được khởi chạy cùng với mỗi tab trình duyệt. Nó có quyền truy cập vào một phần API của tiện ích mở rộng và cây DOM của trang web. Chính các tập lệnh nội dung chịu trách nhiệm tương tác với trang. Các tiện ích mở rộng thao tác cây DOM thực hiện việc này trong tập lệnh nội dung - ví dụ: trình chặn quảng cáo hoặc trình dịch. Ngoài ra, tập lệnh nội dung có thể giao tiếp với trang thông qua tiêu chuẩn postMessage.

Bối cảnh trang web

Đây chính là trang web thực tế. Nó không liên quan gì đến tiện ích mở rộng và không có quyền truy cập vào đó, ngoại trừ trường hợp tên miền của trang này không được chỉ định rõ ràng trong bảng kê khai (xem thêm về điều này bên dưới).

Trao đổi tin nhắn

Các phần khác nhau của ứng dụng phải trao đổi tin nhắn với nhau. Có một API cho việc này runtime.sendMessage gửi tin nhắn background и tabs.sendMessage để gửi tin nhắn đến một trang (tập lệnh nội dung, cửa sổ bật lên hoặc trang web nếu có externally_connectable). Dưới đây là ví dụ khi truy cập API Chrome.

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

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

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

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

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

Để giao tiếp đầy đủ, bạn có thể tạo kết nối thông qua runtime.connect. Đáp lại chúng tôi sẽ nhận được runtime.Port, trong khi nó mở, bạn có thể gửi bất kỳ số lượng tin nhắn nào. Về phía khách hàng, ví dụ, contentscript, nó trông như thế này:

// Опять же 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"});

Máy chủ hoặc nền:

// Обработчик для подключения 'своих' вкладок. Контент скриптов, 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) {
    ...
});

Ngoài ra còn có một sự kiện onDisconnect và phương pháp disconnect.

Sơ đồ ứng dụng

Hãy tạo một tiện ích mở rộng trình duyệt lưu trữ khóa riêng, cung cấp quyền truy cập vào thông tin công khai (địa chỉ, khóa chung giao tiếp với trang và cho phép các ứng dụng của bên thứ ba yêu cầu chữ ký cho các giao dịch.

Phát triển ứng dụng

Ứng dụng của chúng tôi vừa phải tương tác với người dùng vừa phải cung cấp cho trang API để gọi các phương thức (ví dụ: để ký giao dịch). Thực hiện chỉ với một contentscript sẽ không hoạt động vì nó chỉ có quyền truy cập vào DOM chứ không có quyền truy cập vào JS của trang. Kết nối qua runtime.connect chúng tôi không thể, vì API là cần thiết trên tất cả các miền và chỉ những miền cụ thể mới có thể được chỉ định trong tệp kê khai. Kết quả là sơ đồ sẽ trông như thế này:

Viết phần mở rộng trình duyệt an toàn

Sẽ có một kịch bản khác - inpage, mà chúng tôi sẽ đưa vào trang. Nó sẽ chạy trong ngữ cảnh của nó và cung cấp API để làm việc với tiện ích mở rộng.

bắt đầu

Tất cả mã mở rộng trình duyệt đều có sẵn tại GitHub. Trong phần mô tả sẽ có liên kết đến các cam kết.

Hãy bắt đầu với bản tuyên ngôn:

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

Tạo Background.js, Popup.js, Inpage.js và Contentscript.js trống. Chúng tôi thêm Popup.html - và ứng dụng của chúng tôi đã có thể được tải vào Google Chrome và đảm bảo rằng nó hoạt động.

Để xác minh điều này, bạn có thể lấy mã do đó. Ngoài những gì chúng tôi đã làm, liên kết đã định cấu hình tập hợp dự án bằng cách sử dụng webpack. Để thêm một ứng dụng vào trình duyệt, trong chrome://extensions, bạn cần chọn tải đã giải nén và thư mục có tiện ích mở rộng tương ứng - trong trường hợp của chúng tôi là dist.

Viết phần mở rộng trình duyệt an toàn

Bây giờ tiện ích mở rộng của chúng tôi đã được cài đặt và hoạt động. Bạn có thể chạy các công cụ dành cho nhà phát triển cho các ngữ cảnh khác nhau như sau:

cửa sổ bật lên ->

Viết phần mở rộng trình duyệt an toàn

Việc truy cập vào bảng điều khiển tập lệnh nội dung được thực hiện thông qua bảng điều khiển của chính trang mà nó được khởi chạy.Viết phần mở rộng trình duyệt an toàn

Trao đổi tin nhắn

Vì vậy, chúng ta cần thiết lập hai kênh liên lạc: inpage <-> nền và cửa sổ bật lên <-> nền. Tất nhiên, bạn có thể chỉ cần gửi tin nhắn đến cổng và phát minh ra giao thức của riêng mình, nhưng tôi thích cách tiếp cận mà tôi đã thấy trong dự án nguồn mở metamask hơn.

Đây là một phần mở rộng trình duyệt để làm việc với mạng Ethereum. Trong đó, các phần khác nhau của ứng dụng giao tiếp qua RPC bằng thư viện dnode. Nó cho phép bạn tổ chức trao đổi khá nhanh chóng và thuận tiện nếu bạn cung cấp cho nó luồng nodejs dưới dạng phương tiện truyền tải (nghĩa là một đối tượng thực hiện cùng một giao diện):

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

Bây giờ chúng ta sẽ tạo một lớp ứng dụng. Nó sẽ tạo các đối tượng API cho cửa sổ bật lên và trang web, đồng thời tạo một dnode cho chúng:

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

Ở đây và bên dưới, thay vì đối tượng Chrome toàn cầu, chúng tôi sử dụng tiện ích mở rộngApi, truy cập Chrome trong trình duyệt của Google và trình duyệt khác. Điều này được thực hiện để tương thích giữa nhiều trình duyệt, nhưng với mục đích của bài viết này, người ta có thể chỉ cần sử dụng 'chrome.runtime.connect'.

Hãy tạo một phiên bản ứng dụng trong tập lệnh nền:

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

Vì dnode hoạt động với các luồng và chúng tôi nhận được một cổng nên cần có một lớp bộ điều hợp. Nó được tạo bằng thư viện luồng có thể đọc được, thư viện này triển khai các luồng nodejs trong trình duyệt:

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

Bây giờ hãy tạo kết nối trong giao diện người dùng:

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

Sau đó, chúng tôi tạo kết nối trong tập lệnh nội dung:

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

Vì chúng tôi cần API không phải trong tập lệnh nội dung mà trực tiếp trên trang nên chúng tôi thực hiện hai việc:

  1. Chúng tôi tạo ra hai luồng. Một - về phía trang, phía trên postMessage. Đối với điều này, chúng tôi sử dụng cái này gói này từ những người tạo ra metamask. Luồng thứ hai là chạy nền trên cổng nhận được từ runtime.connect. Hãy mua chúng. Bây giờ trang sẽ có một luồng tới nền.
  2. Đưa tập lệnh vào DOM. Tải xuống tập lệnh (quyền truy cập vào tập lệnh được cho phép trong tệp kê khai) và tạo thẻ script với nội dung của nó bên trong:

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

Bây giờ chúng ta tạo một đối tượng api trong inpage và đặt nó ở chế độ chung:

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

Chúng tôi sắn sàng Cuộc gọi thủ tục từ xa (RPC) với API riêng cho trang và giao diện người dùng. Khi kết nối một trang mới với nền, chúng ta có thể thấy điều này:

Viết phần mở rộng trình duyệt an toàn

API và nguồn gốc trống. Ở phía trang, chúng ta có thể gọi hàm hello như thế này:

Viết phần mở rộng trình duyệt an toàn

Làm việc với các hàm gọi lại trong JS hiện đại là một cách cư xử tồi, vì vậy hãy viết một trình trợ giúp nhỏ để tạo một dnode cho phép bạn truyền một đối tượng API tới utils.

Các đối tượng API bây giờ sẽ trông như thế này:

export class SignerApp {

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

...

}

Lấy một đối tượng từ xa như thế này:

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

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

Và các hàm gọi sẽ trả về một lời hứa:

Viết phần mở rộng trình duyệt an toàn

Phiên bản có chức năng không đồng bộ có sẵn đây.

Nhìn chung, cách tiếp cận RPC và luồng có vẻ khá linh hoạt: chúng ta có thể sử dụng tính năng ghép kênh hơi và tạo một số API khác nhau cho các tác vụ khác nhau. Về nguyên tắc, dnode có thể được sử dụng ở bất cứ đâu, điều chính là bao bọc quá trình vận chuyển dưới dạng luồng nodejs.

Một giải pháp thay thế là định dạng JSON, triển khai giao thức JSON RPC 2. Tuy nhiên, nó hoạt động với các phương thức truyền tải cụ thể (TCP và HTTP(S)), không áp dụng được trong trường hợp của chúng tôi.

Trạng thái nội bộ và lưu trữ cục bộ

Chúng tôi sẽ cần lưu trữ trạng thái nội bộ của ứng dụng - ít nhất là các khóa ký. Chúng ta có thể dễ dàng thêm trạng thái vào ứng dụng và các phương thức để thay đổi trạng thái đó trong API bật lên:

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

    ...

} 

Ở chế độ nền, chúng ta sẽ gói mọi thứ trong một hàm và ghi đối tượng ứng dụng vào cửa sổ để chúng ta có thể làm việc với nó từ bảng điều khiển:

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

Hãy thêm một vài phím từ bảng điều khiển giao diện người dùng và xem điều gì xảy ra với trạng thái:

Viết phần mở rộng trình duyệt an toàn

Trạng thái cần được duy trì liên tục để các phím không bị mất khi khởi động lại.

Chúng tôi sẽ lưu trữ nó trong localStorage, ghi đè lên nó sau mỗi thay đổi. Sau đó, quyền truy cập vào nó cũng sẽ cần thiết đối với giao diện người dùng và tôi cũng muốn đăng ký các thay đổi. Dựa trên điều này, sẽ thuận tiện hơn khi tạo một bộ lưu trữ có thể quan sát được và đăng ký các thay đổi của nó.

Chúng tôi sẽ sử dụng thư viện mobx (https://github.com/mobxjs/mobx). Sự lựa chọn thuộc về nó vì tôi không phải làm việc với nó, nhưng tôi thực sự muốn nghiên cứu nó.

Hãy thêm khởi tạo trạng thái ban đầu và làm cho cửa hàng có thể quan sát được:

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

    ...

}

“Dưới mui xe,” mobx đã thay thế tất cả các trường lưu trữ bằng proxy và chặn tất cả các cuộc gọi đến chúng. Nó sẽ có thể đăng ký những tin nhắn này.

Dưới đây tôi sẽ thường sử dụng thuật ngữ “khi thay đổi”, mặc dù điều này không hoàn toàn chính xác. Mobx theo dõi quyền truy cập vào các trường. Getters và setters của các đối tượng proxy mà thư viện tạo ra sẽ được sử dụng.

Trang trí hành động phục vụ hai mục đích:

  1. Ở chế độ nghiêm ngặt với cờ thi hành, mobx cấm thay đổi trạng thái trực tiếp. Nó được coi là thực hành tốt để làm việc trong điều kiện nghiêm ngặt.
  2. Ngay cả khi một hàm thay đổi trạng thái nhiều lần - ví dụ: chúng tôi thay đổi một số trường trong một số dòng mã - thì người quan sát chỉ được thông báo khi nó hoàn thành. Điều này đặc biệt quan trọng đối với giao diện người dùng, nơi các cập nhật trạng thái không cần thiết dẫn đến việc hiển thị các phần tử không cần thiết. Trong trường hợp của chúng tôi, cả điều đầu tiên và điều thứ hai đều không có liên quan đặc biệt nhưng chúng tôi sẽ tuân theo các phương pháp hay nhất. Thông thường, người ta thường gắn các bộ trang trí vào tất cả các chức năng thay đổi trạng thái của các trường được quan sát.

Trong nền, chúng tôi sẽ thêm khởi tạo và lưu trạng thái trong 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)
        }
    }
}

Chức năng phản ứng rất thú vị ở đây. Nó có hai đối số:

  1. Bộ chọn dữ liệu.
  2. Trình xử lý sẽ được gọi với dữ liệu này mỗi khi nó thay đổi.

Không giống như redux, nơi chúng tôi nhận trạng thái một cách rõ ràng dưới dạng đối số, mobx ghi nhớ những gì chúng tôi truy cập bên trong bộ chọn và chỉ gọi trình xử lý khi chúng thay đổi.

Điều quan trọng là phải hiểu chính xác cách mobx quyết định những thiết bị quan sát mà chúng ta đăng ký. Nếu tôi viết một bộ chọn bằng mã như thế này() => app.store, thì phản ứng sẽ không bao giờ được gọi, vì bản thân bộ lưu trữ không thể quan sát được mà chỉ có các trường của nó là có.

Nếu tôi viết nó như thế này () => app.store.keys, thì sẽ không có gì xảy ra, vì khi thêm/xóa các phần tử mảng, tham chiếu đến nó sẽ không thay đổi.

Mobx lần đầu tiên hoạt động như một bộ chọn và chỉ theo dõi những thứ có thể quan sát được mà chúng tôi đã truy cập. Điều này được thực hiện thông qua getters proxy. Vì vậy, hàm dựng sẵn được sử dụng ở đây toJS. Nó trả về một đối tượng mới với tất cả các proxy được thay thế bằng các trường ban đầu. Trong quá trình thực thi, nó đọc tất cả các trường của đối tượng - do đó các getters được kích hoạt.

Trong bảng điều khiển bật lên, chúng tôi sẽ lại thêm một số phím. Lần này họ cũng kết thúc ở localStorage:

Viết phần mở rộng trình duyệt an toàn

Khi trang nền được tải lại, thông tin vẫn được giữ nguyên.

Tất cả các mã ứng dụng cho đến thời điểm này có thể được xem đây.

Lưu trữ an toàn các khóa riêng

Việc lưu trữ khóa riêng ở dạng văn bản rõ ràng là không an toàn: luôn có khả năng bạn bị hack, giành quyền truy cập vào máy tính của mình, v.v. Do đó, trong localStorage, chúng tôi sẽ lưu trữ khóa ở dạng được mã hóa bằng mật khẩu.

Để bảo mật tốt hơn, chúng tôi sẽ thêm trạng thái khóa vào ứng dụng, trong đó sẽ không có quyền truy cập vào khóa nào cả. Chúng tôi sẽ tự động chuyển tiện ích mở rộng sang trạng thái bị khóa do hết thời gian chờ.

Mobx cho phép bạn chỉ lưu trữ một bộ dữ liệu tối thiểu và phần còn lại được tự động tính toán dựa trên nó. Đây là cái gọi là thuộc tính tính toán. Chúng có thể được so sánh với các lượt xem trong cơ sở dữ liệu:

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

Bây giờ chúng tôi chỉ lưu trữ các khóa và mật khẩu được mã hóa. Mọi thứ khác đều được tính toán. Chúng tôi thực hiện chuyển sang trạng thái khóa bằng cách xóa mật khẩu khỏi trạng thái. API công khai hiện có phương thức khởi tạo bộ nhớ.

Viết để mã hóa tiện ích sử dụng 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)
}

Trình duyệt có một API nhàn rỗi thông qua đó bạn có thể đăng ký một sự kiện - thay đổi trạng thái. Theo đó, Nhà nước có thể idle, active и locked. Khi không hoạt động, bạn có thể đặt thời gian chờ và khóa được đặt khi hệ điều hành bị chặn. Chúng tôi cũng sẽ thay đổi bộ chọn để lưu vào 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)
        }
    }
}

Mã trước bước này là đây.

Giao dịch

Vì vậy, chúng ta đến với điều quan trọng nhất: tạo và ký các giao dịch trên blockchain. Chúng tôi sẽ sử dụng chuỗi khối và thư viện WAVES giao dịch sóng.

Đầu tiên, hãy thêm vào trạng thái một mảng tin nhắn cần được ký, sau đó thêm các phương thức để thêm tin nhắn mới, xác nhận chữ ký và từ chối:

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

    ...
}

Khi chúng tôi nhận được một tin nhắn mới, chúng tôi sẽ thêm siêu dữ liệu vào đó, thực hiện observable và thêm vào store.messages.

Nếu bạn không observable theo cách thủ công thì mobx sẽ tự thực hiện khi thêm tin nhắn vào mảng. Tuy nhiên, nó sẽ tạo một đối tượng mới mà chúng ta sẽ không có tham chiếu đến nhưng chúng ta sẽ cần nó cho bước tiếp theo.

Tiếp theo, chúng tôi trả lại một lời hứa sẽ được giải quyết khi trạng thái tin nhắn thay đổi. Trạng thái được theo dõi bằng phản ứng, phản ứng này sẽ “tự hủy” khi trạng thái thay đổi.

Mã phương pháp approve и reject rất đơn giản: chúng tôi chỉ cần thay đổi trạng thái của tin nhắn sau khi ký nếu cần.

Chúng tôi đặt Phê duyệt và từ chối trong API UI, newMessage trong API trang:

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

    ...
}

Bây giờ hãy thử ký giao dịch với tiện ích mở rộng:

Viết phần mở rộng trình duyệt an toàn

Nói chung là mọi thứ đã sẵn sàng, chỉ còn lại là thêm giao diện người dùng đơn giản.

UI

Giao diện cần truy cập vào trạng thái ứng dụng. Về phía giao diện người dùng, chúng tôi sẽ làm observable trạng thái và thêm chức năng vào API sẽ thay đổi trạng thái này. Hãy thêm vào observable đến đối tượng API nhận được từ nền:

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

Cuối cùng chúng ta bắt đầu render giao diện ứng dụng. Đây là một ứng dụng phản ứng. Đối tượng nền được truyền đơn giản bằng cách sử dụng đạo cụ. Tất nhiên, sẽ đúng nếu tạo một dịch vụ riêng cho các phương thức và một kho lưu trữ cho trạng thái, nhưng với mục đích của bài viết này thì điều này là đủ:

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

Với mobx, thật dễ dàng để bắt đầu hiển thị khi dữ liệu thay đổi. Chúng tôi chỉ cần treo bộ trang trí quan sát từ gói phản ứng mobx trên thành phần và kết xuất sẽ được gọi tự động khi có bất kỳ vật thể quan sát nào được tham chiếu bởi thành phần thay đổi. Bạn không cần bất kỳ mapStateToProps nào hoặc kết nối như trong redux. Mọi thứ đều hoạt động ngay lập tức:

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

Các thành phần còn lại có thể được xem trong mã trong thư mục giao diện người dùng.

Bây giờ trong lớp ứng dụng, bạn cần tạo bộ chọn trạng thái cho giao diện người dùng và thông báo cho giao diện người dùng khi nó thay đổi. Để làm điều này, hãy thêm một phương thức getState и reactionđang gọi điện 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())

        })
    }

    ...
}

Khi nhận được một vật remote được tạo reaction để thay đổi trạng thái gọi hàm ở phía giao diện người dùng.

Điểm nhấn cuối cùng là thêm hiển thị tin nhắn mới vào biểu tượng tiện ích mở rộng:

function setupApp() {
...

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

...
}

Vậy là ứng dụng đã sẵn sàng. Các trang web có thể yêu cầu chữ ký cho các giao dịch:

Viết phần mở rộng trình duyệt an toàn

Viết phần mở rộng trình duyệt an toàn

Mã có sẵn ở đây liên kết.

Kết luận

Nếu bạn đã đọc đến cuối bài viết mà vẫn còn thắc mắc, bạn có thể hỏi họ tại kho lưu trữ có phần mở rộng. Ở đó bạn cũng sẽ tìm thấy các cam kết cho từng bước được chỉ định.

Và nếu bạn muốn xem mã của tiện ích mở rộng thực tế, bạn có thể tìm thấy mã này đây.

Mã, kho lưu trữ và mô tả công việc từ siemarell

Nguồn: www.habr.com

Thêm một lời nhận xét