mirror of
https://github.com/flutter/flutter
synced 2024-09-20 00:32:02 +00:00
[web] Encode AssetManifest.bin as JSON and use that on the web. (#131382)
This PR modifies the web build slightly to create an `AssetManifest.json`, that is a JSON(base64)-encoded version of the `AssetManifest.bin` file. _(This should enable all browsers to download the file without any interference, and all servers to serve it with the correct headers.)_ It also modifies Flutter's `AssetManifest` class so it loads and uses said file `if (kIsWeb)`. ### Issues * Fixes https://github.com/flutter/flutter/issues/124883 ### Tests * Unit tests added. * Some tests that run on the Web needed to be informed of the new filename, but their behavior didn't have to change (binary contents are the same across all platforms). * I've deployed a test app, so users affected by the BIN issue may take a look at the PR in action: * https://dit-tests.web.app
This commit is contained in:
parent
ba2dde48fa
commit
c7c9d8eea6
|
@ -88,8 +88,8 @@ abstract class AssetBundle {
|
|||
Future<String> loadString(String key, { bool cache = true }) async {
|
||||
final ByteData data = await load(key);
|
||||
// 50 KB of data should take 2-3 ms to parse on a Moto G4, and about 400 μs
|
||||
// on a Pixel 4.
|
||||
if (data.lengthInBytes < 50 * 1024) {
|
||||
// on a Pixel 4. On the web we can't bail to isolates, though...
|
||||
if (data.lengthInBytes < 50 * 1024 || kIsWeb) {
|
||||
return utf8.decode(Uint8List.sublistView(data));
|
||||
}
|
||||
// For strings larger than 50 KB, run the computation in an isolate to
|
||||
|
|
|
@ -2,18 +2,22 @@
|
|||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'asset_bundle.dart';
|
||||
import 'message_codecs.dart';
|
||||
|
||||
// We use .bin as the extension since it is well-known to represent
|
||||
// data in some arbitrary binary format. Using a well-known extension here
|
||||
// is important for web, because some web servers will not serve files with
|
||||
// unrecognized file extensions by default.
|
||||
// See https://github.com/flutter/flutter/issues/128456.
|
||||
// data in some arbitrary binary format.
|
||||
const String _kAssetManifestFilename = 'AssetManifest.bin';
|
||||
|
||||
// We use the same bin file for the web, but re-encoded as JSON(base64(bytes))
|
||||
// so it can be downloaded by even the dumbest of browsers.
|
||||
// See https://github.com/flutter/flutter/issues/128456
|
||||
const String _kAssetManifestWebFilename = 'AssetManifest.bin.json';
|
||||
|
||||
/// Contains details about available assets and their variants.
|
||||
/// See [Resolution-aware image assets](https://docs.flutter.dev/ui/assets-and-images#resolution-aware)
|
||||
/// to learn about asset variants and how to declare them.
|
||||
|
@ -21,6 +25,22 @@ abstract class AssetManifest {
|
|||
/// Loads asset manifest data from an [AssetBundle] object and creates an
|
||||
/// [AssetManifest] object from that data.
|
||||
static Future<AssetManifest> loadFromAssetBundle(AssetBundle bundle) {
|
||||
// The AssetManifest file contains binary data.
|
||||
//
|
||||
// On the web, the build process wraps this binary data in json+base64 so
|
||||
// it can be transmitted over the network without special configuration
|
||||
// (see #131382).
|
||||
if (kIsWeb) {
|
||||
// On the web, the AssetManifest is downloaded as a String, then
|
||||
// json+base64-decoded to get to the binary data.
|
||||
return bundle.loadStructuredData(_kAssetManifestWebFilename, (String jsonData) async {
|
||||
// Decode the manifest JSON file to the underlying BIN, and convert to ByteData.
|
||||
final ByteData message = ByteData.sublistView(base64.decode(json.decode(jsonData) as String));
|
||||
// Now we can keep operating as usual.
|
||||
return _AssetManifestBin.fromStandardMessageCodecMessage(message);
|
||||
});
|
||||
}
|
||||
// On every other platform, the binary file contents are used directly.
|
||||
return bundle.loadStructuredBinaryData(_kAssetManifestFilename, _AssetManifestBin.fromStandardMessageCodecMessage);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 'dart:convert';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
@ -22,6 +23,22 @@ class TestAssetBundle extends CachingAssetBundle {
|
|||
return const StandardMessageCodec().encodeMessage(_assetBundleMap)!;
|
||||
}
|
||||
|
||||
if (key == 'AssetManifest.bin.json') {
|
||||
// Encode the manifest data that will be used by the app
|
||||
final ByteData data = const StandardMessageCodec().encodeMessage(_assetBundleMap)!;
|
||||
// Simulate the behavior of NetworkAssetBundle.load here, for web tests
|
||||
return ByteData.sublistView(
|
||||
utf8.encode(
|
||||
json.encode(
|
||||
base64.encode(
|
||||
// Encode only the actual bytes of the buffer, and no more...
|
||||
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
loadCallCount[key] = loadCallCount[key] ?? 0 + 1;
|
||||
if (key == 'one') {
|
||||
return ByteData(1)
|
||||
|
|
|
@ -25,6 +25,22 @@ class TestAssetBundle extends CachingAssetBundle {
|
|||
.encodeMessage(<String, Object>{'one': <Object>[]})!;
|
||||
}
|
||||
|
||||
if (key == 'AssetManifest.bin.json') {
|
||||
// Encode the manifest data that will be used by the app
|
||||
final ByteData data = const StandardMessageCodec().encodeMessage(<String, Object> {'one': <Object>[]})!;
|
||||
// Simulate the behavior of NetworkAssetBundle.load here, for web tests
|
||||
return ByteData.sublistView(
|
||||
utf8.encode(
|
||||
json.encode(
|
||||
base64.encode(
|
||||
// Encode only the actual bytes of the buffer, and no more...
|
||||
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (key == 'counter') {
|
||||
return ByteData.sublistView(utf8.encode(loadCallCount[key]!.toString()));
|
||||
}
|
||||
|
|
|
@ -2,34 +2,52 @@
|
|||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
class TestAssetBundle extends AssetBundle {
|
||||
static const Map<String, List<Object>> _binManifestData = <String, List<Object>>{
|
||||
'assets/foo.png': <Object>[
|
||||
<String, Object>{
|
||||
'asset': 'assets/foo.png',
|
||||
},
|
||||
<String, Object>{
|
||||
'asset': 'assets/2x/foo.png',
|
||||
'dpr': 2.0
|
||||
},
|
||||
],
|
||||
'assets/bar.png': <Object>[
|
||||
<String, Object>{
|
||||
'asset': 'assets/bar.png',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@override
|
||||
Future<ByteData> load(String key) async {
|
||||
if (key == 'AssetManifest.bin') {
|
||||
final Map<String, List<Object>> binManifestData = <String, List<Object>>{
|
||||
'assets/foo.png': <Object>[
|
||||
<String, Object>{
|
||||
'asset': 'assets/foo.png',
|
||||
},
|
||||
<String, Object>{
|
||||
'asset': 'assets/2x/foo.png',
|
||||
'dpr': 2.0
|
||||
},
|
||||
],
|
||||
'assets/bar.png': <Object>[
|
||||
<String, Object>{
|
||||
'asset': 'assets/bar.png',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
final ByteData data = const StandardMessageCodec().encodeMessage(binManifestData)!;
|
||||
final ByteData data = const StandardMessageCodec().encodeMessage(_binManifestData)!;
|
||||
return data;
|
||||
}
|
||||
|
||||
if (key == 'AssetManifest.bin.json') {
|
||||
// Encode the manifest data that will be used by the app
|
||||
final ByteData data = const StandardMessageCodec().encodeMessage(_binManifestData)!;
|
||||
// Simulate the behavior of NetworkAssetBundle.load here, for web tests
|
||||
return ByteData.sublistView(
|
||||
utf8.encode(
|
||||
json.encode(
|
||||
base64.encode(
|
||||
// Encode only the actual bytes of the buffer, and no more...
|
||||
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
throw ArgumentError('Unexpected key');
|
||||
}
|
||||
|
||||
|
|
|
@ -168,6 +168,7 @@ class ManifestAssetBundle implements AssetBundle {
|
|||
// We assume the main asset is designed for a device pixel ratio of 1.0.
|
||||
static const String _kAssetManifestJsonFilename = 'AssetManifest.json';
|
||||
static const String _kAssetManifestBinFilename = 'AssetManifest.bin';
|
||||
static const String _kAssetManifestBinJsonFilename = 'AssetManifest.bin.json';
|
||||
|
||||
static const String _kNoticeFile = 'NOTICES';
|
||||
// Comically, this can't be name with the more common .gz file extension
|
||||
|
@ -233,8 +234,6 @@ class ManifestAssetBundle implements AssetBundle {
|
|||
// device.
|
||||
_lastBuildTimestamp = DateTime.now();
|
||||
if (flutterManifest.isEmpty) {
|
||||
entries[_kAssetManifestJsonFilename] = DevFSStringContent('{}');
|
||||
entryKinds[_kAssetManifestJsonFilename] = AssetKind.regular;
|
||||
entries[_kAssetManifestJsonFilename] = DevFSStringContent('{}');
|
||||
entryKinds[_kAssetManifestJsonFilename] = AssetKind.regular;
|
||||
final ByteData emptyAssetManifest =
|
||||
|
@ -242,6 +241,11 @@ class ManifestAssetBundle implements AssetBundle {
|
|||
entries[_kAssetManifestBinFilename] =
|
||||
DevFSByteContent(emptyAssetManifest.buffer.asUint8List(0, emptyAssetManifest.lengthInBytes));
|
||||
entryKinds[_kAssetManifestBinFilename] = AssetKind.regular;
|
||||
// Create .bin.json on web builds.
|
||||
if (targetPlatform == TargetPlatform.web_javascript) {
|
||||
entries[_kAssetManifestBinJsonFilename] = DevFSStringContent('""');
|
||||
entryKinds[_kAssetManifestBinJsonFilename] = AssetKind.regular;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -437,8 +441,8 @@ class ManifestAssetBundle implements AssetBundle {
|
|||
|
||||
final Map<String, List<String>> assetManifest =
|
||||
_createAssetManifest(assetVariants, deferredComponentsAssetVariants);
|
||||
final DevFSStringContent assetManifestJson = DevFSStringContent(json.encode(assetManifest));
|
||||
final DevFSByteContent assetManifestBinary = _createAssetManifestBinary(assetManifest);
|
||||
final DevFSStringContent assetManifestJson = DevFSStringContent(json.encode(assetManifest));
|
||||
final DevFSStringContent fontManifest = DevFSStringContent(json.encode(fonts));
|
||||
final LicenseResult licenseResult = _licenseCollector.obtainLicenses(packageConfig, additionalLicenseFiles);
|
||||
if (licenseResult.errorMessages.isNotEmpty) {
|
||||
|
@ -464,6 +468,13 @@ class ManifestAssetBundle implements AssetBundle {
|
|||
|
||||
_setIfChanged(_kAssetManifestJsonFilename, assetManifestJson, AssetKind.regular);
|
||||
_setIfChanged(_kAssetManifestBinFilename, assetManifestBinary, AssetKind.regular);
|
||||
// Create .bin.json on web builds.
|
||||
if (targetPlatform == TargetPlatform.web_javascript) {
|
||||
final DevFSStringContent assetManifestBinaryJson = DevFSStringContent(json.encode(
|
||||
base64.encode(assetManifestBinary.bytes)
|
||||
));
|
||||
_setIfChanged(_kAssetManifestBinJsonFilename, assetManifestBinaryJson, AssetKind.regular);
|
||||
}
|
||||
_setIfChanged(kFontManifestJson, fontManifest, AssetKind.regular);
|
||||
_setLicenseIfChanged(licenseResult.combinedLicenses, targetPlatform);
|
||||
return 0;
|
||||
|
|
|
@ -325,6 +325,102 @@ flutter:
|
|||
});
|
||||
});
|
||||
|
||||
group('AssetBundle.build (web builds)', () {
|
||||
late FileSystem testFileSystem;
|
||||
|
||||
setUp(() async {
|
||||
testFileSystem = MemoryFileSystem(
|
||||
style: globals.platform.isWindows
|
||||
? FileSystemStyle.windows
|
||||
: FileSystemStyle.posix,
|
||||
);
|
||||
testFileSystem.currentDirectory = testFileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.');
|
||||
});
|
||||
|
||||
testUsingContext('empty pubspec', () async {
|
||||
globals.fs.file('pubspec.yaml')
|
||||
..createSync()
|
||||
..writeAsStringSync('');
|
||||
|
||||
final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
|
||||
await bundle.build(packagesPath: '.packages', targetPlatform: TargetPlatform.web_javascript);
|
||||
|
||||
expect(bundle.entries.keys,
|
||||
unorderedEquals(<String>[
|
||||
'AssetManifest.json',
|
||||
'AssetManifest.bin',
|
||||
'AssetManifest.bin.json',
|
||||
])
|
||||
);
|
||||
expect(
|
||||
utf8.decode(await bundle.entries['AssetManifest.json']!.contentsAsBytes()),
|
||||
'{}',
|
||||
);
|
||||
expect(
|
||||
utf8.decode(await bundle.entries['AssetManifest.bin.json']!.contentsAsBytes()),
|
||||
'""',
|
||||
);
|
||||
}, overrides: <Type, Generator>{
|
||||
FileSystem: () => testFileSystem,
|
||||
ProcessManager: () => FakeProcessManager.any(),
|
||||
});
|
||||
|
||||
testUsingContext('pubspec contains an asset', () async {
|
||||
globals.fs.file('.packages').createSync();
|
||||
globals.fs.file('pubspec.yaml').writeAsStringSync(r'''
|
||||
name: test
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter:
|
||||
assets:
|
||||
- assets/bar/lizard.png
|
||||
''');
|
||||
globals.fs.file(
|
||||
globals.fs.path.joinAll(<String>['assets', 'bar', 'lizard.png'])
|
||||
).createSync(recursive: true);
|
||||
|
||||
final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
|
||||
await bundle.build(packagesPath: '.packages', targetPlatform: TargetPlatform.web_javascript);
|
||||
|
||||
expect(bundle.entries.keys,
|
||||
unorderedEquals(<String>[
|
||||
'AssetManifest.json',
|
||||
'AssetManifest.bin',
|
||||
'AssetManifest.bin.json',
|
||||
'FontManifest.json',
|
||||
'NOTICES', // not .Z
|
||||
'assets/bar/lizard.png',
|
||||
])
|
||||
);
|
||||
final Map<Object?, Object?> manifestJson = json.decode(
|
||||
utf8.decode(
|
||||
await bundle.entries['AssetManifest.json']!.contentsAsBytes()
|
||||
)
|
||||
) as Map<Object?, Object?>;
|
||||
expect(manifestJson, isNotEmpty);
|
||||
expect(manifestJson['assets/bar/lizard.png'], isNotNull);
|
||||
|
||||
final Uint8List manifestBinJsonBytes = base64.decode(
|
||||
json.decode(
|
||||
utf8.decode(
|
||||
await bundle.entries['AssetManifest.bin.json']!.contentsAsBytes()
|
||||
)
|
||||
) as String
|
||||
);
|
||||
|
||||
final Uint8List manifestBinBytes = Uint8List.fromList(
|
||||
await bundle.entries['AssetManifest.bin']!.contentsAsBytes()
|
||||
);
|
||||
|
||||
expect(manifestBinJsonBytes, equals(manifestBinBytes),
|
||||
reason: 'JSON-encoded binary content should be identical to BIN file.');
|
||||
}, overrides: <Type, Generator>{
|
||||
FileSystem: () => testFileSystem,
|
||||
ProcessManager: () => FakeProcessManager.any(),
|
||||
});
|
||||
});
|
||||
|
||||
testUsingContext('Failed directory delete shows message', () async {
|
||||
final FileExceptionHandler handler = FileExceptionHandler();
|
||||
final FileSystem fileSystem = MemoryFileSystem.test(opHandle: handler.opHandle);
|
||||
|
|
Loading…
Reference in a new issue