This repository was archived by the owner on Feb 22, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
Add WebView JavaScript channels (Dart side). #1116
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
0e16deb
Add WebView JavaScript channels (Dart side only).
amirh 015f025
format
amirh 452a45c
Enforce channel names
amirh 1699836
Pass messages as JavascriptMessage objects. Add a note about overriding
amirh 83be1fa
format
amirh 2c423dd
fix channel cache update cache bug
amirh 57b3cd5
format with stable flutter
amirh b57117f
Document when adding a channel takes affect.
amirh 59e1248
update changelog and bump version
amirh 9a350dd
Use JavascriptChannels in the example app
amirh 61807a1
fix example comment
amirh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,51 @@ enum JavascriptMode { | |
| unrestricted, | ||
| } | ||
|
|
||
| /// A message that was sent by JavaScript code running in a [WebView]. | ||
| class JavascriptMessage { | ||
| /// Constructs a JavaScript message object. | ||
| /// | ||
| /// The `message` parameter must not be null. | ||
| const JavascriptMessage(this.message) : assert(message != null); | ||
|
|
||
| /// The contents of the message that was sent by the JavaScript code. | ||
| final String message; | ||
| } | ||
|
|
||
| /// Callback type for handling messages sent from Javascript running in a web view. | ||
| typedef void JavascriptMessageHandler(JavascriptMessage message); | ||
|
|
||
| final RegExp _validChannelNames = RegExp('^[a-zA-Z_][a-zA-Z0-9]*\$'); | ||
|
|
||
| /// A named channel for receiving messaged from JavaScript code running inside a web view. | ||
| class JavascriptChannel { | ||
| /// Constructs a Javascript channel. | ||
| /// | ||
| /// The parameters `name` and `onMessageReceived` must not be null. | ||
| JavascriptChannel({ | ||
| @required this.name, | ||
| @required this.onMessageReceived, | ||
| }) : assert(name != null), | ||
| assert(onMessageReceived != null), | ||
| assert(_validChannelNames.hasMatch(name)); | ||
|
|
||
| /// The channel's name. | ||
| /// | ||
| /// Passing this channel object as part of a [WebView.javascriptChannels] adds a channel object to | ||
| /// the Javascript window object's property named `name`. | ||
| /// | ||
| /// The name must start with a letter or underscore(_), followed by any combination of those | ||
| /// characters plus digits. | ||
| /// | ||
| /// Note that any JavaScript existing `window` property with this name will be overriden. | ||
| /// | ||
| /// See also [WebView.javascriptChannels] for more details on the channel registration mechanism. | ||
| final String name; | ||
|
|
||
| /// A callback that's invoked when a message is received through the channel. | ||
| final JavascriptMessageHandler onMessageReceived; | ||
| } | ||
|
|
||
| /// A web view widget for showing html content. | ||
| class WebView extends StatefulWidget { | ||
| /// Creates a new web view. | ||
|
|
@@ -32,6 +77,7 @@ class WebView extends StatefulWidget { | |
| this.onWebViewCreated, | ||
| this.initialUrl, | ||
| this.javascriptMode = JavascriptMode.disabled, | ||
| this.javascriptChannels, | ||
| this.gestureRecognizers, | ||
| }) : assert(javascriptMode != null), | ||
| super(key: key); | ||
|
|
@@ -56,6 +102,35 @@ class WebView extends StatefulWidget { | |
| /// Whether Javascript execution is enabled. | ||
| final JavascriptMode javascriptMode; | ||
|
|
||
| /// The set of [JavascriptChannel]s available to JavaScript code running in the web view. | ||
| /// | ||
| /// For each [JavascriptChannel] in the set, a channel object is made available for the | ||
| /// JavaScript code in a window property named [JavascriptChannel.name]. | ||
| /// The JavaScript code can then call `postMessage` on that object to send a message that will be | ||
| /// passed to [JavascriptChannel.onMessageReceived]. | ||
| /// | ||
| /// For example for the following JavascriptChannel: | ||
| /// | ||
| /// ```dart | ||
| /// JavascriptChannel(name: 'Print', onMessageReceived: (String message) { print(message); }); | ||
| /// ``` | ||
| /// | ||
| /// JavaScript code can call: | ||
| /// | ||
| /// ```javascript | ||
| /// Print.postMessage('Hello'); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be worth establishing here how namespace collision is handled on the JS side. What happens if somebody tries to name their channel
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Discussed offline, we're going to override whatever window property that is there as this is what Android is doing. We don't think it's a security risk as the app developer has the power to do something like this anyway by injecting JavaScript. |
||
| /// ``` | ||
| /// | ||
| /// To asynchronously invoke the message handler which will print the message to standard output. | ||
| /// | ||
| /// Adding a new JavaScript channel only takes affect after the next page is loaded. | ||
| /// | ||
| /// Set values must not be null. A [JavascriptChannel.name] cannot be the same for multiple | ||
| /// channels in the list. | ||
| /// | ||
| /// A null value is equivalent to an empty set. | ||
| final Set<JavascriptChannel> javascriptChannels; | ||
|
|
||
| @override | ||
| State<StatefulWidget> createState() => _WebViewState(); | ||
| } | ||
|
|
@@ -103,45 +178,79 @@ class _WebViewState extends State<WebView> { | |
| '$defaultTargetPlatform is not yet supported by the webview_flutter plugin'); | ||
| } | ||
|
|
||
| @override | ||
| void initState() { | ||
| super.initState(); | ||
| _assertJavascriptChannelNamesAreUnique(); | ||
| } | ||
|
|
||
| @override | ||
| void didUpdateWidget(WebView oldWidget) { | ||
| super.didUpdateWidget(oldWidget); | ||
| _updateSettings(_WebSettings.fromWidget(widget)); | ||
| _assertJavascriptChannelNamesAreUnique(); | ||
| _updateConfiguration(_WebSettings.fromWidget(widget)); | ||
| } | ||
|
|
||
| Future<void> _updateSettings(_WebSettings settings) async { | ||
| Future<void> _updateConfiguration(_WebSettings settings) async { | ||
| _settings = settings; | ||
| final WebViewController controller = await _controller.future; | ||
| controller._updateSettings(settings); | ||
| controller._updateJavascriptChannels(widget.javascriptChannels); | ||
| } | ||
|
|
||
| void _onPlatformViewCreated(int id) { | ||
| final WebViewController controller = | ||
| WebViewController._(id, _WebSettings.fromWidget(widget)); | ||
| final WebViewController controller = WebViewController._( | ||
| id, | ||
| _WebSettings.fromWidget(widget), | ||
| widget.javascriptChannels, | ||
| ); | ||
| _controller.complete(controller); | ||
| if (widget.onWebViewCreated != null) { | ||
| widget.onWebViewCreated(controller); | ||
| } | ||
| } | ||
|
|
||
| void _assertJavascriptChannelNamesAreUnique() { | ||
| if (widget.javascriptChannels == null || | ||
| widget.javascriptChannels.isEmpty) { | ||
| return; | ||
| } | ||
| assert(_extractChannelNames(widget.javascriptChannels).length == | ||
| widget.javascriptChannels.length); | ||
| } | ||
| } | ||
|
|
||
| Set<String> _extractChannelNames(Set<JavascriptChannel> channels) { | ||
| final Set<String> channelNames = channels == null | ||
| ? Set<String>() | ||
| : channels.map((JavascriptChannel channel) => channel.name).toSet(); | ||
| return channelNames; | ||
| } | ||
|
|
||
| class _CreationParams { | ||
| _CreationParams({this.initialUrl, this.settings}); | ||
| _CreationParams( | ||
| {this.initialUrl, this.settings, this.javascriptChannelNames}); | ||
|
|
||
| static _CreationParams fromWidget(WebView widget) { | ||
| return _CreationParams( | ||
| initialUrl: widget.initialUrl, | ||
| settings: _WebSettings.fromWidget(widget), | ||
| javascriptChannelNames: | ||
| _extractChannelNames(widget.javascriptChannels).toList(), | ||
| ); | ||
| } | ||
|
|
||
| final String initialUrl; | ||
|
|
||
| final _WebSettings settings; | ||
|
|
||
| final List<String> javascriptChannelNames; | ||
|
|
||
| Map<String, dynamic> toMap() { | ||
| return <String, dynamic>{ | ||
| 'initialUrl': initialUrl, | ||
| 'settings': settings.toMap(), | ||
| 'javascriptChannelNames': javascriptChannelNames, | ||
| }; | ||
| } | ||
| } | ||
|
|
@@ -178,14 +287,32 @@ class _WebSettings { | |
| /// A [WebViewController] instance can be obtained by setting the [WebView.onWebViewCreated] | ||
| /// callback for a [WebView] widget. | ||
| class WebViewController { | ||
| WebViewController._(int id, _WebSettings settings) | ||
| : _channel = MethodChannel('plugins.flutter.io/webview_$id'), | ||
| _settings = settings; | ||
| WebViewController._( | ||
| int id, this._settings, Set<JavascriptChannel> javascriptChannels) | ||
| : _channel = MethodChannel('plugins.flutter.io/webview_$id') { | ||
| _updateJavascriptChannelsFromSet(javascriptChannels); | ||
| _channel.setMethodCallHandler(_onMethodCall); | ||
| } | ||
|
|
||
| final MethodChannel _channel; | ||
|
|
||
| _WebSettings _settings; | ||
|
|
||
| // Maps a channel name to a channel. | ||
| Map<String, JavascriptChannel> _javascriptChannels = | ||
| <String, JavascriptChannel>{}; | ||
|
|
||
| Future<void> _onMethodCall(MethodCall call) async { | ||
| switch (call.method) { | ||
| case 'javascriptChannelMessage': | ||
| final String channel = call.arguments['channel']; | ||
| final String message = call.arguments['message']; | ||
| _javascriptChannels[channel] | ||
| .onMessageReceived(JavascriptMessage(message)); | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| /// Loads the specified URL. | ||
| /// | ||
| /// `url` must not be null. | ||
|
|
@@ -279,6 +406,40 @@ class WebViewController { | |
| return _channel.invokeMethod('updateSettings', updateMap); | ||
| } | ||
|
|
||
| Future<void> _updateJavascriptChannels( | ||
| Set<JavascriptChannel> newChannels) async { | ||
| final Set<String> currentChannels = _javascriptChannels.keys.toSet(); | ||
| final Set<String> newChannelNames = _extractChannelNames(newChannels); | ||
| final Set<String> channelsToAdd = | ||
| newChannelNames.difference(currentChannels); | ||
| final Set<String> channelsToRemove = | ||
| currentChannels.difference(newChannelNames); | ||
| if (channelsToRemove.isNotEmpty) { | ||
| // TODO(amirh): remove this when the invokeMethod update makes it to stable Flutter. | ||
| // https://github.com/flutter/flutter/issues/26431 | ||
| // ignore: strong_mode_implicit_dynamic_method | ||
| _channel.invokeMethod( | ||
| 'removeJavascriptChannels', channelsToRemove.toList()); | ||
| } | ||
| if (channelsToAdd.isNotEmpty) { | ||
| // TODO(amirh): remove this when the invokeMethod update makes it to stable Flutter. | ||
| // https://github.com/flutter/flutter/issues/26431 | ||
| // ignore: strong_mode_implicit_dynamic_method | ||
| _channel.invokeMethod('addJavascriptChannels', channelsToAdd.toList()); | ||
| } | ||
| _updateJavascriptChannelsFromSet(newChannels); | ||
| } | ||
|
|
||
| void _updateJavascriptChannelsFromSet(Set<JavascriptChannel> channels) { | ||
| _javascriptChannels.clear(); | ||
| if (channels == null) { | ||
| return; | ||
| } | ||
| for (JavascriptChannel channel in channels) { | ||
| _javascriptChannels[channel.name] = channel; | ||
| } | ||
| } | ||
|
|
||
| /// Evaluates a JavaScript expression in the context of the current page. | ||
| /// | ||
| /// On Android returns the evaluation result as a JSON formatted string. | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| name: webview_flutter | ||
| description: A Flutter plugin that provides a WebView widget on Android and iOS. | ||
| version: 0.3.0 | ||
| version: 0.3.1 | ||
| author: Flutter Team <[email protected]> | ||
| homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter | ||
|
|
||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 happens if somebody tries to pass in
foo.baras a name here? I'm asking because it's a common practice to try and namespace everything under one global namespacedfooobject if you need to start creating objects in the global namespace at all in WFE development, so it's something that people are likely to try.It's described as "Nested Namespacing" on this page.
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.
Good catch.
I changed it to require that channel names follow the Dart identifier naming rules (only letters digits and underscores allowed, can't start with a digit).