Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 0d8d295

Browse files
authored
Reland "[web] Accepts assetBase through JS config. (#40615)" (#40677)
Reland "[web] Accepts assetBase through JS config. (#40615)"
1 parent d6a2c0a commit 0d8d295

File tree

4 files changed

+212
-21
lines changed

4 files changed

+212
-21
lines changed

lib/web_ui/lib/src/engine/assets.dart

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,26 +24,48 @@ const Map<String, String> testFontUrls = <String, String>{
2424

2525
/// This class downloads assets over the network.
2626
///
27-
/// The assets are resolved relative to [assetsDir] inside the directory
28-
/// containing the currently executing JS script.
27+
/// Assets are resolved relative to [assetsDir] inside the absolute base
28+
/// specified by [assetBase] (optional).
29+
///
30+
/// By default, URLs are relative to the `<base>` of the current website.
2931
class AssetManager {
30-
/// Initializes [AssetManager] with path to assets relative to baseUrl.
31-
const AssetManager({this.assetsDir = _defaultAssetsDir});
32+
/// Initializes [AssetManager] with paths.
33+
AssetManager({
34+
this.assetsDir = _defaultAssetsDir,
35+
String? assetBase,
36+
}) : assert(
37+
assetBase == null || assetBase.endsWith('/'),
38+
'`assetBase` must end with a `/` character.',
39+
),
40+
_assetBase = assetBase;
3241

3342
static const String _defaultAssetsDir = 'assets';
3443

3544
/// The directory containing the assets.
3645
final String assetsDir;
3746

38-
String? get _baseUrl {
39-
return domWindow.document
40-
.querySelectorAll('meta')
41-
.where((DomElement domNode) => domInstanceOfString(domNode,
42-
'HTMLMetaElement'))
43-
.map((DomElement domNode) => domNode as DomHTMLMetaElement)
44-
.firstWhereOrNull(
45-
(DomHTMLMetaElement element) => element.name == 'assetBase')
46-
?.content;
47+
/// The absolute base URL for assets.
48+
String? _assetBase;
49+
50+
// Cache a value for `_assetBase` so we don't hit the DOM multiple times.
51+
String get _baseUrl => _assetBase ??= _deprecatedAssetBase ?? '';
52+
53+
// Retrieves the `assetBase` value from the DOM.
54+
//
55+
// This warns the user and points them to the new initializeEngine style.
56+
String? get _deprecatedAssetBase {
57+
final DomHTMLMetaElement? meta = domWindow.document
58+
.querySelector('meta[name=assetBase]') as DomHTMLMetaElement?;
59+
60+
final String? fallbackBaseUrl = meta?.content;
61+
62+
if (fallbackBaseUrl != null) {
63+
// Warn users that they're using a deprecated configuration style...
64+
domWindow.console.warn('The `assetBase` meta tag is now deprecated.\n'
65+
'Use engineInitializer.initializeEngine(config) instead.\n'
66+
'See: https://docs.flutter.dev/development/platform-integration/web/initialization');
67+
}
68+
return fallbackBaseUrl;
4769
}
4870

4971
/// Returns the URL to load the asset from, given the asset key.
@@ -67,7 +89,7 @@ class AssetManager {
6789
if (Uri.parse(asset).hasScheme) {
6890
return Uri.encodeFull(asset);
6991
}
70-
return Uri.encodeFull('${_baseUrl ?? ''}$assetsDir/$asset');
92+
return Uri.encodeFull('$_baseUrl$assetsDir/$asset');
7193
}
7294

7395
/// Loads an asset and returns the server response.
@@ -90,7 +112,7 @@ class AssetManager {
90112
}
91113

92114
/// An asset manager that gives fake empty responses for assets.
93-
class WebOnlyMockAssetManager implements AssetManager {
115+
class WebOnlyMockAssetManager extends AssetManager {
94116
/// Mock asset directory relative to base url.
95117
String defaultAssetsDir = '';
96118

@@ -113,9 +135,6 @@ class WebOnlyMockAssetManager implements AssetManager {
113135
@override
114136
String get assetsDir => defaultAssetsDir;
115137

116-
@override
117-
String get _baseUrl => '';
118-
119138
@override
120139
String getAssetUrl(String asset) => asset;
121140

@@ -127,7 +146,7 @@ class WebOnlyMockAssetManager implements AssetManager {
127146
status: 200,
128147
payload: MockHttpFetchPayload(
129148
byteBuffer: _toByteData(utf8.encode(defaultAssetManifest)).buffer,
130-
)
149+
),
131150
);
132151
}
133152
if (asset == getAssetUrl('FontManifest.json')) {
@@ -136,7 +155,7 @@ class WebOnlyMockAssetManager implements AssetManager {
136155
status: 200,
137156
payload: MockHttpFetchPayload(
138157
byteBuffer: _toByteData(utf8.encode(defaultFontManifest)).buffer,
139-
)
158+
),
140159
);
141160
}
142161

lib/web_ui/lib/src/engine/configuration.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,32 @@ class FlutterConfiguration {
158158
// runtime. Runtime-supplied values take precedence over environment
159159
// variables.
160160

161+
/// The absolute base URL of the location of the `assets` directory of the app.
162+
///
163+
/// This value is useful when Flutter web assets are deployed to a separate
164+
/// domain (or subdirectory) from which the index.html is served, for example:
165+
///
166+
/// * Application: https://www.my-app.com/
167+
/// * Flutter Assets: https://cdn.example.com/my-app/build-hash/assets/
168+
///
169+
/// The `assetBase` value would be set to:
170+
///
171+
/// * `'https://cdn.example.com/my-app/build-hash/'`
172+
///
173+
/// It is also useful in the case that a Flutter web application is embedded
174+
/// into another web app, in a way that the `<base>` tag of the index.html
175+
/// cannot be set (because it'd break the host app), for example:
176+
///
177+
/// * Application: https://www.my-app.com/
178+
/// * Flutter Assets: https://www.my-app.com/static/companion/flutter/assets/
179+
///
180+
/// The `assetBase` would be set to:
181+
///
182+
/// * `'/static/companion/flutter/'`
183+
///
184+
/// Do not confuse this configuration value with [canvasKitBaseUrl].
185+
String? get assetBase => _configuration?.assetBase;
186+
161187
/// The base URL to use when downloading the CanvasKit script and associated
162188
/// wasm.
163189
///
@@ -267,6 +293,10 @@ external JsFlutterConfiguration? get _jsConfiguration;
267293
class JsFlutterConfiguration {}
268294

269295
extension JsFlutterConfigurationExtension on JsFlutterConfiguration {
296+
@JS('assetBase')
297+
external JSString? get _assetBase;
298+
String? get assetBase => _assetBase?.toDart;
299+
270300
@JS('canvasKitBaseUrl')
271301
external JSString? get _canvasKitBaseUrl;
272302
String? get canvasKitBaseUrl => _canvasKitBaseUrl?.toDart;

lib/web_ui/lib/src/engine/initialization.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ Future<void> initializeEngineServices({
211211
}
212212
};
213213

214-
assetManager ??= const AssetManager();
214+
assetManager ??= AssetManager(assetBase: configuration.assetBase);
215215
_setAssetManager(assetManager);
216216

217217
Future<void> initializeRendererCallback () async => renderer.initialize();
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
@TestOn('browser')
6+
library;
7+
8+
import 'package:test/bootstrap/browser.dart';
9+
import 'package:test/test.dart';
10+
import 'package:ui/src/engine.dart';
11+
12+
import '../common/matchers.dart';
13+
14+
void main() {
15+
internalBootstrapBrowserTest(() => testMain);
16+
}
17+
18+
void testMain() {
19+
group('AssetManager getAssetUrl', () {
20+
setUp(() {
21+
// Remove the meta-tag from the environment before each test.
22+
removeAssetBaseMeta();
23+
});
24+
25+
test('initializes with default values', () {
26+
final AssetManager assets = AssetManager();
27+
28+
expect(
29+
assets.getAssetUrl('asset.txt'),
30+
'assets/asset.txt',
31+
reason: 'Default `assetsDir` is "assets".',
32+
);
33+
});
34+
35+
test('assetsDir changes the directory where assets are stored', () {
36+
final AssetManager assets = AssetManager(assetsDir: 'static');
37+
38+
expect(assets.getAssetUrl('asset.txt'), 'static/asset.txt');
39+
});
40+
41+
test('assetBase must end with slash', () {
42+
expect(() {
43+
AssetManager(assetBase: '/deployment');
44+
}, throwsAssertionError);
45+
});
46+
47+
test('assetBase can be relative', () {
48+
final AssetManager assets = AssetManager(assetBase: 'base/');
49+
50+
expect(assets.getAssetUrl('asset.txt'), 'base/assets/asset.txt');
51+
});
52+
53+
test('assetBase can be absolute', () {
54+
final AssetManager assets = AssetManager(
55+
assetBase: 'https://www.gstatic.com/my-app/',
56+
);
57+
58+
expect(
59+
assets.getAssetUrl('asset.txt'),
60+
'https://www.gstatic.com/my-app/assets/asset.txt',
61+
);
62+
});
63+
64+
test('assetBase in conjunction with assetsDir, fully custom paths', () {
65+
final AssetManager assets = AssetManager(
66+
assetBase: '/asset/base/',
67+
assetsDir: 'static',
68+
);
69+
70+
expect(assets.getAssetUrl('asset.txt'), '/asset/base/static/asset.txt');
71+
});
72+
73+
test('Fully-qualified asset URLs are untouched', () {
74+
final AssetManager assets = AssetManager();
75+
76+
expect(
77+
assets.getAssetUrl('https://static.my-app.com/favicon.ico'),
78+
'https://static.my-app.com/favicon.ico',
79+
);
80+
});
81+
82+
test('Fully-qualified asset URLs are untouched (even with assetBase)', () {
83+
final AssetManager assets = AssetManager(
84+
assetBase: 'https://static.my-app.com/',
85+
);
86+
87+
expect(
88+
assets.getAssetUrl('https://static.my-app.com/favicon.ico'),
89+
'https://static.my-app.com/favicon.ico',
90+
);
91+
});
92+
});
93+
94+
group('AssetManager getAssetUrl with <meta name=assetBase> tag', () {
95+
setUp(() {
96+
removeAssetBaseMeta();
97+
addAssetBaseMeta('/dom/base/');
98+
});
99+
100+
test('reads value from DOM', () {
101+
final AssetManager assets = AssetManager();
102+
103+
expect(assets.getAssetUrl('asset.txt'), '/dom/base/assets/asset.txt');
104+
});
105+
106+
test('reads value from DOM (only once!)', () {
107+
final AssetManager firstManager = AssetManager();
108+
expect(
109+
firstManager.getAssetUrl('asset.txt'),
110+
'/dom/base/assets/asset.txt',
111+
);
112+
113+
removeAssetBaseMeta();
114+
final AssetManager anotherManager = AssetManager();
115+
116+
expect(
117+
firstManager.getAssetUrl('asset.txt'),
118+
'/dom/base/assets/asset.txt',
119+
reason: 'The old value of the assetBase meta should be cached.',
120+
);
121+
expect(anotherManager.getAssetUrl('asset.txt'), 'assets/asset.txt');
122+
});
123+
});
124+
}
125+
126+
/// Removes all meta-tags with name=assetBase.
127+
void removeAssetBaseMeta() {
128+
domWindow.document
129+
.querySelectorAll('meta[name=assetBase]')
130+
.forEach((DomElement element) {
131+
element.remove();
132+
});
133+
}
134+
135+
/// Adds a meta-tag with name=assetBase and the passed-in [value].
136+
void addAssetBaseMeta(String value) {
137+
final DomHTMLMetaElement meta = createDomHTMLMetaElement()
138+
..name = 'assetBase'
139+
..content = value;
140+
141+
domDocument.head!.append(meta);
142+
}

0 commit comments

Comments
 (0)