blob: 0db7f589353a796da7f76d3a30d5f5103ee45f91 [file] [log] [blame]
<h1>Extending DevTools</h1>
<h2 id="overview">Overview</h2>
<p>A DevTools extension adds functionality to the Chrome DevTools. It can add new
UI panels and sidebars, interact with the inspected page, get information about
network requests, and more. DevTools extensions have access to an additional set
of DevTools-specific extension APIs:</p>
<ul>
<li><code>$(ref:devtools.inspectedWindow)</code></li>
<li><code>$(ref:devtools.network)</code></a></li>
<li><code>$(ref:devtools.panels)</code></a></li>
</ul>
<p>A DevTools extension is structured like any other extension: it can have a
background page, content scripts, and other items. In addition, each DevTools
extension has a DevTools page, which has access to the DevTools APIs.</p>
<p><img src="{{static}}/images/devtools-extension.png" alt="Architecture diagram
showing DevTools page communicating with the inspected window and the background
page. The background page is shown communicating with the content scripts and
accessing extension APIs. The DevTools page has access to the DevTools APIs, for
example, creating panels."/></p>
<h2 id="devtools-page">The DevTools Page</h2>
<p>An instance of the extension's DevTools page is created each time a DevTools
window opens. The DevTools page exists for the lifetime of the DevTools window.
The DevTools page has access to the DevTools APIs and a limited set of extension
APIs. Specifically, the DevTools page can:</p>
<ul>
<li>Create and interact with panels using the <code>$(ref:devtools.panels)</code>
APIs.</li>
<li>Get information about the inspected window and evaluate code in the inspected
window using the <code>$(ref:devtools.inspectedWindow)</code> APIs.</li>
<li>Get information about network requests using the <code>$(ref:devtools.network)</code>
APIs.</li>
</ul>
<p>The DevTools page cannot use most of the extensions APIs directly. It has
access to the same subset of the <code>$(ref:extension)</code>
and <code>$(ref:runtime)</code>
APIs that a content script has access to. Like a content script, a DevTools page
can communicate with the background page using <a href="messaging.html">Message Passing</a>.
For an example, see <a href="#injecting">Injecting a Content Script</a>.</p>
<h2 id="creating">Creating a DevTools Extension</h2>
<p>To create a DevTools page for your extension, add the <code>devtools_page</code>
field in the extension manifest:</p>
<pre>
{
"name": ...
"version": "1.0",
"minimum_chrome_version": "10.0",
"devtools_page": "devtools.html",
...
}
</pre>
<p>An instance of the <code>devtools_page</code> specified in your extension's
manifest is created for every DevTools window opened. The page may add other
extension pages as panels and sidebars to the DevTools window using the
<code>$(ref:devtools.panels)</code> API.</p>
<p class="note">The <code>devtools_page</code> field must point to an HTML page.
This differs from the <code>background</code> field, used for specifying a background page,
which lets you specify JavaScript files directly.</p>
<p>The <code>chrome.devtools.*</code> API modules are available only to the pages
loaded within the DevTools window. Content scripts and other extension pages do not
have these APIs. Thus, the APIs are available only through the lifetime of the
DevTools window.</p>
<p>
There are also some DevTools APIs that are still experimental.
Refer to <a href="http://developer.chrome.com/extensions/experimental.html">chrome.experimental.*
APIs</a> for the list of
experimental APIs and guidelines on how to use them.</p>
<h2 id="devtools-ui">DevTools UI Elements: Panels and Sidebar Panes</h2>
<p>
In addition to the usual extension UI elements, such as browser actions, context
menus and popups, a DevTools extension can add UI elements to the DevTools window:</p>
<ul>
<li>A <em>panel</em> is a top-level tab, like the Elements, Sources, and Network
panels.</li>
<li>A <em>sidebar pane</em> presents supplementary UI related to a panel. The
Styles, Computed Styles, and Event Listeners panes on the Elements panel are
examples of sidebar panes. Currently your extension can only add sidebar panes to
the Elements panel. (Note that the appearance of sidebar panes may not match the
image, depending on the version of Chrome you're using, and where the DevTools
window is docked.)</li>
</ul>
<img src="{{static}}/images/devtools-extension-ui.png"
alt="DevTools window showing Elements panel and Styles sidebar pane." />
<p>
Each panel is its own HTML file, which can include other resources (JavaScript, CSS,
images, and so on). Creating a basic panel looks like this:
</p>
<pre>
chrome.devtools.panels.create("My Panel",
"MyPanelIcon.png",
"Panel.html",
function(panel) {
// code invoked on panel creation
}
);
</pre>
<p>JavaScript executed in a panel or sidebar pane has access to the the same APIs
as the DevTools page.</p>
<p>Creating a basic sidebar pane for the Elements panel looks like this:</p>
<pre>
chrome.devtools.panels.elements.createSidebarPane("My Sidebar",
function(sidebar) {
// sidebar initialization code here
sidebar.setObject({ some_data: "Some data to show" });
});</pre>
<p>There are several ways to display content in a sidebar pane:</p>
<ul>
<li><p>HTML content. Call
<code>$(ref:devtools.panels.ExtensionSidebarPane.setPage setPage)</code> to specify
an HTML page to display in the pane.</p></li>
<li><p>JSON data. Pass a JSON object to
<code>$(ref:devtools.panels.ExtensionSidebarPane.setObject setObject)</code>.
</p></li>
<li><p>JavaScript expression. Pass an expression to
<code>$(ref:devtools.panels.ExtensionSidebarPane.setExpression setExpression)</code>.
DevTools evaluates the expression in the context of the inspected page, and
displays the return value.</p></li>
</ul>
<p>For both <code>setObject</code> and <code>setExpression</code>,
the pane displays the value as it would appear in the DevTools console.
However, <code>setExpression</code> lets you display DOM elements and arbitrary
JavaScript objects, while <code>setObject</code> only supports JSON objects.</p>
<h2 id="solutions">Communicating Between Extension Components</h2>
<p>The following sections describe some typical scenarios for communicating between
the different components of a DevTools extension.</p>
<h3 id="injecting">Injecting a Content Script</h3>
<p>The DevTools page can't call <code>$(ref:tabs.executeScript)</code> directly.
To inject a content script from the DevTools page, you must retrieve the ID
of the inspected window's tab using the <code>$(ref:inspectedWindow.tabId)</code>
property and send a message to the background page. From the background page,
call <code>$(ref:tabs.executeScript)</code> to inject the script.</p>
<p class="note">If a content script has already been injected, you can add
additional context scripts using the <code>eval</code> method. See
<a href="#selected-element">Passing the Selected Element to a Content Script</a>
for more information.</p>
<p>The following code snippets show how to inject a content script using
<code>executeScript</code>.
</p>
<pre>
// DevTools page -- devtools.js
// Create a connection to the background page
var backgroundPageConnection = chrome.runtime.connect({
name: "devtools-page"
});
backgroundPageConnection.onMessage.addListener(function (message) {
// Handle responses from the background page, if any
});
// Relay the tab ID to the background page
chrome.runtime.sendMessage({
tabId: chrome.devtools.inspectedWindow.tabId,
scriptToInject: "content_script.js"
});
</pre>
<p>Code for the background page:</p>
<pre>
// Background page -- background.js
chrome.runtime.onConnect.addListener(function(devToolsConnection) {
// assign the listener function to a variable so we can remove it later
var devToolsListener = function(message, sender, sendResponse) {
// Inject a content script into the identified tab
chrome.tabs.executeScript(message.tabId,
{ file: message.scriptToInject });
}
// add the listener
devToolsConnection.onMessage.addListener(devToolsListener);
devToolsConnection.onDisconnect(function() {
devToolsConnection.onMessage.removeListener(devToolsListener);
});
}
</pre>
<h3 id="evaluating-js">Evaluating JavaScript in the Inspected Window</h3>
<p>You can use the <code>$(ref:inspectedWindow.eval)</code> method to execute
JavaScript code in the context of the inspected page. You can invoke the
<code>eval</code> method from a DevTools page, panel or sidebar pane.</p>
<p>By default, the expression is evaluated in the context of the main frame of the
page. Use the <code>useContentScriptContext: true</code> option to evaluate the
expression in the same context as the content scripts.</p>
<p>Calling <code>eval</code> with <code>useContentScriptContext: true</code> does
not <em>create</em> a content script context, so you must load a context script
before calling <code>eval</code>, either by calling <code>executeScript</code> or
by specifying a content script in the <code>manifest.json</code> file.</p>
<p>Once the context script context exists, you can use this option to inject
additional content scripts.</p>
<p class="warning">The <code>eval</code> method is powerful when used in the right
context and dangerous when used inappropriately. Use the
<code>$(ref:tabs.executeScript)</code> method if you don't need access to the
JavaScript context of the inspected page. For detailed cautions and a comparison
of the two methods, see <code>$(ref:inspectedWindow)</code>.</p>
<h3 id="selected-element">Passing the Selected Element to a Content Script</h3>
<p>The content script doesn't have direct access to the current selected element.
However, any code you execute using <code>$(ref:inspectedWindow.eval)</code> has
access to the DevTools console and command-line APIs. For example, in evaluated code
you can use <code>$0</code> to access the selected element.</p>
<p>To pass the selected element to a content script:</p>
<ul>
<li>Create a method in the content script that takes the selected element as
an argument.</li>
<li>Call the method from the DevTools page using <code>$(ref:inspectedWindow.eval)</code>
with the <code>useContentScriptContext: true</code> option. </li>
</ul>
<p>The code in your content script might look something like this:</p>
<pre>
function setSelectedElement(el) {
// do something with the selected element
}
</pre>
<p>Invoke the method from the DevTools page like this:</p>
<pre>
chrome.devtools.inspectedWindow.eval("setSelectedElement($0)",
{ useContentScriptContext: true });
</pre>
<p>The <code>useContentScriptContext: true</code> option specifies that the
expression must be evaluated in the same context as the content scripts, so it can
access the <code>setSelectedElement</code> method.</p>
<h3 id="content-script-to-devtools">Messaging from Content Scripts to the DevTools Page</h3>
<p>Messaging between the DevTools page and content scripts is indirect, by way of
the background page. </p>
<p>When sending a message <em>to</em> a content script, the background page can use
the <code>$(ref:tabs.sendMessage)</code> method, which directs a message to the
content scripts in a specific tab, as shown in
<a href="#injecting">Injecting a Content Script</a>.</p>
<p>When sending a message <em>from</em> a content script, there is no ready-made
method to deliver a message to the correct DevTools page instance associated with
the current tab. As a workaround, you can have the DevTools page establish a
long-lived connection with the background page, and have the background page keep a
map of tab IDs to connections, so it can route each message to the correct
connection.</p>
<pre>// background.js
var connections = {};
chrome.runtime.onConnect.addListener(function (port) {
var extensionListener = function (message, sender, sendResponse) {
// The original connection event doesn't include the tab ID of the
// DevTools page, so we need to send it explicitly.
if (message.name == "init") {
connections[message.tabId] = port;
return;
}
// other message handling
}
// Listen to messages sent from the DevTools page
port.onMessage.addListener(extensionListener);
port.onDisconnect.addListener(function(port) {
port.onMessage.removeListener(extensionListener);
var tabs = Object.keys(connections);
for (var i=0, len=tabs.length; i &lt; len; i++) {
if (connections[tabs[i]] == port) {
delete connections[tabs[i]]
break;
}
}
});
});
// Receive message from content script and relay to the devTools page for the
// current tab
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
// Messages from content scripts should have sender.tab set
if (sender.tab) {
var tabId = sender.tab.id;
if (tabId in connections) {
connections[tabId].postMessage(request);
} else {
console.log("Tab not found in connection list.");
}
} else {
console.log("sender.tab not defined.");
}
return true;
});
</pre>
<p>The DevTools page (or panel or sidebar pane) establishes the connection like
this:</p>
<pre>
// Create a connection to the background page
var backgroundPageConnection = chrome.runtime.connect({
name: "panel"
});
backgroundPageConnection.postMessage({
name: 'init',
tabId: chrome.devtools.inspectedWindow.tabId
});
</pre>
<h3 id="evaluated-scripts-to-devtools">Messaging from Injected Scripts to the DevTools Page</h3>
<p>While the above solution works for content scripts, code that is injected
directly into the page (e.g. through appending a <code>&lt;script&gt;</code> tag
or through <code>$(ref:inspectedWindow.eval)</code>) requires a different strategy.
In this context, <code>$(ref:runtime.sendMessage)</code> will not pass messages
to the background script as expected.</p>
<p>As a workaround, you can combine your injected script with a content script
that acts as an intermediary. To pass messages to the content script, you can
use the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window.postMessage">
<code>window.postMessage</code></a> API. Here's an example, assuming the
background script from the previous section:</p>
<pre>
// injected-script.js
window.postMessage({
greeting: 'hello there!',
source: 'my-devtools-extension'
}, '*');
</pre>
<pre>
// content-script.js
window.addEventListener('message', function(event) {
// Only accept messages from the same frame
if (event.source !== window) {
return;
}
var message = event.data;
// Only accept messages that we know are ours
if (typeof message !== 'object' || message === null ||
!message.source === 'my-devtools-extension') {
return;
}
chrome.runtime.sendMessage(message);
});
</pre>
Your message will now flow from the injected script, to the content script, to
the background script, and finally to the DevTools page.
<h3 id="detecting-open-close">Detecting When DevTools Opens and Closes</h3>
<p>If your extension needs to track whether the DevTools window is open, you can
add an $(ref:runtime.onConnect onConnect) listener to the background page, and call
$(ref:runtime.connect connect) from the DevTools page. Since each tab can have its
own DevTools window open, you may receive multiple connect events. To track whether
any DevTools window is open, you need to count the connect and disconnect events as
shown below:</p>
<pre>// background.js
var openCount = 0;
chrome.runtime.onConnect.addListener(function (port) {
if (port.name == "devtools-page") {
if (openCount == 0) {
alert("DevTools window opening.");
}
openCount++;
port.onDisconnect.addListener(function(port) {
openCount--;
if (openCount == 0) {
alert("Last DevTools window closing.");
}
});
}
});</pre>
<p>The DevTools page creates a connection like this:</p>
<pre>
// devtools.js
// Create a connection to the background page
var backgroundPageConnection = chrome.runtime.connect({
name: "devtools-page"
});
</pre>
<h2 id="more">More information</h2>
<p>For information on the standard APIs that extensions can use, see
<a href="http://developer.chrome.com/extensions/api_index.html">chrome.* APIs</a>
and <a href="http://developer.chrome.com/extensions/api_other.html">Other APIs</a>.
</p>
<p>
<a href="http://groups.google.com/group/google-chrome-developer-tools/topics">Give
us feedback!</a>
Your comments and suggestions help us improve the APIs.</p>
<h2 id="examples">Examples</h2>
<p>You can find examples that use DevTools APIs in
<a href="http://developer.chrome.com/extensions/samples.html#devtools">Samples</a>.
</p>