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

[DISCUSS] Cross platform API for RN<>WebView 2-way RPC/message passing #66

Closed
fungilation opened this issue Sep 29, 2018 · 72 comments
Closed
Labels
User: question Further information is requested

Comments

@fungilation
Copy link
Contributor

fungilation commented Sep 29, 2018

Referencing (my) prior discussions regarding this, for context and so we don't repeat here. In reverse chronological order:

This consistent, robust, cross platform API is important for any app that blends native and webviews. I list mine as one. A requirement of this API is that an injected postMessage for purpose of RN message passing, not be overridden by a website's JS in the WebView. Which is currently the case with RN core's WebView, and presumably this forked react-native-webview. It could be achieved (and have been attempted in prior discussions and in RN core's WebView) by loading RN's injected postMessage last, but that's unreliable should the website create after onLoad a function of the same name, alongside iframe issues. This is not hypothetical as it does get overridden for a fact, when loading google.com in WebView for example.

A simple function rename that doesn't use the web standard of postMessage would sidestep what is effectively a name collision issue. Call it rnPostMessage or anything else. Open to suggestions on a better alternative to renaming that actually has a guarantee for not being overridden by non-locally loaded JS within WebView.

This new API should consider/port code (if tested as reliable) from react-native-wkwebview, which is iOS only. And implement the same API for other platforms. There is also working RN<>WebView bridge API at react-native-webview-bridge that's well established (old but fully functional, with google.com loaded in webview, and for Android and iOS), but that's based on UIWebView and not WKWebView on the iOS side.

@jamonholmgren
Copy link
Member

Thanks @fungilation !

One other interesting API I found when perusing Github was this one:

https://github.com/pinqy520/react-native-webview-invoke

It exposes an API where you can call an async function directly, as long as you control both the web and mobile side of the connection. Under the hood, it still uses postMessage, but I thought I'd bring this up in case it was interesting as a potential API.

@jamonholmgren jamonholmgren added the User: question Further information is requested label Sep 30, 2018
@fungilation
Copy link
Contributor Author

Interesting. Although requiring below on the Web side is no good. New API should support any WebView src, not only local js:

import invoke from 'react-native-webview-invoke/browser'

@Titozzz
Copy link
Collaborator

Titozzz commented Sep 30, 2018

@fungilation So I was using a fork of alinz/react-native-webview-bridge until I started using this package. I wrote a simple piece of code that allowed me to be 100% non breaking with what was there before. This does not mean it's safe regarding postMessage naming and such but I'll just post it here in case anyone needs it.

const injectedJavascript = `(function() {
  window.WebViewBridge = {
    onMessage: function() {
      return null;
    },
    send: function(data) {
      window.postMessage(data, '*');
    },
  };
  var event = new Event('WebViewBridge');
  window.dispatchEvent(event);
})()`;

const generateOnMessageFunction = data =>
  `(function() {
    window.WebViewBridge.onMessage(${JSON.stringify(data)});
  })()`;

sendMessageToWebView = (message) => {
    this.webView.current.injectJavaScript(generateOnMessageFunction(message));
  };

Then you just need to use onMessage prop to listen and sendMessageToWebView to send.
From the website the api is unchanged. You can use webviewBrigde send and onMessage as before :)

@KoenLav
Copy link

KoenLav commented Oct 2, 2018

I totally agree this is a requirement for even considering React Native, with a WebView, as a suitable replacement for Cordova (without using the native components, rather than their web counterparts).

I created the following schematic and we will be investing some time in working up an example, any advice on improving this implementation strategy would be appreciated!

Also some idea of whether we would want to move this functionality into this package (I believe it should be included) or maintain it as a separate package would be nice.

img_20181002_130735

@Titozzz
Copy link
Collaborator

Titozzz commented Oct 2, 2018

Well that is what I'm doing already (excepted for the callback array, as I'm more in the just dispatching actions without waiting for answers mind)

@KoenLav
Copy link

KoenLav commented Oct 20, 2018

@Titozzz my end goal is to allow calling any react-native package directly from the WebView as if we didn't even have to cross the boundary between the two.

I have a simple (in a good way) working example of a Bridge which does the following; ReactNativeBridge.call('functionName', arg1, arg2, arg..., callback) where you can pass as many arguments as you want and callback is of the form (err, res). On the native side you define what functionName does (and whether it is sync/async), quite similar to the invoke package.

@fungilation I also forked this package and modified the Android implementation of postMessage (in the webview) to no longer overwrite the window.postMessage (this caused issues for our application).

I didn't implement this using injectedJavascript yet, as I actually don't mind importing a package on both sides (native & web), but will work on that soon.

Are there any other use cases I should take into account?

It is my intention to create a PR for this feature which could be enabled by passing bridge={true} to the Webview component and add documentation on how to use it in the WebView.

One limitation of my approach, which could be easily resolved, is that it currently only allows calling from WebView to Native, not the other way around.

Happy to hear your thoughts!

@Titozzz
Copy link
Collaborator

Titozzz commented Oct 21, 2018

Okay I've updated all the repository issues regarding postMessage 😄 .

Let's keep the discussion focused in this thread only, or we will get lost! 😢

I think everyone agrees that using postMessage I not a good idea for several reasons!

Listing what we need:

  • More stability on setup
  • Stop using postMessage
  • 2 way communications

Nice to have ..?

  • Async / Callback return values
  • ?

@KoenLav
Copy link

KoenLav commented Oct 21, 2018

@Titozzz for our use-case async/callback return values are a must-have.

The BridgedWebview repository (https://github.com/KoenLav/bridged-webview) I created already fulfills the first two checks on your list and covers async/callback returns values.

I'm not sure however how you interpret two-way communication? The approach I took is pretty much centered around the WebView having the 'lead' (dictating what is happening), but I can imagine use-cases where one would want the native side of things to have the lead, and dictate what happens in the WebView. What's your take on that?

I could quite easily extend the classes to also cover this use-case, but if we don't expect anyone to use it like that it could just make things confusing.

@Titozzz
Copy link
Collaborator

Titozzz commented Oct 21, 2018

Yeah, I actually have a use-case were both side are reacting to each others, so I don't really have one master but both are emmiting events that the other is free to listen to, which is why I was not using return values, as I'm not waiting for anything. Would not hurt having it tho 👍

@Titozzz
Copy link
Collaborator

Titozzz commented Oct 21, 2018

So I've read your code (super fast, sorry it's late here 😴). I'm however not sure that we want to implement something that is THAT specific. What I mean is that you defined a 'RFC' to communicate and do function calls and all that, but in the end what I think this repo should provid is a solid communication (eg: just posting JSON payload) both ways and then leave the implementation back to the users, as you did in your repository. I don't know if i'm clear. I'll try to explain better tomorrow !

@KoenLav
Copy link

KoenLav commented Oct 21, 2018

I see (with regards to the use-case).

The reason I didn't invest any time into Native => WebView yet is also because it's pretty easy to solve once WebView => Native works. For example: injectJavascript(const result = doSomethingInWebview(); ReactNativeBridge.call('anyNativeFunction', result, someOtherVariables...);

=====

Given the use-case you are describing (just sending data from one side and having it picked up on the other side) I can imagine this might seem specific. What do you mean by solid communication? I think this repo already does that just fine (on Android, at least, not sure of the status of the WKWebView included at the moment). If there is still work to do in making sure that postMessage/injectJavascript work fine on all supported platforms then that definitely needs to happen first!

In our use-case we actually want to port a (Meteor) web-app from Cordova to React Native, but for some functionality we rely on native code (getting the unique device ID, communicating via TCP sockets, etc.). What I was going for is providing a way to do anything from the WebView about as easily as you could from the native side of things.

I don't think this is THAT specific; it can also be used to simply pass JSON from one side to the other, as it doesn't require setting a callback and the underlying postMessage function is still available as well, for users wanting more fine-grained control.

I simply defined a standardized way of instructing the native side how to handle specific messages, which I think is much easier to understand for a new user than what is currently out there (react-native-webview-bridge, react-native-webview-messaging, react-native-webview-invoke) AND supports all use-cases the packages I just mentioned support.

Looking through the code sample you posted earlier I did however notice that I didn't implement the onMessage function within the WebView yet, if I add that then the statement above (supporting all use-cases) is actually true, as far as I can tell.

Happy to hear your thoughts!

@fungilation
Copy link
Contributor Author

Meteor/Cordova! I tried and failed with my last mobile app project using it, good to see another migrant to RN.

I'm copying over my comment from the other issue as I think it's useful for discussion of what 2-way communication means to me, and what I think a good API looks like based on the tried and true in existing libraries:


I'm for a breaking change in API. Especially if that means sidestepping entirely security issues with origin. As a user, it doesn't matter as long as it's documented and works. I've advocated for renaming so postMessage / onMessage equivalent APIs can work independent of what the WebView's original site javascript does. Sticking with web-centric APIs for RN<>webview communication has never been a wanted feature (for me).

For starter, instead of proposing completely new API, what about just borrow one from existing, working library? react-native-wkwebview's API looks comprehensive, but they are using the same window.postMessage and onMessage API. Do they guarantee against function overwriting from websites' javascript and security issues with origin?

react-native-webview-bridge I use actively with my own fork for my own app. It's not the best API but it works, and it works for both iOS and Android. We could start with that as far as API definition? With one exception, on calling from RN to WebView:

react-native-webview-bridge uses this to send string to WebView:

webviewbridge.sendToBridge("hello from react-native")

but react-native-wkwebview's way is better, since users can directly evaluate JS, and define separate functions for different purposes injected in webview. String sending in react-native-webview-bridge means a nest of conditionals to handle a list of strings sent from RN, and it gets messy.

this.webview.evaluateJavaScript('receivedMessageFromReactNative("Hello from the other side.")')

@KoenLav
Copy link

KoenLav commented Oct 23, 2018

@fungilation I read your comments to most issues before I started working on this and I think the package linked in my latest PR, and the API it suggests, will suit your use case.

Any comments are much appreciated!

@Titozzz
Copy link
Collaborator

Titozzz commented Oct 24, 2018

Given the use-case you are describing (just sending data from one side and having it picked up on the other side) I can imagine this might seem specific. What do you mean by solid communication? I think this repo already does that just fine (on Android, at least, not sure of the status of the WKWebView included at the moment). If there is still work to do in making sure that postMessage/injectJavascript work fine on all supported platforms then that definitely needs to happen first!

So about that. This is the part I mean to address in that repository and that really belongs here imho, the current bridge is not OK, as it is using postMessage and this has major drawbacks. We will introduce a breaking change to implement something like react-nativew-webview-bridge (more or less), that will allow for a safer, more reliable communication.


In our use-case we actually want to port a (Meteor) web-app from Cordova to React Native, but for some functionality we rely on native code (getting the unique device ID, communicating via TCP sockets, etc.). What I was going for is providing a way to do anything from the WebView about as easily as you could from the native side of things.

I don't think this is THAT specific; it can also be used to simply pass JSON from one side to the other, as it doesn't require setting a callback and the underlying postMessage function is still available as well, for users wanting more fine-grained control.

I simply defined a standardized way of instructing the native side how to handle specific messages, which I think is much easier to understand for a new user than what is currently out there (react-native-webview-bridge, react-native-webview-messaging, react-native-webview-invoke) AND supports all use-cases the packages I just mentioned support.

Regarding this, I'm not sure that this should belong directly inside the repository and be loaded for everyone. As I said once the bridge is reliable, I feel like (and it's my opinion) that most webview users have very specific use cases, and I think we should not decide how to implement this for them.

As shown above, once the bridge is strong, you can easily with a few lines implement whatever API you need.

One thing we could however do it add some example in the docs (something like your code) to give a more beginner user a template to follow / understand to do whatever he needs.

@KoenLav
Copy link

KoenLav commented Oct 24, 2018

@Titozzz I looked into the implementation of react-native-webview-bridge and I don't think it is much/any safer than the currently implemented solution here.

Can you identify where their implementation is better and/or what is not 'strong' about the current implementation in this repository?

@Titozzz
Copy link
Collaborator

Titozzz commented Oct 24, 2018

Sure I gave the example only because they are not using window.postMessage.

Issue with the current implem as stated in the docs:

Security Warning: Currently, onMessage and postMessage do not allow specifying an origin. This can lead to cross-site scripting attacks if an unexpected document is loaded within a WebView instance. Please refer to the MDN documentation for Window.postMessage() for more details on the security implications of this.

Plus when you instantiate the webview it messes with window.postMessage that was previously defined.

This is why we will probably define something else than postMessage 😄

@KoenLav
Copy link

KoenLav commented Oct 24, 2018

@Titozzz my PR already resolves the second issue.

The cross site scripting one I think is a more difficult one, but I could also take a look at that.

Do you have any suggestions?

Oh wait, I don't think it's that difficult... We could simply define which origins are accepted on the React Native side.

@Titozzz
Copy link
Collaborator

Titozzz commented Oct 24, 2018

Yeah, we need to stop using postMessage behind the scenes !

@KoenLav
Copy link

KoenLav commented Oct 24, 2018

I'm not sure I understand you correctly: in my opinion we need to stop using window.postMessage, but a AnyGlobal.postMessage function would be fine, agreed?

The AnyGlobal.postMessage function is simply a function which is inserted into the window and enables us to send messages to the React Native side.

We could name the function anyhow we wanted as it simply makes use of a JavascriptInterface on Android:
https://github.com/react-native-community/react-native-webview/blob/master/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java#L261

https://developer.android.com/reference/android/webkit/JavascriptInterface

Or a WKScriptMessageHandler on iOS WKWebView:

https://github.com/react-native-community/react-native-webview/blob/master/ios/RNCWKWebView.m#L146

https://spin.atomicobject.com/2016/09/01/sharing-web-data-wkwebview/

It does appear however, when reading the iOS example, that in iOS it is only possible to implement a variable in window.webkit.messageHandlers.SomeName.postMessage.

So I guess, for consistency, we should do the same for Android (which lets us insert the bridge in any place).

@Titozzz
Copy link
Collaborator

Titozzz commented Oct 24, 2018

yeah sure when I say stop using post message I'm talking about the one inside the window 😛 .

@Titozzz
Copy link
Collaborator

Titozzz commented Nov 27, 2018

Oh my god, this is exactly what I want and wish ❤️ ❤️ ❤️

@iddan
Copy link

iddan commented Nov 27, 2018

I think the beauty of a WebView custom implemented of RPC is it can share the same memory representation of objects in both the JS runtimes: RN and WebView as they use the same core. This will make performance much better than postMessage can offer at the moment.

@fungilation
Copy link
Contributor Author

Might not be applicable, but beware of JS runtime differences within WebView and RN. Things like Array.from() is available in RN but not within WebView, currently.

@iddan
Copy link

iddan commented Nov 27, 2018

Yes but primitives and base classes are shared

@swansontec
Copy link
Contributor

@iddan During debugging, the React Native Javascript runs on the desktop PC, but the WebView runs on the phone. The only connection between them is a WebSocket. In other words, the two can only communicate using strings (unless you want to break debugging).

The safest way to improve the postMessage / onMessage performance is to let them send full Javascript objects instead of just strings. This is how WebWorkers and Node.js child processes work. The debug bridge would still serialize the objects into text as before, but production builds can maybe take advantage of the shared memory.

I think postMessage / onMessage is fundamentally the right paradigm. It's just a question of making it fast & reliable. Once you have high-speed object passing, method calls are easy to build on top.

@iddan
Copy link

iddan commented Nov 27, 2018

Cool

@KoenLav
Copy link

KoenLav commented Nov 30, 2018

@swansontec do you have any ideas about extending the current postMessage implementation with the structured clone algorithm? Perhaps something like this? https://www.npmjs.com/package/realistic-structured-clone

Your first suggestion is already implemented in my PR (window.webkit.messageHandlers.ReactNative), to make the behavior between iOS and Android consistent.

Your second suggestion is already possible, I think the WebView already exposes a postMessage command on the React Native side of things. Just took a look at the code and it does.

@swansontec
Copy link
Contributor

@KoenLav Awesome! I didn't realize these fixes were already in the works.

As for the structured clone algorithm, I don't have any particular ideas on the implementation. This was more of a pie-in-the-sky wishlist item for me. Using an NPM module like the one you pointed out might be a reasonable approach.

@Titozzz Titozzz closed this as completed Feb 1, 2019
@Titozzz

This comment has been minimized.

@fungilation
Copy link
Contributor Author

Wow, thanks @Titozzz for making this happen! 🎉

Titozzz added a commit that referenced this issue Feb 1, 2019
…entation (#303)

fixes #29
fixes #272
fixes #221
fixes #105
fixes #66

BREAKING CHANGE: Communication from webview to react-native has been completely rewritten. React-native-webview will not use or override window.postMessage anymore. Reasons behind these changes can be found throughout so many issues that it made sense to go that way.

Instead of using window.postMessage(data, *), please now use window.ReactNativeWebView.postMessage(data).

Side note: if you wish to keep compatibility with the old version when you upgrade, you can use the injectedJavascript prop to do that:

const injectedJavascript = `(function() {
  window.postMessage = function(data) {
    window.ReactNativeWebView.postMessage(data);
  };
})()`;

Huge thanks to @jordansexton and @KoenLav!
Titozzz pushed a commit that referenced this issue Feb 1, 2019
# [5.0.0](v4.1.0...v5.0.0) (2019-02-01)

### Features

* **Android/iOS postMessage:** refactoring the old postMessage implementation ([#303](#303)) ([f3bdab5](f3bdab5)), closes [#29](#29) [#272](#272) [#221](#221) [#105](#105) [#66](#66)

### BREAKING CHANGES

* **Android/iOS postMessage:** Communication from webview to react-native has been completely rewritten. React-native-webview will not use or override window.postMessage anymore. Reasons behind these changes can be found throughout so many issues that it made sense to go that way.

Instead of using window.postMessage(data, *), please now use window.ReactNativeWebView.postMessage(data).

Side note: if you wish to keep compatibility with the old version when you upgrade, you can use the injectedJavascript prop to do that:

const injectedJavascript = `(function() {
  window.postMessage = function(data) {
    window.ReactNativeWebView.postMessage(data);
  };
})()`;

Huge thanks to @jordansexton and @KoenLav!
@Titozzz
Copy link
Collaborator

Titozzz commented Feb 1, 2019

🎉 This issue has been resolved in version 5.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@kesha-antonov
Copy link
Contributor

Awesome! Thanks

@ronhe
Copy link

ronhe commented Apr 26, 2019

I have previously implemented an RPC bridge based on the postMessage api and the Comlink library.
I'm using it in my own react-native project. You may check it out here:
rn-webview-rpc.

phuongwd pushed a commit to phuongwd/react-native-webview that referenced this issue Apr 29, 2020
…entation (react-native-webview#303)

fixes react-native-webview#29
fixes react-native-webview#272
fixes react-native-webview#221
fixes react-native-webview#105
fixes react-native-webview#66

BREAKING CHANGE: Communication from webview to react-native has been completely rewritten. React-native-webview will not use or override window.postMessage anymore. Reasons behind these changes can be found throughout so many issues that it made sense to go that way.

Instead of using window.postMessage(data, *), please now use window.ReactNativeWebView.postMessage(data).

Side note: if you wish to keep compatibility with the old version when you upgrade, you can use the injectedJavascript prop to do that:

const injectedJavascript = `(function() {
  window.postMessage = function(data) {
    window.ReactNativeWebView.postMessage(data);
  };
})()`;

Huge thanks to @jordansexton and @KoenLav!
phuongwd pushed a commit to phuongwd/react-native-webview that referenced this issue Apr 29, 2020
# [5.0.0](react-native-webview/react-native-webview@v4.1.0...v5.0.0) (2019-02-01)

### Features

* **Android/iOS postMessage:** refactoring the old postMessage implementation ([react-native-webview#303](react-native-webview#303)) ([f3bdab5](react-native-webview@f3bdab5)), closes [react-native-webview#29](react-native-webview#29) [react-native-webview#272](react-native-webview#272) [react-native-webview#221](react-native-webview#221) [react-native-webview#105](react-native-webview#105) [react-native-webview#66](react-native-webview#66)

### BREAKING CHANGES

* **Android/iOS postMessage:** Communication from webview to react-native has been completely rewritten. React-native-webview will not use or override window.postMessage anymore. Reasons behind these changes can be found throughout so many issues that it made sense to go that way.

Instead of using window.postMessage(data, *), please now use window.ReactNativeWebView.postMessage(data).

Side note: if you wish to keep compatibility with the old version when you upgrade, you can use the injectedJavascript prop to do that:

const injectedJavascript = `(function() {
  window.postMessage = function(data) {
    window.ReactNativeWebView.postMessage(data);
  };
})()`;

Huge thanks to @jordansexton and @KoenLav!
jaynilson added a commit to jaynilson/reactNative_service_USA that referenced this issue Sep 11, 2024
# [5.0.0](react-native-webview/react-native-webview@v4.1.0...v5.0.0) (2019-02-01)

### Features

* **Android/iOS postMessage:** refactoring the old postMessage implementation ([#303](react-native-webview/react-native-webview#303)) ([f3bdab5](react-native-webview/react-native-webview@f3bdab5)), closes [#29](react-native-webview/react-native-webview#29) [#272](react-native-webview/react-native-webview#272) [#221](react-native-webview/react-native-webview#221) [#105](react-native-webview/react-native-webview#105) [#66](react-native-webview/react-native-webview#66)

### BREAKING CHANGES

* **Android/iOS postMessage:** Communication from webview to react-native has been completely rewritten. React-native-webview will not use or override window.postMessage anymore. Reasons behind these changes can be found throughout so many issues that it made sense to go that way.

Instead of using window.postMessage(data, *), please now use window.ReactNativeWebView.postMessage(data).

Side note: if you wish to keep compatibility with the old version when you upgrade, you can use the injectedJavascript prop to do that:

const injectedJavascript = `(function() {
  window.postMessage = function(data) {
    window.ReactNativeWebView.postMessage(data);
  };
})()`;

Huge thanks to @jordansexton and @KoenLav!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
User: question Further information is requested
Projects
None yet
Development

No branches or pull requests

9 participants