moduleloader.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. // Copyright 2008 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview The module loader for loading modules across the network.
  16. *
  17. * Browsers do not guarantee that scripts appended to the document
  18. * are executed in the order they are added. For production mode, we use
  19. * XHRs to load scripts, because they do not have this problem and they
  20. * have superior mechanisms for handling failure. However, XHR-evaled
  21. * scripts are harder to debug.
  22. *
  23. * In debugging mode, we use normal script tags. In order to make this work,
  24. * we load the scripts in serial: we do not execute script B to the document
  25. * until we are certain that script A is finished loading.
  26. *
  27. */
  28. goog.provide('goog.module.ModuleLoader');
  29. goog.require('goog.Timer');
  30. goog.require('goog.array');
  31. goog.require('goog.events');
  32. goog.require('goog.events.Event');
  33. goog.require('goog.events.EventHandler');
  34. goog.require('goog.events.EventId');
  35. goog.require('goog.events.EventTarget');
  36. goog.require('goog.labs.userAgent.browser');
  37. goog.require('goog.log');
  38. goog.require('goog.module.AbstractModuleLoader');
  39. goog.require('goog.net.BulkLoader');
  40. goog.require('goog.net.EventType');
  41. goog.require('goog.net.jsloader');
  42. goog.require('goog.userAgent');
  43. goog.require('goog.userAgent.product');
  44. /**
  45. * A class that loads Javascript modules.
  46. * @constructor
  47. * @extends {goog.events.EventTarget}
  48. * @implements {goog.module.AbstractModuleLoader}
  49. */
  50. goog.module.ModuleLoader = function() {
  51. goog.module.ModuleLoader.base(this, 'constructor');
  52. /**
  53. * Event handler for managing handling events.
  54. * @type {goog.events.EventHandler<!goog.module.ModuleLoader>}
  55. * @private
  56. */
  57. this.eventHandler_ = new goog.events.EventHandler(this);
  58. /**
  59. * A map from module IDs to goog.module.ModuleLoader.LoadStatus.
  60. * @type {!Object<Array<string>, goog.module.ModuleLoader.LoadStatus>}
  61. * @private
  62. */
  63. this.loadingModulesStatus_ = {};
  64. };
  65. goog.inherits(goog.module.ModuleLoader, goog.events.EventTarget);
  66. /**
  67. * A logger.
  68. * @type {goog.log.Logger}
  69. * @protected
  70. */
  71. goog.module.ModuleLoader.prototype.logger =
  72. goog.log.getLogger('goog.module.ModuleLoader');
  73. /**
  74. * Whether debug mode is enabled.
  75. * @type {boolean}
  76. * @private
  77. */
  78. goog.module.ModuleLoader.prototype.debugMode_ = false;
  79. /**
  80. * Whether source url injection is enabled.
  81. * @type {boolean}
  82. * @private
  83. */
  84. goog.module.ModuleLoader.prototype.sourceUrlInjection_ = false;
  85. /**
  86. * @return {boolean} Whether sourceURL affects stack traces.
  87. */
  88. goog.module.ModuleLoader.supportsSourceUrlStackTraces = function() {
  89. return goog.userAgent.product.CHROME ||
  90. (goog.labs.userAgent.browser.isFirefox() &&
  91. goog.labs.userAgent.browser.isVersionOrHigher('36'));
  92. };
  93. /**
  94. * @return {boolean} Whether sourceURL affects the debugger.
  95. */
  96. goog.module.ModuleLoader.supportsSourceUrlDebugger = function() {
  97. return goog.userAgent.product.CHROME || goog.userAgent.GECKO;
  98. };
  99. /**
  100. * Gets the debug mode for the loader.
  101. * @return {boolean} Whether the debug mode is enabled.
  102. */
  103. goog.module.ModuleLoader.prototype.getDebugMode = function() {
  104. return this.debugMode_;
  105. };
  106. /**
  107. * Sets the debug mode for the loader.
  108. * @param {boolean} debugMode Whether the debug mode is enabled.
  109. */
  110. goog.module.ModuleLoader.prototype.setDebugMode = function(debugMode) {
  111. this.debugMode_ = debugMode;
  112. };
  113. /**
  114. * When enabled, we will add a sourceURL comment to the end of all scripts
  115. * to mark their origin.
  116. *
  117. * On WebKit, stack traces will reflect the sourceURL comment, so this is
  118. * useful for debugging webkit stack traces in production.
  119. *
  120. * Notice that in debug mode, we will use source url injection + eval rather
  121. * then appending script nodes to the DOM, because the scripts will load far
  122. * faster. (Appending script nodes is very slow, because we can't parallelize
  123. * the downloading and evaling of the script).
  124. *
  125. * The cost of appending sourceURL information is negligible when compared to
  126. * the cost of evaling the script. Almost all clients will want this on.
  127. *
  128. * TODO(nicksantos): Turn this on by default. We may want to turn this off
  129. * for clients that inject their own sourceURL.
  130. *
  131. * @param {boolean} enabled Whether source url injection is enabled.
  132. */
  133. goog.module.ModuleLoader.prototype.setSourceUrlInjection = function(enabled) {
  134. this.sourceUrlInjection_ = enabled;
  135. };
  136. /**
  137. * @return {boolean} Whether we're using source url injection.
  138. * @private
  139. */
  140. goog.module.ModuleLoader.prototype.usingSourceUrlInjection_ = function() {
  141. return this.sourceUrlInjection_ ||
  142. (this.getDebugMode() &&
  143. goog.module.ModuleLoader.supportsSourceUrlStackTraces());
  144. };
  145. /** @override */
  146. goog.module.ModuleLoader.prototype.loadModules = function(
  147. ids, moduleInfoMap, opt_successFn, opt_errorFn, opt_timeoutFn,
  148. opt_forceReload) {
  149. var loadStatus = this.loadingModulesStatus_[ids] ||
  150. new goog.module.ModuleLoader.LoadStatus();
  151. loadStatus.loadRequested = true;
  152. loadStatus.successFn = opt_successFn || null;
  153. loadStatus.errorFn = opt_errorFn || null;
  154. if (!this.loadingModulesStatus_[ids]) {
  155. // Modules were not prefetched.
  156. this.loadingModulesStatus_[ids] = loadStatus;
  157. this.downloadModules_(ids, moduleInfoMap);
  158. // TODO(user): Need to handle timeouts in the module loading code.
  159. } else if (goog.isDefAndNotNull(loadStatus.responseTexts)) {
  160. // Modules prefetch is complete.
  161. this.evaluateCode_(ids);
  162. }
  163. // Otherwise modules prefetch is in progress, and these modules will be
  164. // executed after the prefetch is complete.
  165. };
  166. /**
  167. * Evaluate the JS code.
  168. * @param {Array<string>} moduleIds The module ids.
  169. * @private
  170. */
  171. goog.module.ModuleLoader.prototype.evaluateCode_ = function(moduleIds) {
  172. this.dispatchEvent(
  173. new goog.module.ModuleLoader.RequestSuccessEvent(moduleIds));
  174. goog.log.info(this.logger, 'evaluateCode ids:' + moduleIds);
  175. var loadStatus = this.loadingModulesStatus_[moduleIds];
  176. var uris = loadStatus.requestUris;
  177. var texts = loadStatus.responseTexts;
  178. var error = null;
  179. try {
  180. if (this.usingSourceUrlInjection_()) {
  181. for (var i = 0; i < uris.length; i++) {
  182. var uri = uris[i];
  183. goog.globalEval(texts[i] + ' //# sourceURL=' + uri);
  184. }
  185. } else {
  186. goog.globalEval(texts.join('\n'));
  187. }
  188. } catch (e) {
  189. error = e;
  190. // TODO(user): Consider throwing an exception here.
  191. goog.log.warning(
  192. this.logger, 'Loaded incomplete code for module(s): ' + moduleIds, e);
  193. }
  194. this.dispatchEvent(new goog.module.ModuleLoader.EvaluateCodeEvent(moduleIds));
  195. if (error) {
  196. this.handleErrorHelper_(
  197. moduleIds, loadStatus.errorFn, null /* status */, error);
  198. } else if (loadStatus.successFn) {
  199. loadStatus.successFn();
  200. }
  201. delete this.loadingModulesStatus_[moduleIds];
  202. };
  203. /**
  204. * Handles a successful response to a request for prefetch or load one or more
  205. * modules.
  206. *
  207. * @param {goog.net.BulkLoader} bulkLoader The bulk loader.
  208. * @param {Array<string>} moduleIds The ids of the modules requested.
  209. * @private
  210. */
  211. goog.module.ModuleLoader.prototype.handleSuccess_ = function(
  212. bulkLoader, moduleIds) {
  213. goog.log.info(this.logger, 'Code loaded for module(s): ' + moduleIds);
  214. var loadStatus = this.loadingModulesStatus_[moduleIds];
  215. loadStatus.responseTexts = bulkLoader.getResponseTexts();
  216. if (loadStatus.loadRequested) {
  217. this.evaluateCode_(moduleIds);
  218. }
  219. // NOTE: A bulk loader instance is used for loading a set of module ids.
  220. // Once these modules have been loaded successfully or in error the bulk
  221. // loader should be disposed as it is not needed anymore. A new bulk loader
  222. // is instantiated for any new modules to be loaded. The dispose is called
  223. // on a timer so that the bulkloader has a chance to release its
  224. // objects.
  225. goog.Timer.callOnce(bulkLoader.dispose, 5, bulkLoader);
  226. };
  227. /** @override */
  228. goog.module.ModuleLoader.prototype.prefetchModule = function(id, moduleInfo) {
  229. // Do not prefetch in debug mode.
  230. if (this.getDebugMode()) {
  231. return;
  232. }
  233. var loadStatus = this.loadingModulesStatus_[[id]];
  234. if (loadStatus) {
  235. return;
  236. }
  237. var moduleInfoMap = {};
  238. moduleInfoMap[id] = moduleInfo;
  239. this.loadingModulesStatus_[[id]] = new goog.module.ModuleLoader.LoadStatus();
  240. this.downloadModules_([id], moduleInfoMap);
  241. };
  242. /**
  243. * Downloads a list of JavaScript modules.
  244. *
  245. * @param {Array<string>} ids The module ids in dependency order.
  246. * @param {Object} moduleInfoMap A mapping from module id to ModuleInfo object.
  247. * @private
  248. */
  249. goog.module.ModuleLoader.prototype.downloadModules_ = function(
  250. ids, moduleInfoMap) {
  251. var uris = [];
  252. for (var i = 0; i < ids.length; i++) {
  253. goog.array.extend(uris, moduleInfoMap[ids[i]].getUris());
  254. }
  255. goog.log.info(this.logger, 'downloadModules ids:' + ids + ' uris:' + uris);
  256. if (this.getDebugMode() && !this.usingSourceUrlInjection_()) {
  257. // In debug mode use <script> tags rather than XHRs to load the files.
  258. // This makes it possible to debug and inspect stack traces more easily.
  259. // It's also possible to use it to load JavaScript files that are hosted on
  260. // another domain.
  261. // The scripts need to load serially, so this is much slower than parallel
  262. // script loads with source url injection.
  263. goog.net.jsloader.loadMany(uris);
  264. } else {
  265. var loadStatus = this.loadingModulesStatus_[ids];
  266. loadStatus.requestUris = uris;
  267. var bulkLoader = new goog.net.BulkLoader(uris);
  268. var eventHandler = this.eventHandler_;
  269. eventHandler.listen(
  270. bulkLoader, goog.net.EventType.SUCCESS,
  271. goog.bind(this.handleSuccess_, this, bulkLoader, ids));
  272. eventHandler.listen(
  273. bulkLoader, goog.net.EventType.ERROR,
  274. goog.bind(this.handleError_, this, bulkLoader, ids));
  275. bulkLoader.load();
  276. }
  277. };
  278. /**
  279. * Handles an error during a request for one or more modules.
  280. * @param {goog.net.BulkLoader} bulkLoader The bulk loader.
  281. * @param {Array<string>} moduleIds The ids of the modules requested.
  282. * @param {number} status The response status.
  283. * @private
  284. */
  285. goog.module.ModuleLoader.prototype.handleError_ = function(
  286. bulkLoader, moduleIds, status) {
  287. var loadStatus = this.loadingModulesStatus_[moduleIds];
  288. // The bulk loader doesn't cancel other requests when a request fails. We will
  289. // delete the loadStatus in the first failure, so it will be undefined in
  290. // subsequent errors.
  291. if (loadStatus) {
  292. delete this.loadingModulesStatus_[moduleIds];
  293. this.handleErrorHelper_(moduleIds, loadStatus.errorFn, status);
  294. }
  295. // NOTE: A bulk loader instance is used for loading a set of module ids. Once
  296. // these modules have been loaded successfully or in error the bulk loader
  297. // should be disposed as it is not needed anymore. A new bulk loader is
  298. // instantiated for any new modules to be loaded. The dispose is called
  299. // on another thread so that the bulkloader has a chance to release its
  300. // objects.
  301. goog.Timer.callOnce(bulkLoader.dispose, 5, bulkLoader);
  302. };
  303. /**
  304. * Handles an error during a request for one or more modules.
  305. * @param {Array<string>} moduleIds The ids of the modules requested.
  306. * @param {?function(?number)} errorFn The function to call on failure.
  307. * @param {?number} status The response status.
  308. * @param {!Error=} opt_error The error encountered, if available.
  309. * @private
  310. */
  311. goog.module.ModuleLoader.prototype.handleErrorHelper_ = function(
  312. moduleIds, errorFn, status, opt_error) {
  313. this.dispatchEvent(
  314. new goog.module.ModuleLoader.RequestErrorEvent(moduleIds, opt_error));
  315. goog.log.warning(this.logger, 'Request failed for module(s): ' + moduleIds);
  316. if (errorFn) {
  317. errorFn(status);
  318. }
  319. };
  320. /** @override */
  321. goog.module.ModuleLoader.prototype.disposeInternal = function() {
  322. goog.module.ModuleLoader.superClass_.disposeInternal.call(this);
  323. this.eventHandler_.dispose();
  324. this.eventHandler_ = null;
  325. };
  326. /**
  327. * Events dispatched by the ModuleLoader.
  328. * @const
  329. */
  330. goog.module.ModuleLoader.EventType = {
  331. /**
  332. * @const {!goog.events.EventId<
  333. * !goog.module.ModuleLoader.EvaluateCodeEvent>} Called after the code for
  334. * a module is evaluated.
  335. */
  336. EVALUATE_CODE:
  337. new goog.events.EventId(goog.events.getUniqueId('evaluateCode')),
  338. /**
  339. * @const {!goog.events.EventId<
  340. * !goog.module.ModuleLoader.RequestSuccessEvent>} Called when the
  341. * BulkLoader finishes successfully.
  342. */
  343. REQUEST_SUCCESS:
  344. new goog.events.EventId(goog.events.getUniqueId('requestSuccess')),
  345. /**
  346. * @const {!goog.events.EventId<
  347. * !goog.module.ModuleLoader.RequestErrorEvent>} Called when the
  348. * BulkLoader fails, or code loading fails.
  349. */
  350. REQUEST_ERROR:
  351. new goog.events.EventId(goog.events.getUniqueId('requestError'))
  352. };
  353. /**
  354. * @param {Array<string>} moduleIds The ids of the modules being evaluated.
  355. * @constructor
  356. * @extends {goog.events.Event}
  357. * @final
  358. * @protected
  359. */
  360. goog.module.ModuleLoader.EvaluateCodeEvent = function(moduleIds) {
  361. goog.module.ModuleLoader.EvaluateCodeEvent.base(
  362. this, 'constructor', goog.module.ModuleLoader.EventType.EVALUATE_CODE);
  363. /**
  364. * @type {Array<string>}
  365. */
  366. this.moduleIds = moduleIds;
  367. };
  368. goog.inherits(goog.module.ModuleLoader.EvaluateCodeEvent, goog.events.Event);
  369. /**
  370. * @param {Array<string>} moduleIds The ids of the modules being evaluated.
  371. * @constructor
  372. * @extends {goog.events.Event}
  373. * @final
  374. * @protected
  375. */
  376. goog.module.ModuleLoader.RequestSuccessEvent = function(moduleIds) {
  377. goog.module.ModuleLoader.RequestSuccessEvent.base(
  378. this, 'constructor', goog.module.ModuleLoader.EventType.REQUEST_SUCCESS);
  379. /**
  380. * @type {Array<string>}
  381. */
  382. this.moduleIds = moduleIds;
  383. };
  384. goog.inherits(goog.module.ModuleLoader.RequestSuccessEvent, goog.events.Event);
  385. /**
  386. * @param {Array<string>} moduleIds The ids of the modules being evaluated.
  387. * @param {!Error=} opt_error The error encountered, if available.
  388. * @constructor
  389. * @extends {goog.events.Event}
  390. * @final
  391. * @protected
  392. */
  393. goog.module.ModuleLoader.RequestErrorEvent = function(moduleIds, opt_error) {
  394. goog.module.ModuleLoader.RequestErrorEvent.base(
  395. this, 'constructor', goog.module.ModuleLoader.EventType.REQUEST_ERROR);
  396. /**
  397. * @type {Array<string>}
  398. */
  399. this.moduleIds = moduleIds;
  400. /** @type {?Error} */
  401. this.error = opt_error || null;
  402. };
  403. goog.inherits(goog.module.ModuleLoader.RequestErrorEvent, goog.events.Event);
  404. /**
  405. * A class that keeps the state of the module during the loading process. It is
  406. * used to save loading information between modules download and evaluation.
  407. * @constructor
  408. * @final
  409. */
  410. goog.module.ModuleLoader.LoadStatus = function() {
  411. /**
  412. * The request uris.
  413. * @type {Array<string>}
  414. */
  415. this.requestUris = null;
  416. /**
  417. * The response texts.
  418. * @type {Array<string>}
  419. */
  420. this.responseTexts = null;
  421. /**
  422. * Whether loadModules was called for the set of modules referred by this
  423. * status.
  424. * @type {boolean}
  425. */
  426. this.loadRequested = false;
  427. /**
  428. * Success callback.
  429. * @type {?function()}
  430. */
  431. this.successFn = null;
  432. /**
  433. * Error callback.
  434. * @type {?function(?number)}
  435. */
  436. this.errorFn = null;
  437. };