一,electron用typescript实现了函数。比如重写了 windows.history.back go等js 内置函数。不走blink的js binding。
D:\dev\electron7\src\electron\lib\renderer\window-setup.ts
window.open
if (!usesNativeWindowOpen) { // TODO(MarshallOfSound): Make compatible with ctx isolation without hole-punch // Make the browser window or guest view emit "new-window" event. (window as any).open = function (url?: string, frameName?: string, features?: string) { if (url != null && url !== '') { url = resolveURL(url, location.href); } const guestId = ipcRendererInternal.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', url, toString(frameName), toString(features)); if (guestId != null) { return getOrCreateProxy(guestId); } else { return null; } }; if (shouldUseContextBridge) internalContextBridge.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['open'], window.open); }
history接口实现
window.history.forward = function () { ipcRendererInternal.send('ELECTRON_NAVIGATION_CONTROLLER_GO_FORWARD'); }; if (shouldUseContextBridge) internalContextBridge.overrideGlobalValueFromIsolatedWorld(['history', 'forward'], window.history.forward); window.history.go = function (offset: number) { ipcRendererInternal.send('ELECTRON_NAVIGATION_CONTROLLER_GO_TO_OFFSET', +offset); }; if (shouldUseContextBridge) internalContextBridge.overrideGlobalValueFromIsolatedWorld(['history', 'go'], window.history.go); const getHistoryLength = () => ipcRendererInternal.sendSync('ELECTRON_NAVIGATION_CONTROLLER_LENGTH'); Object.defineProperty(window.history, 'length', { get: getHistoryLength, set () {} });
在browser进程中接收到back事件,真正做back的又传回给了chromium内核,通过web contents的loadURL接口。
D:\dev\electron7\src\electron\lib\browser\navigation-controller.js 'use strict'; const { ipcMainInternal } = require('@electron/internal/browser/ipc-main-internal'); // The history operation in renderer is redirected to browser. ipcMainInternal.on('ELECTRON_NAVIGATION_CONTROLLER_GO_BACK', function (event) { event.sender.goBack(); });
event.sender是谁:D:\dev\electron7\src\electron\lib\browser\navigation-controller.js
NavigationController.prototype.goBack = function () { if (!this.canGoBack()) { return; } this.pendingIndex = this.getActiveIndex() - 1; if (this.inPageIndex > -1 && this.pendingIndex >= this.inPageIndex) { return this.webContents._goBack(); } else { return this.webContents._loadURL(this.history[this.pendingIndex], {}); //调用electron的绑定的webContents } };
声明:
export const syncMethods = new Set([ 'getURL', 'getTitle', 'isLoading', 'isLoadingMainFrame', 'isWaitingForResponse', 'stop', 'reload', 'reloadIgnoringCache', 'canGoBack', 'canGoForward', 'canGoToOffset', 'clearHistory', 'goBack',
_loadURL的绑定定义在:D:\dev\electron7\src\electron\shell\browser\api\atom_api_web_contents.cc
// static void WebContents::BuildPrototype(v8::Isolate* isolate, v8::Local<v8::FunctionTemplate> prototype) { prototype->SetClassName(mate::StringToV8(isolate, "WebContents")); mate::ObjectTemplateBuilder(isolate, prototype->PrototypeTemplate()) .MakeDestroyable() .SetMethod("setBackgroundThrottling", &WebContents::SetBackgroundThrottling) .SetMethod("getProcessId", &WebContents::GetProcessID) .SetMethod("getOSProcessId", &WebContents::GetOSProcessID) .SetMethod("_getOSProcessIdForFrame", &WebContents::GetOSProcessIdForFrame) .SetMethod("equal", &WebContents::Equal) .SetMethod("_loadURL", &WebContents::LoadURL)
这里在同文件中又实现:
void WebContents::LoadURL(const GURL& url, const mate::Dictionary& options) { if (!url.is_valid() || url.spec().size() > url::kMaxURLChars) { Emit("did-fail-load", static_cast<int>(net::ERR_INVALID_URL), net::ErrorToShortString(net::ERR_INVALID_URL), url.possibly_invalid_spec(), true); return; } content::NavigationController::LoadURLParams params(url); if (!options.Get("httpReferrer", ¶ms.referrer)) { GURL http_referrer; if (options.Get("httpReferrer", &http_referrer)) params.referrer = content::Referrer(http_referrer.GetAsReferrer(), network::mojom::ReferrerPolicy::kDefault); } std::string user_agent; if (options.Get("userAgent", &user_agent)) web_contents()->SetUserAgentOverride(user_agent, false); std::string extra_headers; if (options.Get("extraHeaders", &extra_headers)) params.extra_headers = extra_headers; scoped_refptr<network::ResourceRequestBody> body; if (options.Get("postData", &body)) { params.post_data = body; params.load_type = content::NavigationController::LOAD_TYPE_HTTP_POST; } GURL base_url_for_data_url; if (options.Get("baseURLForDataURL", &base_url_for_data_url)) { params.base_url_for_data_url = base_url_for_data_url; params.load_type = content::NavigationController::LOAD_TYPE_DATA; } bool reload_ignoring_cache = false; if (options.Get("reloadIgnoringCache", &reload_ignoring_cache) && reload_ignoring_cache) { params.reload_type = content::ReloadType::BYPASSING_CACHE; } params.transition_type = ui::PAGE_TRANSITION_TYPED; params.should_clear_history_list = true; params.override_user_agent = content::NavigationController::UA_OVERRIDE_TRUE; // Discord non-committed entries to ensure that we don't re-use a pending // entry web_contents()->GetController().DiscardNonCommittedEntries(); web_contents()->GetController().LoadURLWithParams(params);//这里到跑了下面位置:
//D:\dev\electron7\src\content\browser\frame_host\navigation_controller_impl.cc
// void NavigationControllerImpl::LoadURLWithParams(const LoadURLParams& params) // Set the background color of RenderWidgetHostView. // We have to call it right after LoadURL because the RenderViewHost is only // created after loading a page. auto* const view = web_contents()->GetRenderWidgetHostView(); if (view) { auto* web_preferences = WebContentsPreferences::From(web_contents()); std::string color_name; if (web_preferences->GetPreference(options::kBackgroundColor, &color_name)) { view->SetBackgroundColor(ParseHexColor(color_name)); } else { view->SetBackgroundColor(SK_ColorTRANSPARENT); } } }
跑到了内核的
web_contents()->GetController().LoadURLWithParams(params);
二、window.open的测试代码。其中调用了内部ipc,用js找到了ipcRenderer通道。spec里面是测试代码。
w.webContents.executeJavaScript
D:\dev\electron7\src\electron\spec-main\chromium-spec.ts
describe('window.open', () => { it('denies custom open when nativeWindowOpen: true', async () => { const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: false, nodeIntegration: true, nativeWindowOpen: true } }); w.loadURL('about:blank'); const previousListeners = process.listeners('uncaughtException'); process.removeAllListeners('uncaughtException'); try { const uncaughtException = new Promise<Error>(resolve => { process.once('uncaughtException', resolve); }); expect(await w.webContents.executeJavaScript(`(${function () { const ipc = process.electronBinding('ipc').ipc; return ipc.sendSync(true, 'ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', ['', '', ''])[0]; }})()`)).to.be.null('null'); const exception = await uncaughtException; expect(exception.message).to.match(/denied: expected native window\.open/); } finally { previousListeners.forEach(l => process.on('uncaughtException', l)); } }); }); })
三、electron定义自定义的接口:D:\dev\electron7\src\electron\electron.d.ts 如IpcRenderer, ipcMain
interface IpcRenderer extends NodeJS.EventEmitter { // Docs: http://electronjs.org/docs\api\ipc-renderer /** * Resolves with the response from the main process. * * Send a message to the main process asynchronously via `channel` and expect an * asynchronous result. Arguments will be serialized as JSON internally and hence * no functions or prototype chain will be included. * * The main process should listen for `channel` with `ipcMain.handle()`. * For example: */ invoke(channel: string, ...args: any[]): Promise<any>; /** * Listens to `channel`, when a new message arrives `listener` would be called with * `listener(event, args...)`. */ on(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): this; /** * Adds a one time `listener` function for the event. This `listener` is invoked * only the next time a message is sent to `channel`, after which it is removed. */ once(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): this; /** * Removes all listeners, or those of the specified `channel`. */ removeAllListeners(channel: string): this; /** * Removes the specified `listener` from the listener array for the specified * `channel`. */ removeListener(channel: string, listener: (...args: any[]) => void): this; /** * Send a message to the main process asynchronously via `channel`, you can also * send arbitrary arguments. Arguments will be serialized as JSON internally and * hence no functions or prototype chain will be included. * * The main process handles it by listening for `channel` with the `ipcMain` * module. */ send(channel: string, ...args: any[]): void; /** * The value sent back by the `ipcMain` handler. * * Send a message to the main process synchronously via `channel`, you can also * send arbitrary arguments. Arguments will be serialized in JSON internally and * hence no functions or prototype chain will be included. * * The main process handles it by listening for `channel` with `ipcMain` module, * and replies by setting `event.returnValue`. * * **Note:** Sending a synchronous message will block the whole renderer process, * unless you know what you are doing you should never use it. */ sendSync(channel: string, ...args: any[]): any; sendSyncEx(channel: string, ...args: any[]): any; /** * Sends a message to a window with `webContentsId` via `channel`. */ sendTo(webContentsId: number, channel: string, ...args: any[]): void; /** * Like `ipcRenderer.send` but the event will be sent to the `<webview>` element in * the host page instead of the main process. */ sendToHost(channel: string, ...args: any[]): void; }