electron/lib/browser/navigation-controller.ts

229 lines
7.8 KiB
TypeScript
Raw Normal View History

2020-07-06 17:50:03 +00:00
import type { WebContents, LoadURLOptions } from 'electron/main';
import { EventEmitter } from 'events';
2016-01-12 02:40:23 +00:00
2016-01-14 18:44:21 +00:00
// JavaScript implementation of Chromium's NavigationController.
// Instead of relying on Chromium for history control, we completely do history
2016-01-14 18:44:21 +00:00
// control on user land, and only rely on WebContents.loadURL for navigation.
// This helps us avoid Chromium's various optimizations so we can ensure renderer
// process is restarted every time.
2020-07-06 17:50:03 +00:00
export class NavigationController extends EventEmitter {
currentIndex: number = -1;
inPageIndex: number = -1;
pendingIndex: number = -1;
history: string[] = [];
constructor (private webContents: WebContents) {
super();
2020-03-20 20:28:31 +00:00
this.clearHistory();
2016-01-12 02:40:23 +00:00
// webContents may have already navigated to a page.
if (this.webContents._getURL()) {
2020-03-20 20:28:31 +00:00
this.currentIndex++;
this.history.push(this.webContents._getURL());
}
this.webContents.on('navigation-entry-committed' as any, (event: Electron.Event, url: string, inPage: boolean, replaceEntry: boolean) => {
if (this.inPageIndex > -1 && !inPage) {
// Navigated to a new page, clear in-page mark.
2020-03-20 20:28:31 +00:00
this.inPageIndex = -1;
} else if (this.inPageIndex === -1 && inPage && !replaceEntry) {
// Started in-page navigations.
2020-03-20 20:28:31 +00:00
this.inPageIndex = this.currentIndex;
}
if (this.pendingIndex >= 0) {
// Go to index.
2020-03-20 20:28:31 +00:00
this.currentIndex = this.pendingIndex;
this.pendingIndex = -1;
this.history[this.currentIndex] = url;
} else if (replaceEntry) {
// Non-user initialized navigation.
2020-03-20 20:28:31 +00:00
this.history[this.currentIndex] = url;
} else {
// Normal navigation. Clear history.
2020-03-20 20:28:31 +00:00
this.history = this.history.slice(0, this.currentIndex + 1);
this.currentIndex++;
this.history.push(url);
}
2020-03-20 20:28:31 +00:00
});
2016-01-12 02:40:23 +00:00
}
2020-07-06 17:50:03 +00:00
loadURL (url: string, options?: LoadURLOptions): Promise<void> {
2016-01-12 02:40:23 +00:00
if (options == null) {
2020-03-20 20:28:31 +00:00
options = {};
2016-01-12 02:40:23 +00:00
}
2020-07-06 17:50:03 +00:00
const p = new Promise<void>((resolve, reject) => {
const resolveAndCleanup = () => {
2020-03-20 20:28:31 +00:00
removeListeners();
resolve();
};
2020-07-06 17:50:03 +00:00
const rejectAndCleanup = (errorCode: number, errorDescription: string, url: string) => {
2020-03-20 20:28:31 +00:00
const err = new Error(`${errorDescription} (${errorCode}) loading '${typeof url === 'string' ? url.substr(0, 2048) : url}'`);
Object.assign(err, { errno: errorCode, code: errorDescription, url });
removeListeners();
reject(err);
};
const finishListener = () => {
2020-03-20 20:28:31 +00:00
resolveAndCleanup();
};
const failListener = (event: Electron.Event, errorCode: number, errorDescription: string, validatedURL: string, isMainFrame: boolean) => {
if (isMainFrame) {
2020-03-20 20:28:31 +00:00
rejectAndCleanup(errorCode, errorDescription, validatedURL);
}
2020-03-20 20:28:31 +00:00
};
2020-03-20 20:28:31 +00:00
let navigationStarted = false;
const navigationListener = (event: Electron.Event, url: string, isSameDocument: boolean, isMainFrame: boolean) => {
if (isMainFrame) {
if (navigationStarted && !isSameDocument) {
// the webcontents has started another unrelated navigation in the
// main frame (probably from the app calling `loadURL` again); reject
// the promise
// We should only consider the request aborted if the "navigation" is
// actually navigating and not simply transitioning URL state in the
// current context. E.g. pushState and `location.hash` changes are
// considered navigation events but are triggered with isSameDocument.
// We can ignore these to allow virtual routing on page load as long
// as the routing does not leave the document
2020-03-20 20:28:31 +00:00
return rejectAndCleanup(-3, 'ERR_ABORTED', url);
}
2020-03-20 20:28:31 +00:00
navigationStarted = true;
}
2020-03-20 20:28:31 +00:00
};
const stopLoadingListener = () => {
// By the time we get here, either 'finish' or 'fail' should have fired
// if the navigation occurred. However, in some situations (e.g. when
// attempting to load a page with a bad scheme), loading will stop
// without emitting finish or fail. In this case, we reject the promise
// with a generic failure.
// TODO(jeremy): enumerate all the cases in which this can happen. If
// the only one is with a bad scheme, perhaps ERR_INVALID_ARGUMENT
// would be more appropriate.
2020-03-20 20:28:31 +00:00
rejectAndCleanup(-2, 'ERR_FAILED', url);
};
const removeListeners = () => {
2020-03-20 20:28:31 +00:00
this.webContents.removeListener('did-finish-load', finishListener);
this.webContents.removeListener('did-fail-load', failListener);
this.webContents.removeListener('did-start-navigation', navigationListener);
this.webContents.removeListener('did-stop-loading', stopLoadingListener);
this.webContents.removeListener('destroyed', stopLoadingListener);
2020-03-20 20:28:31 +00:00
};
this.webContents.on('did-finish-load', finishListener);
this.webContents.on('did-fail-load', failListener);
this.webContents.on('did-start-navigation', navigationListener);
this.webContents.on('did-stop-loading', stopLoadingListener);
this.webContents.on('destroyed', stopLoadingListener);
2020-03-20 20:28:31 +00:00
});
// Add a no-op rejection handler to silence the unhandled rejection error.
2020-03-20 20:28:31 +00:00
p.catch(() => {});
this.pendingIndex = -1;
this.webContents._loadURL(url, options);
this.webContents.emit('load-url', url, options);
return p;
2020-07-06 17:50:03 +00:00
}
2016-01-12 02:40:23 +00:00
2020-07-06 17:50:03 +00:00
getURL () {
2016-01-12 02:40:23 +00:00
if (this.currentIndex === -1) {
2020-03-20 20:28:31 +00:00
return '';
2016-01-12 02:40:23 +00:00
} else {
2020-03-20 20:28:31 +00:00
return this.history[this.currentIndex];
2016-01-12 02:40:23 +00:00
}
2020-07-06 17:50:03 +00:00
}
2016-01-12 02:40:23 +00:00
2020-07-06 17:50:03 +00:00
stop () {
2020-03-20 20:28:31 +00:00
this.pendingIndex = -1;
return this.webContents._stop();
2020-07-06 17:50:03 +00:00
}
2016-01-12 02:40:23 +00:00
2020-07-06 17:50:03 +00:00
reload () {
2020-03-20 20:28:31 +00:00
this.pendingIndex = this.currentIndex;
return this.webContents._loadURL(this.getURL(), {});
2020-07-06 17:50:03 +00:00
}
2016-01-12 02:40:23 +00:00
2020-07-06 17:50:03 +00:00
reloadIgnoringCache () {
2020-03-20 20:28:31 +00:00
this.pendingIndex = this.currentIndex;
2016-01-12 02:40:23 +00:00
return this.webContents._loadURL(this.getURL(), {
extraHeaders: 'pragma: no-cache\n',
reloadIgnoringCache: true
});
2020-07-06 17:50:03 +00:00
}
2020-07-06 17:50:03 +00:00
canGoBack () {
2020-03-20 20:28:31 +00:00
return this.getActiveIndex() > 0;
2020-07-06 17:50:03 +00:00
}
2020-07-06 17:50:03 +00:00
canGoForward () {
2020-03-20 20:28:31 +00:00
return this.getActiveIndex() < this.history.length - 1;
2020-07-06 17:50:03 +00:00
}
2020-07-06 17:50:03 +00:00
canGoToIndex (index: number) {
2020-03-20 20:28:31 +00:00
return index >= 0 && index < this.history.length;
2020-07-06 17:50:03 +00:00
}
2020-07-06 17:50:03 +00:00
canGoToOffset (offset: number) {
2020-03-20 20:28:31 +00:00
return this.canGoToIndex(this.currentIndex + offset);
2020-07-06 17:50:03 +00:00
}
2020-07-06 17:50:03 +00:00
clearHistory () {
2020-03-20 20:28:31 +00:00
this.history = [];
this.currentIndex = -1;
this.pendingIndex = -1;
this.inPageIndex = -1;
2020-07-06 17:50:03 +00:00
}
2020-07-06 17:50:03 +00:00
goBack () {
2016-01-12 02:40:23 +00:00
if (!this.canGoBack()) {
2020-03-20 20:28:31 +00:00
return;
2016-01-12 02:40:23 +00:00
}
2020-03-20 20:28:31 +00:00
this.pendingIndex = this.getActiveIndex() - 1;
2016-01-12 02:40:23 +00:00
if (this.inPageIndex > -1 && this.pendingIndex >= this.inPageIndex) {
2020-03-20 20:28:31 +00:00
return this.webContents._goBack();
2016-01-12 02:40:23 +00:00
} else {
2020-03-20 20:28:31 +00:00
return this.webContents._loadURL(this.history[this.pendingIndex], {});
2016-01-12 02:40:23 +00:00
}
2020-07-06 17:50:03 +00:00
}
2016-01-12 02:40:23 +00:00
2020-07-06 17:50:03 +00:00
goForward () {
2016-01-12 02:40:23 +00:00
if (!this.canGoForward()) {
2020-03-20 20:28:31 +00:00
return;
2016-01-12 02:40:23 +00:00
}
2020-03-20 20:28:31 +00:00
this.pendingIndex = this.getActiveIndex() + 1;
2016-01-12 02:40:23 +00:00
if (this.inPageIndex > -1 && this.pendingIndex >= this.inPageIndex) {
2020-03-20 20:28:31 +00:00
return this.webContents._goForward();
2016-01-12 02:40:23 +00:00
} else {
2020-03-20 20:28:31 +00:00
return this.webContents._loadURL(this.history[this.pendingIndex], {});
2016-01-12 02:40:23 +00:00
}
2020-07-06 17:50:03 +00:00
}
2016-01-12 02:40:23 +00:00
2020-07-06 17:50:03 +00:00
goToIndex (index: number) {
2016-01-12 02:40:23 +00:00
if (!this.canGoToIndex(index)) {
2020-03-20 20:28:31 +00:00
return;
2016-01-12 02:40:23 +00:00
}
2020-03-20 20:28:31 +00:00
this.pendingIndex = index;
return this.webContents._loadURL(this.history[this.pendingIndex], {});
2020-07-06 17:50:03 +00:00
}
2016-01-12 02:40:23 +00:00
2020-07-06 17:50:03 +00:00
goToOffset (offset: number) {
2016-01-12 02:40:23 +00:00
if (!this.canGoToOffset(offset)) {
2020-03-20 20:28:31 +00:00
return;
2016-01-12 02:40:23 +00:00
}
2020-03-20 20:28:31 +00:00
const pendingIndex = this.currentIndex + offset;
2016-01-12 02:40:23 +00:00
if (this.inPageIndex > -1 && pendingIndex >= this.inPageIndex) {
2020-03-20 20:28:31 +00:00
this.pendingIndex = pendingIndex;
return this.webContents._goToOffset(offset);
2016-01-12 02:40:23 +00:00
} else {
2020-03-20 20:28:31 +00:00
return this.goToIndex(pendingIndex);
2016-01-12 02:40:23 +00:00
}
2020-07-06 17:50:03 +00:00
}
2016-01-12 02:40:23 +00:00
2020-07-06 17:50:03 +00:00
getActiveIndex () {
2016-01-12 02:40:23 +00:00
if (this.pendingIndex === -1) {
2020-03-20 20:28:31 +00:00
return this.currentIndex;
2016-01-12 02:40:23 +00:00
} else {
2020-03-20 20:28:31 +00:00
return this.pendingIndex;
2016-01-12 02:40:23 +00:00
}
2020-07-06 17:50:03 +00:00
}
2016-01-12 02:40:23 +00:00
2020-07-06 17:50:03 +00:00
length () {
2020-03-20 20:28:31 +00:00
return this.history.length;
2020-07-06 17:50:03 +00:00
}
}