-
Notifications
You must be signed in to change notification settings - Fork 3.6k
[webview_flutter_web] Add support for webview->host JavaScript channels #3655
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
Conversation
|
how to postMessage to webview and reture value to html |
This PR is about sending messages from a webview to Flutter. I think you're asking how to send messages the other way: i.e. from Flutter to webview, right? To send messages from Flutter to web, one can use something like:
runJavaScript(
"window.postMessage(${jsonEncode(message)}, '$targetOrigin')",
);
import 'dart:html' as html;
....
final iframe =
html.querySelector('iframe') as html.IFrameElement?;
iframe?.contentWindow?.postMessage(message, targetOrigin); |
|
It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption to this rule, contact Hixie on the #hackers channel in Chat (don't just cc him here, he won't see it! He's on Discord!). If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix? Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
stuartmorgan-g
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This fundamentally doesn't match the semantics of plugin API; it doesn't behave the same way on either the Dart or JS side, so trying to fit it into this construction seems more harmful than beneficial.
I think the right way to handle this is to define a new set of webview_flutter_web APIs specific to postMessage, to address bidirectional communication. For this direction, a setWindowMessageHandler (or addWindowMessageHandler and a corresponding remove... if we want to track multiple handlers) seems less confusing than trying to repurpose addJavaScriptChannel. We can override that to provide a specific UnimplementedError that points to the alternate API.
@ditman Does that sound good to you?
|
@ditman Ping on the API question above. |
ditman
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with @stuartmorgan in that this is a little bit too much shoe-horning of the existing API into postMessage.
On top of the caveats mentioned by the author, this has an issue where every message will end up being "handled" by every possible registered channel.
IMO this feature should be designed a little bit more to address some of the concerns:
- How to prevent the
*accepting messages? - How to prevent handlers from reading messages that they don't care about?
- How to make this extensible so 2-way communication is doable? (we don't need to implement it at first, but at least have a plausible plan to get it)
- How to not rely on static objects present on the page, like
window.flutterAppor similar.
Some of the issues can be mitigated by creating new methods for the web version to align the API closer to what the web can do, but others need a little bit more thought, for example:
- Can we define a message format that must be passed to postMessage that the plugin (and receiving iframe) will refuse to handle if it's not correct (instead of calling .toString on the data)?
- Can we provide utilities for the iframed content to be easily setup in "collaborative mode" with the parent flutter App? This is to ensure that the message handlers in the iframe only listens to the messages we send from flutter.
I think we need a small writeup to solve this (a detailed Issue would be probably enough), rather than jumping the gun. We'll have to live with these APIs for a long time, after all :/
| } | ||
|
|
||
| javascriptChannels[javaScriptChannelParams.name] = handler; | ||
| html.window.addEventListener('message', handler); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a programmer registers multiple 'message' handlers (one per name), this means that every onMessageReceived registered will be called for every message received, right? I think this is potentially expensive. Also calling .toString() on the object that is being passed around is not great, you lose the ability to move around Transferable objects, which might be interesting to users of this API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a programmer registers multiple 'message' handlers (one per name), this means that every onMessageReceived registered will be called for every message received, right? I think this is potentially expensive.
That is true. This can easily happen when you have multiple webviews in a single application. Since the webviews do not know anything from each other, we did not see another option. I think if you have a low amount of handlers (e.g. a handful), there is no performance hit.
Also calling .toString() on the object that is being passed around is not great, you lose the ability to move around Transferable objects, which might be interesting to users of this API.
On Android and iOS it is only possible to send string messages. We tried to mimic that API by limiting the allowed message types to string-values only. This way everything can be handled the same way in Flutter (i.e. all incoming messages are always string). If one wants to send complex messages one can always send a stringified object.
| `flutterApp`, the following construction can be used in the web application: | ||
|
|
||
| ```ts | ||
| if (window.flutterApp) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is window.flutterApp? Who's setting this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
webview_flutter is setting this on iOS and Android. If it's not available we assume we are on web and use the alternative.
|
Thanks for your replies.
That is true. We improved this in a follow-up PR, while awaiting a response on this PR. By using unique channel names it is possible to ensure a message is only processed once. The improved version can be found in gynzy#2.
An additional security improvement is included here on the dart side: https://github.com/gynzy/flutter-packages/blob/cd8452c89454ff24677aebf2795cb5704afc7ceb/packages/webview_flutter/webview_flutter_web/lib/src/web_webview_controller.dart#L126-L129. On the web side, when sending a message, the following code can be used: const HOST_NAME_ALLOWLIST = [...]; // Allowed hosts
...
function getTargetOrigin() {
if (!document.referrer) {
return;
}
const parentUrl = new URL(document.referrer);
if (!HOST_NAME_ALLOWLIST.includes(parentUrl.hostname)) {
return;
}
return parentUrl.origin;
}
...
const targetOrigin = getTargetOrigin();
if (targetOrigin) {
window.parent.postMessage(message, targetOrigin);
}Since the webview is implemented as an iframe on web,
I think this is solved by gynzy#2.
If I understand correctly, on Android and iOS the JavaScript channels can also only be used for one-way communication. This PR makes it possible to do that same communication on web. Communication in the other direction can be done using something like: #3655 (comment).
The
I agree that a well-thought solution is the way to go. What we tried to do is to implement the JavaScript channels in such a way that they equal the iOS and Android implementation as close as possible. A result of this is that we restricted Curious to hear your thoughts on how to proceed. Should we go back to the drawing board or is the improvements PR a step in the right direction and can we build upon that? |
A short design document listing options with pros and cons would be the best place to discuss and evaluate that. |
|
@Frank3K Are you still interested in moving this forward via the design doc process described above? |
I'm definitely interested in bringing this functionality forward. I'd be glad to help out on the design document, but don't feel confident in setting up the design document myself. I understand your reasoning for an alternative API; we are however already using the initial proposal (implementing the JavaScript channel on web using postMessage) on production. |
There are instructions in the template linked from that page; we're happy to answer any questions about those instructions. (I'm going to mark this PR as a draft for now, so it doesn't show up in our review queue, pending a design doc where there's agreement on the approach to take.) |
|
Since this is marked as a draft and hasn't been updated in several months I'm going to close it to clean out our review queue. Please don't hesitate to submit a new PR if you decide to revisit this. Thanks! |
This pull request implements JavaScript channels for webview_flutter_web, allowing postMessage communication from a web application to Flutter.
The regular webview_flutter package already supports JavaScript channels for iOS and Android, making is possible to send messages from a web application to Flutter.
Related issues
Flutter issue asking for support: flutter/flutter#101758.
How it works
On iOS and Android it is possible to send messages on a JavaScript channel using the postMessage API. This API is very simple: it only allows a single string argument (docs). On web, there's the postMessage API on window, on which arbitrary data can be sent. Since the postMessage API for Android and iOS is a subset (string vs object), the web postMessage API can be used to implement the JavaScript channel.
Demo-videos
The following demo show a small application which sends a postMessage to the Flutter application.
iOS
Screen.Recording.2023-04-06.at.12.42.57.mov
Web
Screen.Recording.2023-04-06.at.20.33.57.mov
PoC code
Flutter side
... webViewController = WebViewController(); webViewController.addJavaScriptChannel( 'flutterApp', onMessageReceived: (message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message.message), ), ); }, ); ...Web side
HTML
JavaScript
Web implementation notes and limitations
Security
In the above PoC the postMessage is sent (on web) with
*as target origin. This is considered bad practice. The web application should use the origin of the flutter web application as the target origin.Named JavaScript channels
On iOS and Android it is possible to name a JavaScript channel. On web, this is not possible since that would require injecting JavaScript code into the iframe (which is not possible cross-origin). Therefore the web application uses
window.parent(usingwindow["flutterApp"] || window.parent) to get to the flutter application. Therefore the web implementation only allows a single channel.Tests
No tests have been added yet since we first like to see whether the current approach is a good one, or if there is a better approach. Once the approach is greenlit, we want to add tests.
Pre-launch Checklist
dart format.)[shared_preferences]pubspec.yamlwith an appropriate new version according to the pub versioning philosophy, or this PR is exempt from version changes.CHANGELOG.mdto add a description of the change, following repository CHANGELOG style.///).If you need help, consider asking for advice on the #hackers-new channel on Discord.