The browser extension framework
View the Project on GitHub exteranto/exteranto.github.io
Getting StartedThis article assumes you are familiar with a basic extension architecture. If you are just getting started with browser extensions, see this overview by Chrome.
It is also beneficial to understand the IOC Container and App booting. Especially if you experiment with the code samples.
Events are messages fired only within the context of a single script. If you fire an event in tab A’s content, other scripts will have no knowledge of it. Events are baked into Exteranto. They are
a bridge between native browser events and your app. For example the
chrome.runtime.onInstalled event is in Exteranto fired as
ExtensionInstalledEvent
and
ExtensionUpdatedEvent
;
a convenient way to inform about changes in the application. For example events
WindowLoadedEvent
and AppBootedEvent
;
useful if you have to handle interaction between multiple independent parts of your app.
Creating a new event is simple. Any class can be an event in exteranto as
long as it extends Event
class.
The SampleEvent.ts
file:
import { Event } from '@exteranto/core'
export class SampleEvent extends Event {
/**
* @param message Payload to be sent with an event
*/
public constructor (public message: string) {
//
}
}
We have to trigger the SampleEvent
somewhere. Let’s create a listener
that listens to AppBootedEvent
and then fires SampleEvent
every second.
The FiresSampleEventAfterBoot.ts
file:
import { SampleEvent } from './SampleEvent'
import { AppBootedEvent, Dispatcher, Listener } from '@exteranto/core'
export class FiresSampleEventAfterBoot implements Listener {
/**
* Dispatcher to fire events with.
*/
@Autowired
private dispatcher: Dispatcher
/**
* Starts firing events after app boot.
*
* @param event Exteranto app is ready
*/
public handle (event: AppBootedEvent) : void {
setInterval(() => {
this.dispatcher.fire(new SampleEvent('Hello world!'))
}, 1000)
}
}
Now we have to create another listener that handles the event.
The PrintsContent.ts
file:
import { Listener } from '@exteranto/core'
import { SampleEvent } from './SampleEvent'
export class PrintsContent implements Listener {
/**
* Prints an event message.
*
* @param event Typed event
*/
public handle (event: SampleEvent) : void {
console.log(`[${new Date}] ${event.message}`)
}
}
Let’s route the events to their listeners.
The events.ts
file:
import { SampleEvent } from './SampleEvent'
import { PrintsContent } from './PrintsContent'
import { AppBootedEvent } from '@exteranto/core'
import { FiresSampleEventAfterBoot } from './FiresSampleEventAfterBoot'
export default (touch: (event: Class<Event>) => ListenerBag) => {
touch(AppBootedEvent)
.addListener(new FiresSampleEventAfterBoot)
touch(SampleEvent)
.addListener(new PrintsContent)
}
And finally booting the app. After we run this code, we have to open the
background console. Every second, a new Hello world!
string is printed along
with the timestamp.
The main.js
file:
import events from './events'
import { App, Script } from '@exteranto/core'
const app: App = new App(
Script.BACKGROUND,
{
providers: [],
bound: {},
},
events,
)
app.bootstrap()
There are two actor types. Background, server-like master script, and content. There is always one background and an arbitrary number of contents.
Background can send a message to any content, and each content can only send messages to the background. This is not different to the usual extension architecture.
Before we can start sending messages around, we have to boot the channels.
Messaging directly depends on MessagingProvider
. Sending
messages from background to content depends on TabsProvider
.
Create a new listener BootsMessaging.ts
.
import { Messaging } from '@exteranto/api'
import { Autowired, Listener } from '@exteranto/core'
export class BootsMessageListener implements Listener {
/**
* The messaging API implementation.
*/
@Autowired
private messaging: Messaging
/**
* Handle the fired event.
*/
public handle () : void {
this.messaging.listen()
}
}
And let it listen to AppBootedEvent
in each script that
should have messaging active. This will usually be both background and contents.
All messages have to extend Message
class, which is a child of
Event
. A message has to have public payload: T
constructor
parameter which is not cyclic. This payload
property goes through
JSON.stringify
and is sent to the receiving script. There the message is
instantiated again with the JSON.parse
d payload which is passed into the
constructor.
The whole process looks like this:
SampleMessage
class accepts object into its constructorsend(new SampleMessage({ text: 'hello' }))
{ text: 'hello' }
is stringified to "{"text":"hello"}"
and sent
to the receiving scriptJSON.parse
s the payload which results in { text: 'hello' }
new SampleMessage({ text: 'hello' })
and it
is routed to the appropriate listeners.The SampleMessage.ts
file has to be somewhere in between background and content
files as it’s a common language between the two:
import { Message } from `@exteranto/api`
export interface SampleMessagePayload {
/**
* Text field!
*/
text: string
}
export class SampleMessage extends Message {
/**
* We can also define methods that work with the data in the payload.
*
* @return Upper-cased text
*/
public get textUppercase () : string {
return this.payload.text.toUpperCase()
}
public constructor (public payload: SampleMessagePayload) {
//
}
}
A background listener in the PrintsContent.ts
file:
import { Listener } from '@exteranto/core'
import { SampleMessage } from '...'
export class PrintsContent {
/**
* Prints out uppercase text.
*
* @param message Typed message
*/
public handle (message: SampleMessage) : void {
console.log(`Message sent from tab ${message.context.tabId}:`)
console.log(`[${new Date}] ${message.textUppercase}`)
}
}
And now we can send the message from content in a similar manner we have sent the events in previous section:
import { SampleMessage } from '...'
import { Messaging } from '@exteranto/api'
import {
Autowired,
Listener,
WindowLoadedEvent,
} from '@exteranto/core'
export class FiresSampleMessagesOnWindowLoad {
/**
* Messaging instance to send the messages.
*/
@Autowired
private messaging: Messaging
/**
* Once the window object is loaded in the content script, we fire SampleMessage.
*
* @param _ Typed event
*/
public handle (_: WindowLoadedEvent) : void {
setTimeout(() => {
this.messaging.send(new SampleMessage({
text: 'Hello world from content script!',
}))
}, 1000)
}
}
We boot the app in both content and background script and route the messages
in events.ts
-like file as we did in the previous section.
In order to send a message to a certain content script, the tab id has to be
known. Then we use the Tabs
APIs provided by @exteranto/api
.
import { SampleMessage } from '...'
import { Tabs } from '@exteranto/api'
const tabs: Tabs = ...
const tabId: number = 1
const message: SampleMessage = new SampleMessage({
text: 'Hello',
})
tabs.get(tabId).then(tab => tab.send(message))
Since there is only one background script, no additional context information is needed to send a message (unlike the tab id in background -> content communication).
import { SampleMessage } from '...'
import { Messaging } from '@exteranto/api'
const messaging: Messaging = ...
const message: SampleMessage = new SampleMessage({
text: 'Hello',
})
messaging.send(message)