Skip to content

Commit

Permalink
feat: support preloadedBundles in WebF (#500)
Browse files Browse the repository at this point in the history
This PR adds a new preloadedBundles property to the WebF widget,
allowing users to preload external resources (HTML, CSS, JavaScript, or
Images) before WebF is mounted in Flutter.

This feature can save a lot of time if all resources are preloaded
before WebF is mounted.

Here is a example to use this feature:


```dart
Future<List<WebFBundle>> preloadWebFBundles(List<String> entryPoints) async {
    List<Future<WebFBundle>> bundles = entryPoints.map((e) async {
      WebFBundle bundle = WebFBundle.fromUrl(e);
      await bundle.resolve();
      await bundle.obtainData();
      return bundle;
    }).toList();
    return await Future.wait(bundles);
  }

@OverRide
  Widget build(BuildContext context) {
    final MediaQueryData queryData = MediaQuery.of(context);
    final Size viewportSize = queryData.size;
    return Scaffold(
        body: Center(
          child: Column(
            children: [
              FutureBuilder<List<WebFBundle>>(future: preloadWebFBundles([
                // The lists with absolute path for remote resources
                'assets:assets/bundle.html',
                'assets:assets/bundle.js',
                'assets:assets/webf.png',
              ]), builder: (BuildContext context, AsyncSnapshot<List<WebFBundle>> snapshot) {
                if (snapshot.data != null) {
                  return WebF(
                    devToolsService: ChromeDevToolsService(),
                    viewportWidth: viewportSize.width - queryData.padding.horizontal,
                    viewportHeight: viewportSize.height - appBar.preferredSize.height - queryData.padding.vertical,
                    preloadedBundles: snapshot.data,
                    bundle: snapshot.data![0]
                  );
                }

                return Text('Load error');
              })
            ],
          ),
        ));
  }

```
  • Loading branch information
andycall authored Nov 6, 2023
1 parent 80bbada commit e4efa00
Show file tree
Hide file tree
Showing 11 changed files with 113 additions and 66 deletions.
24 changes: 22 additions & 2 deletions webf/lib/src/css/background.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@

import 'dart:convert';
import 'dart:math' as math;
import 'dart:typed_data';
import 'dart:ui';

import 'package:flutter/painting.dart';
import 'package:flutter/rendering.dart';
import 'package:webf/painting.dart';
import 'package:webf/html.dart';
import 'package:webf/css.dart';
import 'package:webf/launcher.dart';
import 'package:webf/rendering.dart';
Expand Down Expand Up @@ -253,6 +256,18 @@ class CSSBackgroundImage {

ImageProvider? _image;

Future<ImageLoadResponse> _obtainImage(Uri url) async {
ImageRequest request = ImageRequest.fromUri(url);
// Increment count when request.
controller.view.document.incrementRequestCount();

ImageLoadResponse data = await request.obtainImage(controller);

// Decrement count when response.
controller.view.document.decrementRequestCount();
return data;
}

ImageProvider? get image {
if (_image != null) return _image;
for (CSSFunctionalNotation method in functions) {
Expand All @@ -267,8 +282,13 @@ class CSSBackgroundImage {
Uri uri = Uri.parse(url);
if (url.isNotEmpty) {
uri = controller.uriParser!.resolve(Uri.parse(baseHref ?? controller.url), uri);
_image = getImageProvider(uri, contextId: controller.view.contextId);
return _image;
FlutterView ownerFlutterView = controller.ownerFlutterView;
return BoxFitImage(
boxFit: renderStyle.backgroundSize.fit,
url: uri,
loadImage: _obtainImage,
devicePixelRatio: ownerFlutterView.devicePixelRatio
);
}
}
}
Expand Down
6 changes: 4 additions & 2 deletions webf/lib/src/css/font_face.dart
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,10 @@ class CSSFontFace {
} else {
Uri? uri = _resolveFontSource(contextId, targetFont.src, baseHref);
if (uri == null) return;
WebFBundle bundle = WebFBundle.fromUrl(uri.toString());
await bundle.resolve(contextId);
final WebFController controller = WebFController.getControllerOfJSContextId(contextId)!;
WebFBundle bundle = controller.getPreloadBundleFromUrl(uri.toString()) ?? WebFBundle.fromUrl(uri.toString());
await bundle.resolve(baseUrl: controller.url, uriParser: controller.uriParser);
await bundle.obtainData();
assert(bundle.isResolved, 'Failed to obtain $url');
FontLoader loader = FontLoader(removeQuotationMark(fontFamily));
Future<ByteData> bytes = Future.value(bundle.data?.buffer.asByteData());
Expand Down
38 changes: 19 additions & 19 deletions webf/lib/src/foundation/bundle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:webf/foundation.dart';
import 'package:webf/launcher.dart';
import 'package:webf/module.dart';

const String DEFAULT_URL = 'about:blank';
Expand Down Expand Up @@ -97,26 +96,27 @@ abstract class WebFBundle {
Uint8List? data;

// Indicate the bundle is resolved.
bool get isResolved => _uri != null && data != null;
bool get isResolved => _uri != null;

// Content type for data.
// The default value is plain text.
ContentType contentType = ContentType.text;

@mustCallSuper
Future<void> resolve(int? contextId) async {
Future<void> resolve({ String? baseUrl, UriParser? uriParser }) async {
if (isResolved) return;

// Source is input by user, do not trust it's a valid URL.
_uri = Uri.tryParse(url);
if (contextId != null && _uri != null) {
WebFController? controller = WebFController.getControllerOfJSContextId(contextId);
if (controller != null) {
_uri = controller.uriParser!.resolve(Uri.parse(controller.url), _uri!);
}

if (baseUrl != null && _uri != null) {
uriParser ??= UriParser();
_uri = uriParser.resolve(Uri.parse(baseUrl), _uri!);
}
}

Future<void> obtainData();

// Dispose the memory obtained by bundle.
@mustCallSuper
void dispose() {
Expand Down Expand Up @@ -183,6 +183,9 @@ class DataBundle extends WebFBundle {
data = uriData.contentAsBytes();
this.contentType = contentType ?? ContentType.parse('${uriData.mimeType}; charset=${uriData.charset}');
}

@override
Future<void> obtainData() async {}
}

// The bundle that source from http or https.
Expand All @@ -196,16 +199,14 @@ class NetworkBundle extends WebFBundle {
Map<String, String>? additionalHttpHeaders = {};

@override
Future<void> resolve(int? contextId) async {
super.resolve(contextId);
Future<void> obtainData() async {
if (data != null) return;

final HttpClientRequest request = await _sharedHttpClient.getUrl(_uri!);

// Prepare request headers.
request.headers.set('Accept', _acceptHeader());
additionalHttpHeaders?.forEach(request.headers.set);
if (contextId != null) {
WebFHttpOverrides.setContextHeader(request.headers, contextId);
}

final HttpClientResponse response = await request.close();
if (response.statusCode != HttpStatus.ok)
Expand Down Expand Up @@ -235,8 +236,9 @@ class AssetsBundle extends WebFBundle with _ExtensionContentTypeResolver {
AssetsBundle(String url) : super(url);

@override
Future<WebFBundle> resolve(int? contextId) async {
super.resolve(contextId);
Future<void> obtainData() async {
if (data != null) return;

final Uri? _resolvedUri = resolvedUri;
if (_resolvedUri != null) {
final String assetName = getAssetName(_resolvedUri);
Expand All @@ -245,7 +247,6 @@ class AssetsBundle extends WebFBundle with _ExtensionContentTypeResolver {
} else {
_failedToResolveBundle(url);
}
return this;
}

/// Get flutter asset name from uri scheme asset.
Expand All @@ -267,8 +268,8 @@ class FileBundle extends WebFBundle with _ExtensionContentTypeResolver {
FileBundle(String url) : super(url);

@override
Future<WebFBundle> resolve(int? contextId) async {
super.resolve(contextId);
Future<void> obtainData() async {
if (data != null) return;

Uri uri = _uri!;
final String path = uri.path;
Expand All @@ -279,7 +280,6 @@ class FileBundle extends WebFBundle with _ExtensionContentTypeResolver {
} else {
_failedToResolveBundle(url);
}
return this;
}
}

Expand Down
5 changes: 3 additions & 2 deletions webf/lib/src/html/head.dart
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,15 @@ class LinkElement extends Element {
isConnected &&
!_stylesheetLoaded.containsKey(_resolvedHyperlink.toString())) {
String url = _resolvedHyperlink.toString();
WebFBundle bundle = WebFBundle.fromUrl(url);
WebFBundle bundle = ownerDocument.controller.getPreloadBundleFromUrl(url) ?? WebFBundle.fromUrl(url);
_stylesheetLoaded[url] = true;
try {
_loading = true;
// Increment count when request.
ownerDocument.incrementRequestCount();

await bundle.resolve(contextId);
await bundle.resolve(baseUrl: ownerDocument.controller.url, uriParser: ownerDocument.controller.uriParser);
await bundle.obtainData();
assert(bundle.isResolved, 'Failed to obtain $url');
_loading = false;
// Decrement count when response.
Expand Down
22 changes: 12 additions & 10 deletions webf/lib/src/html/img.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:webf/css.dart';
import 'package:webf/dom.dart';
import 'package:webf/bridge.dart';
import 'package:webf/foundation.dart';
import 'package:webf/launcher.dart';
import 'package:webf/painting.dart';
import 'package:webf/rendering.dart';
import 'package:webf/svg.dart';
Expand Down Expand Up @@ -567,7 +568,7 @@ class ImageElement extends Element {

void _loadSVGImage() {
final builder =
SVGRenderBoxBuilder(_obtainImage(_resolvedUri!), target: this);
SVGRenderBoxBuilder(obtainImage(_resolvedUri!), target: this);

builder.decode().then((renderObject) {
final size = builder.getIntrinsicSize();
Expand Down Expand Up @@ -601,8 +602,9 @@ class ImageElement extends Element {
provider = _currentImageProvider = BoxFitImage(
boxFit: objectFit,
url: _resolvedUri!,
loadImage: _obtainImage,
loadImage: obtainImage,
onImageLoad: _onImageLoad,
devicePixelRatio: ownerDocument.defaultView.devicePixelRatio
);
}

Expand Down Expand Up @@ -639,12 +641,12 @@ class ImageElement extends Element {

// To load the resource, and dispatch load event.
// https://html.spec.whatwg.org/multipage/images.html#when-to-obtain-images
Future<ImageLoadResponse> _obtainImage(Uri url) async {
Future<ImageLoadResponse> obtainImage(Uri url) async {
ImageRequest request = _currentRequest = ImageRequest.fromUri(url);
// Increment count when request.
ownerDocument.incrementRequestCount();

final data = await request._obtainImage(contextId);
final data = await request.obtainImage(ownerDocument.controller);

// Decrement count when response.
ownerDocument.decrementRequestCount();
Expand Down Expand Up @@ -738,10 +740,11 @@ class ImageRequest {
state == _ImageRequestState.completelyAvailable ||
state == _ImageRequestState.partiallyAvailable;

Future<ImageLoadResponse> _obtainImage(int? contextId) async {
final WebFBundle bundle = WebFBundle.fromUrl(currentUri.toString());

await bundle.resolve(contextId);
Future<ImageLoadResponse> obtainImage(WebFController controller) async {
final WebFBundle bundle =
controller.getPreloadBundleFromUrl(currentUri.toString()) ?? WebFBundle.fromUrl(currentUri.toString());
await bundle.resolve(baseUrl: controller.url, uriParser: controller.uriParser);
await bundle.obtainData();

if (!bundle.isResolved) {
throw FlutterError('Failed to load $currentUri');
Expand All @@ -752,7 +755,6 @@ class ImageRequest {
// Free the bundle memory.
bundle.dispose();

return ImageLoadResponse(data,
mime: bundle.contentType.toString());
return ImageLoadResponse(data, mime: bundle.contentType.toString());
}
}
5 changes: 3 additions & 2 deletions webf/lib/src/html/script.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ class ScriptRunner {
bundle = WebFBundle.fromContent(scriptCode);
} else {
String url = element.src.toString();
bundle = WebFBundle.fromUrl(url);
bundle = _document.controller.getPreloadBundleFromUrl(url) ?? WebFBundle.fromUrl(url);
}

element.readyState = ScriptReadyState.interactive;
Expand Down Expand Up @@ -121,7 +121,8 @@ class ScriptRunner {
// Increment count when request.
_document.incrementDOMContentLoadedEventDelayCount();
try {
await bundle.resolve(_contextId);
await bundle.resolve(baseUrl: _document.controller.url, uriParser: _document.controller.uriParser);
await bundle.obtainData();

if (!bundle.isResolved) {
throw FlutterError('Network error.');
Expand Down
21 changes: 19 additions & 2 deletions webf/lib/src/launcher/controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,7 @@ class WebFViewController implements WidgetsBindingObserver {

switch (action.navigationType) {
case WebFNavigationType.navigate:
await rootController.load(WebFBundle.fromUrl(action.target));
await rootController.load(rootController.getPreloadBundleFromUrl(action.target) ?? WebFBundle.fromUrl(action.target));
break;
case WebFNavigationType.reload:
await rootController.reload();
Expand Down Expand Up @@ -814,6 +814,19 @@ class WebFController {

final GestureListener? _gestureListener;

final List<WebFBundle>? preloadedBundles;
Map<String, WebFBundle>? _preloadBundleIndex;
WebFBundle? getPreloadBundleFromUrl(String url) {
return _preloadBundleIndex?[url];
}
void _initializePreloadBundle() {
if (preloadedBundles == null) return;
_preloadBundleIndex = {};
preloadedBundles!.forEach((bundle) {
_preloadBundleIndex![bundle.url] = bundle;
});
}

// The kraken view entrypoint bundle.
WebFBundle? _entrypoint;

Expand All @@ -838,13 +851,16 @@ class WebFController {
this.httpClientInterceptor,
this.devToolsService,
this.uriParser,
this.preloadedBundles,
this.initialCookies,
required this.ownerFlutterView,
this.resizeToAvoidBottomInsets = true,
}) : _name = name,
_entrypoint = entrypoint,
_gestureListener = gestureListener {

_initializePreloadBundle();

_methodChannel = methodChannel;
WebFMethodChannel.setJSMethodCallCallback(this);

Expand Down Expand Up @@ -1096,7 +1112,8 @@ class WebFController {

// Resolve the bundle, including network download or other fetching ways.
try {
await bundleToLoad.resolve(view.contextId);
await bundleToLoad.resolve(baseUrl: url, uriParser: uriParser);
await bundleToLoad.obtainData();
} catch (e, stack) {
if (onLoadError != null) {
onLoadError!(FlutterError(e.toString()), stack);
Expand Down
4 changes: 2 additions & 2 deletions webf/lib/src/module/history.dart
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ class HistoryModule extends BaseModule {
return;
}

WebFBundle bundle = WebFBundle.fromUrl(uri.toString());
WebFBundle bundle = controller.getPreloadBundleFromUrl(uri.toString()) ?? WebFBundle.fromUrl(uri.toString());
HistoryItem history = HistoryItem(bundle, state, false);
_addItem(history);
}
Expand All @@ -130,7 +130,7 @@ class HistoryModule extends BaseModule {
return;
}

WebFBundle bundle = WebFBundle.fromUrl(uri.toString());
WebFBundle bundle = controller.getPreloadBundleFromUrl(uri.toString()) ?? WebFBundle.fromUrl(uri.toString());
HistoryItem history = HistoryItem(bundle, state, false);

_previousStack.removeFirst();
Expand Down
14 changes: 12 additions & 2 deletions webf/lib/src/painting/box_fit_image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,15 @@ class BoxFitImage extends ImageProvider<BoxFitImageKey> {
required LoadImage loadImage,
required this.url,
required this.boxFit,
required this.devicePixelRatio,
this.onImageLoad,
}): _loadImage = loadImage;

final LoadImage _loadImage;
final Uri url;
final BoxFit boxFit;
final OnImageLoad? onImageLoad;
final double devicePixelRatio;

@override
Future<BoxFitImageKey> obtainKey(ImageConfiguration configuration) {
Expand Down Expand Up @@ -84,11 +86,19 @@ class BoxFitImage extends ImageProvider<BoxFitImageKey> {

final ImmutableBuffer buffer = await ImmutableBuffer.fromUint8List(bytes);
final ImageDescriptor descriptor = await ImageDescriptor.encoded(buffer);

int? preferredWidth;
int? preferredHeight;
if (key.configuration?.size != null) {
preferredWidth = (key.configuration!.size!.width * devicePixelRatio).toInt();
preferredHeight = (key.configuration!.size!.height * devicePixelRatio).toInt();
}

final Codec codec = await _instantiateImageCodec(
descriptor,
boxFit: boxFit,
preferredWidth: key.configuration?.size?.width.toInt(),
preferredHeight: key.configuration?.size?.height.toInt(),
preferredWidth: preferredWidth,
preferredHeight: preferredHeight,
);

// Fire image on load after codec created.
Expand Down
Loading

0 comments on commit e4efa00

Please sign in to comment.