هغه کلیدونه چې په مشخصاتو کې ندي "ممکن" له پامه غورځول شي (دواړه د کروم او فایرفوکس راپور تېروتنې، مګر توسیع کار ته دوام ورکوي).
او غواړم څو ټکو ته متوجه شم.
شاليد - یو څیز چې لاندې برخې پکې شاملې دي:
سکرېپټونه - د سکریپټونو لړۍ چې د شالید شرایطو کې به اجرا شي (موږ به لږ وروسته پدې اړه وغږیږو)؛
مخ - د سکریپټونو پرځای چې په خالي پاڼه کې به اجرا شي، تاسو کولی شئ د منځپانګې سره html مشخص کړئ. په دې حالت کې، د سکریپټ ساحه به له پامه غورځول شي، او سکریپټونه به د منځپانګې پاڼې ته داخل شي؛
دوام - یو بائنری بیرغ، که مشخص شوی نه وي، براوزر به د شالید پروسه "وژني" کله چې دا فکر کوي چې دا هیڅ نه کوي، او د اړتیا په صورت کې یې بیا پیل کړئ. که نه نو، پاڼه به یوازې هغه وخت پورته شي کله چې براوزر بند وي. په فایرفوکس کې نه ملاتړ کیږي.
منځپانګې_سکریپټونه - د شیانو لړۍ چې تاسو ته اجازه درکوي مختلف ویب پاڼو ته مختلف سکریپټونه پورته کړئ. هر څیز لاندې مهمې برخې لري:
لوبې - نمونه url، کوم چې ټاکي چې ایا د ځانګړي مینځپانګې سکریپټ به پکې شامل وي یا نه.
js - د سکریپټونو لیست چې پدې لوبه کې به پورته شي؛
exclude_maches - له ساحې څخه ایستل کیږي match URLs چې د دې ساحې سره سمون لري.
پاڼه_عمل - په حقیقت کې یو څیز دی چې د آیکون لپاره مسؤل دی چې په براوزر کې د ادرس بار سره نږدې ښودل شوی او ورسره تعامل دی. دا تاسو ته اجازه درکوي چې د پاپ اپ کړکۍ ښکاره کړئ، کوم چې ستاسو د خپل HTML، CSS او JS په کارولو سره تعریف شوی.
default_popup - د پاپ اپ انٹرفیس سره د HTML فایل ته لاره، کیدای شي CSS او JS ولري.
پرېښلې - د تمدید حقونو اداره کولو لپاره یو لړ. حقوق درې ډوله دي، چې په تفصیل سره بیان شوي دي دلته
دا شرایط په خپلواک ډول د براوزر وینډوز او ټبونو څخه شتون لري. پس منظر پاڼه په یوه کاپي کې شتون لري او تل کار کوي (استثنا د پیښې پا pageه ده ، کله چې د شالید سکریپټ د پیښې لخوا پیل کیږي او د هغې له اجرا کولو وروسته "مړ" کیږي). پاپ اپ پاڼه شتون لري کله چې د پاپ اپ کړکۍ پرانيستې وي، او دودیز پاڼه - پداسې حال کې چې د دې سره ټب خلاص وي. د دې شرایطو څخه نورو ټبونو او د دوی مینځپانګو ته لاسرسی نشته.
د منځپانګې سکریپټ شرایط
د منځپانګې سکریپټ فایل د هر براوزر ټب سره پیل شوی. دا د توسیع API برخې او د ویب پاڼې DOM ونې ته لاسرسی لري. دا د منځپانګې سکریپټونه دي چې د پاڼې سره د متقابل عمل مسولیت لري. تمدیدونه چې د DOM ونې اداره کوي دا د مینځپانګې سکریپټونو کې کوي - د مثال په توګه ، د اعلاناتو بلاکونکي یا ژباړونکي. همدارنګه، د منځپانګې سکریپټ کولی شي د معیار له لارې د پاڼې سره اړیکه ونیسي postMessage.
د ویب پاڼې شرایط
دا پخپله اصلي ویب پاڼه ده. دا د تمدید سره هیڅ تړاو نلري او هلته لاسرسی نلري، پرته له هغه حالتونو کې چې د دې پاڼې ډومین په ښکاره توګه په منشور کې نه دی ښودل شوی (لاندې په دې اړه نور).
د پیغام تبادله
د غوښتنلیک مختلفې برخې باید یو بل سره پیغامونه تبادله کړي. د دې لپاره یو API شتون لري runtime.sendMessage پیغام لیږلو لپاره background и tabs.sendMessage یوې پاڼې ته د پیغام لیږلو لپاره (د منځپانګې سکریپټ، پاپ اپ یا ویب پاڼه که شتون ولري externally_connectable). لاندې یو مثال دی کله چې کروم 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.
{
// Имя и описание, версия. Все это будет видно в браузере в 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 اضافه کوو - او زموږ غوښتنلیک دمخه په ګوګل کروم کې بار کیدی شي او ډاډ ترلاسه کړئ چې دا کار کوي.
دا د Ethereum شبکې سره کار کولو لپاره د براوزر توسیع دی. په دې کې، د غوښتنلیک مختلفې برخې د dnode کتابتون په کارولو سره د RPC له لارې اړیکه نیسي. دا تاسو ته اجازه درکوي چې تبادله په ګړندۍ او اسانۍ سره تنظیم کړئ که تاسو دا د ټرانسپورټ په توګه د نوډج جریان چمتو کړئ (د هغه شی معنی چې ورته انٹرفیس پلي کوي):
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)))
})
اوس موږ به د غوښتنلیک ټولګی جوړ کړو. دا به د پاپ اپ او ویب پا pageې لپاره 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)
})
}
}
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)
}
}
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);
}
}
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 کنسول څخه یو څو کیلي اضافه کړو او وګورو چې د دولت سره څه پیښیږي:
دولت باید دوامداره شي ترڅو د بیا پیل کولو پرمهال کیلي له لاسه ورنکړي.
موږ به دا په محلي ذخیره کې ذخیره کړو، د هر بدلون سره به یې له سره لیکو. په تعقیب ، دې ته لاسرسی به د UI لپاره هم اړین وي ، او زه غواړم بدلونونو ته هم ګډون وکړم. د دې پراساس ، دا به اسانه وي چې د لید وړ ذخیره رامینځته کړئ او د هغې بدلونونو کې ګډون وکړئ.
موږ به د موبکس کتابتون وکاروو (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 په مستقیم ډول د دولت بدلول منع کوي. د سختو شرایطو لاندې کار کول ښه عمل ګڼل کیږي.
حتی که یو فعالیت څو ځله حالت بدل کړي - د مثال په توګه، موږ د کوډ په څو کرښو کې ډیری ساحې بدلوو - څارونکو ته یوازې هغه وخت خبر ورکول کیږي کله چې بشپړ شي. دا په ځانګړې توګه د فرنټ اینډ لپاره مهم دی، چیرې چې غیر ضروري حالت تازه کول د عناصرو غیر ضروري رینډینګ لامل کیږي. زموږ په قضیه کې، نه لومړی او نه دویمه په ځانګړې توګه اړونده ده، مګر موږ به غوره عملونه تعقیب کړو. دا دود دی چې ډیکورټرونه په ټولو دندو کې وصل کړئ چې د لیدل شوي ساحو حالت بدلوي.
په شالید کې به موږ په محلي ذخیره کې د دولت ابتدا او خوندي کول اضافه کړو:
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)
}
}
}
د عکس العمل فعالیت دلته په زړه پوری دی. دا دوه دلیلونه لري:
په عموم کې، هرڅه چمتو دي، ټول هغه څه دي چې پاتې دي ساده 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)
}
په پای کې موږ د غوښتنلیک انٹرفیس وړاندې کول پیل کوو. دا د عکس العمل غوښتنلیک دی. د شالید څیز په ساده ډول د پروپس په کارولو سره تیریږي. دا به سمه وي، البته، د میتودونو او د دولت لپاره د پلورنځي لپاره جلا خدمت کول، مګر د دې مقالې موخو لپاره دا کافی دی:
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')
);
}
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>
}
}
که تاسو مقاله تر پایه لوستلې وي، مګر بیا هم پوښتنې لرئ، تاسو کولی شئ له دوی څخه وپوښتئ د تمدید سره ذخیره. هلته به تاسو د هر ټاکل شوي مرحلې لپاره ژمنې هم ومومئ.
او که تاسو د ریښتیني توسیع لپاره د کوډ په لټه کې علاقه لرئ ، تاسو کولی شئ دا ومومئ دلته.