Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
04f7ea84
Unverified
Commit
04f7ea84
authored
Aug 03, 2022
by
David Iglesias
Committed by
GitHub
Aug 03, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
[web] Add onEntrypointLoaded to FlutterLoader. (#108776)
parent
f7b00234
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
299 additions
and
117 deletions
+299
-117
service_worker_test.dart
dev/bots/service_worker_test.dart
+6
-0
test.dart
dev/bots/test.dart
+2
-0
index_with_flutterjs_entrypoint_loaded.html
...tests/web/web/index_with_flutterjs_entrypoint_loaded.html
+42
-0
md
dev/md
+0
-0
flutter_js.dart
...flutter_tools/lib/src/web/file_generators/flutter_js.dart
+249
-117
No files found.
dev/bots/service_worker_test.dart
View file @
04f7ea84
...
...
@@ -29,6 +29,7 @@ enum ServiceWorkerTestType {
withoutFlutterJs
,
withFlutterJs
,
withFlutterJsShort
,
withFlutterJsEntrypointLoadedEvent
,
}
// Run a web service worker test as a standalone Dart program.
...
...
@@ -36,9 +37,11 @@ Future<void> main() async {
await
runWebServiceWorkerTest
(
headless:
false
,
testType:
ServiceWorkerTestType
.
withoutFlutterJs
);
await
runWebServiceWorkerTest
(
headless:
false
,
testType:
ServiceWorkerTestType
.
withFlutterJs
);
await
runWebServiceWorkerTest
(
headless:
false
,
testType:
ServiceWorkerTestType
.
withFlutterJsShort
);
await
runWebServiceWorkerTest
(
headless:
false
,
testType:
ServiceWorkerTestType
.
withFlutterJsEntrypointLoadedEvent
);
await
runWebServiceWorkerTestWithCachingResources
(
headless:
false
,
testType:
ServiceWorkerTestType
.
withoutFlutterJs
);
await
runWebServiceWorkerTestWithCachingResources
(
headless:
false
,
testType:
ServiceWorkerTestType
.
withFlutterJs
);
await
runWebServiceWorkerTestWithCachingResources
(
headless:
false
,
testType:
ServiceWorkerTestType
.
withFlutterJsShort
);
await
runWebServiceWorkerTestWithCachingResources
(
headless:
false
,
testType:
ServiceWorkerTestType
.
withFlutterJsEntrypointLoadedEvent
);
await
runWebServiceWorkerTestWithBlockedServiceWorkers
(
headless:
false
);
}
...
...
@@ -67,6 +70,9 @@ String _testTypeToIndexFile(ServiceWorkerTestType type) {
case
ServiceWorkerTestType
.
withFlutterJsShort
:
indexFile
=
'index_with_flutterjs_short.html'
;
break
;
case
ServiceWorkerTestType
.
withFlutterJsEntrypointLoadedEvent
:
indexFile
=
'index_with_flutterjs_entrypoint_loaded.html'
;
break
;
}
return
indexFile
;
}
...
...
dev/bots/test.dart
View file @
04f7ea84
...
...
@@ -1092,9 +1092,11 @@ Future<void> _runWebLongRunningTests() async {
()
=>
runWebServiceWorkerTest
(
headless:
true
,
testType:
ServiceWorkerTestType
.
withoutFlutterJs
),
()
=>
runWebServiceWorkerTest
(
headless:
true
,
testType:
ServiceWorkerTestType
.
withFlutterJs
),
()
=>
runWebServiceWorkerTest
(
headless:
true
,
testType:
ServiceWorkerTestType
.
withFlutterJsShort
),
()
=>
runWebServiceWorkerTest
(
headless:
true
,
testType:
ServiceWorkerTestType
.
withFlutterJsEntrypointLoadedEvent
),
()
=>
runWebServiceWorkerTestWithCachingResources
(
headless:
true
,
testType:
ServiceWorkerTestType
.
withoutFlutterJs
),
()
=>
runWebServiceWorkerTestWithCachingResources
(
headless:
true
,
testType:
ServiceWorkerTestType
.
withFlutterJs
),
()
=>
runWebServiceWorkerTestWithCachingResources
(
headless:
true
,
testType:
ServiceWorkerTestType
.
withFlutterJsShort
),
()
=>
runWebServiceWorkerTestWithCachingResources
(
headless:
true
,
testType:
ServiceWorkerTestType
.
withFlutterJsEntrypointLoadedEvent
),
()
=>
runWebServiceWorkerTestWithBlockedServiceWorkers
(
headless:
true
),
()
=>
_runWebStackTraceTest
(
'profile'
,
'lib/stack_trace.dart'
),
()
=>
_runWebStackTraceTest
(
'release'
,
'lib/stack_trace.dart'
),
...
...
dev/integration_tests/web/web/index_with_flutterjs_entrypoint_loaded.html
0 → 100644
View file @
04f7ea84
<!DOCTYPE HTML>
<!-- Copyright 2014 The Flutter Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<html>
<head>
<meta
charset=
"UTF-8"
>
<meta
content=
"IE=Edge"
http-equiv=
"X-UA-Compatible"
>
<title>
Integration test. App load with flutter.js and onEntrypointLoaded API
</title>
<!-- iOS meta tags & icons -->
<meta
name=
"apple-mobile-web-app-capable"
content=
"yes"
>
<meta
name=
"apple-mobile-web-app-status-bar-style"
content=
"black"
>
<meta
name=
"apple-mobile-web-app-title"
content=
"Web Test"
>
<link
rel=
"manifest"
href=
"manifest.json"
>
<script>
// The value below is injected by flutter build, do not touch.
var
serviceWorkerVersion
=
null
;
</script>
<!-- This script adds the flutter initialization JS code -->
<script
src=
"flutter.js"
defer
></script>
</head>
<body>
<script>
window
.
addEventListener
(
'load'
,
function
(
ev
)
{
// Download main.dart.js
_flutter
.
loader
.
loadEntrypoint
({
onEntrypointLoaded
:
onEntrypointLoaded
,
serviceWorker
:
{
serviceWorkerVersion
:
serviceWorkerVersion
,
}
});
// Once the entrypoint is ready, do things!
async
function
onEntrypointLoaded
(
engineInitializer
)
{
const
appRunner
=
await
engineInitializer
.
initializeEngine
();
appRunner
.
runApp
();
}
});
</script>
</body>
</html>
dev/md
0 → 100644
View file @
04f7ea84
packages/flutter_tools/lib/src/web/file_generators/flutter_js.dart
View file @
04f7ea84
...
...
@@ -8,169 +8,301 @@
/// flutter.js should be completely static, so **do not use any parameter or
/// environment variable to generate this file**.
String
generateFlutterJsFile
(
)
{
return
'''
return
r
''
'
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* This script installs service_worker.js to provide PWA functionality to
* application. For more information, see:
* https://developers.google.com/web/fundamentals/primers/service-workers
*/
if (!_flutter) {
var _flutter = {};
}
_flutter.loader = null;
(function() {
(function
() {
"use strict";
class FlutterLoader {
/**
* Creates a FlutterLoader, and initializes its instance methods.
*/
constructor() {
// TODO: Move the below methods to "#private" once supported by all the browsers
// we support. In the meantime, we use the "revealing module" pattern.
// Watchdog to prevent injecting the main entrypoint multiple times.
this._scriptLoaded = null;
// Resolver for the pending promise returned by loadEntrypoint.
this._didCreateEngineInitializerResolve = null;
// Called by Flutter web.
// Bound to `this` now, so "this" is preserved across JS <-> Flutter jumps.
this.didCreateEngineInitializer = this._didCreateEngineInitializer.bind(this);
/**
* Wraps `promise` in a timeout of the given `duration` in ms.
*
* Resolves/rejects with whatever the original `promises` does, or rejects
* if `promise` takes longer to complete than `duration`. In that case,
* `debugName` is used to compose a legible error message.
*
* If `duration` is < 0, the original `promise` is returned unchanged.
* @param {Promise} promise
* @param {number} duration
* @param {string} debugName
* @returns {Promise} a wrapped promise.
*/
async function timeout(promise, duration, debugName) {
if (duration < 0) {
return promise;
}
let timeoutId;
const _clock = new Promise((_, reject) => {
timeoutId = setTimeout(() => {
reject(
new Error(
`
${debugName}
took more than
${duration}
ms to resolve. Moving on.`,
{
cause: timeout,
}
)
);
}, duration);
});
return Promise.race([promise, _clock]).finally(() => {
clearTimeout(timeoutId);
});
}
/**
* Handles loading/reloading Flutter'
s
service
worker
,
if
configured
.
*
*
@see
:
https:
//developers.google.com/web/fundamentals/primers/service-workers
*/
class
FlutterServiceWorkerLoader
{
/**
* Initializes the main.dart.js with/without serviceWorker.
* @param {*} options
* @returns a Promise that will eventually resolve with an EngineInitializer,
* or will be rejected with the error caused by the loader.
* Returns a Promise that resolves when the latest Flutter service worker,
* configured by `settings` has been loaded and activated.
*
* Otherwise, the promise is rejected with an error message.
* @param {*} settings Service worker settings
* @returns {Promise} that resolves when the latest serviceWorker is ready.
*/
loadEntrypoint(options) {
loadServiceWorker
(
settings
)
{
if
(!(
"serviceWorker"
in
navigator
)
||
settings
==
null
)
{
// In the future, settings = null -> uninstall service worker?
return
Promise
.
reject
(
new
Error
(
"Service worker not supported (or configured)."
)
);
}
const
{
entrypointUrl = "main.dart.js",
serviceWorker,
} = (options || {});
return this._loadWithServiceWorker(entrypointUrl, serviceWorker);
serviceWorkerVersion
,
serviceWorkerUrl
=
"flutter_service_worker.js?v="
+
serviceWorkerVersion
,
timeoutMillis
=
4000
,
}
=
settings
;
const
serviceWorkerActivation
=
navigator
.
serviceWorker
.
register
(
serviceWorkerUrl
)
.
then
(
this
.
_getNewServiceWorker
)
.
then
(
this
.
_waitForServiceWorkerActivation
);
// Timeout race promise
return
timeout
(
serviceWorkerActivation
,
timeoutMillis
,
"prepareServiceWorker"
);
}
/**
* Resolves the promise created by loadEntrypoint.
* Called by Flutter through the public `didCreateEngineInitializer` method,
* which is bound to the correct instance of the FlutterLoader on the page.
* @param {*} engineInitializer
* Returns the latest service worker for the given `serviceWorkerRegistrationPromise`.
*
* This might return the current service worker, if there's no new service worker
* awaiting to be installed/updated.
*
* @param {Promise<ServiceWorkerRegistration>} serviceWorkerRegistrationPromise
* @returns {Promise<ServiceWorker>}
*/
_didCreateEngineInitializer(engineInitializer) {
if (typeof this._didCreateEngineInitializerResolve != "function") {
console.warn("Do not call didCreateEngineInitializer by hand. Start with loadEntrypoint instead.");
}
this._didCreateEngineInitializerResolve(engineInitializer);
// Remove the public method after it'
s
done
,
so
Flutter
Web
can
hot
restart
.
delete
this
.
didCreateEngineInitializer
;
}
async
_getNewServiceWorker
(
serviceWorkerRegistrationPromise
)
{
const
reg
=
await
serviceWorkerRegistrationPromise
;
_loadEntrypoint
(
entrypointUrl
)
{
if
(!
this
.
_scriptLoaded
)
{
console
.
debug
(
"Injecting <script> tag."
);
this
.
_scriptLoaded
=
new
Promise
((
resolve
,
reject
)
=>
{
let
scriptTag
=
document
.
createElement
(
"script"
);
scriptTag
.
src
=
entrypointUrl
;
scriptTag
.
type
=
"application/javascript"
;
// Cache the resolve, so it can be called from Flutter.
// Note: Flutter hot restart doesn't re-create this promise, so this
// can only be called once. Instead, we need to model this as a stream
// of `engineCreated` events coming from Flutter that are handled by JS.
this
.
_didCreateEngineInitializerResolve
=
resolve
;
scriptTag
.
addEventListener
(
"error"
,
reject
);
document
.
body
.
append
(
scriptTag
);
if
(!
reg
.
active
&&
(
reg
.
installing
||
reg
.
waiting
))
{
// No active web worker and we have installed or are installing
// one for the first time. Simply wait for it to activate.
console
.
debug
(
"Installing/Activating first service worker."
);
return
reg
.
installing
||
reg
.
waiting
;
}
else
if
(!
reg
.
active
.
scriptURL
.
endsWith
(
serviceWorkerVersion
))
{
// When the app updates the serviceWorkerVersion changes, so we
// need to ask the service worker to update.
return
reg
.
update
().
then
((
newReg
)
=>
{
console
.
debug
(
"Updating service worker."
);
return
newReg
.
installing
||
newReg
.
waiting
||
newReg
.
active
;
});
}
else
{
console
.
debug
(
"Loading from existing service worker."
);
return
reg
.
active
;
}
return
this
.
_scriptLoaded
;
}
_waitForServiceWorkerActivation
(
serviceWorker
,
entrypointUrl
)
{
/**
* Returns a Promise that resolves when the `latestServiceWorker` changes its
* state to "activated".
*
* @param {Promise<ServiceWorker>} latestServiceWorkerPromise
* @returns {Promise<void>}
*/
async
_waitForServiceWorkerActivation
(
latestServiceWorkerPromise
)
{
const
serviceWorker
=
await
latestServiceWorkerPromise
;
if
(!
serviceWorker
||
serviceWorker
.
state
==
"activated"
)
{
if
(!
serviceWorker
)
{
console
.
warn
(
"Cannot activate a null service worker."
);
return
Promise
.
reject
(
new
Error
(
"Cannot activate a null service worker!"
)
);
}
else
{
console
.
debug
(
"Service worker already active."
);
return
Promise
.
resolve
();
}
return
this
.
_loadEntrypoint
(
entrypointUrl
);
}
return
new
Promise
((
resolve
,
_
)
=>
{
serviceWorker
.
addEventListener
(
"statechange"
,
()
=>
{
if
(
serviceWorker
.
state
==
"activated"
)
{
console
.
debug
(
"
Install
ed new service worker."
);
resolve
(
this
.
_loadEntrypoint
(
entrypointUrl
)
);
console
.
debug
(
"
Activat
ed new service worker."
);
resolve
();
}
});
});
}
}
/**
* Handles injecting the main Flutter web entrypoint (main.dart.js), and notifying
* the user when Flutter is ready, through `didCreateEngineInitializer`.
*
* @see https://docs.flutter.dev/development/platform-integration/web/initialization
*/
class
FlutterEntrypointLoader
{
/**
* Creates a FlutterEntrypointLoader.
*/
constructor
()
{
// Watchdog to prevent injecting the main entrypoint multiple times.
this
.
_scriptLoaded
=
false
;
}
_loadWithServiceWorker
(
entrypointUrl
,
serviceWorkerOptions
)
{
if
(!(
"serviceWorker"
in
navigator
)
||
serviceWorkerOptions
==
null
)
{
console
.
warn
(
"Service worker not supported (or configured)."
,
serviceWorkerOptions
);
return
this
.
_loadEntrypoint
(
entrypointUrl
);
/**
* Loads flutter main entrypoint, specified by `entrypointUrl`, and calls a
* user-specified `onEntrypointLoaded` callback with an EngineInitializer
* object when it's done.
*
* @param {*} options
* @returns {Promise | undefined} that will eventually resolve with an
* EngineInitializer, or will be rejected with the error caused by the loader.
* Returns undefined when an `onEntrypointLoaded` callback is supplied in `options`.
*/
async
loadEntrypoint
(
options
)
{
const
{
entrypointUrl
=
"main.dart.js"
,
onEntrypointLoaded
}
=
options
||
{};
return
this
.
_loadEntrypoint
(
entrypointUrl
,
onEntrypointLoaded
);
}
/**
* Resolves the promise created by loadEntrypoint, and calls the `onEntrypointLoaded`
* function supplied by the user (if needed).
*
* Called by Flutter through `_flutter.loader.didCreateEngineInitializer` method,
* which is bound to the correct instance of the FlutterEntrypointLoader by
* the FlutterLoader object.
*
* @param {Function} engineInitializer @see https://github.com/flutter/engine/blob/main/lib/web_ui/lib/src/engine/js_interop/js_loader.dart#L42
*/
didCreateEngineInitializer
(
engineInitializer
)
{
if
(
typeof
this
.
_didCreateEngineInitializerResolve
===
"function"
)
{
this
.
_didCreateEngineInitializerResolve
(
engineInitializer
);
// Remove the resolver after the first time, so Flutter Web can hot restart.
this
.
_didCreateEngineInitializerResolve
=
null
;
}
if
(
typeof
this
.
_onEntrypointLoaded
===
"function"
)
{
this
.
_onEntrypointLoaded
(
engineInitializer
);
}
}
const
{
serviceWorkerVersion
,
timeoutMillis
=
4000
,
}
=
serviceWorkerOptions
;
let
serviceWorkerUrl
=
"flutter_service_worker.js?v="
+
serviceWorkerVersion
;
let
loader
=
navigator
.
serviceWorker
.
register
(
serviceWorkerUrl
)
.
then
((
reg
)
=>
{
if
(!
reg
.
active
&&
(
reg
.
installing
||
reg
.
waiting
))
{
// No active web worker and we have installed or are installing
// one for the first time. Simply wait for it to activate.
let
sw
=
reg
.
installing
||
reg
.
waiting
;
return
this
.
_waitForServiceWorkerActivation
(
sw
,
entrypointUrl
);
}
else
if
(!
reg
.
active
.
scriptURL
.
endsWith
(
serviceWorkerVersion
))
{
// When the app updates the serviceWorkerVersion changes, so we
// need to ask the service worker to update.
console
.
debug
(
"New service worker available."
);
return
reg
.
update
().
then
((
reg
)
=>
{
console
.
debug
(
"Service worker updated."
);
let
sw
=
reg
.
installing
||
reg
.
waiting
||
reg
.
active
;
return
this
.
_waitForServiceWorkerActivation
(
sw
,
entrypointUrl
);
});
}
else
{
// Existing service worker is still good.
console
.
debug
(
"Loading app from service worker."
);
return
this
.
_loadEntrypoint
(
entrypointUrl
);
}
})
.
catch
((
error
)
=>
{
// Some exception happened while registering/activating the service worker.
console
.
warn
(
"Failed to register or activate service worker:"
,
error
);
return
this
.
_loadEntrypoint
(
entrypointUrl
);
});
/**
* Injects a script tag into the DOM, and configures this loader to be able to
* handle the "entrypoint loaded" notifications received from Flutter web.
*
* @param {string} entrypointUrl the URL of the script that will initialize
* Flutter.
* @param {Function} onEntrypointLoaded a callback that will be called when
* Flutter web notifies this object that the entrypoint is
* loaded.
* @returns {Promise | undefined} a Promise that resolves when the entrypoint
* is loaded, or undefined if `onEntrypointLoaded`
* is a function.
*/
_loadEntrypoint
(
entrypointUrl
,
onEntrypointLoaded
)
{
const
useCallback
=
typeof
onEntrypointLoaded
===
"function"
;
// Timeout race promise
let
timeout
;
if
(
timeoutMillis
>
0
)
{
timeout
=
new
Promise
((
resolve
,
_
)
=>
{
setTimeout
(()
=>
{
if
(!
this
.
_scriptLoaded
)
{
console
.
warn
(
"Loading from service worker timed out after"
,
timeoutMillis
,
"milliseconds."
);
resolve
(
this
.
_loadEntrypoint
(
entrypointUrl
));
}
},
timeoutMillis
);
});
if
(!
this
.
_scriptLoaded
)
{
this
.
_scriptLoaded
=
true
;
const
scriptTag
=
this
.
_createScriptTag
(
entrypointUrl
);
if
(
useCallback
)
{
// Just inject the script tag, and return nothing; Flutter will call
// `didCreateEngineInitializer` when it's done.
console
.
debug
(
"Injecting <script> tag. Using callback."
);
this
.
_onEntrypointLoaded
=
onEntrypointLoaded
;
document
.
body
.
append
(
scriptTag
);
}
else
{
// Inject the script tag and return a promise that will get resolved
// with the EngineInitializer object from Flutter when it calls
// `didCreateEngineInitializer` later.
return
new
Promise
((
resolve
,
reject
)
=>
{
console
.
debug
(
"Injecting <script> tag. Using Promises. Use the callback approach instead!"
);
this
.
_didCreateEngineInitializerResolve
=
resolve
;
scriptTag
.
addEventListener
(
"error"
,
reject
);
document
.
body
.
append
(
scriptTag
);
});
}
}
}
/**
* Creates a script tag for the given URL.
* @param {string} url
* @returns {HTMLScriptElement}
*/
_createScriptTag
(
url
)
{
const
scriptTag
=
document
.
createElement
(
"script"
);
scriptTag
.
type
=
"application/javascript"
;
scriptTag
.
src
=
url
;
return
scriptTag
;
}
}
return
Promise
.
race
([
loader
,
timeout
]);
/**
* The public interface of _flutter.loader. Exposes two methods:
* * loadEntrypoint (which coordinates the default Flutter web loading procedure)
* * didCreateEngineInitializer (which is called by Flutter to notify that its
* Engine is ready to be initialized)
*/
class
FlutterLoader
{
/**
* Initializes the Flutter web app.
* @param {*} options
* @returns {Promise?} a (Deprecated) Promise that will eventually resolve
* with an EngineInitializer, or will be rejected with
* any error caused by the loader. Or Null, if the user
* supplies an `onEntrypointLoaded` Function as an option.
*/
async
loadEntrypoint
(
options
)
{
const
{
serviceWorker
,
...
entrypoint
}
=
options
||
{};
// The FlutterServiceWorkerLoader instance could be injected as a dependency
// (and dynamically imported from a module if not present).
const
serviceWorkerLoader
=
new
FlutterServiceWorkerLoader
();
await
serviceWorkerLoader
.
loadServiceWorker
(
serviceWorker
).
catch
(
e
=>
{
// Regardless of what happens with the injection of the SW, the show must go on
console
.
warn
(
"Exception while loading service worker:"
,
e
);
});
// The FlutterEntrypointLoader instance could be injected as a dependency
// (and dynamically imported from a module if not present).
const
entrypointLoader
=
new
FlutterEntrypointLoader
();
// Install the `didCreateEngineInitializer` listener where Flutter web expects it to be.
this
.
didCreateEngineInitializer
=
entrypointLoader
.
didCreateEngineInitializer
.
bind
(
entrypointLoader
);
return
entrypointLoader
.
loadEntrypoint
(
entrypoint
);
}
}
_flutter
.
loader
=
new
FlutterLoader
();
}());
})();
''';
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment