[web] new service worker loading mechanism (#75535)
This commit is contained in:
parent
78ce11d7e3
commit
e7953b3be4
@ -57,3 +57,57 @@ Future<String> evalTestAppInChrome({
|
||||
await server?.close();
|
||||
}
|
||||
}
|
||||
|
||||
typedef ServerRequestListener = void Function(Request);
|
||||
|
||||
class AppServer {
|
||||
AppServer._(this._server, this.chrome, this.onChromeError);
|
||||
|
||||
static Future<AppServer> start({
|
||||
@required String appUrl,
|
||||
@required String appDirectory,
|
||||
@required String cacheControl,
|
||||
int serverPort = 8080,
|
||||
int browserDebugPort = 8081,
|
||||
bool headless = true,
|
||||
List<Handler> additionalRequestHandlers,
|
||||
}) async {
|
||||
io.HttpServer server;
|
||||
Chrome chrome;
|
||||
server = await io.HttpServer.bind('localhost', serverPort);
|
||||
final Handler staticHandler = createStaticHandler(appDirectory, defaultDocument: 'index.html');
|
||||
Cascade cascade = Cascade();
|
||||
if (additionalRequestHandlers != null) {
|
||||
for (final Handler handler in additionalRequestHandlers) {
|
||||
cascade = cascade.add(handler);
|
||||
}
|
||||
}
|
||||
cascade = cascade.add((Request request) async {
|
||||
final Response response = await staticHandler(request);
|
||||
return response.change(headers: <String, Object>{
|
||||
'cache-control': cacheControl,
|
||||
});
|
||||
});
|
||||
shelf_io.serveRequests(server, cascade.handler);
|
||||
final io.Directory userDataDirectory = io.Directory.systemTemp.createTempSync('chrome_user_data_');
|
||||
final Completer<String> chromeErrorCompleter = Completer<String>();
|
||||
chrome = await Chrome.launch(ChromeOptions(
|
||||
headless: headless,
|
||||
debugPort: browserDebugPort,
|
||||
url: appUrl,
|
||||
userDataDirectory: userDataDirectory.path,
|
||||
windowHeight: 1024,
|
||||
windowWidth: 1024,
|
||||
), onError: chromeErrorCompleter.complete);
|
||||
return AppServer._(server, chrome, chromeErrorCompleter.future);
|
||||
}
|
||||
|
||||
final Future<String> onChromeError;
|
||||
final io.HttpServer _server;
|
||||
final Chrome chrome;
|
||||
|
||||
Future<void> stop() async {
|
||||
chrome?.stop();
|
||||
await _server?.close();
|
||||
}
|
||||
}
|
||||
|
@ -6,138 +6,262 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:flutter_devicelab/framework/browser.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf_static/shelf_static.dart';
|
||||
import 'package:shelf/shelf_io.dart' as shelf_io;
|
||||
|
||||
final String bat = Platform.isWindows ? '.bat' : '';
|
||||
final String flutterRoot = path.dirname(path.dirname(path.dirname(path.fromUri(Platform.script))));
|
||||
final String flutter = path.join(flutterRoot, 'bin', 'flutter$bat');
|
||||
import 'browser.dart';
|
||||
import 'run_command.dart';
|
||||
import 'test/common.dart';
|
||||
|
||||
final String _bat = Platform.isWindows ? '.bat' : '';
|
||||
final String _flutterRoot = path.dirname(path.dirname(path.dirname(path.fromUri(Platform.script))));
|
||||
final String _flutter = path.join(_flutterRoot, 'bin', 'flutter$_bat');
|
||||
final String _testAppDirectory = path.join(_flutterRoot, 'dev', 'integration_tests', 'web');
|
||||
final String _appBuildDirectory = path.join(_testAppDirectory, 'build', 'web');
|
||||
final String _target = path.join('lib', 'service_worker_test.dart');
|
||||
final String _targetPath = path.join(_testAppDirectory, _target);
|
||||
|
||||
// Run a web service worker test. The expectations are currently stored here
|
||||
// instead of in the application. This is not run on CI due to the requirement
|
||||
// of having a headful chrome instance.
|
||||
// Run a web service worker test as a standalone Dart program.
|
||||
Future<void> main() async {
|
||||
await _runWebServiceWorkerTest('lib/service_worker_test.dart');
|
||||
await runWebServiceWorkerTest(headless: false);
|
||||
}
|
||||
|
||||
Future<void> _runWebServiceWorkerTest(String target, {
|
||||
List<String> additionalArguments = const<String>[],
|
||||
}) async {
|
||||
final String testAppDirectory = path.join(flutterRoot, 'dev', 'integration_tests', 'web');
|
||||
final String appBuildDirectory = path.join(testAppDirectory, 'build', 'web');
|
||||
|
||||
// Build the app.
|
||||
await Process.run(
|
||||
flutter,
|
||||
<String>[ 'clean' ],
|
||||
workingDirectory: testAppDirectory,
|
||||
Future<void> _setAppVersion(int version) async {
|
||||
final File targetFile = File(_targetPath);
|
||||
await targetFile.writeAsString(
|
||||
(await targetFile.readAsString()).replaceFirst(
|
||||
RegExp(r'CLOSE\?version=\d+'),
|
||||
'CLOSE?version=$version',
|
||||
)
|
||||
);
|
||||
await Process.run(
|
||||
flutter,
|
||||
<String>[
|
||||
'build',
|
||||
'web',
|
||||
'--release',
|
||||
...additionalArguments,
|
||||
'-t',
|
||||
target,
|
||||
],
|
||||
workingDirectory: testAppDirectory,
|
||||
}
|
||||
|
||||
Future<void> _rebuildApp({ @required int version }) async {
|
||||
await _setAppVersion(version);
|
||||
await runCommand(
|
||||
_flutter,
|
||||
<String>[ 'clean' ],
|
||||
workingDirectory: _testAppDirectory,
|
||||
);
|
||||
await runCommand(
|
||||
_flutter,
|
||||
<String>['build', 'web', '--profile', '-t', _target],
|
||||
workingDirectory: _testAppDirectory,
|
||||
environment: <String, String>{
|
||||
'FLUTTER_WEB': 'true',
|
||||
},
|
||||
);
|
||||
final List<Uri> requests = <Uri>[];
|
||||
final List<Map<String, String>> headers = <Map<String, String>>[];
|
||||
await runRecordingServer(
|
||||
appUrl: 'http://localhost:8080/',
|
||||
appDirectory: appBuildDirectory,
|
||||
requests: requests,
|
||||
headers: headers,
|
||||
browserDebugPort: null,
|
||||
);
|
||||
|
||||
final List<String> requestedPaths = requests.map((Uri uri) => uri.toString()).toList();
|
||||
final List<String> expectedPaths = <String>[
|
||||
// Initial page load
|
||||
'',
|
||||
'main.dart.js',
|
||||
'assets/FontManifest.json',
|
||||
'flutter_service_worker.js',
|
||||
'manifest.json',
|
||||
'favicon.ico',
|
||||
// Service worker install.
|
||||
'main.dart.js',
|
||||
'index.html',
|
||||
'assets/LICENSE',
|
||||
'assets/AssetManifest.json',
|
||||
'assets/FontManifest.json',
|
||||
'',
|
||||
// Second page load all cached.
|
||||
];
|
||||
print('requests: $requestedPaths');
|
||||
// The exact order isn't important or deterministic.
|
||||
for (final String path in requestedPaths) {
|
||||
if (!expectedPaths.remove(path)) {
|
||||
print('unexpected service worker request: $path');
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
if (expectedPaths.isNotEmpty) {
|
||||
print('Missing service worker requests from expected paths: $expectedPaths');
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/// This server runs a release web application and verifies that the service worker
|
||||
/// caches files correctly, by checking the request resources over HTTP.
|
||||
///
|
||||
/// When it receives a request for `CLOSE` the server will be torn down.
|
||||
///
|
||||
/// Expects a path to the `build/web` directory produced from `flutter build web`.
|
||||
Future<void> runRecordingServer({
|
||||
@required String appUrl,
|
||||
@required String appDirectory,
|
||||
@required List<Uri> requests,
|
||||
@required List<Map<String, String>> headers,
|
||||
int serverPort = 8080,
|
||||
int browserDebugPort = 8081,
|
||||
Future<void> runWebServiceWorkerTest({
|
||||
@required bool headless,
|
||||
}) async {
|
||||
Chrome chrome;
|
||||
HttpServer server;
|
||||
final Completer<void> completer = Completer<void>();
|
||||
Directory userDataDirectory;
|
||||
try {
|
||||
server = await HttpServer.bind('localhost', serverPort);
|
||||
final Cascade cascade = Cascade()
|
||||
.add((Request request) async {
|
||||
if (request.url.toString().contains('CLOSE')) {
|
||||
completer.complete();
|
||||
return Response.notFound('');
|
||||
}
|
||||
requests.add(request.url);
|
||||
headers.add(request.headers);
|
||||
return Response.notFound('');
|
||||
})
|
||||
.add(createStaticHandler(appDirectory, defaultDocument: 'index.html'));
|
||||
shelf_io.serveRequests(server, cascade.handler);
|
||||
userDataDirectory = Directory.systemTemp.createTempSync('chrome_user_data_');
|
||||
chrome = await Chrome.launch(ChromeOptions(
|
||||
headless: false,
|
||||
debugPort: browserDebugPort,
|
||||
url: appUrl,
|
||||
userDataDirectory: userDataDirectory.path,
|
||||
windowHeight: 500,
|
||||
windowWidth: 500,
|
||||
), onError: completer.completeError);
|
||||
await completer.future;
|
||||
} finally {
|
||||
chrome?.stop();
|
||||
await server?.close();
|
||||
userDataDirectory.deleteSync(recursive: true);
|
||||
}
|
||||
test('flutter_service_worker.js', () async {
|
||||
await _rebuildApp(version: 1);
|
||||
|
||||
final Map<String, int> requestedPathCounts = <String, int>{};
|
||||
void expectRequestCounts(Map<String, int> expectedCounts) {
|
||||
expect(requestedPathCounts, expectedCounts);
|
||||
requestedPathCounts.clear();
|
||||
}
|
||||
|
||||
AppServer server;
|
||||
Future<void> waitForAppToLoad(Map<String, int> waitForCounts) async {
|
||||
print('Waiting for app to load $waitForCounts');
|
||||
await Future.any(<Future<void>>[
|
||||
() async {
|
||||
while (!waitForCounts.entries.every((MapEntry<String, int> entry) => (requestedPathCounts[entry.key] ?? 0) >= entry.value)) {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
}
|
||||
}(),
|
||||
server.onChromeError.then((String error) {
|
||||
throw Exception('Chrome error: $error');
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
String reportedVersion;
|
||||
|
||||
Future<void> startAppServer({
|
||||
@required String cacheControl,
|
||||
}) async {
|
||||
server = await AppServer.start(
|
||||
headless: headless,
|
||||
cacheControl: cacheControl,
|
||||
appUrl: 'http://localhost:8080/index.html',
|
||||
appDirectory: _appBuildDirectory,
|
||||
additionalRequestHandlers: <Handler>[
|
||||
(Request request) {
|
||||
final String requestedPath = request.url.path;
|
||||
requestedPathCounts.putIfAbsent(requestedPath, () => 0);
|
||||
requestedPathCounts[requestedPath] += 1;
|
||||
if (requestedPath == 'CLOSE') {
|
||||
reportedVersion = request.url.queryParameters['version'];
|
||||
return Response.ok('OK');
|
||||
}
|
||||
return Response.notFound('');
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
//////////////////////////////////////////////////////
|
||||
// Caching server
|
||||
//////////////////////////////////////////////////////
|
||||
print('With cache: test first page load');
|
||||
await startAppServer(cacheControl: 'max-age=3600');
|
||||
await waitForAppToLoad(<String, int>{
|
||||
'CLOSE': 1,
|
||||
'flutter_service_worker.js': 1,
|
||||
});
|
||||
|
||||
expectRequestCounts(<String, int>{
|
||||
'': 1,
|
||||
// Even though the server is caching index.html is downloaded twice,
|
||||
// once by the initial page load, and once by the service worker.
|
||||
// Other resources are loaded once only by the service worker.
|
||||
'index.html': 2,
|
||||
'main.dart.js': 1,
|
||||
'flutter_service_worker.js': 1,
|
||||
'assets/FontManifest.json': 1,
|
||||
'assets/NOTICES': 1,
|
||||
'assets/AssetManifest.json': 1,
|
||||
'CLOSE': 1,
|
||||
// In headless mode Chrome does not load 'manifest.json' and 'favicon.ico'.
|
||||
if (!headless)
|
||||
...<String, int>{
|
||||
'manifest.json': 1,
|
||||
'favicon.ico': 1,
|
||||
}
|
||||
});
|
||||
expect(reportedVersion, '1');
|
||||
reportedVersion = null;
|
||||
|
||||
print('With cache: test page reload');
|
||||
await server.chrome.reloadPage();
|
||||
await waitForAppToLoad(<String, int>{
|
||||
'CLOSE': 1,
|
||||
'flutter_service_worker.js': 1,
|
||||
});
|
||||
|
||||
expectRequestCounts(<String, int>{
|
||||
'flutter_service_worker.js': 1,
|
||||
'CLOSE': 1,
|
||||
});
|
||||
expect(reportedVersion, '1');
|
||||
reportedVersion = null;
|
||||
|
||||
print('With cache: test page reload after rebuild');
|
||||
await _rebuildApp(version: 2);
|
||||
|
||||
// Since we're caching, we need to ignore cache when reloading the page.
|
||||
await server.chrome.reloadPage(ignoreCache: true);
|
||||
await waitForAppToLoad(<String, int>{
|
||||
'CLOSE': 1,
|
||||
'flutter_service_worker.js': 2,
|
||||
});
|
||||
expectRequestCounts(<String, int>{
|
||||
'index.html': 2,
|
||||
'flutter_service_worker.js': 2,
|
||||
'': 1,
|
||||
'main.dart.js': 1,
|
||||
'assets/NOTICES': 1,
|
||||
'assets/AssetManifest.json': 1,
|
||||
'assets/FontManifest.json': 1,
|
||||
'CLOSE': 1,
|
||||
if (!headless)
|
||||
'favicon.ico': 1,
|
||||
});
|
||||
|
||||
expect(reportedVersion, '2');
|
||||
reportedVersion = null;
|
||||
await server.stop();
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
// Non-caching server
|
||||
//////////////////////////////////////////////////////
|
||||
print('No cache: test first page load');
|
||||
await _rebuildApp(version: 3);
|
||||
await startAppServer(cacheControl: 'max-age=0');
|
||||
await waitForAppToLoad(<String, int>{
|
||||
'CLOSE': 1,
|
||||
'flutter_service_worker.js': 1,
|
||||
});
|
||||
|
||||
expectRequestCounts(<String, int>{
|
||||
'': 1,
|
||||
'index.html': 2,
|
||||
// We still download some resources multiple times if the server is non-caching.
|
||||
'main.dart.js': 2,
|
||||
'assets/FontManifest.json': 2,
|
||||
'flutter_service_worker.js': 1,
|
||||
'assets/NOTICES': 1,
|
||||
'assets/AssetManifest.json': 1,
|
||||
'CLOSE': 1,
|
||||
// In headless mode Chrome does not load 'manifest.json' and 'favicon.ico'.
|
||||
if (!headless)
|
||||
...<String, int>{
|
||||
'manifest.json': 1,
|
||||
'favicon.ico': 1,
|
||||
}
|
||||
});
|
||||
|
||||
expect(reportedVersion, '3');
|
||||
reportedVersion = null;
|
||||
|
||||
print('No cache: test page reload');
|
||||
await server.chrome.reloadPage();
|
||||
await waitForAppToLoad(<String, int>{
|
||||
'CLOSE': 1,
|
||||
'flutter_service_worker.js': 1,
|
||||
});
|
||||
|
||||
expectRequestCounts(<String, int>{
|
||||
'flutter_service_worker.js': 1,
|
||||
'CLOSE': 1,
|
||||
if (!headless)
|
||||
'manifest.json': 1,
|
||||
});
|
||||
expect(reportedVersion, '3');
|
||||
reportedVersion = null;
|
||||
|
||||
print('No cache: test page reload after rebuild');
|
||||
await _rebuildApp(version: 4);
|
||||
|
||||
// TODO(yjbanov): when running Chrome with DevTools protocol, for some
|
||||
// reason a hard refresh is still required. This works without a hard
|
||||
// refresh when running Chrome manually as normal. At the time of writing
|
||||
// this test I wasn't able to figure out what's wrong with the way we run
|
||||
// Chrome from tests.
|
||||
await server.chrome.reloadPage(ignoreCache: true);
|
||||
await waitForAppToLoad(<String, int>{
|
||||
'CLOSE': 1,
|
||||
'flutter_service_worker.js': 1,
|
||||
});
|
||||
expectRequestCounts(<String, int>{
|
||||
'': 1,
|
||||
'index.html': 2,
|
||||
'flutter_service_worker.js': 2,
|
||||
'main.dart.js': 2,
|
||||
'assets/NOTICES': 1,
|
||||
'assets/AssetManifest.json': 1,
|
||||
'assets/FontManifest.json': 2,
|
||||
'CLOSE': 1,
|
||||
if (!headless)
|
||||
...<String, int>{
|
||||
'manifest.json': 1,
|
||||
'favicon.ico': 1,
|
||||
}
|
||||
});
|
||||
|
||||
expect(reportedVersion, '4');
|
||||
reportedVersion = null;
|
||||
} finally {
|
||||
await _setAppVersion(1);
|
||||
await server?.stop();
|
||||
}
|
||||
// This is a long test. The default 30 seconds is not enough.
|
||||
}, timeout: const Timeout(Duration(minutes: 10)));
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import 'package:path/path.dart' as path;
|
||||
import 'browser.dart';
|
||||
import 'flutter_compact_formatter.dart';
|
||||
import 'run_command.dart';
|
||||
import 'service_worker_test.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
typedef ShardRunner = Future<void> Function();
|
||||
@ -811,6 +812,7 @@ Future<void> _runWebLongRunningTests() async {
|
||||
() => _runGalleryE2eWebTest('profile', canvasKit: true),
|
||||
() => _runGalleryE2eWebTest('release'),
|
||||
() => _runGalleryE2eWebTest('release', canvasKit: true),
|
||||
() => runWebServiceWorkerTest(headless: true),
|
||||
];
|
||||
await _ensureChromeDriverIsRunning();
|
||||
await _runShardRunnerIndexOfTotalSubshard(tests);
|
||||
|
@ -196,6 +196,10 @@ class Chrome {
|
||||
return data;
|
||||
}
|
||||
|
||||
Future<void> reloadPage({bool ignoreCache = false}) async {
|
||||
await _debugConnection.page.reload(ignoreCache: ignoreCache);
|
||||
}
|
||||
|
||||
/// Stops the Chrome process.
|
||||
void stop() {
|
||||
_isStopped = true;
|
||||
|
@ -23,12 +23,68 @@ found in the LICENSE file. -->
|
||||
application. For more information, see:
|
||||
https://developers.google.com/web/fundamentals/primers/service-workers -->
|
||||
<script>
|
||||
var serviceWorkerVersion = null;
|
||||
var scriptLoaded = false;
|
||||
function loadMainDartJs() {
|
||||
if (scriptLoaded) {
|
||||
return;
|
||||
}
|
||||
scriptLoaded = true;
|
||||
var scriptTag = document.createElement('script');
|
||||
scriptTag.src = 'main.dart.js';
|
||||
scriptTag.type = 'application/javascript';
|
||||
document.body.append(scriptTag);
|
||||
}
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('flutter-first-frame', function () {
|
||||
navigator.serviceWorker.register('flutter_service_worker.js');
|
||||
// Service workers are supported. Use them.
|
||||
window.addEventListener('load', function () {
|
||||
// Wait for registration to finish before dropping the <script> tag.
|
||||
// Otherwise, the browser will load the script multiple times,
|
||||
// potentially different versions.
|
||||
var serviceWorkerUrl = 'flutter_service_worker.js?v=' + serviceWorkerVersion;
|
||||
navigator.serviceWorker.register(serviceWorkerUrl)
|
||||
.then((reg) => {
|
||||
function waitForActivation(serviceWorker) {
|
||||
serviceWorker.addEventListener('statechange', () => {
|
||||
if (serviceWorker.state == 'activated') {
|
||||
console.log('Installed new service worker.');
|
||||
loadMainDartJs();
|
||||
}
|
||||
});
|
||||
}
|
||||
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.
|
||||
waitForActivation(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.
|
||||
console.log('New service worker available.');
|
||||
reg.update();
|
||||
waitForActivation(reg.installing);
|
||||
} else {
|
||||
// Existing service worker is still good.
|
||||
console.log('Loading app from service worker.');
|
||||
loadMainDartJs();
|
||||
}
|
||||
});
|
||||
|
||||
// If service worker doesn't succeed in a reasonable amount of time,
|
||||
// fallback to plaint <script> tag.
|
||||
setTimeout(() => {
|
||||
if (!scriptLoaded) {
|
||||
console.warn(
|
||||
'Failed to load app from service worker. Falling back to plain <script> tag.',
|
||||
);
|
||||
loadMainDartJs();
|
||||
}
|
||||
}, 4000);
|
||||
});
|
||||
} else {
|
||||
// Service workers not supported. Just drop the <script> tag.
|
||||
loadMainDartJs();
|
||||
}
|
||||
</script>
|
||||
<script src="main.dart.js" type="application/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -4,16 +4,8 @@
|
||||
|
||||
import 'dart:html' as html;
|
||||
Future<void> main() async {
|
||||
final html.ServiceWorkerRegistration worker = await html.window.navigator.serviceWorker.ready;
|
||||
if (worker.active != null) {
|
||||
await Future.delayed(const Duration(seconds: 5));
|
||||
await html.HttpRequest.getString('CLOSE');
|
||||
return;
|
||||
}
|
||||
worker.addEventListener('statechange', (event) async {
|
||||
if (worker.active != null) {
|
||||
await Future.delayed(const Duration(seconds: 5));
|
||||
await html.HttpRequest.getString('CLOSE');
|
||||
}
|
||||
});
|
||||
await html.window.navigator.serviceWorker.ready;
|
||||
final String response = 'CLOSE?version=1';
|
||||
await html.HttpRequest.getString(response);
|
||||
html.document.body.appendHtml(response);
|
||||
}
|
||||
|
@ -8,6 +8,10 @@ found in the LICENSE file. -->
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
|
||||
<title>Web Test</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">
|
||||
</head>
|
||||
<body>
|
||||
@ -15,12 +19,68 @@ found in the LICENSE file. -->
|
||||
application. For more information, see:
|
||||
https://developers.google.com/web/fundamentals/primers/service-workers -->
|
||||
<script>
|
||||
var serviceWorkerVersion = null;
|
||||
var scriptLoaded = false;
|
||||
function loadMainDartJs() {
|
||||
if (scriptLoaded) {
|
||||
return;
|
||||
}
|
||||
scriptLoaded = true;
|
||||
var scriptTag = document.createElement('script');
|
||||
scriptTag.src = 'main.dart.js';
|
||||
scriptTag.type = 'application/javascript';
|
||||
document.body.append(scriptTag);
|
||||
}
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
// Service workers are supported. Use them.
|
||||
window.addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('flutter_service_worker.js');
|
||||
// Wait for registration to finish before dropping the <script> tag.
|
||||
// Otherwise, the browser will load the script multiple times,
|
||||
// potentially different versions.
|
||||
var serviceWorkerUrl = 'flutter_service_worker.js?v=' + serviceWorkerVersion;
|
||||
navigator.serviceWorker.register(serviceWorkerUrl)
|
||||
.then((reg) => {
|
||||
function waitForActivation(serviceWorker) {
|
||||
serviceWorker.addEventListener('statechange', () => {
|
||||
if (serviceWorker.state == 'activated') {
|
||||
console.log('Installed new service worker.');
|
||||
loadMainDartJs();
|
||||
}
|
||||
});
|
||||
}
|
||||
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.
|
||||
waitForActivation(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.
|
||||
console.log('New service worker available.');
|
||||
reg.update();
|
||||
waitForActivation(reg.installing);
|
||||
} else {
|
||||
// Existing service worker is still good.
|
||||
console.log('Loading app from service worker.');
|
||||
loadMainDartJs();
|
||||
}
|
||||
});
|
||||
|
||||
// If service worker doesn't succeed in a reasonable amount of time,
|
||||
// fallback to plaint <script> tag.
|
||||
setTimeout(() => {
|
||||
if (!scriptLoaded) {
|
||||
console.warn(
|
||||
'Failed to load app from service worker. Falling back to plain <script> tag.',
|
||||
);
|
||||
loadMainDartJs();
|
||||
}
|
||||
}, 4000);
|
||||
});
|
||||
} else {
|
||||
// Service workers not supported. Just drop the <script> tag.
|
||||
loadMainDartJs();
|
||||
}
|
||||
</script>
|
||||
<script src="main.dart.js" type="application/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -356,6 +356,12 @@ class WebReleaseBundle extends Target {
|
||||
if (environment.fileSystem.path.basename(inputFile.path) == 'index.html') {
|
||||
final String randomHash = Random().nextInt(4294967296).toString();
|
||||
final String resultString = inputFile.readAsStringSync()
|
||||
.replaceFirst(
|
||||
'var serviceWorkerVersion = null',
|
||||
"var serviceWorkerVersion = '$randomHash'",
|
||||
)
|
||||
// This is for legacy index.html that still use the old service
|
||||
// worker loading mechanism.
|
||||
.replaceFirst(
|
||||
"navigator.serviceWorker.register('flutter_service_worker.js')",
|
||||
"navigator.serviceWorker.register('flutter_service_worker.js?v=$randomHash')",
|
||||
@ -492,7 +498,7 @@ self.addEventListener("install", (event) => {
|
||||
return event.waitUntil(
|
||||
caches.open(TEMP).then((cache) => {
|
||||
return cache.addAll(
|
||||
CORE.map((value) => new Request(value + '?revision=' + RESOURCES[value], {'cache': 'reload'})));
|
||||
CORE.map((value) => new Request(value, {'cache': 'reload'})));
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -23,9 +23,6 @@
|
||||
<meta name="apple-mobile-web-app-title" content="{{projectName}}">
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
|
||||
<title>{{projectName}}</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
@ -34,12 +31,68 @@
|
||||
application. For more information, see:
|
||||
https://developers.google.com/web/fundamentals/primers/service-workers -->
|
||||
<script>
|
||||
var serviceWorkerVersion = null;
|
||||
var scriptLoaded = false;
|
||||
function loadMainDartJs() {
|
||||
if (scriptLoaded) {
|
||||
return;
|
||||
}
|
||||
scriptLoaded = true;
|
||||
var scriptTag = document.createElement('script');
|
||||
scriptTag.src = 'main.dart.js';
|
||||
scriptTag.type = 'application/javascript';
|
||||
document.body.append(scriptTag);
|
||||
}
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('flutter-first-frame', function () {
|
||||
navigator.serviceWorker.register('flutter_service_worker.js');
|
||||
// Service workers are supported. Use them.
|
||||
window.addEventListener('load', function () {
|
||||
// Wait for registration to finish before dropping the <script> tag.
|
||||
// Otherwise, the browser will load the script multiple times,
|
||||
// potentially different versions.
|
||||
var serviceWorkerUrl = 'flutter_service_worker.js?v=' + serviceWorkerVersion;
|
||||
navigator.serviceWorker.register(serviceWorkerUrl)
|
||||
.then((reg) => {
|
||||
function waitForActivation(serviceWorker) {
|
||||
serviceWorker.addEventListener('statechange', () => {
|
||||
if (serviceWorker.state == 'activated') {
|
||||
console.log('Installed new service worker.');
|
||||
loadMainDartJs();
|
||||
}
|
||||
});
|
||||
}
|
||||
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.
|
||||
waitForActivation(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.
|
||||
console.log('New service worker available.');
|
||||
reg.update();
|
||||
waitForActivation(reg.installing);
|
||||
} else {
|
||||
// Existing service worker is still good.
|
||||
console.log('Loading app from service worker.');
|
||||
loadMainDartJs();
|
||||
}
|
||||
});
|
||||
|
||||
// If service worker doesn't succeed in a reasonable amount of time,
|
||||
// fallback to plaint <script> tag.
|
||||
setTimeout(() => {
|
||||
if (!scriptLoaded) {
|
||||
console.warn(
|
||||
'Failed to load app from service worker. Falling back to plain <script> tag.',
|
||||
);
|
||||
loadMainDartJs();
|
||||
}
|
||||
}, 4000);
|
||||
});
|
||||
} else {
|
||||
// Service workers not supported. Just drop the <script> tag.
|
||||
loadMainDartJs();
|
||||
}
|
||||
</script>
|
||||
<script src="main.dart.js" type="application/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
Loading…
x
Reference in New Issue
Block a user