Unverified Commit e7953b3b authored by Yegor's avatar Yegor Committed by GitHub

[web] new service worker loading mechanism (#75535)

parent 78ce11d7
......@@ -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();
}
}
This diff is collapsed.
......@@ -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>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment