refactor: implement <webview> using contextBridge (#29037)
* refactor: implement <webview> using contextBridge * chore: address PR feedback * chore: address PR feedback * fix: check for HTMLIFrameElement instance in attachGuest
This commit is contained in:
parent
5e6f8349ec
commit
c68c65f383
17 changed files with 220 additions and 214 deletions
|
@ -1,20 +1,22 @@
|
|||
import { webFrame } from 'electron';
|
||||
import { ipcRendererInternal } from '@electron/internal/renderer/ipc-renderer-internal';
|
||||
import * as ipcRendererUtils from '@electron/internal/renderer/ipc-renderer-internal-utils';
|
||||
import { webViewEvents } from '@electron/internal/common/web-view-events';
|
||||
|
||||
import { WebViewImpl } from '@electron/internal/renderer/web-view/web-view-impl';
|
||||
import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
|
||||
|
||||
const { mainFrame: webFrame } = process._linkedBinding('electron_renderer_web_frame');
|
||||
|
||||
export interface GuestViewDelegate {
|
||||
dispatchEvent (eventName: string, props: Record<string, any>): void;
|
||||
reset(): void;
|
||||
}
|
||||
|
||||
const DEPRECATED_EVENTS: Record<string, string> = {
|
||||
'page-title-updated': 'page-title-set'
|
||||
} as const;
|
||||
|
||||
const dispatchEvent = function (
|
||||
webView: WebViewImpl, eventName: string, eventKey: string, ...args: Array<any>
|
||||
) {
|
||||
const dispatchEvent = function (delegate: GuestViewDelegate, eventName: string, eventKey: string, ...args: Array<any>) {
|
||||
if (DEPRECATED_EVENTS[eventName] != null) {
|
||||
dispatchEvent(webView, DEPRECATED_EVENTS[eventName], eventKey, ...args);
|
||||
dispatchEvent(delegate, DEPRECATED_EVENTS[eventName], eventKey, ...args);
|
||||
}
|
||||
|
||||
const props: Record<string, any> = {};
|
||||
|
@ -22,28 +24,21 @@ const dispatchEvent = function (
|
|||
props[prop] = args[index];
|
||||
});
|
||||
|
||||
webView.dispatchEvent(eventName, props);
|
||||
|
||||
if (eventName === 'load-commit') {
|
||||
webView.onLoadCommit(props);
|
||||
} else if (eventName === '-focus-change') {
|
||||
webView.onFocusChange();
|
||||
}
|
||||
delegate.dispatchEvent(eventName, props);
|
||||
};
|
||||
|
||||
export function registerEvents (webView: WebViewImpl, viewInstanceId: number) {
|
||||
export function registerEvents (viewInstanceId: number, delegate: GuestViewDelegate) {
|
||||
ipcRendererInternal.on(`${IPC_MESSAGES.GUEST_VIEW_INTERNAL_DESTROY_GUEST}-${viewInstanceId}`, function () {
|
||||
webView.guestInstanceId = undefined;
|
||||
webView.reset();
|
||||
webView.dispatchEvent('destroyed');
|
||||
delegate.reset();
|
||||
delegate.dispatchEvent('destroyed', {});
|
||||
});
|
||||
|
||||
ipcRendererInternal.on(`${IPC_MESSAGES.GUEST_VIEW_INTERNAL_DISPATCH_EVENT}-${viewInstanceId}`, function (event, eventName, ...args) {
|
||||
dispatchEvent(webView, eventName, eventName, ...args);
|
||||
dispatchEvent(delegate, eventName, eventName, ...args);
|
||||
});
|
||||
|
||||
ipcRendererInternal.on(`${IPC_MESSAGES.GUEST_VIEW_INTERNAL_IPC_MESSAGE}-${viewInstanceId}`, function (event, channel, ...args) {
|
||||
webView.dispatchEvent('ipc-message', { channel, args });
|
||||
delegate.dispatchEvent('ipc-message', { channel, args });
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -57,16 +52,39 @@ export function createGuest (params: Record<string, any>): Promise<number> {
|
|||
return ipcRendererInternal.invoke(IPC_MESSAGES.GUEST_VIEW_MANAGER_CREATE_GUEST, params);
|
||||
}
|
||||
|
||||
export function attachGuest (
|
||||
elementInstanceId: number, guestInstanceId: number, params: Record<string, any>, contentWindow: Window
|
||||
) {
|
||||
const embedderFrameId = webFrame.getWebFrameId(contentWindow);
|
||||
export function attachGuest (iframe: HTMLIFrameElement, elementInstanceId: number, guestInstanceId: number, params: Record<string, any>) {
|
||||
if (!(iframe instanceof HTMLIFrameElement)) {
|
||||
throw new Error('Invalid embedder frame');
|
||||
}
|
||||
|
||||
const embedderFrameId = webFrame.getWebFrameId(iframe.contentWindow!);
|
||||
if (embedderFrameId < 0) { // this error should not happen.
|
||||
throw new Error('Invalid embedder frame');
|
||||
}
|
||||
ipcRendererInternal.invoke(IPC_MESSAGES.GUEST_VIEW_MANAGER_ATTACH_GUEST, embedderFrameId, elementInstanceId, guestInstanceId, params);
|
||||
|
||||
return ipcRendererInternal.invoke(IPC_MESSAGES.GUEST_VIEW_MANAGER_ATTACH_GUEST, embedderFrameId, elementInstanceId, guestInstanceId, params);
|
||||
}
|
||||
|
||||
export function detachGuest (guestInstanceId: number) {
|
||||
return ipcRendererUtils.invokeSync(IPC_MESSAGES.GUEST_VIEW_MANAGER_DETACH_GUEST, guestInstanceId);
|
||||
}
|
||||
|
||||
export function capturePage (guestInstanceId: number, args: any[]) {
|
||||
return ipcRendererInternal.invoke(IPC_MESSAGES.GUEST_VIEW_MANAGER_CAPTURE_PAGE, guestInstanceId, args);
|
||||
}
|
||||
|
||||
export function invoke (guestInstanceId: number, method: string, args: any[]) {
|
||||
return ipcRendererInternal.invoke(IPC_MESSAGES.GUEST_VIEW_MANAGER_CALL, guestInstanceId, method, args);
|
||||
}
|
||||
|
||||
export function invokeSync (guestInstanceId: number, method: string, args: any[]) {
|
||||
return ipcRendererUtils.invokeSync(IPC_MESSAGES.GUEST_VIEW_MANAGER_CALL, guestInstanceId, method, args);
|
||||
}
|
||||
|
||||
export function propertyGet (guestInstanceId: number, name: string) {
|
||||
return ipcRendererUtils.invokeSync(IPC_MESSAGES.GUEST_VIEW_MANAGER_PROPERTY_GET, guestInstanceId, name);
|
||||
}
|
||||
|
||||
export function propertySet (guestInstanceId: number, name: string, value: any) {
|
||||
return ipcRendererUtils.invokeSync(IPC_MESSAGES.GUEST_VIEW_MANAGER_PROPERTY_SET, guestInstanceId, name, value);
|
||||
}
|
||||
|
|
|
@ -9,14 +9,13 @@
|
|||
// modules must be passed from outside, all included files must be plain JS.
|
||||
|
||||
import { WEB_VIEW_CONSTANTS } from '@electron/internal/renderer/web-view/web-view-constants';
|
||||
import type * as webViewImplModule from '@electron/internal/renderer/web-view/web-view-impl';
|
||||
import { WebViewImpl, WebViewImplHooks, setupMethods } from '@electron/internal/renderer/web-view/web-view-impl';
|
||||
import type { SrcAttribute } from '@electron/internal/renderer/web-view/web-view-attributes';
|
||||
|
||||
const internals = new WeakMap<HTMLElement, webViewImplModule.WebViewImpl>();
|
||||
const internals = new WeakMap<HTMLElement, WebViewImpl>();
|
||||
|
||||
// Return a WebViewElement class that is defined in this context.
|
||||
const defineWebViewElement = (webViewImpl: typeof webViewImplModule) => {
|
||||
const { guestViewInternal, WebViewImpl } = webViewImpl;
|
||||
const defineWebViewElement = (hooks: WebViewImplHooks) => {
|
||||
return class WebViewElement extends HTMLElement {
|
||||
static get observedAttributes () {
|
||||
return [
|
||||
|
@ -38,13 +37,7 @@ const defineWebViewElement = (webViewImpl: typeof webViewImplModule) => {
|
|||
|
||||
constructor () {
|
||||
super();
|
||||
const internal = new WebViewImpl(this);
|
||||
internal.dispatchEventInMainWorld = (eventName, props) => {
|
||||
const event = new Event(eventName);
|
||||
Object.assign(event, props);
|
||||
return internal.webviewNode.dispatchEvent(event);
|
||||
};
|
||||
internals.set(this, internal);
|
||||
internals.set(this, new WebViewImpl(this, hooks));
|
||||
}
|
||||
|
||||
getWebContentsId () {
|
||||
|
@ -61,7 +54,10 @@ const defineWebViewElement = (webViewImpl: typeof webViewImplModule) => {
|
|||
return;
|
||||
}
|
||||
if (!internal.elementAttached) {
|
||||
guestViewInternal.registerEvents(internal, internal.viewInstanceId);
|
||||
hooks.guestViewInternal.registerEvents(internal.viewInstanceId, {
|
||||
dispatchEvent: internal.dispatchEvent.bind(internal),
|
||||
reset: internal.reset.bind(internal)
|
||||
});
|
||||
internal.elementAttached = true;
|
||||
(internal.attributes.get(WEB_VIEW_CONSTANTS.ATTRIBUTE_SRC) as SrcAttribute).parse();
|
||||
}
|
||||
|
@ -79,9 +75,9 @@ const defineWebViewElement = (webViewImpl: typeof webViewImplModule) => {
|
|||
if (!internal) {
|
||||
return;
|
||||
}
|
||||
guestViewInternal.deregisterEvents(internal.viewInstanceId);
|
||||
hooks.guestViewInternal.deregisterEvents(internal.viewInstanceId);
|
||||
if (internal.guestInstanceId) {
|
||||
guestViewInternal.detachGuest(internal.guestInstanceId);
|
||||
hooks.guestViewInternal.detachGuest(internal.guestInstanceId);
|
||||
}
|
||||
internal.elementAttached = false;
|
||||
internal.reset();
|
||||
|
@ -90,15 +86,15 @@ const defineWebViewElement = (webViewImpl: typeof webViewImplModule) => {
|
|||
};
|
||||
|
||||
// Register <webview> custom element.
|
||||
const registerWebViewElement = (webViewImpl: typeof webViewImplModule) => {
|
||||
const registerWebViewElement = (hooks: WebViewImplHooks) => {
|
||||
// I wish eslint wasn't so stupid, but it is
|
||||
// eslint-disable-next-line
|
||||
const WebViewElement = defineWebViewElement(webViewImpl) as unknown as typeof ElectronInternal.WebViewElement
|
||||
const WebViewElement = defineWebViewElement(hooks) as unknown as typeof ElectronInternal.WebViewElement
|
||||
|
||||
webViewImpl.setupMethods(WebViewElement);
|
||||
setupMethods(WebViewElement, hooks);
|
||||
|
||||
// The customElements.define has to be called in a special scope.
|
||||
webViewImpl.webFrame.allowGuestViewElementDefinition(window, () => {
|
||||
hooks.allowGuestViewElementDefinition(window, () => {
|
||||
window.customElements.define('webview', WebViewElement);
|
||||
window.WebView = WebViewElement;
|
||||
|
||||
|
@ -116,14 +112,14 @@ const registerWebViewElement = (webViewImpl: typeof webViewImplModule) => {
|
|||
};
|
||||
|
||||
// Prepare to register the <webview> element.
|
||||
export const setupWebView = (webViewImpl: typeof webViewImplModule) => {
|
||||
export const setupWebView = (hooks: WebViewImplHooks) => {
|
||||
const useCapture = true;
|
||||
const listener = (event: Event) => {
|
||||
if (document.readyState === 'loading') {
|
||||
return;
|
||||
}
|
||||
|
||||
registerWebViewElement(webViewImpl);
|
||||
registerWebViewElement(hooks);
|
||||
|
||||
window.removeEventListener(event.type, listener, useCapture);
|
||||
};
|
||||
|
|
|
@ -1,17 +1,9 @@
|
|||
import { ipcRendererInternal } from '@electron/internal/renderer/ipc-renderer-internal';
|
||||
import * as ipcRendererUtils from '@electron/internal/renderer/ipc-renderer-internal-utils';
|
||||
import * as guestViewInternal from '@electron/internal/renderer/web-view/guest-view-internal';
|
||||
import type * as guestViewInternalModule from '@electron/internal/renderer/web-view/guest-view-internal';
|
||||
import { WEB_VIEW_CONSTANTS } from '@electron/internal/renderer/web-view/web-view-constants';
|
||||
import { syncMethods, asyncMethods, properties } from '@electron/internal/common/web-view-methods';
|
||||
import type { WebViewAttribute, PartitionAttribute } from '@electron/internal/renderer/web-view/web-view-attributes';
|
||||
import { setupWebViewAttributes } from '@electron/internal/renderer/web-view/web-view-attributes';
|
||||
import { deserialize } from '@electron/internal/common/type-utils';
|
||||
import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
|
||||
|
||||
export { webFrame } from 'electron';
|
||||
export * as guestViewInternal from '@electron/internal/renderer/web-view/guest-view-internal';
|
||||
|
||||
const v8Util = process._linkedBinding('electron_common_v8_util');
|
||||
|
||||
// ID generator.
|
||||
let nextId = 0;
|
||||
|
@ -20,6 +12,13 @@ const getNextId = function () {
|
|||
return ++nextId;
|
||||
};
|
||||
|
||||
export interface WebViewImplHooks {
|
||||
readonly guestViewInternal: typeof guestViewInternalModule;
|
||||
readonly allowGuestViewElementDefinition: NodeJS.InternalWebFrame['allowGuestViewElementDefinition'];
|
||||
readonly setIsWebView: (iframe: HTMLIFrameElement) => void;
|
||||
readonly createNativeImage?: typeof Electron.nativeImage['createEmpty'];
|
||||
}
|
||||
|
||||
// Represents the internal state of the WebView node.
|
||||
export class WebViewImpl {
|
||||
public beforeFirstNavigation = true
|
||||
|
@ -37,9 +36,7 @@ export class WebViewImpl {
|
|||
|
||||
public attributes: Map<string, WebViewAttribute>;
|
||||
|
||||
public dispatchEventInMainWorld?: (eventName: string, props: any) => boolean;
|
||||
|
||||
constructor (public webviewNode: HTMLElement) {
|
||||
constructor (public webviewNode: HTMLElement, private hooks: WebViewImplHooks) {
|
||||
// Create internal iframe element.
|
||||
this.internalElement = this.createInternalElement();
|
||||
const shadowRoot = this.webviewNode.attachShadow({ mode: 'open' });
|
||||
|
@ -65,7 +62,7 @@ export class WebViewImpl {
|
|||
iframeElement.style.width = '100%';
|
||||
iframeElement.style.border = '0';
|
||||
// used by RendererClientBase::IsWebViewFrame
|
||||
v8Util.setHiddenValue(iframeElement, 'internal', this);
|
||||
this.hooks.setIsWebView(iframeElement);
|
||||
return iframeElement;
|
||||
}
|
||||
|
||||
|
@ -118,13 +115,21 @@ export class WebViewImpl {
|
|||
}
|
||||
|
||||
createGuest () {
|
||||
guestViewInternal.createGuest(this.buildParams()).then(guestInstanceId => {
|
||||
this.hooks.guestViewInternal.createGuest(this.buildParams()).then(guestInstanceId => {
|
||||
this.attachGuestInstance(guestInstanceId);
|
||||
});
|
||||
}
|
||||
|
||||
dispatchEvent (eventName: string, props: Record<string, any> = {}) {
|
||||
this.dispatchEventInMainWorld!(eventName, props);
|
||||
const event = new Event(eventName);
|
||||
Object.assign(event, props);
|
||||
this.webviewNode.dispatchEvent(event);
|
||||
|
||||
if (eventName === 'load-commit') {
|
||||
this.onLoadCommit(props);
|
||||
} else if (eventName === '-focus-change') {
|
||||
this.onFocusChange();
|
||||
}
|
||||
}
|
||||
|
||||
// Adds an 'on<event>' property on the webview, which can be used to set/unset
|
||||
|
@ -194,11 +199,11 @@ export class WebViewImpl {
|
|||
this.internalInstanceId = getNextId();
|
||||
this.guestInstanceId = guestInstanceId;
|
||||
|
||||
guestViewInternal.attachGuest(
|
||||
this.hooks.guestViewInternal.attachGuest(
|
||||
this.internalElement,
|
||||
this.internalInstanceId,
|
||||
this.guestInstanceId,
|
||||
this.buildParams(),
|
||||
this.internalElement.contentWindow!
|
||||
this.buildParams()
|
||||
);
|
||||
|
||||
// TODO(zcbenz): Should we deprecate the "resize" event? Wait, it is not
|
||||
|
@ -210,46 +215,38 @@ export class WebViewImpl {
|
|||
|
||||
// I wish eslint wasn't so stupid, but it is
|
||||
// eslint-disable-next-line
|
||||
export const setupMethods = (WebViewElement: typeof ElectronInternal.WebViewElement) => {
|
||||
export const setupMethods = (WebViewElement: typeof ElectronInternal.WebViewElement, hooks: WebViewImplHooks) => {
|
||||
// Focusing the webview should move page focus to the underlying iframe.
|
||||
WebViewElement.prototype.focus = function () {
|
||||
this.contentWindow.focus();
|
||||
};
|
||||
|
||||
// Forward proto.foo* method calls to WebViewImpl.foo*.
|
||||
const createBlockHandler = function (method: string) {
|
||||
return function (this: ElectronInternal.WebViewElement, ...args: Array<any>) {
|
||||
return ipcRendererUtils.invokeSync(IPC_MESSAGES.GUEST_VIEW_MANAGER_CALL, this.getWebContentsId(), method, args);
|
||||
};
|
||||
};
|
||||
|
||||
for (const method of syncMethods) {
|
||||
(WebViewElement.prototype as Record<string, any>)[method] = createBlockHandler(method);
|
||||
(WebViewElement.prototype as Record<string, any>)[method] = function (this: ElectronInternal.WebViewElement, ...args: Array<any>) {
|
||||
return hooks.guestViewInternal.invokeSync(this.getWebContentsId(), method, args);
|
||||
};
|
||||
}
|
||||
|
||||
const createNonBlockHandler = function (method: string) {
|
||||
return function (this: ElectronInternal.WebViewElement, ...args: Array<any>) {
|
||||
return ipcRendererInternal.invoke(IPC_MESSAGES.GUEST_VIEW_MANAGER_CALL, this.getWebContentsId(), method, args);
|
||||
};
|
||||
};
|
||||
|
||||
for (const method of asyncMethods) {
|
||||
(WebViewElement.prototype as Record<string, any>)[method] = createNonBlockHandler(method);
|
||||
(WebViewElement.prototype as Record<string, any>)[method] = function (this: ElectronInternal.WebViewElement, ...args: Array<any>) {
|
||||
return hooks.guestViewInternal.invoke(this.getWebContentsId(), method, args);
|
||||
};
|
||||
}
|
||||
|
||||
WebViewElement.prototype.capturePage = async function (...args) {
|
||||
return deserialize(await ipcRendererInternal.invoke(IPC_MESSAGES.GUEST_VIEW_MANAGER_CAPTURE_PAGE, this.getWebContentsId(), args));
|
||||
return deserialize(await hooks.guestViewInternal.capturePage(this.getWebContentsId(), args), hooks.createNativeImage);
|
||||
};
|
||||
|
||||
const createPropertyGetter = function (property: string) {
|
||||
return function (this: ElectronInternal.WebViewElement) {
|
||||
return ipcRendererUtils.invokeSync(IPC_MESSAGES.GUEST_VIEW_MANAGER_PROPERTY_GET, this.getWebContentsId(), property);
|
||||
return hooks.guestViewInternal.propertyGet(this.getWebContentsId(), property);
|
||||
};
|
||||
};
|
||||
|
||||
const createPropertySetter = function (property: string) {
|
||||
return function (this: ElectronInternal.WebViewElement, arg: any) {
|
||||
return ipcRendererUtils.invokeSync(IPC_MESSAGES.GUEST_VIEW_MANAGER_PROPERTY_SET, this.getWebContentsId(), property, arg);
|
||||
return hooks.guestViewInternal.propertySet(this.getWebContentsId(), property, arg);
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { ipcRendererInternal } from '@electron/internal/renderer/ipc-renderer-internal';
|
||||
import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
|
||||
|
||||
import type * as webViewImpl from '@electron/internal/renderer/web-view/web-view-impl';
|
||||
import type * as webViewElement from '@electron/internal/renderer/web-view/web-view-element';
|
||||
import type * as webViewElementModule from '@electron/internal/renderer/web-view/web-view-element';
|
||||
import type * as guestViewInternalModule from '@electron/internal/renderer/web-view/guest-view-internal';
|
||||
|
||||
const v8Util = process._linkedBinding('electron_common_v8_util');
|
||||
const { mainFrame: webFrame } = process._linkedBinding('electron_renderer_web_frame');
|
||||
|
||||
function handleFocusBlur () {
|
||||
// Note that while Chromium content APIs have observer for focus/blur, they
|
||||
|
@ -22,12 +23,16 @@ function handleFocusBlur () {
|
|||
export function webViewInit (contextIsolation: boolean, webviewTag: boolean, guestInstanceId: number) {
|
||||
// Don't allow recursive `<webview>`.
|
||||
if (webviewTag && !guestInstanceId) {
|
||||
const webViewImplModule = require('@electron/internal/renderer/web-view/web-view-impl') as typeof webViewImpl;
|
||||
const guestViewInternal = require('@electron/internal/renderer/web-view/guest-view-internal') as typeof guestViewInternalModule;
|
||||
if (contextIsolation) {
|
||||
v8Util.setHiddenValue(window, 'web-view-impl', webViewImplModule);
|
||||
v8Util.setHiddenValue(window, 'guestViewInternal', guestViewInternal);
|
||||
} else {
|
||||
const { setupWebView } = require('@electron/internal/renderer/web-view/web-view-element') as typeof webViewElement;
|
||||
setupWebView(webViewImplModule);
|
||||
const { setupWebView } = require('@electron/internal/renderer/web-view/web-view-element') as typeof webViewElementModule;
|
||||
setupWebView({
|
||||
guestViewInternal,
|
||||
allowGuestViewElementDefinition: webFrame.allowGuestViewElementDefinition,
|
||||
setIsWebView: iframe => v8Util.setHiddenValue(iframe, 'isWebView', true)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue