mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[flutter_tools][web] Add basic service worker generation support to web applications (#48344)
This commit is contained in:
parent
0a600e1ddc
commit
9f145f6ced
@ -18,6 +18,16 @@ found in the LICENSE file. -->
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
</head>
|
||||
<body>
|
||||
<!-- 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 -->
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('/flutter_service_worker.js');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script src="main.dart.js" type="application/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
|
||||
import '../../artifacts.dart';
|
||||
import '../../base/file_system.dart';
|
||||
import '../../base/io.dart';
|
||||
@ -231,7 +233,12 @@ class WebReleaseBundle extends Target {
|
||||
@override
|
||||
Future<void> build(Environment environment) async {
|
||||
for (final File outputFile in environment.buildDir.listSync(recursive: true).whereType<File>()) {
|
||||
if (!globals.fs.path.basename(outputFile.path).contains('main.dart.js')) {
|
||||
final String basename = globals.fs.path.basename(outputFile.path);
|
||||
if (!basename.contains('main.dart.js')) {
|
||||
continue;
|
||||
}
|
||||
// Do not copy the deps file.
|
||||
if (basename.endsWith('.deps')) {
|
||||
continue;
|
||||
}
|
||||
outputFile.copySync(
|
||||
@ -267,3 +274,96 @@ class WebReleaseBundle extends Target {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a service worker for a web target.
|
||||
class WebServiceWorker extends Target {
|
||||
const WebServiceWorker();
|
||||
|
||||
@override
|
||||
String get name => 'web_service_worker';
|
||||
|
||||
@override
|
||||
List<Target> get dependencies => const <Target>[
|
||||
Dart2JSTarget(),
|
||||
WebReleaseBundle(),
|
||||
];
|
||||
|
||||
@override
|
||||
List<String> get depfiles => const <String>[
|
||||
'service_worker.d',
|
||||
];
|
||||
|
||||
@override
|
||||
List<Source> get inputs => const <Source>[];
|
||||
|
||||
@override
|
||||
List<Source> get outputs => const <Source>[];
|
||||
|
||||
@override
|
||||
Future<void> build(Environment environment) async {
|
||||
final List<File> contents = environment.outputDir
|
||||
.listSync(recursive: true)
|
||||
.whereType<File>()
|
||||
.where((File file) => !file.path.endsWith('flutter_service_worker.js')
|
||||
&& !globals.fs.path.basename(file.path).startsWith('.'))
|
||||
.toList();
|
||||
// TODO(jonahwilliams): determine whether this needs to be made more efficient.
|
||||
final Map<String, String> uriToHash = <String, String>{
|
||||
for (File file in contents)
|
||||
// Do not force caching of source maps.
|
||||
if (!file.path.endsWith('main.dart.js.map'))
|
||||
'/${globals.fs.path.relative(file.path, from: environment.outputDir.path)}':
|
||||
md5.convert(await file.readAsBytes()).toString(),
|
||||
};
|
||||
final File serviceWorkerFile = environment.outputDir
|
||||
.childFile('flutter_service_worker.js');
|
||||
final Depfile depfile = Depfile(contents, <File>[serviceWorkerFile]);
|
||||
final String serviceWorker = generateServiceWorker(uriToHash);
|
||||
serviceWorkerFile
|
||||
.writeAsStringSync(serviceWorker);
|
||||
depfile.writeToFile(environment.buildDir.childFile('service_worker.d'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a service worker with an app-specific cache name a map of
|
||||
/// resource files.
|
||||
///
|
||||
/// We embed file hashes directly into the worker so that the byte for byte
|
||||
/// invalidation will automatically reactivate workers whenever a new
|
||||
/// version is deployed.
|
||||
// TODO(jonahwilliams): on re-activate, only evict stale assets.
|
||||
String generateServiceWorker(Map<String, String> resources) {
|
||||
return '''
|
||||
'use strict';
|
||||
const CACHE_NAME = 'flutter-app-cache';
|
||||
const RESOURCES = {
|
||||
${resources.entries.map((MapEntry<String, String> entry) => '"${entry.key}": "${entry.value}"').join(",\n")}
|
||||
};
|
||||
|
||||
self.addEventListener('activate', function (event) {
|
||||
event.waitUntil(
|
||||
caches.keys().then(function (cacheName) {
|
||||
return caches.delete(cacheName);
|
||||
}).then(function (_) {
|
||||
return caches.open(CACHE_NAME);
|
||||
}).then(function (cache) {
|
||||
return cache.addAll(Object.keys(RESOURCES));
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', function (event) {
|
||||
event.respondWith(
|
||||
caches.match(event.request)
|
||||
.then(function (response) {
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
return fetch(event.request, {
|
||||
credentials: 'include'
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
''';
|
||||
}
|
||||
|
||||
@ -35,7 +35,7 @@ const List<Target> _kDefaultTargets = <Target>[
|
||||
ProfileMacOSBundleFlutterAssets(),
|
||||
ReleaseMacOSBundleFlutterAssets(),
|
||||
DebugBundleLinuxAssets(),
|
||||
WebReleaseBundle(),
|
||||
WebServiceWorker(),
|
||||
DebugAndroidApplication(),
|
||||
FastStartAndroidApplication(),
|
||||
ProfileAndroidApplication(),
|
||||
|
||||
@ -39,7 +39,7 @@ Future<void> buildWeb(
|
||||
final Status status = globals.logger.startProgress('Compiling $target for the Web...', timeout: null);
|
||||
final Stopwatch sw = Stopwatch()..start();
|
||||
try {
|
||||
final BuildResult result = await buildSystem.build(const WebReleaseBundle(), Environment(
|
||||
final BuildResult result = await buildSystem.build(const WebServiceWorker(), Environment(
|
||||
outputDir: globals.fs.directory(getWebBuildDirectory()),
|
||||
projectDir: globals.fs.currentDirectory,
|
||||
buildDir: flutterProject.directory
|
||||
|
||||
@ -15,6 +15,16 @@
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
</head>
|
||||
<body>
|
||||
<!-- 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 -->
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('/service_worker.js');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script src="main.dart.js" type="application/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:file_testing/file_testing.dart';
|
||||
import 'package:flutter_tools/src/base/file_system.dart';
|
||||
|
||||
import 'package:flutter_tools/src/build_system/build_system.dart';
|
||||
@ -80,6 +81,7 @@ void main() {
|
||||
}));
|
||||
|
||||
test('WebReleaseBundle copies dart2js output and resource files to output directory', () => testbed.run(() async {
|
||||
environment.defines[kBuildMode] = 'release';
|
||||
final Directory webResources = environment.projectDir.childDirectory('web');
|
||||
webResources.childFile('index.html')
|
||||
..createSync(recursive: true);
|
||||
@ -383,6 +385,27 @@ void main() {
|
||||
}, overrides: <Type, Generator>{
|
||||
ProcessManager: () => MockProcessManager(),
|
||||
}));
|
||||
|
||||
test('Generated service worker correctly inlines file hashes', () {
|
||||
final String result = generateServiceWorker(<String, String>{'/foo': 'abcd'});
|
||||
|
||||
expect(result, contains('{\n "/foo": "abcd"\n};'));
|
||||
});
|
||||
|
||||
test('WebServiceWorker generates a service_worker for a web resource folder', () => testbed.run(() async {
|
||||
environment.outputDir.childFile('a.txt')
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('A');
|
||||
await const WebServiceWorker().build(environment);
|
||||
|
||||
expect(environment.outputDir.childFile('flutter_service_worker.js'), exists);
|
||||
// Contains file hash.
|
||||
expect(environment.outputDir.childFile('flutter_service_worker.js').readAsStringSync(),
|
||||
contains('"/a.txt": "7fc56270e7a70fa81a5935b72eacbe29"'));
|
||||
expect(environment.buildDir.childFile('service_worker.d'), exists);
|
||||
// Depends on resource file.
|
||||
expect(environment.buildDir.childFile('service_worker.d').readAsStringSync(), contains('a.txt'));
|
||||
}));
|
||||
}
|
||||
|
||||
class MockProcessManager extends Mock implements ProcessManager {}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user