Вступление

Всякий раз, когда я слышу о людях, создающих расширения Chrome, я всегда думаю «ох, хорошо для них». Это не должно быть сложной задачей, в конце концов, это просто изолированный веб-сайт с некоторыми высокоуровневыми API-интерфейсами для взаимодействия с браузером, верно?

Неправильно.

Я всегда был жертвой веры в то, что создание расширения для Chrome может быть таким же простым, как редактирование шаблона, объединение нескольких страниц и связывание нескольких прослушивателей событий, чтобы завершить испытание.

Кто бы мог подумать, что я когда-нибудь так ошибаюсь?

Оказывается, создание такого приложения может иметь больше ограничений, чем создание веб-страницы.

Когда у TronWatch впервые возникла идея создания TronLink, меня обрадовала мысль о создании приложения, которое могло бы действительно принести пользу блокчейну. Вы слышите, что этот термин используется довольно свободно, но очень редко он действительно имеет какие-либо достоинства, приписываемые продукту, находящемуся под ним. TronWatch всегда стремился помочь продвинуть блокчейн Tron способами, которых невозможно было просто бездельничать и ждать, и TronLink был идеальным кандидатом.

Давайте сначала рассмотрим большую проблему виртуальной машины Tron: веб-приложения не могут взаимодействовать с ней. По крайней мере, мы так думали.
Что хорошего в том, чтобы тратить все финансовые вложения и бесчисленные часы усилий на создание новой технологии, к которой пользователи фактически не смогут получить доступ? Нет ничего хуже, чем создать такое приложение, которым фактически нельзя пользоваться. Конечно, собственные приложения или бэкэнд-серверы могут напрямую связываться с узлами, но какой в ​​этом смысл, когда одной из основных целей Tron является децентрализация?
Пользователи хотят контролировать свои собственные средства и подпитывать блокчейн, не беспокоясь о взломе крупной корпорации или объявлении о банкротстве.
Почему люди должны ограничиваться этим?

TronLink устраняет этот разрыв между виртуальной машиной Tron и конечными пользователями технологии блокчейн.

Не выходя из собственного браузера, пользователи теперь могут работать с Dapps одним нажатием кнопки. Внедрив глобальный API TronLink во все участвующие веб-сайты, разработчики теперь могут легко получить доступ к блокчейну. Будь то запрос средств, голосование за позицию суперпредставителя или вызов функций смарт-контракта, TronLink обеспечит его работу.

Первая неудача

Поначалу я мог быть немного неправдивым; в конце концов, расширения chrome - это просто изолированные веб-приложения, которые имеют доступ к определенным компонентам браузера пользователя. Просто связать все движущиеся части вместе может быть довольно сложно.
Из-за изолированного характера приложения вы не можете просто «совместно использовать код» между компонентами или использовать стандартную шину событий для обработки действий в браузере. Как и в большинстве реализаций песочницы, вы можете быть весьма ограничены с точки зрения связи с хостом.

Мне было поручено создать архитектуру коммуникационной платформы, на которой будет построен TronLink, а также создать первую версию TronLink Web API.
Благодаря изолированным расширениям Chrome веб-сайты не могут свободно обращаться к внутренним компонентам приложения без создания канала связи. Вот тут-то и пригодятся contentScript и другие коммуникационные модули.

Давайте сначала обсудим четыре основных компонента приложения:

  • backgroundScript
    Ядро приложения, backgroundScript, управляет постоянным состоянием, с которым связан основной интерфейс всплывающих окон. Здесь происходит волшебство: подписываются транзакции, создаются учетные записи и отправляются события API. Здесь происходит все, что связано с основным управлением расширением.
  • pageHook
    Хотя он и не такой радикальный, как backgroundScript, pageHook все же имеет большое значение. Задача pageHook - внедрить и предоставить версионный веб-API TronLink для участвующих сайтов. Это то, что обрабатывает подключение веб-сайтов к самому расширению.
  • contentScript
    По сравнению с другими модулями можно утверждать, что contentScript - это просто еще один крошечный движущийся элемент в большом приложении. В самом деле - он может быть маленьким, но не крошечным по любому определению. contentScript фактически туннелирует и объединяет все коммуникации между pageHook и backgroundScript.
  • всплывающее окно
    Целое приложение, всплывающее окно - это то, как мы относимся к пользовательскому интерфейсу расширения. Здесь подтверждения происходят вместе с общим просмотром и контролем над расширением. Это может быть вызвано нажатием значка TronLink на панели навигации браузера или веб-сайтами, запрашивающими подтверждение.

Теперь, когда все готово, давайте обсудим проблему связывания четырех модулей.

Как кратко объяснялось ранее, использование песочницы ограничивает свободное взаимодействие расширения и веб-страницы. contentScript запускается в контексте веб-страницы, тогда как backgroundScript запускается в изолированной программной среде расширения. Это означает, что все коммуникации между ними должны проходить между зарегистрированными каналами.

Чтобы сценарий содержимого мог взаимодействовать с фоновой страницей, оба сценария должны сначала установить канал связи и прослушать его.
Эти каналы предоставляют обоим возможность беспрепятственно передавать объекты JSON от одного конца к другому. Важно отметить, что это объекты JSON, а не обычные объекты JavaScript.
Это означает, что мы не могли передать обещание, например, вместе с сообщением, что позволило бы нам просто разрешить (или отклонить) обещание вернуть как подтверждение, так и / или ответ на запрос.

На картинке выше показан довольно общий пример того, как формируются каналы. Каждая вкладка будет иметь собственный contentScript, который будет устанавливать канал связи с backgroundScript. Затем он может отправлять сообщения на каждую отдельную вкладку, ссылаясь на входящий tabID.

При создании вкладки ей присваивается уникальный идентификатор - обычно трехзначное целое число. Доступ к нему осуществляется через port (обработчик канала), который назначен каждому каналу. Связывая каждый port с его tabID, мы получаем уникальную ссылку на каждую отдельную вкладку.

Давайте создадим быстрый прослушиватель событий backgroundScript:

chrome.extension.onconnect.addListener(port => {
    const source = `${port.name}-${port.sender.tab.id}`;
    this._channels[source] = port;
});

Теперь каждый раз, когда мы получаем событие от contentScript, мы можем просто вернуть событие, отправив сообщение на порт, назначенный его уникальной ссылке. Мы можем расширить приведенное выше, чтобы реализовать это:

chrome.extension.onconnect.addListener(port => {
    const source = `${port.name}-${port.sender.tab.id}`;
    this.channels[source] = port;
    port.onMessage.addListener(({ action, data } = event) => {
        this.emit(event.action, { data, source });
    });
});

Каждый раз при получении сообщения мы генерируем новое событие с предоставленными данными и источником канала связи. Чтобы ответить на сообщение, мы просто сделаем:

communication.on('someEvent', ({ data, source }) => {
    this.channels[source].postMessage({
        action: 'response',
        data: { ts: Date.now() }
    });
});

Легко, правда?

И снова ошибка.

Конечно, мы могли бы просто отслеживать и ссылаться на каждое входящее сообщение и источник события для каждого отдельного события, но что в этом интересного? Это не только приведет к раздуванию кодовой базы, но и затруднит дальнейшую адаптацию приложения, а отладку станет еще более сложной задачей.

Чтобы преодолеть это, я реализовал уровень абстракции, который я назвал LinkedResponse - у подобия contentScript есть свой собственный уровень под названием LinkedRequest. backgroundScript пересылает все события, происходящие под действием tunnel, на этот LinkedResponse, который затем объединяет ответы на исходные события.
Я реализовал это, отправив уникальный идентификатор для каждого сообщения - LinkedResponse будет принимать этот уникальный идентификатор, генерировать событие с обещанием, а после выполнения обещания (или отклонения) генерировать новое событие для contentScript, который будет содержать такой же уникальный идентификатор.

Помните, я упоминал contentScript ранее? Он также прослушивает все tunnelevents и берет этот уникальный идентификатор, соединяет его с исходным запросом и выполняет обещание, которое он сгенерировал первым.

Основное использование выглядит следующим образом:

# contentScript
linkedRequest.build({ 
    method: 'testMethod', 
    data: { ts: Date.now() } 
}).then(response => {
    logger.info(`Received from backgroundScript: ${response}`);
}).catch(error => {
    logger.info(`Error from backgroundScript: ${error}`);
});
# backgroundScript
linkedResponse.on('request', ({ reject, resolve, payload }) => {
    switch(payload.method) {
        case 'testMethod':
            resolve(payload.data);
    }
});

В приведенной выше демонстрации contentScript генерирует событие для backgroundScript с именем testMethod, которое содержит текущую временную метку в качестве данных.

backgroundScript получит событие, обработает инициируемый testMethod и вернет ту же метку времени, которая была отправлена ​​изначально.

Это гораздо более простой способ связи между ними.
Если вы хотите увидеть, как реализованы эти конкретные методы, вы можете взглянуть на наш репозиторий на github.

Вторая неудача

О, как бы мне хотелось, чтобы общение между веб-сайтами и расширением было таким простым. Теперь мы также можем передать сообщение во всплывающее окно!

К счастью, это была не такая уж сложная задача. Я просто использовал методы, которые реализовал ранее.
Вместо вкладок, подключаемых к backgroundScript, используется всплывающее окно.

popup не имеет tabID, так как это не вкладка, а это значит, что я могу просто ссылаться на канал связи как на всплывающее окно.

Я изменил первую часть, чтобы справиться с этим:

chrome.extension.onconnect.addListener(port => {
    let source = port.name;
    
    if(port.sender.tab)
        source += `-${port.sender.tab.id}`;
}});

Теперь на все сообщения, поступающие из всплывающего окна, можно было отвечать, вызывая канал, связанный с всплывающим окном.

Если бы это было так просто.

Оказывается, расширение не может запускать собственное всплывающее окно. Это означает, что если веб-сайт просит пользователя отправить TRON на адрес, расширение не может программно открыть всплывающее окно, чтобы пользователь мог подтвердить транзакцию.
Это означало, что нам нужно было открыть диалоговое окно, чтобы показать подтверждение, но это связано с отдельной проблемой.

Как многие из вас, возможно, ожидали, открытие диалогового окна - это просто причудливый термин для открытия новой вкладки с заданным размером в новом окне.
Проблема в том, что канал связи для диалогового окна (который является просто экземпляром всплывающего окна) больше не popup, а вместо popup-123, или popup-736, или, может быть, popup-614.
Ага, у него есть tabID, который доставил нам много проблем, так как его так легко не заметить.

Сначала мы подумали, что, возможно, диалоговое окно создает новый экземпляр backgroundScript, поэтому данные учетной записи не загружаются. Однако это не имеет смысла, поскольку backgroundScript представляет собой единый постоянный сценарий.

Тогда мы подумали, что backgroundScript вылетает из всплывающего окна, которое теперь открывается вручную через диалоговое окно.
Нет, во всплывающем окне браузера связь работает нормально.

Я совершенно забыл, что у него есть tabID. К счастью, это было простое решение:

chrome.extension.onconnect.addListener(port => {
    let source = port.name;
    
    if(port.sender.tab && source !== 'popup')
        source += `-${port.sender.tab.id}`;
}});

Теперь мы можем взаимодействовать с диалоговым окном так же, как с всплывающим окном.
Мы все равно хотели синхронизировать состояние, что было для нас бонусом.

Внедрение TronLink API

С самого начала я всегда хотел, чтобы API TronLink был версионным. Виртуальная машина Tron находится на ранней стадии своего развития, и я знал, что она будет подвержена изменениям, а это означает, что она потенциально может нарушить работу моего API.
Если бы я хотел изменить или удалить функции, мне пришлось бы учитывать веб-сайты, использующие старый API. К сожалению, я не смог внести критические изменения, когда от расширения могут полагаться тысячи сайтов.

Я решил, что также хочу предложить разработчикам возможность внедрять TronLink в выбранную переменную вместо TronLink. Если по какой-либо причине у них возникнет конфликт имен, им не придется переписывать собственную кодовую базу. Это приводит нас к pageHook.

pageHook имеет единственную цель: внедрить API (и другие различные данные) на страницу, чтобы разработчики могли получить к ней доступ.

Я хотел ввести следующее:

  • API TronLink под версионным пространством имен.
  • Среда TronLink (была ли она запущена в производстве или в разработке)
  • Версия расширения TronLink (может быть полезно для аналитики или отчетов о сбоях, если они возникнут)

Обработка пользовательской переменной была довольно простой. Я просто определил имя переменной с помощью:

const scriptVariable = (window.TRON_LINK_VARIABLE || 'TronLink').toString();

К счастью, я внедрил API в пространство имен window, что позволило мне определить его следующим образом:

window[scriptVariable] = {
    v1: (network = 'mainnet') => new TronLink(linkedRequest, network)
};

Как видите, первая итерация API принимает два аргумента - linkedRequest и network. Я не хотел раскрывать linkedRequest на веб-сайте, поскольку разработчики потенциально могут нарушить реализацию и вызвать проблемы для пользователей.

Хотя разработчики могут взламывать linkedRequest и создавать необработанные события вручную, преимущество состоит в том, что они не требуют от них импорта самого класса LinkedRequest, что на один критический момент меньше для тех, кто плохо знаком с библиотекой.

Цель TronLink - предоставить разработчикам как можно больший доступ к сети блокчейнов. Это означало бы необходимость поддержки множества функций, из-за чего я решил, что все методы должны иметь пространство имен. Я решил использовать wallet, node и utils.

Команда TronWatch и я реализовали следующие функции:

  • кошелек
    - sendTron
    - sendAsset
    - заморозить
    - разморозить
    - sendTransaction
    - signTransaction
    - simulateSmartContract
    - createSmartContract
    - getAccount
  • узел
    - getLatestBlock
    - getWitness
    - getTokens
    - getBlock
    - getTransaction
    - getAccount
  • utils
    - validateAddress
    - sunToTron
    - tronToSun

Я добавил вышеупомянутые методы с намерением, чтобы у нас было время полностью интегрировать функциональность каждого метода, оставив при этом возможность для улучшений в будущем.

TVM в настоящее время все еще находится в стадии разработки, поэтому я не хотел раздвигать границы того, что может измениться.
Как только TVM будет запущен в официальной тестовой сети Tron, команда добавит поддержку для вызова определенных методов контракта, а не только для их моделирования.

Если вы хотите ознакомиться с некоторыми простыми примерами по началу работы с TronWatch, я предлагаю вам взглянуть на наш пример веб-сайта.

На заключительном примечании

Создание TronLink было сложным, но и интересным проектом.
TronWatch считает, что мы открыли двери для TVM, чтобы пользователи могли использовать dApps в молниеносной и прозрачной сети.

TronLink - первый из многих проектов, над которыми будет работать TronWatch. Следите за новостями о нашей децентрализованной бирже, которую мы планируем открыть для публики к концу четвертого квартала 2018 года.

Если вы хотите принять участие в TronLink, мы предлагаем вам присоединиться к нам в нашем Telegram-канале.
Вы также можете перейти в репозиторий Github, чтобы более подробно изучить внутреннюю работу приложения.

Если у вас есть какие-либо вопросы относительно TronLink или TronWatch, не стесняйтесь обращаться к нам в Twitter или Telegram.

- Тайлер Кинг

от TronWatch Team