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:
Milan Burda 2021-05-15 09:42:07 +02:00 committed by GitHub
parent 5e6f8349ec
commit c68c65f383
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 220 additions and 214 deletions

View file

@ -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);
}

View file

@ -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);
};

View file

@ -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);
};
};

View file

@ -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)
});
}
}