flutter.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. // Copyright 2014 The Flutter Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. if (!_flutter) {
  5. var _flutter = {};
  6. }
  7. _flutter.loader = null;
  8. (function () {
  9. "use strict";
  10. /**
  11. * Wraps `promise` in a timeout of the given `duration` in ms.
  12. *
  13. * Resolves/rejects with whatever the original `promises` does, or rejects
  14. * if `promise` takes longer to complete than `duration`. In that case,
  15. * `debugName` is used to compose a legible error message.
  16. *
  17. * If `duration` is < 0, the original `promise` is returned unchanged.
  18. * @param {Promise} promise
  19. * @param {number} duration
  20. * @param {string} debugName
  21. * @returns {Promise} a wrapped promise.
  22. */
  23. async function timeout(promise, duration, debugName) {
  24. if (duration < 0) {
  25. return promise;
  26. }
  27. let timeoutId;
  28. const _clock = new Promise((_, reject) => {
  29. timeoutId = setTimeout(() => {
  30. reject(
  31. new Error(
  32. `${debugName} took more than ${duration}ms to resolve. Moving on.`,
  33. {
  34. cause: timeout,
  35. }
  36. )
  37. );
  38. }, duration);
  39. });
  40. return Promise.race([promise, _clock]).finally(() => {
  41. clearTimeout(timeoutId);
  42. });
  43. }
  44. /**
  45. * Handles the creation of a TrustedTypes `policy` that validates URLs based
  46. * on an (optional) incoming array of RegExes.
  47. */
  48. class FlutterTrustedTypesPolicy {
  49. /**
  50. * Constructs the policy.
  51. * @param {[RegExp]} validPatterns the patterns to test URLs
  52. * @param {String} policyName the policy name (optional)
  53. */
  54. constructor(validPatterns, policyName = "flutter-js") {
  55. const patterns = validPatterns || [
  56. /\.dart\.js$/,
  57. /^flutter_service_worker.js$/
  58. ];
  59. if (window.trustedTypes) {
  60. this.policy = trustedTypes.createPolicy(policyName, {
  61. createScriptURL: function(url) {
  62. const parsed = new URL(url, window.location);
  63. const file = parsed.pathname.split("/").pop();
  64. const matches = patterns.some((pattern) => pattern.test(file));
  65. if (matches) {
  66. return parsed.toString();
  67. }
  68. console.error(
  69. "URL rejected by TrustedTypes policy",
  70. policyName, ":", url, "(download prevented)");
  71. }
  72. });
  73. }
  74. }
  75. }
  76. /**
  77. * Handles loading/reloading Flutter's service worker, if configured.
  78. *
  79. * @see: https://developers.google.com/web/fundamentals/primers/service-workers
  80. */
  81. class FlutterServiceWorkerLoader {
  82. /**
  83. * Injects a TrustedTypesPolicy (or undefined if the feature is not supported).
  84. * @param {TrustedTypesPolicy | undefined} policy
  85. */
  86. setTrustedTypesPolicy(policy) {
  87. this._ttPolicy = policy;
  88. }
  89. /**
  90. * Returns a Promise that resolves when the latest Flutter service worker,
  91. * configured by `settings` has been loaded and activated.
  92. *
  93. * Otherwise, the promise is rejected with an error message.
  94. * @param {*} settings Service worker settings
  95. * @returns {Promise} that resolves when the latest serviceWorker is ready.
  96. */
  97. loadServiceWorker(settings) {
  98. if (!("serviceWorker" in navigator) || settings == null) {
  99. // In the future, settings = null -> uninstall service worker?
  100. return Promise.reject(
  101. new Error("Service worker not supported (or configured).")
  102. );
  103. }
  104. const {
  105. serviceWorkerVersion,
  106. serviceWorkerUrl = "flutter_service_worker.js?v=" +
  107. serviceWorkerVersion,
  108. timeoutMillis = 4000,
  109. } = settings;
  110. // Apply the TrustedTypes policy, if present.
  111. let url = serviceWorkerUrl;
  112. if (this._ttPolicy != null) {
  113. url = this._ttPolicy.createScriptURL(url);
  114. }
  115. const serviceWorkerActivation = navigator.serviceWorker
  116. .register(url)
  117. .then(this._getNewServiceWorker)
  118. .then(this._waitForServiceWorkerActivation);
  119. // Timeout race promise
  120. return timeout(
  121. serviceWorkerActivation,
  122. timeoutMillis,
  123. "prepareServiceWorker"
  124. );
  125. }
  126. /**
  127. * Returns the latest service worker for the given `serviceWorkerRegistrationPromise`.
  128. *
  129. * This might return the current service worker, if there's no new service worker
  130. * awaiting to be installed/updated.
  131. *
  132. * @param {Promise<ServiceWorkerRegistration>} serviceWorkerRegistrationPromise
  133. * @returns {Promise<ServiceWorker>}
  134. */
  135. async _getNewServiceWorker(serviceWorkerRegistrationPromise) {
  136. const reg = await serviceWorkerRegistrationPromise;
  137. if (!reg.active && (reg.installing || reg.waiting)) {
  138. // No active web worker and we have installed or are installing
  139. // one for the first time. Simply wait for it to activate.
  140. console.debug("Installing/Activating first service worker.");
  141. return reg.installing || reg.waiting;
  142. } else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
  143. // When the app updates the serviceWorkerVersion changes, so we
  144. // need to ask the service worker to update.
  145. return reg.update().then((newReg) => {
  146. console.debug("Updating service worker.");
  147. return newReg.installing || newReg.waiting || newReg.active;
  148. });
  149. } else {
  150. console.debug("Loading from existing service worker.");
  151. return reg.active;
  152. }
  153. }
  154. /**
  155. * Returns a Promise that resolves when the `latestServiceWorker` changes its
  156. * state to "activated".
  157. *
  158. * @param {Promise<ServiceWorker>} latestServiceWorkerPromise
  159. * @returns {Promise<void>}
  160. */
  161. async _waitForServiceWorkerActivation(latestServiceWorkerPromise) {
  162. const serviceWorker = await latestServiceWorkerPromise;
  163. if (!serviceWorker || serviceWorker.state == "activated") {
  164. if (!serviceWorker) {
  165. return Promise.reject(
  166. new Error("Cannot activate a null service worker!")
  167. );
  168. } else {
  169. console.debug("Service worker already active.");
  170. return Promise.resolve();
  171. }
  172. }
  173. return new Promise((resolve, _) => {
  174. serviceWorker.addEventListener("statechange", () => {
  175. if (serviceWorker.state == "activated") {
  176. console.debug("Activated new service worker.");
  177. resolve();
  178. }
  179. });
  180. });
  181. }
  182. }
  183. /**
  184. * Handles injecting the main Flutter web entrypoint (main.dart.js), and notifying
  185. * the user when Flutter is ready, through `didCreateEngineInitializer`.
  186. *
  187. * @see https://docs.flutter.dev/development/platform-integration/web/initialization
  188. */
  189. class FlutterEntrypointLoader {
  190. /**
  191. * Creates a FlutterEntrypointLoader.
  192. */
  193. constructor() {
  194. // Watchdog to prevent injecting the main entrypoint multiple times.
  195. this._scriptLoaded = false;
  196. }
  197. /**
  198. * Injects a TrustedTypesPolicy (or undefined if the feature is not supported).
  199. * @param {TrustedTypesPolicy | undefined} policy
  200. */
  201. setTrustedTypesPolicy(policy) {
  202. this._ttPolicy = policy;
  203. }
  204. /**
  205. * Loads flutter main entrypoint, specified by `entrypointUrl`, and calls a
  206. * user-specified `onEntrypointLoaded` callback with an EngineInitializer
  207. * object when it's done.
  208. *
  209. * @param {*} options
  210. * @returns {Promise | undefined} that will eventually resolve with an
  211. * EngineInitializer, or will be rejected with the error caused by the loader.
  212. * Returns undefined when an `onEntrypointLoaded` callback is supplied in `options`.
  213. */
  214. async loadEntrypoint(options) {
  215. const { entrypointUrl = "main.dart.js", onEntrypointLoaded } =
  216. options || {};
  217. return this._loadEntrypoint(entrypointUrl, onEntrypointLoaded);
  218. }
  219. /**
  220. * Resolves the promise created by loadEntrypoint, and calls the `onEntrypointLoaded`
  221. * function supplied by the user (if needed).
  222. *
  223. * Called by Flutter through `_flutter.loader.didCreateEngineInitializer` method,
  224. * which is bound to the correct instance of the FlutterEntrypointLoader by
  225. * the FlutterLoader object.
  226. *
  227. * @param {Function} engineInitializer @see https://github.com/flutter/engine/blob/main/lib/web_ui/lib/src/engine/js_interop/js_loader.dart#L42
  228. */
  229. didCreateEngineInitializer(engineInitializer) {
  230. if (typeof this._didCreateEngineInitializerResolve === "function") {
  231. this._didCreateEngineInitializerResolve(engineInitializer);
  232. // Remove the resolver after the first time, so Flutter Web can hot restart.
  233. this._didCreateEngineInitializerResolve = null;
  234. // Make the engine revert to "auto" initialization on hot restart.
  235. delete _flutter.loader.didCreateEngineInitializer;
  236. }
  237. if (typeof this._onEntrypointLoaded === "function") {
  238. this._onEntrypointLoaded(engineInitializer);
  239. }
  240. }
  241. /**
  242. * Injects a script tag into the DOM, and configures this loader to be able to
  243. * handle the "entrypoint loaded" notifications received from Flutter web.
  244. *
  245. * @param {string} entrypointUrl the URL of the script that will initialize
  246. * Flutter.
  247. * @param {Function} onEntrypointLoaded a callback that will be called when
  248. * Flutter web notifies this object that the entrypoint is
  249. * loaded.
  250. * @returns {Promise | undefined} a Promise that resolves when the entrypoint
  251. * is loaded, or undefined if `onEntrypointLoaded`
  252. * is a function.
  253. */
  254. _loadEntrypoint(entrypointUrl, onEntrypointLoaded) {
  255. const useCallback = typeof onEntrypointLoaded === "function";
  256. if (!this._scriptLoaded) {
  257. this._scriptLoaded = true;
  258. const scriptTag = this._createScriptTag(entrypointUrl);
  259. if (useCallback) {
  260. // Just inject the script tag, and return nothing; Flutter will call
  261. // `didCreateEngineInitializer` when it's done.
  262. console.debug("Injecting <script> tag. Using callback.");
  263. this._onEntrypointLoaded = onEntrypointLoaded;
  264. document.body.append(scriptTag);
  265. } else {
  266. // Inject the script tag and return a promise that will get resolved
  267. // with the EngineInitializer object from Flutter when it calls
  268. // `didCreateEngineInitializer` later.
  269. return new Promise((resolve, reject) => {
  270. console.debug(
  271. "Injecting <script> tag. Using Promises. Use the callback approach instead!"
  272. );
  273. this._didCreateEngineInitializerResolve = resolve;
  274. scriptTag.addEventListener("error", reject);
  275. document.body.append(scriptTag);
  276. });
  277. }
  278. }
  279. }
  280. /**
  281. * Creates a script tag for the given URL.
  282. * @param {string} url
  283. * @returns {HTMLScriptElement}
  284. */
  285. _createScriptTag(url) {
  286. const scriptTag = document.createElement("script");
  287. scriptTag.type = "application/javascript";
  288. // Apply TrustedTypes validation, if available.
  289. let trustedUrl = url;
  290. if (this._ttPolicy != null) {
  291. trustedUrl = this._ttPolicy.createScriptURL(url);
  292. }
  293. scriptTag.src = trustedUrl;
  294. return scriptTag;
  295. }
  296. }
  297. /**
  298. * The public interface of _flutter.loader. Exposes two methods:
  299. * * loadEntrypoint (which coordinates the default Flutter web loading procedure)
  300. * * didCreateEngineInitializer (which is called by Flutter to notify that its
  301. * Engine is ready to be initialized)
  302. */
  303. class FlutterLoader {
  304. /**
  305. * Initializes the Flutter web app.
  306. * @param {*} options
  307. * @returns {Promise?} a (Deprecated) Promise that will eventually resolve
  308. * with an EngineInitializer, or will be rejected with
  309. * any error caused by the loader. Or Null, if the user
  310. * supplies an `onEntrypointLoaded` Function as an option.
  311. */
  312. async loadEntrypoint(options) {
  313. const { serviceWorker, ...entrypoint } = options || {};
  314. // A Trusted Types policy that is going to be used by the loader.
  315. const flutterTT = new FlutterTrustedTypesPolicy();
  316. // The FlutterServiceWorkerLoader instance could be injected as a dependency
  317. // (and dynamically imported from a module if not present).
  318. const serviceWorkerLoader = new FlutterServiceWorkerLoader();
  319. serviceWorkerLoader.setTrustedTypesPolicy(flutterTT.policy);
  320. await serviceWorkerLoader.loadServiceWorker(serviceWorker).catch(e => {
  321. // Regardless of what happens with the injection of the SW, the show must go on
  322. console.warn("Exception while loading service worker:", e);
  323. });
  324. // The FlutterEntrypointLoader instance could be injected as a dependency
  325. // (and dynamically imported from a module if not present).
  326. const entrypointLoader = new FlutterEntrypointLoader();
  327. entrypointLoader.setTrustedTypesPolicy(flutterTT.policy);
  328. // Install the `didCreateEngineInitializer` listener where Flutter web expects it to be.
  329. this.didCreateEngineInitializer =
  330. entrypointLoader.didCreateEngineInitializer.bind(entrypointLoader);
  331. return entrypointLoader.loadEntrypoint(entrypoint);
  332. }
  333. }
  334. _flutter.loader = new FlutterLoader();
  335. })();