Loading presentation...

Present Remotely

Send the link below via email or IM

Copy

Present to your audience

Start remote presentation

  • Invited audience members will follow you as you navigate and present
  • People invited to a presentation do not need a Prezi account
  • This link expires 10 minutes after you close the presentation
  • A maximum of 30 users can follow your presentation
  • Learn more about this feature in our knowledge base article

Do you really want to delete this prezi?

Neither you, nor the coeditors you shared it with will be able to recover it again.

DeleteCancel

Make your likes visible on Facebook?

Connect your Facebook account to Prezi and let your likes appear on your timeline.
You can change this under Settings & Account at any time.

No, thanks

Chrome Extension Skeleton

https://github.com/salsita/chrome-extension-skeleton
by

Roman Kaspar

on 11 August 2014

Comments (0)

Please log in to add your comment.

Report abuse

Transcript of Chrome Extension Skeleton

Who we are...
What we do...
Contact us:
simona@salsitasoft.com
(+420) 270 005 255
github.com/salsita
twitter.com/salsitasoft
facebook.com/salsitasoft
Chrome Extension Skeleton
Roman Kaspar (roman@salsitasoft.com)
https://github.com/salsita/chrome-extension-skeleton
http://prezi.com/yxj7zs7ixlmw
May 23, 2014
Content:
motivation for new extension skeleton
what do most (all?) extensions need?
addressed with messaging module
implementation
API description
under the hood
workshop!
unit testing
In previous version of the skeleton, we already have:
RequireJS modules (dependencies solved at run-time)
in-browser unit tests, later rewritten for jasmine-node runner
messaging system:
suited for background content script exchange only (ignoring other contexts, e.g. developer tools, options, popups, ...)
using obsolete API (UDP-like):
chrome.extension.onMessage
chrome.extension.sendMessage
In new version, we'd rather have:
efficient JS modules (concatenated, minified)
extension written as node.js modules:
can unit-test individual modules in mocha
can leverage NPM eco-system (modules)
messaging system:
taking all possible contexts into account
taking all possible messaging scenarios into account
using recent API (TCP-like):
chrome.runtime.onConnect
chrome.runtime.connect
chrome.runtime.Port
What do most (all?) extensions need?
some shared state stored in background
background needs to know about all other extension components and their respective states (content scripts, popups, options, ...)
some means for easy information exchange among all the pieces:
90% content script requesting some part of shared state from background (or requesting its update), or requesting some operation to be performed and passing result back
10% background requesting something from contexts, or non-background contexts communication (direct, broadcasting)
Addressed with messaging module
let's use
chrome.runtime.Port
for communication among the extension contexts
in background, we listen on
chrome.runtime.onConnect
event
in any other context we call
chrome.runtime.connect
method
this will create communication channel between the context and background
also, background knows about all other extensions components, as it was notified right after the component (context) was started; providing we call connect() when the context starts
Chrome browser is so kind, that when it destroys (unloads) any context, it closes all its opened Ports, so we are notified in background in
chrome.runtime.onDisconnect
event listener
Killing two birds with one stone
Implementation -- goals
single messaging module, used the same way in all contexts
no assumptions about the extension
as little conventional "magic" as possible
extremely simple and intuitive API
powerful API (covering all possible scenarios)
robust (must handle unloaded contexts well)
Implementation -- reality
single messaging module, used the same way in all contexts
no assumptions about the extension
conventional "magic":
hard-coded name for background context "bg"
hard-coded names of special background handlers ("onConnect", "onDisconnect")
(due to Chromium bug) hard-coded name for developer tools context "dt"
extremely simple and intuitive API
powerful API (covering all possible scenarios)
robust (must handle unloading contexts well)
Implementation -- overview
// background code:
var

msg
= require(
'./msg'
).init(
'bg'
, { ... });
msg
.bcast([
'ct'
],
'getURL'
,
function
(
urls
) { ... });


// content script:
var

msg
= require(
'./msg'
).init(
'ct'
, { ... });
msg
.bg(
'getUser'
,
function
(
user
) { ... });
Implementation -- init part
// init method:
var

msg
= require(
'./msg'
).init(
/* mandatory */

<name>
,
/* optional */

<handlers>
);

<name>
: any string,
'bg'
for background,
'dt'
for developer tools
<handlers>
: function lookup table for commands that given context will be able to respond to

Command handler may take any number of arguments. Last argument for each command handler
must
be callback that the handler invokes
once
it is done with processing. The callback accepts one argument that, when passed, is treated as return value of given command handler (i.e. the handler can be asynchronous).

Example (for content script above):
var

handlers
= {
getURL:
function
(
done
) {
done
(document.location.href); }
};
Implementation -- special handlers
there are two special handlers that, when provided as part of
background
handlers lookup table in
init()
call, are invoked by the messaging system itself (i.e. you don't need to issue
msg.bcast()
,
msg.cmd()
or
msg.bg()
to trigger these handlers)
they allow the background script to be aware of all the other connected extension contexts
the special handlers are:

onConnect
:
function
(
contextName
,
tabId
) { ... }, and

onDisconnect
:
function
(
contextName
,
tabId
) { ... }
note that these handlers do
not
take the callback as the last argument!
Implementation -- messaging object
var

msg
= require(
'./msg'
).init(...);

msg
object comes with generic communication function
bcast
(for "broadcast"). for convenience there is also function
cmd
(for "command") and in non-background contexts also function
bg
(for "background").

msg
.bcast(

/* optional */

<tabId>
,
// integer; if omitted, broadcast to all, also supports
SAME_TAB

/* optional */

<contexts>
,
// string array; if omitted, broadcast to all contexts

/* mandatory */

<commandName>
,
// string; command name

/* as needed */

<commandArgument1>
,
// depends on command handler signature

/* as needed */

<commandArgument2>
,
...

/* optional */

<callback>

// function(results) { ... }; invoked once when all results are collected
);

Example:
msg
.bcast(3, [
'ct'
,
'dt'
],
'run'
, 1, 2, true, {x:1},
function
(
resps
) { ... });
Implementation -- messaging object
Convenience functions:

msg
.cmd():
takes exactly the same optional / mandatory arguments as
msg
.bcast(), the only difference is that the value passed to the callback is the first collected command handler response (from all of the possibly invoked handlers).
this is useful when you know that you'll get only single response, so that you don't need to get array of responses, from which you'll then pick the first element.
example:

msg
.cmd(
SAME_TAB
, [
'ct'
],
'getData'
,
function
(
data
) { ... });
// invoked from dev tools context

msg
.bg():
is the same as
msg
.cmd(), but doesn't take the first two optional arguments.
instead of that it puts [
'bg'
] as
<contexts>
argument, so the command handler is executed in (one and only) background context only.
this is useful when any context needs anything from background.
example:

msg
.bg(
'getUser'
,
function
(
user
) { ... });
// invoked from content script

Under the hood -- background
every context talks to background only
background dispatches if / as needed


var

portMap
= {
// what is passed to init() call

<contextName>
: {

<contextId1>
: { tabId:
<int>
, port:
<chrome.runtime.Port>
}
...
},
// unique exchanged in hand-shake after connect()
...
// passed in each message between background and context
};
Under the hood -- background
each message exchange (request / response) has unique id
all pending requests from background to non-bg contexts are stored in the following structure:


var

bgPendingReqs
= {

<contextId>
: [ { id:
<requestId>
, cb:
<callback>
}, { ... } ],
...
};

if Port is closed, we know to which
<contextId>
it belonged to, and we can invoke the callbacks for all related pending requests "manually".
Under the hood -- background
Under the hood -- non-bg context
Simpler callback table:


var

myCbTable
= {

<requestId>
:
<callback>
,
...
};

The context sends the request to background, lets the background dispatch the request further and collect the responses, when responses are collected, they are sent back to originating requester, where (based on the record in
myCbTable
) the original callback is invoked (if it was provided).
WORKSHOP TIME!
Unit testing of the messaging system
How to test all that I just showed you in 100ms?
unit tests written in mocha
mock of
chrome.runtime.{onConnect,connect,Port}
connect()
creates a pair of interconnected
Ports
(
A
,
B
):
A
.postMessage() triggers
B
.onMessage(),
A
.disconnect() triggers
B
.onDisconnect(),
and vice versa
binding implemented using
EventEmitter2
completely asynchronous (setImmediate())
one
Port
passed to
onConnect()
, simulating the background part
second
Port
is "free", simulating the non-bg context part
this mock has its own unit tests :-)
Unit testing of the messaging system
How to simulate:
1 background context
8 content script contexts
8 developer tools contexts
2 options contexts
2 popup contexts
... with only one msg.js module?

How to verify that all the 21 contexts invoke handlers correctly and pass correct return values back to provided callbacks?

How to verify the requests are dispatched to appropriate contexts (and not to contexts that were not specified by the user)?
Unit testing of the messaging system
node.js require() function:
for any given filepath, it returns singleton of what that module exports (even if you call it multiple times, you'll get the same singleton each time)
so for different filepaths, it returns unique singleton instances

var

ctxMain
= []; // unique contexts storage
var

data
= fs.readFileSync(path.join(__dirname,
'msg.js'
));
// tested module to clone
for
(
var

i
= 0;
i
< CTX_COUNT;
i
++) {

var

tmp
= path.join(__dirname,
'__test_msg__'
+
i
+
'.js'
);
fs.writeFileSync(
tmp
, data);
// create clone with unique name
ctxMain.push(require(
tmp
));
// require the clone
fs.unlink(
tmp
);
// and get rid of it again
}
Unit testing of the messaging system
when creating the handlers for all the contexts, we pass singleton array (
handlerLog
) to all the handlers and let the handlers leave a trace that they were invoked in the log
they record the name of the context they were invoked in, the parameters they received, the return value they provided back
after each unit-test is executed, the log is examined to see what handlers and callbacks got invoked in what contexts and with what arguments


46 tests
352 assert statement
< 100ms

background determines (based on
portMap
and the parameters provided to .
bcast()
or .
cmd()
calls), what contexts to send the message to, let's say it is
N
contexts / ports
it creates callback that collects responses from all the contexts (as they are sent back), and once this callback is invoked
N
times, it triggers the callback passed to .
bcast()
or .
cmd()
(if provided)
request message (describing the command we want to execute and its arguments) is then sent to
N
contexts / ports
unique (request ids, generated callback reference) pairs are added to the
bgPendingReqs
if target context supports given command (has a handler for it), the handler is executed and the value provided to
done()
callback is sent back to background as the result
if target context doesn't support given command (doesn't have requested command as a key in its
handlers
lookup table), response is sent back to background, indicating this fact; no value is added to the array of collected responses
when the context is unloaded and there are some pending requests, the corresponding callbacks are also invoked, again indicating that no value should be added to the array of valid responses
all collected responses are passed (as array argument) to .
bcast()
callback, or the first item of such array is passed as argument to .
cmd()
callback
NO LONGER USED, CODE UPDATED FOR BETTER TESTABILITY.
Full transcript