From 5e4914700eb7fa7c65f23c35412a3272a1eed1c9 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Thu, 30 Nov 2017 16:37:26 +0900 Subject: [PATCH 1/8] Enable using external WebContents as devtools --- atom/browser/api/atom_api_web_contents.cc | 13 ++- atom/browser/api/atom_api_web_contents.h | 1 + atom/browser/native_window.h | 4 + brightray/browser/inspectable_web_contents.h | 1 + .../browser/inspectable_web_contents_impl.cc | 103 +++++++++++------- .../browser/inspectable_web_contents_impl.h | 9 +- 6 files changed, 87 insertions(+), 44 deletions(-) diff --git a/atom/browser/api/atom_api_web_contents.cc b/atom/browser/api/atom_api_web_contents.cc index b4748d81630a..f59b72bd0c9c 100644 --- a/atom/browser/api/atom_api_web_contents.cc +++ b/atom/browser/api/atom_api_web_contents.cc @@ -896,9 +896,10 @@ void WebContents::DevToolsOpened() { "DevToolsAPI.setInspectedTabId", &tab_id, nullptr, nullptr); // Inherit owner window in devtools. - if (owner_window()) - handle->SetOwnerWindow(managed_web_contents()->GetDevToolsWebContents(), - owner_window()); + auto* devtools = managed_web_contents()->GetDevToolsWebContents(); + if (owner_window() && + !devtools->GetUserData(NativeWindowRelay::UserDataKey())) + handle->SetOwnerWindow(devtools, owner_window()); Emit("devtools-opened"); } @@ -1808,6 +1809,11 @@ void WebContents::SetEmbedder(const WebContents* embedder) { } } +void WebContents::SetDevToolsWebContents(const WebContents* devtools) { + if (managed_web_contents()) + managed_web_contents()->SetDevToolsWebContents(devtools->web_contents()); +} + v8::Local WebContents::GetNativeView() const { gfx::NativeView ptr = web_contents()->GetNativeView(); auto buffer = node::Buffer::Copy( @@ -1929,6 +1935,7 @@ void WebContents::BuildPrototype(v8::Isolate* isolate, .SetMethod("copyImageAt", &WebContents::CopyImageAt) .SetMethod("capturePage", &WebContents::CapturePage) .SetMethod("setEmbedder", &WebContents::SetEmbedder) + .SetMethod("setDevToolsWebContents", &WebContents::SetDevToolsWebContents) .SetMethod("getNativeView", &WebContents::GetNativeView) .SetMethod("setWebRTCIPHandlingPolicy", &WebContents::SetWebRTCIPHandlingPolicy) diff --git a/atom/browser/api/atom_api_web_contents.h b/atom/browser/api/atom_api_web_contents.h index 2abdd2accc28..16f23210557c 100644 --- a/atom/browser/api/atom_api_web_contents.h +++ b/atom/browser/api/atom_api_web_contents.h @@ -126,6 +126,7 @@ class WebContents : public mate::TrackableObject, void Print(mate::Arguments* args); std::vector GetPrinterList(); void SetEmbedder(const WebContents* embedder); + void SetDevToolsWebContents(const WebContents* devtools); v8::Local GetNativeView() const; // Print current page as PDF. diff --git a/atom/browser/native_window.h b/atom/browser/native_window.h index 1e02a37fc07d..09780be079f1 100644 --- a/atom/browser/native_window.h +++ b/atom/browser/native_window.h @@ -391,6 +391,10 @@ class NativeWindowRelay : explicit NativeWindowRelay(base::WeakPtr window) : key(UserDataKey()), window(window) {} + static void* UserDataKey() { + return content::WebContentsUserData::UserDataKey(); + } + void* key; base::WeakPtr window; diff --git a/brightray/browser/inspectable_web_contents.h b/brightray/browser/inspectable_web_contents.h index 28c00f6d72df..7b0a010b9e21 100644 --- a/brightray/browser/inspectable_web_contents.h +++ b/brightray/browser/inspectable_web_contents.h @@ -37,6 +37,7 @@ class InspectableWebContents { virtual void SetDelegate(InspectableWebContentsDelegate* delegate) = 0; virtual InspectableWebContentsDelegate* GetDelegate() const = 0; + virtual void SetDevToolsWebContents(content::WebContents* devtools) = 0; virtual void SetDockState(const std::string& state) = 0; virtual void ShowDevTools() = 0; virtual void CloseDevTools() = 0; diff --git a/brightray/browser/inspectable_web_contents_impl.cc b/brightray/browser/inspectable_web_contents_impl.cc index 4d0826990c8c..ad18f69bce50 100644 --- a/brightray/browser/inspectable_web_contents_impl.cc +++ b/brightray/browser/inspectable_web_contents_impl.cc @@ -242,13 +242,14 @@ InspectableWebContentsImpl::InspectableWebContentsImpl( InspectableWebContentsImpl::~InspectableWebContentsImpl() { // Unsubscribe from devtools and Clean up resources. - if (devtools_web_contents_) { - devtools_web_contents_->SetDelegate(nullptr); + if (GetDevToolsWebContents()) { + if (managed_devtools_web_contents_) + managed_devtools_web_contents_->SetDelegate(nullptr); // Calling this also unsubscribes the observer, so WebContentsDestroyed // won't be called again. WebContentsDestroyed(); } - // Let destructor destroy devtools_web_contents_. + // Let destructor destroy managed_devtools_web_contents_. } InspectableWebContentsView* InspectableWebContentsImpl::GetView() const { @@ -261,7 +262,10 @@ content::WebContents* InspectableWebContentsImpl::GetWebContents() const { content::WebContents* InspectableWebContentsImpl::GetDevToolsWebContents() const { - return devtools_web_contents_.get(); + if (external_devtools_web_contents_) + return external_devtools_web_contents_; + else + return managed_devtools_web_contents_.get(); } void InspectableWebContentsImpl::InspectElement(int x, int y) { @@ -288,43 +292,56 @@ void InspectableWebContentsImpl::SetDockState(const std::string& state) { } } +void InspectableWebContentsImpl::SetDevToolsWebContents( + content::WebContents* devtools) { + if (!managed_devtools_web_contents_) + external_devtools_web_contents_ = devtools; +} + void InspectableWebContentsImpl::ShowDevTools() { + if (embedder_message_dispatcher_) { + if (managed_devtools_web_contents_) + view_->ShowDevTools(); + return; + } + // Show devtools only after it has done loading, this is to make sure the // SetIsDocked is called *BEFORE* ShowDevTools. - if (!devtools_web_contents_) { - embedder_message_dispatcher_.reset( - DevToolsEmbedderMessageDispatcher::CreateForDevToolsFrontend(this)); + embedder_message_dispatcher_.reset( + DevToolsEmbedderMessageDispatcher::CreateForDevToolsFrontend(this)); - content::WebContents::CreateParams create_params( - web_contents_->GetBrowserContext()); - devtools_web_contents_.reset(content::WebContents::Create(create_params)); - - Observe(devtools_web_contents_.get()); - devtools_web_contents_->SetDelegate(this); - - AttachTo(content::DevToolsAgentHost::GetOrCreateFor(web_contents_.get())); - - devtools_web_contents_->GetController().LoadURL( - GetDevToolsURL(can_dock_), - content::Referrer(), - ui::PAGE_TRANSITION_AUTO_TOPLEVEL, - std::string()); - } else { - view_->ShowDevTools(); + if (!external_devtools_web_contents_) { // no external devtools + managed_devtools_web_contents_.reset( + content::WebContents::Create( + content::WebContents::CreateParams( + web_contents_->GetBrowserContext()))); + managed_devtools_web_contents_->SetDelegate(this); } + + Observe(GetDevToolsWebContents()); + AttachTo(content::DevToolsAgentHost::GetOrCreateFor(web_contents_.get())); + + GetDevToolsWebContents()->GetController().LoadURL( + GetDevToolsURL(can_dock_), + content::Referrer(), + ui::PAGE_TRANSITION_AUTO_TOPLEVEL, + std::string()); } void InspectableWebContentsImpl::CloseDevTools() { - if (devtools_web_contents_) { + if (GetDevToolsWebContents()) { frontend_loaded_ = false; - view_->CloseDevTools(); - devtools_web_contents_.reset(); + if (managed_devtools_web_contents_) { + view_->CloseDevTools(); + managed_devtools_web_contents_.reset(); + } + embedder_message_dispatcher_.reset(); web_contents_->Focus(); } } bool InspectableWebContentsImpl::IsDevToolsViewShowing() { - return devtools_web_contents_ && view_->IsDevToolsViewShowing(); + return managed_devtools_web_contents_ && view_->IsDevToolsViewShowing(); } void InspectableWebContentsImpl::AttachTo( @@ -347,7 +364,7 @@ void InspectableWebContentsImpl::CallClientFunction( const base::Value* arg1, const base::Value* arg2, const base::Value* arg3) { - if (!devtools_web_contents_) + if (!GetDevToolsWebContents()) return; std::string javascript = function_name + "("; @@ -365,7 +382,7 @@ void InspectableWebContentsImpl::CallClientFunction( } } javascript.append(");"); - devtools_web_contents_->GetMainFrame()->ExecuteJavaScript( + GetDevToolsWebContents()->GetMainFrame()->ExecuteJavaScript( base::UTF8ToUTF16(javascript)); } @@ -400,7 +417,8 @@ void InspectableWebContentsImpl::CloseWindow() { void InspectableWebContentsImpl::LoadCompleted() { frontend_loaded_ = true; - view_->ShowDevTools(); + if (managed_devtools_web_contents_) + view_->ShowDevTools(); // If the devtools can dock, "SetIsDocked" will be called by devtools itself. if (!can_dock_) { @@ -415,7 +433,7 @@ void InspectableWebContentsImpl::LoadCompleted() { } base::string16 javascript = base::UTF8ToUTF16( "Components.dockController.setDockSide(\"" + dock_state_ + "\");"); - devtools_web_contents_->GetMainFrame()->ExecuteJavaScript(javascript); + GetDevToolsWebContents()->GetMainFrame()->ExecuteJavaScript(javascript); } if (view_->GetDelegate()) @@ -428,15 +446,17 @@ void InspectableWebContentsImpl::SetInspectedPageBounds(const gfx::Rect& rect) { return; contents_resizing_strategy_.CopyFrom(strategy); - view_->SetContentsResizingStrategy(contents_resizing_strategy_); + if (managed_devtools_web_contents_) + view_->SetContentsResizingStrategy(contents_resizing_strategy_); } void InspectableWebContentsImpl::InspectElementCompleted() { } void InspectableWebContentsImpl::InspectedURLChanged(const std::string& url) { - view_->SetTitle(base::UTF8ToUTF16(base::StringPrintf(kTitleFormat, - url.c_str()))); + if (managed_devtools_web_contents_) + view_->SetTitle(base::UTF8ToUTF16(base::StringPrintf(kTitleFormat, + url.c_str()))); } void InspectableWebContentsImpl::LoadNetworkResource( @@ -452,8 +472,8 @@ void InspectableWebContentsImpl::LoadNetworkResource( return; } - auto browser_context = - static_cast(devtools_web_contents_->GetBrowserContext()); + auto* browser_context = static_cast( + GetDevToolsWebContents()->GetBrowserContext()); net::URLFetcher* fetcher = (net::URLFetcher::Create(gurl, net::URLFetcher::GET, this)).release(); @@ -468,7 +488,8 @@ void InspectableWebContentsImpl::LoadNetworkResource( void InspectableWebContentsImpl::SetIsDocked(const DispatchCallback& callback, bool docked) { - view_->SetIsDocked(docked); + if (managed_devtools_web_contents_) + view_->SetIsDocked(docked); if (!callback.is_null()) callback.Run(nullptr); } @@ -640,7 +661,7 @@ void InspectableWebContentsImpl::DispatchProtocolMessage( if (message.length() < kMaxMessageChunkSize) { base::string16 javascript = base::UTF8ToUTF16( "DevToolsAPI.dispatchMessage(" + message + ");"); - devtools_web_contents_->GetMainFrame()->ExecuteJavaScript(javascript); + GetDevToolsWebContents()->GetMainFrame()->ExecuteJavaScript(javascript); return; } @@ -664,13 +685,15 @@ void InspectableWebContentsImpl::RenderFrameHostChanged( frontend_host_.reset(content::DevToolsFrontendHost::Create( new_host, base::Bind(&InspectableWebContentsImpl::HandleMessageFromDevToolsFrontend, - base::Unretained(this)))); + weak_factory_.GetWeakPtr()))); } void InspectableWebContentsImpl::WebContentsDestroyed() { frontend_loaded_ = false; + external_devtools_web_contents_ = nullptr; Observe(nullptr); Detach(); + embedder_message_dispatcher_.reset(); for (const auto& pair : pending_requests_) delete pair.first; @@ -758,7 +781,7 @@ void InspectableWebContentsImpl::ReadyToCommitNavigation( content::NavigationHandle* navigation_handle) { if (navigation_handle->IsInMainFrame()) { if (navigation_handle->GetRenderFrameHost() == - devtools_web_contents_->GetMainFrame() && + GetDevToolsWebContents()->GetMainFrame() && frontend_host_) { return; } diff --git a/brightray/browser/inspectable_web_contents_impl.h b/brightray/browser/inspectable_web_contents_impl.h index c2f359bcd297..6c1b34d8635e 100644 --- a/brightray/browser/inspectable_web_contents_impl.h +++ b/brightray/browser/inspectable_web_contents_impl.h @@ -48,6 +48,7 @@ class InspectableWebContentsImpl : void SetDelegate(InspectableWebContentsDelegate* delegate) override; InspectableWebContentsDelegate* GetDelegate() const override; + void SetDevToolsWebContents(content::WebContents* devtools) override; void SetDockState(const std::string& state) override; void ShowDevTools() override; void CloseDevTools() override; @@ -192,7 +193,13 @@ class InspectableWebContentsImpl : PrefService* pref_service_; // weak reference. std::unique_ptr web_contents_; - std::unique_ptr devtools_web_contents_; + + // The default devtools created by this class when we don't have an external + // one assigned by SetDevToolsWebContents. + std::unique_ptr managed_devtools_web_contents_; + // The external devtools assigned by SetDevToolsWebContents. + content::WebContents* external_devtools_web_contents_ = nullptr; + std::unique_ptr view_; using ExtensionsAPIs = std::map; From 59476f0b30979a3bd895b68dbb4bcd24785b17ea Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Thu, 30 Nov 2017 20:08:56 +0900 Subject: [PATCH 2/8] Create webContents for webview even when src is not set --- lib/renderer/web-view/web-view-attributes.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/renderer/web-view/web-view-attributes.js b/lib/renderer/web-view/web-view-attributes.js index 204046bd6054..16bfe7e61ac6 100644 --- a/lib/renderer/web-view/web-view-attributes.js +++ b/lib/renderer/web-view/web-view-attributes.js @@ -230,7 +230,7 @@ class SrcAttribute extends WebViewAttribute { } parse () { - if (!this.webViewImpl.elementAttached || !this.webViewImpl.attributes[webViewConstants.ATTRIBUTE_PARTITION].validPartitionId || !this.getValue()) { + if (!this.webViewImpl.elementAttached || !this.webViewImpl.attributes[webViewConstants.ATTRIBUTE_PARTITION].validPartitionId) { return } if (this.webViewImpl.guestInstanceId == null) { @@ -240,6 +240,9 @@ class SrcAttribute extends WebViewAttribute { } return } + if (!this.getValue()) { + return + } // Navigate to |this.src|. const opts = {} From 580abedbb707da1fbc427e2f54602cf5c3ff9a3e Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Thu, 30 Nov 2017 20:25:57 +0900 Subject: [PATCH 3/8] docs: contents.setDevToolsWebContents --- docs/api/web-contents.md | 61 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index 35d8c5833a6c..11ee401978bc 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -1074,6 +1074,67 @@ win.webContents.on('devtools-opened', () => { Removes the specified path from DevTools workspace. +#### `contents.setDevToolsWebContents(devToolsWebContents)` + +* `devToolsWebContents` WebContents + +Uses the `devToolsWebContents` as the target `WebContents` to show devtools. + +The `devToolsWebContents` must not have done any navigation, and it should not +be used for other purposes after the call. + +By default Electron manages the devtools by creating an internal `WebContents` +with native view, which developers have very limited control of. With the +`setDevToolsWebContents` method, developers can use any `WebContents` to show +the devtools in it, including `BrowserWindow`, `BrowserView` and `` +tag. + +An example of showing devtools in a `` tag: + +```html + + + + + + + + + + +``` + +An example of showing devtools in a `BrowserWindow`: + +```js +const {app, BrowserWindow} = require('electron') + +let win = null +let devtools = null + +app.once('ready', () => { + win = new BrowserWindow() + win.loadURL('https://github.com') + win.show() + devtools = new BrowserWindow({show: false}) + devtools.show() + win.webContents.setDevToolsWebContents(devtools.webContents) + win.webContents.openDevTools() +}) +``` + #### `contents.openDevTools([options])` * `options` Object (optional) From 25c5938c761f4f51bddafa33c07f18508d5fe33d Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Thu, 30 Nov 2017 20:42:11 +0900 Subject: [PATCH 4/8] docs: Open detached devtools in example --- docs/api/web-contents.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index 11ee401978bc..4def99b9e16c 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -1131,7 +1131,7 @@ app.once('ready', () => { devtools = new BrowserWindow({show: false}) devtools.show() win.webContents.setDevToolsWebContents(devtools.webContents) - win.webContents.openDevTools() + win.webContents.openDevTools({mode: 'detach'}) }) ``` From ea9771702bb3535e7917a60987538913c3f60329 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Thu, 30 Nov 2017 21:04:50 +0900 Subject: [PATCH 5/8] spec: setDevToolsWebContents --- spec/api-web-contents-spec.js | 16 ++++++++++++++++ spec/webview-spec.js | 24 ++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/spec/api-web-contents-spec.js b/spec/api-web-contents-spec.js index 4e5cc022df7c..122170292838 100644 --- a/spec/api-web-contents-spec.js +++ b/spec/api-web-contents-spec.js @@ -92,6 +92,22 @@ describe('webContents module', () => { }) }) + describe('setDevToolsWebCotnents() API', () => { + it('sets arbitry webContents as devtools', (done) => { + let devtools = new BrowserWindow({show: false}) + devtools.webContents.once('dom-ready', () => { + assert.ok(devtools.getURL().startsWith('chrome-devtools://devtools')) + devtools.webContents.executeJavaScript('InspectorFrontendHost.constructor.name', (name) => { + assert.ok(name, 'InspectorFrontendHostImpl') + devtools.destroy() + done() + }) + }) + w.webContents.setDevToolsWebContents(devtools.webContents) + w.webContents.openDevTools() + }) + }) + describe('isFocused() API', () => { it('returns false when the window is hidden', () => { BrowserWindow.getAllWindows().forEach((window) => { diff --git a/spec/webview-spec.js b/spec/webview-spec.js index 0e4d8cf291ee..e3be5e3fd5dd 100644 --- a/spec/webview-spec.js +++ b/spec/webview-spec.js @@ -655,6 +655,30 @@ describe(' tag', function () { }) }) + describe('setDevToolsWebCotnents() API', () => { + it('sets webContents of webview as devtools', (done) => { + const webview2 = new WebView() + webview2.addEventListener('did-attach', () => { + webview2.addEventListener('dom-ready', () => { + const devtools = webview2.getWebContents() + assert.ok(devtools.getURL().startsWith('chrome-devtools://devtools')) + devtools.executeJavaScript('InspectorFrontendHost.constructor.name', (name) => { + assert.ok(name, 'InspectorFrontendHostImpl') + document.body.removeChild(webview2) + done() + }) + }) + webview.addEventListener('dom-ready', () => { + webview.getWebContents().setDevToolsWebContents(webview2.getWebContents()) + webview.getWebContents().openDevTools() + }) + webview.src = 'about:blank' + document.body.appendChild(webview) + }) + document.body.appendChild(webview2) + }) + }) + describe('devtools-opened event', () => { it('should fire when webview.openDevTools() is called', (done) => { const listener = () => { From 6b408de88420d81d9f98eb54cc4a6549653b3ed9 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Fri, 1 Dec 2017 11:11:37 +0900 Subject: [PATCH 6/8] Allow using docked devtools for webview --- atom/browser/api/atom_api_web_contents.cc | 9 +++++---- docs/api/web-contents.md | 3 +++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/atom/browser/api/atom_api_web_contents.cc b/atom/browser/api/atom_api_web_contents.cc index f59b72bd0c9c..aa9f539bd93a 100644 --- a/atom/browser/api/atom_api_web_contents.cc +++ b/atom/browser/api/atom_api_web_contents.cc @@ -895,10 +895,10 @@ void WebContents::DevToolsOpened() { managed_web_contents()->CallClientFunction( "DevToolsAPI.setInspectedTabId", &tab_id, nullptr, nullptr); - // Inherit owner window in devtools. + // Inherit owner window in devtools when it doesn't have one. auto* devtools = managed_web_contents()->GetDevToolsWebContents(); - if (owner_window() && - !devtools->GetUserData(NativeWindowRelay::UserDataKey())) + bool has_window = devtools->GetUserData(NativeWindowRelay::UserDataKey()); + if (owner_window() && !has_window) handle->SetOwnerWindow(devtools, owner_window()); Emit("devtools-opened"); @@ -1179,7 +1179,8 @@ void WebContents::OpenDevTools(mate::Arguments* args) { std::string state; if (type_ == WEB_VIEW || !owner_window()) { state = "detach"; - } else if (args && args->Length() == 1) { + } + if (args && args->Length() == 1) { bool detach = false; mate::Dictionary options; if (args->GetNext(&options)) { diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index 4def99b9e16c..1a346c968815 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -1144,6 +1144,9 @@ app.once('ready', () => { Opens the devtools. +When `contents` is a `` tag, the `mode` would be `detach` by default, +explicitly passing an empty `mode` can force using last used dock state. + #### `contents.closeDevTools()` Closes the devtools. From 88c4fd3f2b33758726081063a6056cbd6b8fd57b Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Fri, 1 Dec 2017 12:04:50 +0900 Subject: [PATCH 7/8] docs: Remove unnecessary shows --- docs/api/web-contents.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index 1a346c968815..3a36cc461228 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -1126,10 +1126,8 @@ let devtools = null app.once('ready', () => { win = new BrowserWindow() + devtools = new BrowserWindow() win.loadURL('https://github.com') - win.show() - devtools = new BrowserWindow({show: false}) - devtools.show() win.webContents.setDevToolsWebContents(devtools.webContents) win.webContents.openDevTools({mode: 'detach'}) }) From 52c6f4bcc5f40599c02db6e0d45ba68d954293c1 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Fri, 1 Dec 2017 19:30:23 +0900 Subject: [PATCH 8/8] docs: responsiblity of destroying devtoolsWebContents --- docs/api/web-contents.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index 3a36cc461228..7e334d7559ab 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -1089,6 +1089,9 @@ with native view, which developers have very limited control of. With the the devtools in it, including `BrowserWindow`, `BrowserView` and `` tag. +Note that closing the devtools does not destory the `devToolsWebContents`, it +is caller's responsibility to destroy `devToolsWebContents`. + An example of showing devtools in a `` tag: ```html