宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能!
When I migrated my addon Tree Style Tab from XUL to WebExtensions, I wrote some concerns about communication between addons. Let's share my knowledge around the topic.
browser.tabs.onHighlighted.addListener()
TST provides some APIs for other addons. Some APIs are callable via browser.runtime.sendMessage()
like as:
const kTST_ID = 'treestyletab@piro.sakura.ne.jp';
browser.runtime.sendMessage(kTST_ID, {
type: 'expand-tree',
tab: 2 // Tab.id
});
This example expands a collapsed tree belonging to the specified tab. As you saw, such a "request-resposne" type API is very easy to implement. The receiver addon (in this case, Tree Style Tab) just need to define a listener for browser.runtime.onMessageExternal
like as:
browser.runtime.onMessageExternal.addListener((message, sender) => {
if (!message || !message.type)
return; // Ignore invalid type API call.
switch (message.type) {
case 'expand-tree':
// Here is a code to expand specified tree.
// If needed, you can return a promise to wait until the operation finishes.
return Promise.resolve(true);
}
});
Then there is one problem: when a client addon sends any message to missing server addon, it will be reported as an error like "Error: Could not establish connection. Receiving end does not exist."
One of solutions is: providing an option to activate/deactivate such an integration. Here is an example based on storage.local
:
let enableIntegration = false;
browser.storage.local.get({ enableIntegration })
.then(values => {
enableIntegration = values.enableIntegration;
});
function someFeature() {
// ...
if (enableIntegration)
browser.runtime.sendMessage(kSERVER_ID, { ... });
}
But it introduces a new problem: which is the best default configuration, enabled or disabled? If enabled by default, users will see mysterious errors in the debug console. If disabled by default, users will need to activate the option manually to activate integration and some people won't realize there is such an option.
management
API of WEClient addons can know that the server addon is installed or not via the browser.management.getAll()
. Thus you don't need to provide such an option, instead you can activate the integration only when the server addon is available:
let serverIsActive = false;
browser.management.getAll().then(items => {
for (const item of items) {
if (item.id == kSERVER_ID && item.enabled) {
serverIsActive = true;
break;
}
}
});
function someFeature() {
// ...
if (serverIsActive)
browser.runtime.sendMessage(kSERVER_ID, { ... });
}
However, you need to add the management
permission to the list of static permissions in your manifest.json
. Sadly it is impossible to be listed in the optional_permissions
. The installation prompt for your addon will show a new extra line like: "Monitor extension usage and manage themes". I think most people dislike any needless permission for an addon, so this can be an disadvantage to the previous solution.
Both solutions 1 and 2 have concerns. So I ordinarily do nothing - I usually send messages to server addons without confirmation. Those messages will be processed if luckily the server is activated, otherwise they will be ignored. Only some developers will look errors via the debugger console, but most people don't realize that.
Of course you can suppressed such errors by an error handler like:
function handleMissingReceiverError(error) {
if (!error ||
!error.message ||
error.message.indexOf('Could not establish connection. Receiving end does not exist.') == -1)
throw error;
}
browser.runtime.sendMessage(kSERVER_ID, { ... })
.catch(handleMissingReceiverError);
This is the easiest way and I use this in most cases.
And, for developers I sometimes providing an option to deactivate integration. Because they intentionally deactivate the integration, I believe that I have no need to notify existence of the integration feature again.
Anyway, on the other hand, there are more other type of APIs: "broadcast" and "publish-subscribe".
Very sad news, an WE-based addon cannot "broadcast" messages to other unknown addons due to WE API's restriction. browser.runtime.sendMessage()
can send any message to other addons, but it requires the exact ID of the receiver. Thus a receiver addon must notify its ID to the broadcaster addon. As you know, such an API model is called as "publish-subscribe" (aka "pub-sub").
(Added at 2018.11.6) Around tabs and bookmarks, some limited information are shared between addons so they may work like as broadcasting. For example, Firefox 63 and later have ability to "multiselect" tabs via browser.tabs.highlight({ windowId: <id>, tabs: <indexesOfTabs> })
or browser.tabs.update(<id>, { highlight: <boolean> })
and updated state is notified to listeners registered by browser.tabs.onHighlighted.addListener()
, and it will allow you to develop addons which collaborate around multiselected tabs. But such APIs are very specific and unavailable to share general information.
How about browser.storage.onChanged.addListener()
? It is also unavailable for the purpose. Storage API are completely isolated for each addon, and changes produced by other addons won't be notified. browser.sessions.setTabValue()/getTabValue()/setWindowValue()/getWindowValue()
are also. Values associated to tabs or windows via these APIs are isolated for each addon and inaccessible from others.
In short, we always need to implement any API to notify something general information from an server addon to other client addons, as pub-sub model.
Don't worry, you can implement such pub-sub type APIs based on the technology same to request-response type APIs. The "subscribe" (and "unsubscribe") API will be implemented a simple req-res API like as:
const subscribers = new Set();
browser.runtime.onMessageExternal.addListener((message, sender) => {
if (!message || !message.type)
return; // Ignore invalid type API call.
switch (message.type) {
case 'subscribe':
subscribers.add(sender.id);
return Promise.resolve(true);
case 'unsubscribe':
subscribers.delete(sender.id);
return Promise.resolve(true);
}
});
And you just need to send messages to known subscribers like as:
async function doSomething() {
// ... do something ...
const message = { type: 'published', ... }; // The message to be published
await Promise.all(
Array.from(subscribers)
.map(id => browser.runtime.sendMessage(id, message))
);
}
Client addons will "subscribe" and receive published messages like as:
browser.runtime.sendMessage(kSERVER_ID, {
type: 'subscribe'
});
browser.runtime.onMessageExternal.addListener((message, sender) => {
if (sender.id != kSERVER_ID)
return;
switch (message.type) {
case 'published':
// Handle published message.
}
});
But there is one big problem. Any "subscribing" API calls will fail if client addons send them to the server before the server addon start to listen messages from others.
(This figure is initially published as a part of the migration story of TST from XUL to WE.)
Such a situation will happen in cases like:
This is the easiest way.
const subscribeTimer = setInterval(async () => {
try {
const response = await browser.runtime.sendMessage(kSERVER_ID, {
type: 'subscribe'
});
if (response) // Assume that the server returns "true" for subscribing request.
clearInterval(subscribeTimer);
}
catch(e) {
}
// If there is no response or any error, retry with delay.
}, 5000);
But this is a bad solution because the addon will try to subscribe infinitely if the server addon is not installed or enabled.
This is the one TST does. TST caches subscribers' ID to its private storage, and sends ready
type message to them after TST's initialization process is completed. Subscribers just need to listen the ready
message, then they will "subscribe" successfully like as:
(This figure is initially published as a part of the migration story of TST from XUL to WE.)
Here is an example at a client addon to subscribe triggered by a ready
message from the server:
// Try to subscribe at onload. This will succeed if the server is already active.
browser.runtime.sendMessage(kSERVER_ID, {
type: 'subscribe'
});
// Subscribing triggered by `ready`.
// This is required even if the subscribing at onload succeeded, because
// the server addon can be reloaded and you need to subscribe again.
browser.runtime.onMessageExternal.addListener((message, sender) => {
if (sender.id != kSERVER_ID)
return;
switch (message.type) {
case 'ready':
browser.runtime.sendMessage(kSERVER_ID, {
type: 'subscribe'
});
break;
}
});
This is better than the previous solution, but still there are some problems:
management
WE API.ready
message from the server, when the server addon is installed after the client is installed. Then you need to reload client addons manually.If the server addon was able to know the list of possible client addons, it was possible that the server addon send ready
message to all possible client addons. Who does provides such a list? - No one yet. We addon authors need to start a repository project like the npmjs.com or the cpan.org. Addon authors will register their addons with API information to the repository, then addons can collaborate aggressively with others based on information provided by the repository.
But this idea introduces a new problem: the repository will become the SPOF (single point of failure). So I think this is a worse stupid idea.
I think that client addons may/should send API messages to server addons without any confirmation. And an option to disable such integration will help developers who don't want to see needless errors.
On the other hand, pub-sub communication of WE addons is really complex, and not perfect. The solution 2 for the case 3 looks better than others, but I still finding more better solution.
の末尾に2020年11月30日時点の日本の首相のファミリーネーム(ローマ字で回答)を繋げて下さい。例えば「noda」なら、「2018-07-28_best-way-to-communicate-we-addons.trackbacknoda」です。これは機械的なトラックバックスパムを防止するための措置です。
writeback message: Ready to post a comment.