[web] Unify JS configuration. Make it available from initEngine. (flutter/engine#37187)

* Add the 'windows' parameter to the initializeEngine function.

* Wire the initEngine configuration into the engine.

* Extend configuration.dart
* Reuse JsFlutterConfiguration JS-interop as the configuration source
  both for initEngine, and the window.flutterConfig object.
* Add 'renderer' and 'targetElement' fields to the
  JsFlutterConfiguration object.
* Modify the `FlutterConfiguration` object so that it supports more
  than one configuration source. Return the first non-null value that
  was most recently set, or fall back to a default.

* Silence bootstrap initialization log to debug level.

* targetElement to hostElement

* jsParams -> runtimeConfiguration

* Add configuration_test.dart

* Add test to check init stores config.

* Renamed test so it actually runs.

* Update configuration object. Make it throwy at init/override.

* Use new config object, tweak some docs.

* Tweak warn/assert messages.

* Some renaming:

* runtimeConfig -> configuration (as unnamed function parameter)
* runtimeConfiguration -> jsConfiguration (as named function parameter,
to prevent clashes with the configuration singleton in the engine code)
* initEngine -> initializeEngine (because no need to abbreviate that)

* Ensure init test does not use global config.

* Sort JsFlutterConfiguration getters alphabetically.

* Addresses PR comments.
This commit is contained in:
David Iglesias 2022-11-04 15:36:23 -07:00 committed by GitHub
parent e815cda497
commit 0710f7aeee
12 changed files with 251 additions and 85 deletions

View File

@ -66,8 +66,8 @@ Future<void> webOnlyWarmupEngine({
}) async {
// Create the object that knows how to bootstrap an app from JS and Dart.
final engine.AppBootstrap bootstrap = engine.AppBootstrap(
initEngine: () async {
await engine.initializeEngineServices();
initializeEngine: ([engine.JsFlutterConfiguration? configuration]) async {
await engine.initializeEngineServices(jsConfiguration: configuration);
}, runApp: () async {
if (registerPlugins != null) {
registerPlugins();
@ -86,11 +86,11 @@ Future<void> webOnlyWarmupEngine({
}
if (autoStart) {
// The user does not want control of the app, bootstrap immediately.
print('Flutter Web Bootstrap: Auto');
engine.domWindow.console.debug('Flutter Web Bootstrap: Auto.');
await bootstrap.autoStart();
} else {
// Yield control of the bootstrap procedure to the user.
print('Flutter Web Bootstrap: Programmatic');
engine.domWindow.console.debug('Flutter Web Bootstrap: Programmatic.');
engine.didCreateEngineInitializer!(bootstrap.prepareEngineInitializer());
}
}

View File

@ -4,28 +4,33 @@
import 'package:js/js.dart';
import 'configuration.dart';
import 'js_interop/js_loader.dart';
import 'js_interop/js_promise.dart';
/// The type of a function that initializes an engine (in Dart).
typedef InitEngineFn = Future<void> Function([JsFlutterConfiguration? params]);
/// A class that controls the coarse lifecycle of a Flutter app.
class AppBootstrap {
/// Construct a FlutterLoader
AppBootstrap({required Function initEngine, required Function runApp}) :
_initEngine = initEngine, _runApp = runApp;
/// Construct an AppBootstrap.
AppBootstrap({required InitEngineFn initializeEngine, required Function runApp}) :
_initializeEngine = initializeEngine, _runApp = runApp;
// TODO(dit): Be more strict with the below typedefs, so we can add incoming params for each function.
// A function to initialize the engine.
final InitEngineFn _initializeEngine;
// A function to initialize the engine
final Function _initEngine;
// A function to run the app
// A function to run the app.
//
// TODO(dit): Be more strict with the typedef of this function, so we can add
// typed params to the function. (See InitEngineFn).
final Function _runApp;
/// Immediately bootstraps the app.
///
/// This calls `initEngine` and `runApp` in succession.
Future<void> autoStart() async {
await _initEngine();
await _initializeEngine();
await _runApp();
}
@ -47,15 +52,14 @@ class AppBootstrap {
}),
// Calls [_initEngine], and returns a JS Promise that resolves to an
// app runner object.
initializeEngine: allowInterop(([InitializeEngineFnParameters? params]) {
initializeEngine: allowInterop(([JsFlutterConfiguration? configuration]) {
// `params` coming from Javascript may be used to configure the engine intialization.
// The internal `initEngine` function must accept those params, and then this
// code needs to be slightly modified to pass them to the initEngine call below.
// The internal `initEngine` function must accept those params.
return Promise<FlutterAppRunner>(allowInterop((
PromiseResolver<FlutterAppRunner> resolve,
PromiseRejecter _,
) async {
await _initEngine();
await _initializeEngine(configuration);
// Return an app runner object
resolve(_prepareAppRunner());
}));

View File

@ -5,29 +5,48 @@
/// JavaScript API a Flutter Web application can use to configure the Web
/// Engine.
///
/// The configuration is a plain JavaScript object set as the
/// `flutterConfiguration` property of the top-level `window` object.
/// The configuration is passed from JavaScript to the engine as part of the
/// bootstrap process, through the `FlutterEngineInitializer.initializeEngine`
/// JS method, with an (optional) object of type [JsFlutterConfiguration].
///
/// This library also supports the legacy method of setting a plain JavaScript
/// object set as the `flutterConfiguration` property of the top-level `window`
/// object, but that approach is now deprecated and will warn users.
///
/// Both methods are **disallowed** to be used at the same time.
///
/// Example:
///
/// <head>
/// <script>
/// window.flutterConfiguration = {
/// canvasKitBaseUrl: "https://example.com/my-custom-canvaskit/"
/// };
/// </script>
/// </head>
/// _flutter.loader.loadEntrypoint({
/// // ...
/// onEntrypointLoaded: async function(engineInitializer) {
/// let appRunner = await engineInitializer.initializeEngine({
/// // JsFlutterConfiguration goes here...
/// canvasKitBaseUrl: "https://example.com/my-custom-canvaskit/",
/// });
/// appRunner.runApp();
/// }
/// });
///
/// Configuration properties supplied via `window.flutterConfiguration`
/// override those supplied using the corresponding environment variables. For
/// example, if both `window.flutterConfiguration.canvasKitBaseUrl` and the
/// `FLUTTER_WEB_CANVASKIT_URL` environment variables are provided,
/// `window.flutterConfiguration.canvasKitBaseUrl` is used.
/// Example of the **deprecated** style (this will issue a JS console warning!):
///
/// <script>
/// window.flutterConfiguration = {
/// canvasKitBaseUrl: "https://example.com/my-custom-canvaskit/"
/// };
/// </script>
///
/// Configuration properties supplied via this object override those supplied
/// using the corresponding environment variables. For example, if both the
/// `canvasKitBaseUrl` config entry and the `FLUTTER_WEB_CANVASKIT_URL`
/// environment variables are provided, the `canvasKitBaseUrl` entry is used.
@JS()
library configuration;
import 'package:js/js.dart';
import 'package:meta/meta.dart';
import 'dom.dart';
/// The version of CanvasKit used by the web engine by default.
// DO NOT EDIT THE NEXT LINE OF CODE MANUALLY
@ -35,7 +54,8 @@ import 'package:js/js.dart';
const String _canvaskitVersion = '0.37.0';
/// The Web Engine configuration for the current application.
FlutterConfiguration get configuration => _configuration ??= FlutterConfiguration(_jsConfiguration);
FlutterConfiguration get configuration =>
_configuration ??= FlutterConfiguration.legacy(_jsConfiguration);
FlutterConfiguration? _configuration;
/// Sets the given configuration as the current one.
@ -43,22 +63,71 @@ FlutterConfiguration? _configuration;
/// This must be called before the engine is initialized. Calling it after the
/// engine is initialized will result in some of the properties not taking
/// effect because they are consumed during initialization.
@visibleForTesting
void debugSetConfiguration(FlutterConfiguration configuration) {
_configuration = configuration;
}
/// Supplies Web Engine configuration properties.
class FlutterConfiguration {
/// Constructs a configuration from a JavaScript object containing
/// runtime-supplied properties.
FlutterConfiguration(this._js);
/// Constructs an unitialized configuration object.
@visibleForTesting
FlutterConfiguration();
final JsFlutterConfiguration? _js;
/// Constucts a "tainted by JS globals" configuration object.
///
/// This configuration style is deprecated. It will warn the user about the
/// new API (if used)
FlutterConfiguration.legacy(JsFlutterConfiguration? config) {
if (config != null) {
_usedLegacyConfigStyle = true;
_configuration = config;
}
// Warn the user of the deprecated behavior.
assert(() {
if (config != null) {
domWindow.console.warn('window.flutterConfiguration is now deprecated.\n'
'Use engineInitializer.initializeEngine(config) instead.\n'
'See: https://docs.flutter.dev/development/platform-integration/web/initialization');
}
if (_requestedRendererType != null) {
domWindow.console.warn('window.flutterWebRenderer is now deprecated.\n'
'Use engineInitializer.initializeEngine(config) instead.\n'
'See: https://docs.flutter.dev/development/platform-integration/web/initialization');
}
return true;
}());
}
bool _usedLegacyConfigStyle = false;
JsFlutterConfiguration? _configuration;
/// Sets a value for [_configuration].
///
/// This method is called by the engine initialization process, through the
/// [initEngineServices] method.
///
/// This method throws an AssertionError, if the _configuration object has
/// been set to anything non-null through the [FlutterConfiguration.legacy]
/// constructor.
void setUserConfiguration(JsFlutterConfiguration? configuration) {
if (configuration != null) {
assert(!_usedLegacyConfigStyle,
'Use engineInitializer.initializeEngine(config) only. '
'Using the (deprecated) window.flutterConfiguration and initializeEngine '
'configuration simultaneously is not supported.');
assert(_requestedRendererType == null || configuration.renderer == null,
'Use engineInitializer.initializeEngine(config) only. '
'Using the (deprecated) window.flutterWebRenderer and initializeEngine '
'configuration simultaneously is not supported.');
_configuration = configuration;
}
}
// Static constant parameters.
//
// These properties affect tree shaking and therefore cannot be supplied at
// runtime. They must be static constants for the compiler to remove dead
// runtime. They must be static constants for the compiler to remove dead code
// effectively.
/// Auto detect which rendering backend to use.
@ -110,7 +179,7 @@ class FlutterConfiguration {
/// --web-renderer=canvaskit \
/// --dart-define=FLUTTER_WEB_CANVASKIT_URL=https://example.com/custom-canvaskit-build/
/// ```
String get canvasKitBaseUrl => _js?.canvasKitBaseUrl ?? _defaultCanvasKitBaseUrl;
String get canvasKitBaseUrl => _configuration?.canvasKitBaseUrl ?? _defaultCanvasKitBaseUrl;
static const String _defaultCanvasKitBaseUrl = String.fromEnvironment(
'FLUTTER_WEB_CANVASKIT_URL',
defaultValue: 'https://unpkg.com/canvaskit-wasm@$_canvaskitVersion/bin/',
@ -121,7 +190,7 @@ class FlutterConfiguration {
///
/// This is mainly used for testing or for apps that want to ensure they
/// run on devices which don't support WebGL.
bool get canvasKitForceCpuOnly => _js?.canvasKitForceCpuOnly ?? _defaultCanvasKitForceCpuOnly;
bool get canvasKitForceCpuOnly => _configuration?.canvasKitForceCpuOnly ?? _defaultCanvasKitForceCpuOnly;
static const bool _defaultCanvasKitForceCpuOnly = bool.fromEnvironment(
'FLUTTER_WEB_CANVASKIT_FORCE_CPU_ONLY',
);
@ -135,7 +204,7 @@ class FlutterConfiguration {
///
/// This value can be specified using either the `FLUTTER_WEB_MAXIMUM_SURFACES`
/// environment variable, or using the runtime configuration.
int get canvasKitMaximumSurfaces => _js?.canvasKitMaximumSurfaces ?? _defaultCanvasKitMaximumSurfaces;
int get canvasKitMaximumSurfaces => _configuration?.canvasKitMaximumSurfaces ?? _defaultCanvasKitMaximumSurfaces;
static const int _defaultCanvasKitMaximumSurfaces = int.fromEnvironment(
'FLUTTER_WEB_MAXIMUM_SURFACES',
defaultValue: 8,
@ -152,10 +221,23 @@ class FlutterConfiguration {
/// ```
/// flutter run -d chrome --profile --dart-define=FLUTTER_WEB_DEBUG_SHOW_SEMANTICS=true
/// ```
bool get debugShowSemanticsNodes => _js?.debugShowSemanticsNodes ?? _defaultDebugShowSemanticsNodes;
bool get debugShowSemanticsNodes => _configuration?.debugShowSemanticsNodes ?? _defaultDebugShowSemanticsNodes;
static const bool _defaultDebugShowSemanticsNodes = bool.fromEnvironment(
'FLUTTER_WEB_DEBUG_SHOW_SEMANTICS',
);
/// Returns the [hostElement] in which the Flutter Application is supposed
/// to render, or `null` if the user hasn't specified anything.
DomElement? get hostElement => _configuration?.hostElement;
/// Returns the [requestedRendererType] to be used with the current Flutter
/// application, normally 'canvaskit' or 'auto'.
///
/// This value may come from the JS configuration, but also a specific JS value:
/// `window.flutterWebRenderer`.
///
/// This is used by the Renderer class to decide how to initialize the engine.
String? get requestedRendererType => _configuration?.renderer ?? _requestedRendererType;
}
@JS('window.flutterConfiguration')
@ -169,13 +251,13 @@ class JsFlutterConfiguration {}
extension JsFlutterConfigurationExtension on JsFlutterConfiguration {
external String? get canvasKitBaseUrl;
external bool? get canvasKitForceCpuOnly;
external bool? get debugShowSemanticsNodes;
external int? get canvasKitMaximumSurfaces;
external set canvasKitMaximumSurfaces(int? maxSurfaces);
external bool? get debugShowSemanticsNodes;
external DomElement? get hostElement;
external String? get renderer;
}
/// A JavaScript entrypoint that allows developer to set rendering backend
/// at runtime before launching the application.
@JS('window.flutterWebRenderer')
external String? get requestedRendererType;
external String? get _requestedRendererType;

View File

@ -77,6 +77,7 @@ class DomConsole {}
extension DomConsoleExtension on DomConsole {
external void warn(Object? arg);
external void error(Object? arg);
external void debug(Object? arg);
}
@JS('window')

View File

@ -7,6 +7,7 @@ import 'dart:developer' as developer;
import 'package:ui/src/engine/assets.dart';
import 'package:ui/src/engine/browser_detection.dart';
import 'package:ui/src/engine/configuration.dart';
import 'package:ui/src/engine/embedder.dart';
import 'package:ui/src/engine/mouse_cursor.dart';
import 'package:ui/src/engine/navigation.dart';
@ -126,6 +127,7 @@ void debugResetEngineInitializationState() {
/// puts UI elements on the page.
Future<void> initializeEngineServices({
AssetManager? assetManager,
JsFlutterConfiguration? jsConfiguration
}) async {
if (_initializationState != DebugEngineInitializationState.uninitialized) {
assert(() {
@ -139,6 +141,9 @@ Future<void> initializeEngineServices({
}
_initializationState = DebugEngineInitializationState.initializingServices;
// Store `jsConfiguration` so user settings are available to the engine.
configuration.setUserConfiguration(jsConfiguration);
// Setup the hook that allows users to customize URL strategy before running
// the app.
_addUrlStrategyListener();

View File

@ -7,6 +7,7 @@ library js_loader;
import 'package:js/js.dart';
import '../configuration.dart';
import 'js_promise.dart';
/// Typedef for the function that notifies JS that the main entrypoint is up and running.
@ -25,26 +26,6 @@ external Object? get loader;
@JS('_flutter.loader.didCreateEngineInitializer')
external DidCreateEngineInitializerFn? get didCreateEngineInitializer;
// /// window._flutter
// @JS('_flutter')
// external FlutterJsNamespace? get flutterjs;
// /// window._flutter.loader
// @JS()
// @anonymous
// class FlutterJsNamespace {
// external FlutterJsLoaderNamespace? get loader;
// }
// /// The bits of window._flutter.loader that the Flutter Engine cares about.
// @JS()
// @anonymous
// class FlutterJsLoaderNamespace {
// /// A hook to notify JavaScript that Flutter is up and running!
// /// This is setup by flutter.js when the main entrypoint bundle is injected.
// external DidCreateEngineInitializerFn? get didCreateEngineInitializer;
// }
// FlutterEngineInitializer
/// An object that allows the user to initialize the Engine of a Flutter App.
@ -61,17 +42,12 @@ abstract class FlutterEngineInitializer{
});
}
/// The shape of the object that can be passed as parameter to the
/// initializeEngine function of the FlutterEngineInitializer object
/// (when called from JS).
@JS()
@anonymous
@staticInterop
abstract class InitializeEngineFnParameters {
}
/// Typedef for the function that initializes the flutter engine.
typedef InitializeEngineFn = Promise<FlutterAppRunner?> Function([InitializeEngineFnParameters?]);
///
/// [JsFlutterConfiguration] comes from `../configuration.dart`. It is the same
/// object that can be used to configure flutter "inline", through the
/// (to be deprecated) `window.flutterConfiguration` object.
typedef InitializeEngineFn = Promise<FlutterAppRunner?> Function([JsFlutterConfiguration?]);
/// Typedef for the `autoStart` function that can be called straight from an engine initializer instance.
/// (Similar to [RunAppFn], but taking no specific "runApp" parameters).

View File

@ -32,8 +32,8 @@ abstract class Renderer {
}
bool useCanvasKit;
if (FlutterConfiguration.flutterWebAutoDetect) {
if (requestedRendererType != null) {
useCanvasKit = requestedRendererType == 'canvaskit';
if (configuration.requestedRendererType != null) {
useCanvasKit = configuration.requestedRendererType == 'canvaskit';
} else {
// If requestedRendererType is not specified, use CanvasKit for desktop and
// html for mobile.

View File

@ -4,6 +4,7 @@
import 'dart:async';
import 'package:js/js_util.dart' as js_util;
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
@ -804,8 +805,13 @@ void testMain() {
test('works correctly with max overlays == 2', () async {
final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer;
debugSetConfiguration(FlutterConfiguration(
JsFlutterConfiguration()..canvasKitMaximumSurfaces = 2));
final FlutterConfiguration config = FlutterConfiguration()
..setUserConfiguration(
js_util.jsify(<String, Object?>{
'canvasKitMaximumSurfaces': 2,
}) as JsFlutterConfiguration);
debugSetConfiguration(config);
SurfaceFactory.instance.debugClear();
expect(SurfaceFactory.instance.maximumSurfaces, 2);
@ -847,7 +853,7 @@ void testMain() {
]);
// Reset configuration
debugSetConfiguration(FlutterConfiguration(null));
debugSetConfiguration(FlutterConfiguration());
});
test(

View File

@ -0,0 +1,29 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:js/js_util.dart' as js_util;
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
group('initializeEngineServices', () {
test('stores user configuration', () async {
// dev/test_platform.dart injects a global configuration object. Let's
// fetch that, override one of its properties (under test), then delete it
// from window (so our configuration asserts don't fire!)
final JsFlutterConfiguration config = js_util.getProperty(domWindow, 'flutterConfiguration');
js_util.setProperty(config, 'canvasKitMaximumSurfaces', 32);
js_util.setProperty(domWindow, 'flutterConfiguration', null);
await initializeEngineServices(jsConfiguration: config);
expect(configuration.canvasKitMaximumSurfaces, 32);
});
});
}

View File

@ -26,7 +26,7 @@ void testMain() {
runCalled = 0;
});
Future<void> mockInit () async {
Future<void> mockInit ([JsFlutterConfiguration? configuration]) async {
initCalled = callOrder++;
await Future<void>.delayed(const Duration(milliseconds: 1));
}
@ -37,7 +37,7 @@ void testMain() {
test('autoStart() immediately calls init and run', () async {
final AppBootstrap bootstrap = AppBootstrap(
initEngine: mockInit,
initializeEngine: mockInit,
runApp: mockRunApp,
);
@ -49,7 +49,7 @@ void testMain() {
test('engineInitializer autoStart() does the same as Dart autoStart()', () async {
final AppBootstrap bootstrap = AppBootstrap(
initEngine: mockInit,
initializeEngine: mockInit,
runApp: mockRunApp,
);
@ -66,7 +66,7 @@ void testMain() {
test('engineInitializer initEngine() calls init and returns an appRunner', () async {
final AppBootstrap bootstrap = AppBootstrap(
initEngine: mockInit,
initializeEngine: mockInit,
runApp: mockRunApp,
);
@ -81,7 +81,7 @@ void testMain() {
test('appRunner runApp() calls run and returns a FlutterApp', () async {
final AppBootstrap bootstrap = AppBootstrap(
initEngine: mockInit,
initializeEngine: mockInit,
runApp: mockRunApp,
);

View File

@ -0,0 +1,63 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
@TestOn('browser')
import 'package:js/js_util.dart' as js_util;
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import '../matchers.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
group('FlutterConfiguration', () {
test('initializes with null', () async {
final FlutterConfiguration config = FlutterConfiguration.legacy(null);
expect(config.canvasKitMaximumSurfaces, 8); // _defaultCanvasKitMaximumSurfaces
});
test('legacy constructor initializes with a Js Object', () async {
final FlutterConfiguration config = FlutterConfiguration.legacy(
js_util.jsify(<String, Object?>{
'canvasKitMaximumSurfaces': 16,
}) as JsFlutterConfiguration);
expect(config.canvasKitMaximumSurfaces, 16);
});
});
group('setUserConfiguration', () {
test('throws assertion error if already initialized from JS', () async {
final FlutterConfiguration config = FlutterConfiguration.legacy(
js_util.jsify(<String, Object?>{
'canvasKitMaximumSurfaces': 12,
}) as JsFlutterConfiguration);
expect(() {
config.setUserConfiguration(
js_util.jsify(<String, Object?>{
'canvasKitMaximumSurfaces': 16,
}) as JsFlutterConfiguration);
}, throwsAssertionError);
});
test('stores config if JS configuration was null', () async {
final FlutterConfiguration config = FlutterConfiguration.legacy(null);
config.setUserConfiguration(
js_util.jsify(<String, Object?>{
'canvasKitMaximumSurfaces': 16,
}) as JsFlutterConfiguration);
expect(config.canvasKitMaximumSurfaces, 16);
});
});
}