2 анбори нисбатан бехатар барои калидҳои корбар мавҷуданд - ҳамёнҳои сахтафзор ва васеъшавии браузер. Ҳамёнҳои сахтафзор асосан бениҳоят бехатаранд, аммо истифодаашон мушкил ва аз озод дур аст, аммо васеъшавии браузер маҷмӯи комили амният ва осонии истифода мебошанд ва инчунин метавонанд барои корбарони ниҳоӣ комилан ройгон бошанд.
Бо дарназардошти ҳамаи ин, мо мехостем, ки тамдиди бехатартаринеро созем, ки таҳияи замимаҳои ғайримарказиро тавассути пешниҳоди API оддӣ барои кор бо транзаксияҳо ва имзоҳо осонтар мекунад.
Мо ба шумо дар бораи ин таҷриба дар поён нақл мекунем.
Дар мақола дастурҳои зина ба зина дар бораи чӣ гуна навиштани тамдиди браузер бо мисолҳои код ва скриншотҳо оварда мешаванд. Шумо метавонед ҳамаи кодҳоро дар анборҳо. Ҳар як иҷро мантиқӣ ба як қисми ин мақола мувофиқат мекунад.
Таърихи мухтасари васеъшавии браузер
Васеъ кардани браузер муддати тӯлонӣ вуҷуд дорад. Онҳо дар Internet Explorer дар соли 1999, дар Firefox дар соли 2004 пайдо шуданд. Бо вуҷуди ин, дар муддати хеле тӯлонӣ стандарти ягонаи васеъкунӣ вуҷуд надошт.
Мо гуфта метавонем, ки он дар баробари васеъшавӣ дар версияи чоруми Google Chrome пайдо шудааст. Албатта, он вақт ягон мушаххасот вуҷуд надошт, аммо ин Chrome API буд, ки асоси он гардид: бо забт кардани аксари бозори браузер ва дорои мағозаи дарунсохт, Chrome воқеан стандартро барои васеъшавии браузер муқаррар кард.
Mozilla стандарти худро дошт, аммо бо дидани маъруфияти васеъшавии Chrome, ширкат тасмим гирифт, ки API-и мувофиқ созад. Дар соли 2015, бо ташаббуси Mozilla, дар доираи Консорсиуми умумиҷаҳонии веб (W3C) як гурӯҳи махсус таъсис дода шуд, ки дар мушаххасоти васеъкунии кросс-браузер кор мекунад.
Васеъкуниҳои мавҷудаи API барои Chrome ҳамчун асос гирифта шуданд. Кор бо дастгирии Microsoft анҷом дода шуд (Google аз иштирок дар таҳияи стандарт даст кашид) ва дар натиҷа лоиҳа пайдо шуд. хусусиятҳои.
Ба таври расмӣ, мушаххасот аз ҷониби Edge, Firefox ва Opera дастгирӣ карда мешавад (дар хотир доред, ки Chrome дар ин рӯйхат нест). Аммо дар асл, стандарт асосан бо Chrome мувофиқ аст, зеро он воқеан дар асоси васеъшавии он навишта шудааст. Шумо метавонед дар бораи API WebExtensions бештар хонед дар ин ҷо.
Сохтори васеъшавӣ
Ягона файле, ки барои васеъкунӣ талаб карда мешавад, манифест (manifest.json). Он инчунин "нуқтаи вуруд" ба густариш аст.
Қисмҳои гуногуни барнома бояд бо ҳамдигар паём мубодила кунанд. Барои ин API мавҷуд аст runtime.sendMessage фиристодани паём background и tabs.sendMessage барои фиристодани паём ба саҳифа (скрипти мундариҷа, поп-ап ё саҳифаи веб, агар дастрас бошад externally_connectable). Дар зер як мисол ҳангоми дастрасӣ ба 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))
}
)
Барои муоширати пурра, шумо метавонед пайвастҳоро тавассути 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.
{
// Имя и описание, версия. Все это будет видно в браузере в 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 бор карда шавад ва боварӣ ҳосил кунед, ки он кор мекунад.
Барои тасдиқи ин, шумо метавонед рамзро гиред аз ин ҷо. Илова бар он коре, ки мо анҷом додем, истиноди конфигуратсияи лоиҳаро бо истифода аз webpack танзим кард. Барои илова кардани замима ба браузер, дар chrome://extensions шумо бояд load unpacked ва ҷузвдони дорои тамдиди мувофиқро интихоб кунед - дар ҳолати мо dist.
Ин васеъшавии браузер барои кор бо шабакаи Ethereum мебошад. Дар он қисмҳои гуногуни барнома тавассути 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)
})
}
}
Дар ин ҷо ва дар поён, ба ҷои объекти глобалии Chrome, мо extensionApi-ро истифода мебарем, ки ба Chrome дар браузери Google ва дар дигар браузерҳо дастрасӣ пайдо мекунад. Ин барои мутобиқати байни браузерҳо анҷом дода мешавад, аммо барои мақсадҳои ин мақола, шумо метавонед танҳо 'chrome.runtime.connect' -ро истифода баред.
Биёед як мисоли барномаро дар скрипти замина эҷод кунем:
import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {SignerApp} from "./SignerApp";
const app = new SignerApp();
// onConnect срабатывает при подключении 'процессов' (contentscript, popup, или страница расширения)
extensionApi.runtime.onConnect.addListener(connectRemote);
function connectRemote(remotePort) {
const processName = remotePort.name;
const portStream = new PortStream(remotePort);
// При установке соединения можно указывать имя, по этому имени мы и оппределяем кто к нам подлючился, контентскрипт или ui
if (processName === 'contentscript'){
const origin = remotePort.sender.url
app.connectPage(portStream, origin)
}else{
app.connectPopup(portStream)
}
}
Азбаски dnode бо ҷараёнҳо кор мекунад ва мо порт мегирем, синфи адаптер лозим аст. Он бо истифода аз китобхонаи readable-stream сохта шудааст, ки ҷараёнҳои nodejs-ро дар браузер амалӣ мекунад:
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 {cbToPromise, transformMethods} from "../../src/utils/setupDnode";
const pageApi = await new Promise(resolve => {
dnode.once('remote', remoteApi => {
// С помощью утилит меняем все callback на promise
resolve(transformMethods(cbToPromise, remoteApi))
})
});
Ва даъват кардани функсияҳо ваъда медиҳад:
Версия бо функсияҳои асинхронӣ дастрас аст дар ин ҷо.
Дар замина, мо ҳама чизро дар функсия мепӯшем ва объекти барномаро ба тиреза менависем, то ки мо бо он аз консол кор кунем:
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)
}
...
}
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)
}
}
}
import React, {Component, Fragment} from 'react'
import {observer} from "mobx-react";
import Init from './components/Initialize'
import Keys from './components/Keys'
import Sign from './components/Sign'
import Unlock from './components/Unlock'
@observer // У Компонета с этим декоратом будет автоматически вызван метод render, если будут изменены observable на которые он ссылается
export default class App extends Component {
// Правильно конечно вынести логику рендера страниц в роутинг и не использовать вложенные тернарные операторы,
// и привязывать observable и методы background непосредственно к тем компонентам, которые их используют
render() {
const {keys, messages, initialized, locked} = this.props.background.state;
const {lock, unlock, addKey, removeKey, initVault, deleteVault, approve, reject} = this.props.background;
return <Fragment>
{!initialized
?
<Init onInit={initVault}/>
:
locked
?
<Unlock onUnlock={unlock}/>
:
messages.length > 0
?
<Sign keys={keys} message={messages[messages.length - 1]} onApprove={approve} onReject={reject}/>
:
<Keys keys={keys} onAdd={addKey} onRemove={removeKey}/>
}
<div>
{!locked && <button onClick={() => lock()}>Lock App</button>}
{initialized && <button onClick={() => deleteVault()}>Delete all keys and init</button>}
</div>
</Fragment>
}
}
Қисмҳои боқимондаро дар код дидан мумкин аст дар папкаи UI.
Ҳоло дар синфи барнома ба шумо лозим аст, ки интихобкунандаи ҳолати UI созед ва ҳангоми тағир додани он UI-ро огоҳ кунед. Барои ин, биёед як усулро илова кунем getState и reactionзанг задан remote.updateState:
import {action, observable, reaction} from 'mobx';
import uuid from 'uuid/v4';
import {signTx} from '@waves/waves-transactions'
import {setupDnode} from "./utils/setupDnode";
import {decrypt, encrypt} from "./utils/cryptoUtils";
export class SignerApp {
...
// 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())
})
}
...
}
Агар шумо мақоларо то ба охир хонда бошед, аммо ба ҳар ҳол саволҳо дошта бошед, метавонед аз онҳо пурсед анборҳо бо васеъшавӣ. Дар он ҷо шумо инчунин ӯҳдадориҳоро барои ҳар як қадами таъиншуда хоҳед ёфт.
Ва агар шумо ба коди васеъшавии воқеӣ таваҷҷӯҳ дошта бошед, шумо метавонед инро пайдо кунед дар ин ҷо.