Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[webview_flutter_web] Migrate to package:web. #6792

Merged
merged 20 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions packages/webview_flutter/webview_flutter_web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
## NEXT
## 0.2.3

* Updates minimum supported SDK version to Flutter 3.16/Dart 3.2.
* Migrates to `package:web`
* Updates `HttpRequestFactory.request` to use the Fetch API.
* Updates `index.html` in the example to use `flutter_bootstrap.js`
* Updates minimum supported SDK version to Flutter 3.16/Dart 3.3.

## 0.2.2+4

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<html>

<head>
<!--
If you are serving your web app in a path other than the root, change the
Expand Down Expand Up @@ -32,73 +33,12 @@
<title>webview_flutter_web Example</title>
<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>
var serviceWorkerVersion = null;
var scriptLoaded = false;
function loadMainDartJs() {
if (scriptLoaded) {
return;
}
scriptLoaded = true;
var scriptTag = document.createElement('script');
scriptTag.src = 'main.dart.js';
scriptTag.type = 'application/javascript';
document.body.append(scriptTag);
}

if ('serviceWorker' in navigator) {
// Service workers are supported. Use them.
window.addEventListener('load', function () {
// Wait for registration to finish before dropping the <script> tag.
// Otherwise, the browser will load the script multiple times,
// potentially different versions.
var serviceWorkerUrl = 'flutter_service_worker.js?v=' + serviceWorkerVersion;
navigator.serviceWorker.register(serviceWorkerUrl)
.then((reg) => {
function waitForActivation(serviceWorker) {
serviceWorker.addEventListener('statechange', () => {
if (serviceWorker.state == 'activated') {
console.log('Installed new service worker.');
loadMainDartJs();
}
});
}
if (!reg.active && (reg.installing || reg.waiting)) {
// No active web worker and we have installed or are installing
// one for the first time. Simply wait for it to activate.
waitForActivation(reg.installing || reg.waiting);
} else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
// When the app updates the serviceWorkerVersion changes, so we
// need to ask the service worker to update.
console.log('New service worker available.');
reg.update();
waitForActivation(reg.installing);
} else {
// Existing service worker is still good.
console.log('Loading app from service worker.');
loadMainDartJs();
}
});

// If service worker doesn't succeed in a reasonable amount of time,
// fallback to plaint <script> tag.
setTimeout(() => {
if (!scriptLoaded) {
console.warn(
'Failed to load app from service worker. Falling back to plain <script> tag.',
);
loadMainDartJs();
}
}, 4000);
});
} else {
// Service workers not supported. Just drop the <script> tag.
loadMainDartJs();
}
</script>
https://developers.google.com/web/fundamentals/primers/service-workers -->
<script src="flutter_bootstrap.js" async></script>
</body>
</html>

</html>
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:html';
import 'dart:async';
import 'dart:js_interop';
import 'dart:typed_data';

import 'package:web/web.dart' as web;

/// Factory class for creating [HttpRequest] instances.
class HttpRequestFactory {
Expand All @@ -11,20 +15,16 @@ class HttpRequestFactory {

/// Creates and sends a URL request for the specified [url].
///
/// Returns an `Object` (so this class can be mocked by mockito), which can be
/// cast as [web.Response] from `package:web`.
///
/// By default `request` will perform an HTTP GET request, but a different
/// method (`POST`, `PUT`, `DELETE`, etc) can be used by specifying the
/// [method] parameter. (See also [HttpRequest.postFormData] for `POST`
/// requests only.
///
/// The Future is completed when the response is available.
/// [method] parameter.
///
/// If specified, `sendData` will send data in the form of a [ByteBuffer],
/// [Blob], [Document], [String], or [FormData] along with the HttpRequest.
/// The Future is completed when the [web.Response] is available.
///
/// If specified, [responseType] sets the desired response format for the
/// request. By default it is [String], but can also be 'arraybuffer', 'blob',
/// 'document', 'json', or 'text'. See also [HttpRequest.responseType]
/// for more information.
/// If specified, [sendData] will be sent as the `body` of the fetch.
///
/// The [withCredentials] parameter specified that credentials such as a cookie
/// (already) set in the header or
Expand Down Expand Up @@ -55,27 +55,33 @@ class HttpRequestFactory {
/// // Do something with the response.
/// });
///
/// Note that requests for file:// URIs are only supported by Chrome extensions
/// Requests for `file://` URIs are only supported by Chrome extensions
/// with appropriate permissions in their manifest. Requests to file:// URIs
/// will also never fail- the Future will always complete successfully, even
/// when the file cannot be found.
///
/// See also: [authorization headers](http://en.wikipedia.org/wiki/Basic_access_authentication).
Future<HttpRequest> request(String url,
{String? method,
bool? withCredentials,
String? responseType,
String? mimeType,
Map<String, String>? requestHeaders,
dynamic sendData,
void Function(ProgressEvent e)? onProgress}) {
return HttpRequest.request(url,
method: method,
withCredentials: withCredentials,
responseType: responseType,
mimeType: mimeType,
requestHeaders: requestHeaders,
sendData: sendData,
onProgress: onProgress);
Future<Object> request(
String url, {
String method = 'GET',
bool withCredentials = false,
String? mimeType,
Map<String, String>? requestHeaders,
Uint8List? sendData,
}) async {
final Map<String, String> headers = <String, String>{
if (mimeType != null) 'content-type': mimeType,
...?requestHeaders,
};
return web.window
.fetch(
url.toJS,
web.RequestInit(
method: method,
body: sendData?.toJS,
credentials: withCredentials ? 'include' : 'same-origin',
headers: headers.jsify()! as web.HeadersInit,
Copy link
Contributor

@navaronbracke navaronbracke Jul 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we have support for the JS Map type yet (in JS interop that is), so perhaps we should add a TODO for that? Unless we have a constructor for web.HeadersInit that takes a Map<String, String> ?

In my opinion we can eventually get rid of the jsify() in the long run?

Not sure how @ditman feels about doing that, though?

Copy link
Member

@ditman ditman Jul 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(@navaronbracke I wrote the .jsify() bits :P)

Currently HeadersInit is just an alias of JSObject.

The init in JS itself is a vague object-like thing that doesn't have a very specific way of being constructed: Headers init parameter:

init
An object containing any HTTP headers that you want to pre-populate your Headers object with. This can be a simple object literal with String values, an array of name-value pairs, where each pair is a 2-element string array; or an existing Headers object. In the last case, the new Headers object copies its data from the existing Headers object.

I'm using the simplest/most legible way I've found so far of creating an object literal of a JS-interop type (a Map<String, Object?>.jsify()! as JsInteropType). I agree that a more semantic constructor in package:web could be better, but this way I can use all the power of Dart Maps until the very last moment. /cc @srujzs for The Ultimate Expert Opinion™!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Maybe something with _createObjectLiteral? source)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could add a better constructor here if that's easier to read, but it's not going to do anything more complicated than just jsify :) e.g.

JSObject.fromMap(Map m) => m.jsify() as JSObject;

Copy link
Member

@ditman ditman Jul 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@srujzs yeah, I'd like to have a jsify in non-nullable-object so I don't have to Map.jsify()! and maybe a T jsifyAs<T>() that does the casting for me, so I could replace:

headers.jsify()! as web.HeadersInit

by

headers.jsifyAs<web.HeadersInit>();

(Not that it saves too much typing, though)

))
.toDart;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
// found in the LICENSE file.

import 'dart:convert';
import 'dart:html' as html;
import 'dart:js_interop';
import 'dart:ui_web' as ui_web;

import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:web/web.dart' as web;
import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';

import 'content_type.dart';
Expand Down Expand Up @@ -38,7 +39,7 @@ class WebWebViewControllerCreationParams

/// The underlying element used as the WebView.
@visibleForTesting
final html.IFrameElement iFrame = html.IFrameElement()
final web.HTMLIFrameElement iFrame = web.HTMLIFrameElement()
..id = 'webView${_nextIFrameId++}'
..style.width = '100%'
..style.height = '100%'
Expand Down Expand Up @@ -86,22 +87,21 @@ class WebWebViewController extends PlatformWebViewController {

/// Performs an AJAX request defined by [params].
Future<void> _updateIFrameFromXhr(LoadRequestParams params) async {
final html.HttpRequest httpReq =
final web.Response response =
await _webWebViewParams.httpRequestFactory.request(
params.uri.toString(),
method: params.method.serialize(),
requestHeaders: params.headers,
sendData: params.body,
);
) as web.Response;

final String header =
httpReq.getResponseHeader('content-type') ?? 'text/html';
final String header = response.headers.get('content-type') ?? 'text/html';
final ContentType contentType = ContentType.parse(header);
final Encoding encoding = Encoding.getByName(contentType.charset) ?? utf8;

// ignore: unsafe_html
_webWebViewParams.iFrame.src = Uri.dataFromString(
httpReq.responseText ?? '',
(await response.text().toDart).toDart,
mimeType: contentType.mimeType,
encoding: encoding,
).toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@

import 'dart:async';
import 'dart:convert';
import 'dart:html';
import 'dart:js_interop';
import 'dart:ui_web' as ui_web;

import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
import 'package:web/web.dart' as web;
// ignore: implementation_imports
import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart';

import 'http_request_factory.dart';

/// Builds an iframe based WebView.
Expand All @@ -23,7 +25,7 @@ class WebWebViewPlatform implements WebViewPlatform {
WebWebViewPlatform() {
ui_web.platformViewRegistry.registerViewFactory(
'webview-iframe',
(int viewId) => IFrameElement()
(int viewId) => web.HTMLIFrameElement()
..id = 'webview-$viewId'
..width = '100%'
..height = '100%'
Expand All @@ -45,11 +47,13 @@ class WebWebViewPlatform implements WebViewPlatform {
if (onWebViewPlatformCreated == null) {
return;
}
final IFrameElement element =
document.getElementById('webview-$viewId')! as IFrameElement;
if (creationParams.initialUrl != null) {
final web.HTMLIFrameElement element = web.document
.getElementById('webview-$viewId')! as web.HTMLIFrameElement;

final String? initialUrl = creationParams.initialUrl;
if (initialUrl != null) {
// ignore: unsafe_html
element.src = creationParams.initialUrl;
element.src = initialUrl;
}
onWebViewPlatformCreated(WebWebViewPlatformController(
element,
Expand All @@ -70,7 +74,7 @@ class WebWebViewPlatformController implements WebViewPlatformController {
/// Constructs a [WebWebViewPlatformController].
WebWebViewPlatformController(this._element);

final IFrameElement _element;
final web.HTMLIFrameElement _element;
HttpRequestFactory _httpRequestFactory = const HttpRequestFactory();

/// Setter for setting the HttpRequestFactory, for testing purposes.
Expand Down Expand Up @@ -199,16 +203,17 @@ class WebWebViewPlatformController implements WebViewPlatformController {
if (!request.uri.hasScheme) {
throw ArgumentError('WebViewRequest#uri is required to have a scheme.');
}
final HttpRequest httpReq = await _httpRequestFactory.request(
final web.Response response = await _httpRequestFactory.request(
request.uri.toString(),
method: request.method.serialize(),
requestHeaders: request.headers,
sendData: request.body);
sendData: request.body) as web.Response;

final String contentType =
httpReq.getResponseHeader('content-type') ?? 'text/html';
// ignore: unsafe_html
response.headers.get('content-type') ?? 'text/html';

_element.src = Uri.dataFromString(
httpReq.responseText ?? '',
(await response.text().toDart).toDart,
mimeType: contentType,
encoding: utf8,
).toString();
Expand Down
7 changes: 4 additions & 3 deletions packages/webview_flutter/webview_flutter_web/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ name: webview_flutter_web
description: A Flutter plugin that provides a WebView widget on web.
repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter_web
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22
version: 0.2.2+4
version: 0.2.3

environment:
sdk: ^3.2.0
flutter: ">=3.16.0"
sdk: ^3.3.0
flutter: ">=3.19.0"

flutter:
plugin:
Expand All @@ -21,6 +21,7 @@ dependencies:
sdk: flutter
flutter_web_plugins:
sdk: flutter
web: ^0.5.0
webview_flutter_platform_interface: ^2.0.0

dev_dependencies:
Expand Down
Loading