Opera Extensions: messaging example
I already vented out my frustration with examples that have flaws, so this one is more constructive and tries to actually explain and solve things. You’re welcome to skip all of that and just get the example code.
It may not be 100% correct, it may have it’s own flaws, and obviously it may soon become obsolete, as Opera 11 is still in alpha, but I did create a more reliable example of how to communicate between the user scripts and popup windows.
This article assumes you already grok the introductory information on Opera extension. In fact, knowledge of Chrome extension flows may also help.
The flaws
Let’s skip the ranting about lack of documentation (hint hint) and look at the fundamental flaws in the original Communicating between popup and injected script example. You can download the modified version of it that attempts to display the page title. I also documented the steps to reproduce two bugs in it.
- There is only one communication channel – and it’s always reset to the page which was loaded last.
- The above means, that we may not be communicating with the focused tab.
- The above also means that the channel is not re-established once a new popup opens – even if it’s on the same tab. I suspect that
if (port)
is supposed to check whether the channel is still active, but (potentially – due to a browser bug) it returns a truthy value, hence the new channel is not sent to the popup. - The check inside
onconnect
only validates that a communication channel was created – it never validates that the connecting script is actually a popup window
The solution
In here, I have a couple of things that are merely assumptions on how things work, and how the API should respond – this is mostly due to lack of documentation (hint hint, again) and is based on experience with Chrome extensions.
The core point of the solution is to avoid storing the communication channel in the background script – instead – store a reference to the popup window. I feel that it is safe to do, since the popup window closes automatically, and it seems that all data associated with it gets destroyed.
The background script still plays an important role – it’s job is to pass the MessagePort
from the user script to the popup, but it only does that when requested. As mentioned above, when the popup window closes, we also need to re-establish the connection, so the final flow becomes like this:
- Browser loads, background script is initialized
- A page in a tab loads, user script gets initialized
- User clicks on the toolbar icon for the popup
- The popup loads
- Background page receives an
onconnect
event - It then checks the event origin – all widget related files have URLs that start with
widget://
- If the thing that just connected is a popup window, the background script determines the active tab, and sends a
getPorts
message to initiate messaging connection. It also stores the reference to the popup for future communication. - The user script receives the message and creates a new communication channel (it assumes that the previous channel is long dead)
- It then sends that communication channel back to the background script using
establishConnection
message. - The background script forwards that message to the previously stored popup reference. I feel that it is safe enough to trust that, since the steps 5-9 should really be pretty much instantaneous without any user interaction possible in between.
- The popup receives the communication channel – it is free to do with it whatever it likes, e.g. request information from the user script using
postMessage()
and receive that information usingonmessage
. - Once the popup closes, we end up at point 2
Download example code (I release it into public domain, without any guarantees)
The gotchas, suggestions and pleas to the Opera team
Document MessageChannel
and MessagePort
While this is an obvious request, I’d also like to mention that I find Chrome’s model of two way messaging much simpler – just pass around a callback function for each message.
Rename port1
and port2
to something meaningful
My initial response to these two properties was that one of them is for sending messages, the other is for receiving. As far as I understand that right now – one is to be used as a “local” communication port, and the other is for the use by “remote” script. They should be named accordingly. However, a much better approach would be to not expose them at all – same as the previous suggestion – it should be possible to just create a MessageChannel
with postMessage()
and onmessage
– which are inversed once they change context.
Clarify if objects are allowed as event.data
I’ll admit – I didn’t read the W3C spec. My example uses simple objects, but the API documentation says the message data has to be a DOMString
. While it’s not a biggie, and I’m sure everyone is good friends with JSON.stringify()
and JSON.parse()
, it either should be documented as allowed or should throw errors when used.
Create an easier way to determine who is the event.origin
At the moment, to check if the message was sent by a popup, I’m testing the URL in the event.origin
, but I’d really like to see some methods, such as isBackgroundScript()
, isUserScript()
and isPopup()
for simplicity.
More graphical information
I’m guilty here, since I probably should have included the flow diagram instead of an 11 point list in this blog, but I do find the visual guide to the API very useful. The Dev.Opera article about messaging inside extensions should have included some of that love.
Explain best practices
Having looked through most of the example extensions, I find certain things quite interesting – and they do raise a lot of “why” type questions:
- Why do we need to do stuff on
load
in the background scripts? - How does that make a difference as compared to just executing the code in the
<script>
tag? - Do we also have to do things on
load
in the popup scripts? - Why do some examples check for existence of
opera.extension
? When it might not be available? - What are the “dos” and “donts” for the background process? What are the limitations? Considering that it’s a page that’s always active, I’m sure there are plenty.
I hope the time spent with frustration, figuring things out and writing up this article was worth it. I also hope that it’s useful for at least one other person. However, in case of the information in here is already published and the questions are already answered – please – drop me a link in the comments or on twitter @dymonaz.