navigation-controller.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. 'use strict';
  2. const { ipcMainInternal } = require('@electron/internal/browser/ipc-main-internal');
  3. // The history operation in renderer is redirected to browser.
  4. ipcMainInternal.on('ELECTRON_NAVIGATION_CONTROLLER_GO_BACK', function (event) {
  5. event.sender.goBack();
  6. });
  7. ipcMainInternal.on('ELECTRON_NAVIGATION_CONTROLLER_GO_FORWARD', function (event) {
  8. event.sender.goForward();
  9. });
  10. ipcMainInternal.on('ELECTRON_NAVIGATION_CONTROLLER_GO_TO_OFFSET', function (event, offset) {
  11. event.sender.goToOffset(offset);
  12. });
  13. ipcMainInternal.on('ELECTRON_NAVIGATION_CONTROLLER_LENGTH', function (event) {
  14. event.returnValue = event.sender.length();
  15. });
  16. // JavaScript implementation of Chromium's NavigationController.
  17. // Instead of relying on Chromium for history control, we compeletely do history
  18. // control on user land, and only rely on WebContents.loadURL for navigation.
  19. // This helps us avoid Chromium's various optimizations so we can ensure renderer
  20. // process is restarted everytime.
  21. const NavigationController = (function () {
  22. function NavigationController (webContents) {
  23. this.webContents = webContents;
  24. this.clearHistory();
  25. // webContents may have already navigated to a page.
  26. if (this.webContents._getURL()) {
  27. this.currentIndex++;
  28. this.history.push(this.webContents._getURL());
  29. }
  30. this.webContents.on('navigation-entry-committed', (event, url, inPage, replaceEntry) => {
  31. if (this.inPageIndex > -1 && !inPage) {
  32. // Navigated to a new page, clear in-page mark.
  33. this.inPageIndex = -1;
  34. } else if (this.inPageIndex === -1 && inPage && !replaceEntry) {
  35. // Started in-page navigations.
  36. this.inPageIndex = this.currentIndex;
  37. }
  38. if (this.pendingIndex >= 0) {
  39. // Go to index.
  40. this.currentIndex = this.pendingIndex;
  41. this.pendingIndex = -1;
  42. this.history[this.currentIndex] = url;
  43. } else if (replaceEntry) {
  44. // Non-user initialized navigation.
  45. this.history[this.currentIndex] = url;
  46. } else {
  47. // Normal navigation. Clear history.
  48. this.history = this.history.slice(0, this.currentIndex + 1);
  49. this.currentIndex++;
  50. this.history.push(url);
  51. }
  52. });
  53. }
  54. NavigationController.prototype.loadURL = function (url, options) {
  55. if (options == null) {
  56. options = {};
  57. }
  58. const p = new Promise((resolve, reject) => {
  59. const resolveAndCleanup = () => {
  60. removeListeners();
  61. resolve();
  62. };
  63. const rejectAndCleanup = (errorCode, errorDescription, url) => {
  64. const err = new Error(`${errorDescription} (${errorCode}) loading '${typeof url === 'string' ? url.substr(0, 2048) : url}'`);
  65. Object.assign(err, { errno: errorCode, code: errorDescription, url });
  66. removeListeners();
  67. reject(err);
  68. };
  69. const finishListener = () => {
  70. resolveAndCleanup();
  71. };
  72. const failListener = (event, errorCode, errorDescription, validatedURL, isMainFrame, frameProcessId, frameRoutingId) => {
  73. if (isMainFrame) {
  74. rejectAndCleanup(errorCode, errorDescription, validatedURL);
  75. }
  76. };
  77. let navigationStarted = false;
  78. const navigationListener = (event, url, isSameDocument, isMainFrame, frameProcessId, frameRoutingId, navigationId) => {
  79. if (isMainFrame) {
  80. if (navigationStarted && !isSameDocument) {
  81. // the webcontents has started another unrelated navigation in the
  82. // main frame (probably from the app calling `loadURL` again); reject
  83. // the promise
  84. // We should only consider the request aborted if the "navigation" is
  85. // actually navigating and not simply transitioning URL state in the
  86. // current context. E.g. pushState and `location.hash` changes are
  87. // considered navigation events but are triggered with isSameDocument.
  88. // We can ignore these to allow virtual routing on page load as long
  89. // as the routing does not leave the document
  90. return rejectAndCleanup(-3, 'ERR_ABORTED', url);
  91. }
  92. navigationStarted = true;
  93. }
  94. };
  95. const stopLoadingListener = () => {
  96. // By the time we get here, either 'finish' or 'fail' should have fired
  97. // if the navigation occurred. However, in some situations (e.g. when
  98. // attempting to load a page with a bad scheme), loading will stop
  99. // without emitting finish or fail. In this case, we reject the promise
  100. // with a generic failure.
  101. // TODO(jeremy): enumerate all the cases in which this can happen. If
  102. // the only one is with a bad scheme, perhaps ERR_INVALID_ARGUMENT
  103. // would be more appropriate.
  104. rejectAndCleanup(-2, 'ERR_FAILED', url);
  105. };
  106. const removeListeners = () => {
  107. this.webContents.removeListener('did-finish-load', finishListener);
  108. this.webContents.removeListener('did-fail-load', failListener);
  109. this.webContents.removeListener('did-start-navigation', navigationListener);
  110. this.webContents.removeListener('did-stop-loading', stopLoadingListener);
  111. };
  112. this.webContents.on('did-finish-load', finishListener);
  113. this.webContents.on('did-fail-load', failListener);
  114. this.webContents.on('did-start-navigation', navigationListener);
  115. this.webContents.on('did-stop-loading', stopLoadingListener);
  116. });
  117. // Add a no-op rejection handler to silence the unhandled rejection error.
  118. p.catch(() => {});
  119. this.pendingIndex = -1;
  120. this.webContents._loadURL(url, options);
  121. this.webContents.emit('load-url', url, options);
  122. return p;
  123. };
  124. NavigationController.prototype.getURL = function () {
  125. if (this.currentIndex === -1) {
  126. return '';
  127. } else {
  128. return this.history[this.currentIndex];
  129. }
  130. };
  131. NavigationController.prototype.stop = function () {
  132. this.pendingIndex = -1;
  133. return this.webContents._stop();
  134. };
  135. NavigationController.prototype.reload = function () {
  136. this.pendingIndex = this.currentIndex;
  137. return this.webContents._loadURL(this.getURL(), {});
  138. };
  139. NavigationController.prototype.reloadIgnoringCache = function () {
  140. this.pendingIndex = this.currentIndex;
  141. return this.webContents._loadURL(this.getURL(), {
  142. extraHeaders: 'pragma: no-cache\n',
  143. reloadIgnoringCache: true
  144. });
  145. };
  146. NavigationController.prototype.canGoBack = function () {
  147. return this.getActiveIndex() > 0;
  148. };
  149. NavigationController.prototype.canGoForward = function () {
  150. return this.getActiveIndex() < this.history.length - 1;
  151. };
  152. NavigationController.prototype.canGoToIndex = function (index) {
  153. return index >= 0 && index < this.history.length;
  154. };
  155. NavigationController.prototype.canGoToOffset = function (offset) {
  156. return this.canGoToIndex(this.currentIndex + offset);
  157. };
  158. NavigationController.prototype.clearHistory = function () {
  159. this.history = [];
  160. this.currentIndex = -1;
  161. this.pendingIndex = -1;
  162. this.inPageIndex = -1;
  163. };
  164. NavigationController.prototype.goBack = function () {
  165. if (!this.canGoBack()) {
  166. return;
  167. }
  168. this.pendingIndex = this.getActiveIndex() - 1;
  169. if (this.inPageIndex > -1 && this.pendingIndex >= this.inPageIndex) {
  170. return this.webContents._goBack();
  171. } else {
  172. return this.webContents._loadURL(this.history[this.pendingIndex], {});
  173. }
  174. };
  175. NavigationController.prototype.goForward = function () {
  176. if (!this.canGoForward()) {
  177. return;
  178. }
  179. this.pendingIndex = this.getActiveIndex() + 1;
  180. if (this.inPageIndex > -1 && this.pendingIndex >= this.inPageIndex) {
  181. return this.webContents._goForward();
  182. } else {
  183. return this.webContents._loadURL(this.history[this.pendingIndex], {});
  184. }
  185. };
  186. NavigationController.prototype.goToIndex = function (index) {
  187. if (!this.canGoToIndex(index)) {
  188. return;
  189. }
  190. this.pendingIndex = index;
  191. return this.webContents._loadURL(this.history[this.pendingIndex], {});
  192. };
  193. NavigationController.prototype.goToOffset = function (offset) {
  194. if (!this.canGoToOffset(offset)) {
  195. return;
  196. }
  197. const pendingIndex = this.currentIndex + offset;
  198. if (this.inPageIndex > -1 && pendingIndex >= this.inPageIndex) {
  199. this.pendingIndex = pendingIndex;
  200. return this.webContents._goToOffset(offset);
  201. } else {
  202. return this.goToIndex(pendingIndex);
  203. }
  204. };
  205. NavigationController.prototype.getActiveIndex = function () {
  206. if (this.pendingIndex === -1) {
  207. return this.currentIndex;
  208. } else {
  209. return this.pendingIndex;
  210. }
  211. };
  212. NavigationController.prototype.length = function () {
  213. return this.history.length;
  214. };
  215. return NavigationController;
  216. })();
  217. module.exports = NavigationController;