简体   繁体   English

MAUI上JS/.NET交互 WebView

[英]JS/.NET interact on MAUI WebView

I am migrating our app from Xamarin to MAUI, and I am a bit struggling with migrating the code that handles JS/.NET interactions in a WebView on both Android and iOS. Let's focus on Android. It's especially about calling .NET code from JS in the WebView.我正在将我们的应用程序从 Xamarin 迁移到 MAUI,我有点难以迁移处理 WebView 中 Android 和 iOS 上的 JS/.NET 交互的代码。让我们专注于 Android 中的 code6038340988。 WebView。

In Xamarin, we could do something like this (basically according to this tutorial https://learn.microsoft.com/en-us/xamarin/xamarin.forms/app-fundamentals/custom-renderer/hybridwebview ):在 Xamarin 中,我们可以做这样的事情(基本上根据本教程https://learn.microsoft.com/en-us/xamarin/xamarin.forms/app-fundamentals/custom-renderer/hybridwebview ):

protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
{
    base.OnElementChanged(e);

    if (e.OldElement != null)
    {
        Control.RemoveJavascriptInterface("jsBridge");
    }

    if (e.NewElement != null)
    {
        Control.SetWebViewClient(new JavascriptWebViewClient(this, $"javascript: {JavascriptFunction}"));
        Control.AddJavascriptInterface(new JsBridge(this), "jsBridge");
    }
}

and

public class JavascriptWebViewClient : FormsWebViewClient
{
    private readonly string javascript;

    public JavascriptWebViewClient(HybridWebViewRenderer renderer, string javascript) : base(renderer)
    {
        this.javascript = javascript;
    }

    public override void OnPageFinished(WebView view, string url)
    {
        base.OnPageFinished(view, url);
        view.EvaluateJavascript(javascript, null);
    }
}

In .NET 6 with MAUI, this is deprecated.在带有 MAUI 的 .NET 6 中,这已被弃用。 I tried to build it with handlers, but then the OnPageFinished is never called.我尝试使用处理程序构建它,但从未调用过OnPageFinished The lack of examples is making it difficult to figure out what I miss.缺乏例子让我很难弄清楚我错过了什么。

Microsoft.Maui.Handlers.WebViewHandler.Mapper.AppendToMapping("MyCustomization", (handler, view) =>
  {
#if ANDROID
    handler.PlatformView.SetWebViewClient(new JavascriptWebViewClient($"javascript: {JavascriptFunction}"));
    handler.PlatformView.AddJavascriptInterface(new JsBridge(this), "jsBridge");
#endif
  });

with

public class JavascriptWebViewClient : WebViewClient
{
  private readonly string javascript;

  public JavascriptWebViewClient(string javascript) : base()
  {
    this.javascript = javascript;
  }

  public override void OnPageFinished(WebView view, string url)
  {
    base.OnPageFinished(view, url);
    view.EvaluateJavascript(javascript, null);
  }
}

Where should I put this code?我应该把这段代码放在哪里? Is this the correct way?这是正确的方法吗? What am I missing?我错过了什么? I now put this in a subclassed WebView, but probably that's not the right way.我现在把它放在子类 WebView 中,但可能这不是正确的方法。

OK, figured it out.好的,想通了。 Adding information for those looking to the same problem.为那些寻找相同问题的人添加信息。

What you need to do:你需要做什么:

  1. Override WebView client.覆盖 WebView 客户端。

Generic:通用的:

public partial class CustomWebView : WebView
{
    partial void ChangedHandler(object sender);
    partial void ChangingHandler(object sender, HandlerChangingEventArgs e);

    protected override void OnHandlerChanging(HandlerChangingEventArgs args)
    {
        base.OnHandlerChanging(args);
        ChangingHandler(this, args);
    }

    protected override void OnHandlerChanged()
    {
        base.OnHandlerChanged();
        ChangedHandler(this);
    }

    public void InvokeAction(string data)
    {
        // your custom code
    }
}

Android: Android:

public partial class CustomWebView
{
    const string JavascriptFunction = "function invokeActionInCS(data){jsBridge.invokeAction(data);}";

    partial void ChangedHandler(object sender)
    {
        if (sender is not WebView { Handler: { PlatformView: Android.Webkit.WebView nativeWebView } }) return;

        nativeWebView.SetWebViewClient(new JavascriptWebViewClient($"javascript: {JavascriptFunction}"));
        nativeWebView.AddJavascriptInterface(new JsBridge(this), "jsBridge");
    }

    partial void ChangingHandler(object sender, HandlerChangingEventArgs e)
    {
        if (e.OldHandler != null)
        {
            if (sender is not WebView { Handler: { PlatformView: Android.Webkit.WebView nativeWebView } }) return;
            nativeWebView.RemoveJavascriptInterface("jsBridge");
        }
    }
}
  1. Add this custom view to your XAML将此自定义视图添加到您的 XAML
<views:CustomWebView x:Name="CustomWebViewName"/>
  1. Modify the JS Bridge修改 JS 桥
public class JsBridge : Java.Lang.Object
    {
        private readonly HarmonyWebView webView;

        public JsBridge(HarmonyWebView webView)
        {
            this.webView = webView;
        }

        [JavascriptInterface]
        [Export("invokeAction")]
        public void InvokeAction(string data)
        {
            webView.InvokeAction(data);
        }
    }

TL;DR - https://github.com/nmoschkin/MAUIWebViewExample TL;DR - https://github.com/nmoschkin/MAUIWebViewExample

I have come up with a MAUI solution that work for both iOS and Android, using the new Handler pattern as described in:我提出了一个适用于 iOS 和 Android 的 MAUI 解决方案,使用如下所述的新处理程序模式:

Porting Custom Renderers To Handlers 将自定义渲染器移植到处理程序

The above documentation was somewhat poor, and did not feature an implementation for the iOS version.上面的文档有点差,并且没有针对 iOS 版本的实现。 I provide that, here.我在这里提供。

This adaptation also makes the Source property a BindableProperty .这种改编还使 Source 属性成为BindableProperty Unlike the example in the above link, I do not actually add the property to the PropertyMapper in the platform handler in the traditional way.与上面链接中的示例不同,我实际上并没有以传统方式将属性添加到平台处理程序中的PropertyMapper Rather, we will be listening for an event to be fired by the property changed notification method of the bindable property.相反,我们将监听由可绑定属性的属性更改通知方法触发的事件。

This example implements a 100% custom WebView.此示例实现了 100% 自定义 WebView。 If there are additional properties and methods you would like to port over from the native components, you will have to add that additional functionality, yourself.如果您想从本机组件移植其他属性和方法,则必须自己添加这些附加功能。

Shared Code:共享代码:

In the shared code file, you want to create your custom view by implementing the classes and interface as described in the above link in the following way (with additional classes provided for events that we will provide to the consumer):在共享代码文件中,您希望通过以下方式实现上述链接中描述的类和接口来创建自定义视图(为我们将提供给消费者的事件提供额外的类):

public class SourceChangedEventArgs : EventArgs
{
    public WebViewSource Source
    {
        get;
        private set;
    }

    public SourceChangedEventArgs(WebViewSource source)
    {
        Source = source;
    }
}

public class JavaScriptActionEventArgs : EventArgs
{
    public string Payload { get; private set; }

    public JavaScriptActionEventArgs(string payload)
    {
        Payload = payload;
    }
}

public interface IHybridWebView : IView
{
    event EventHandler<SourceChangedEventArgs> SourceChanged;
    event EventHandler<JavaScriptActionEventArgs> JavaScriptAction;
    
    void Refresh();

    WebViewSource Source { get; set; }

    void Cleanup();

    void InvokeAction(string data);

}
    

public class HybridWebView : View, IHybridWebView
{
    public event EventHandler<SourceChangedEventArgs> SourceChanged;
    public event EventHandler<JavaScriptActionEventArgs> JavaScriptAction;

    public HybridWebView()
    {

    }

    public void Refresh()
    {
        if (Source == null) return;
        var s = Source;
        Source = null;
        Source = s;
    }

    public WebViewSource Source
    {
        get { return (WebViewSource)GetValue(SourceProperty); }
        set { SetValue(SourceProperty, value); }
    }

    public static readonly BindableProperty SourceProperty = BindableProperty.Create(
      propertyName: "Source",
      returnType: typeof(WebViewSource),
      declaringType: typeof(HybridWebView),
      defaultValue: new UrlWebViewSource() { Url = "about:blank" },
      propertyChanged: OnSourceChanged);

    private static void OnSourceChanged(BindableObject bindable, object oldValue, object newValue)
    {
        var view = bindable as HybridWebView;

        bindable.Dispatcher.Dispatch(() =>
        {
            view.SourceChanged?.Invoke(view, new SourceChangedEventArgs(newValue as WebViewSource));

        });
    }

    public void Cleanup()
    {
        JavaScriptAction = null;
    }

    public void InvokeAction(string data)
    {
        JavaScriptAction?.Invoke(this, new JavaScriptActionEventArgs(data));
    }
}

Then you would have to declare the handler for each platform, as follows:然后您必须为每个平台声明处理程序,如下所示:

Android Implementation: Android 实现:

public class HybridWebViewHandler : ViewHandler<IHybridWebView, Android.Webkit.WebView>
{
    public static PropertyMapper<IHybridWebView, HybridWebViewHandler> HybridWebViewMapper = new PropertyMapper<IHybridWebView, HybridWebViewHandler>(ViewHandler.ViewMapper);

    const string JavascriptFunction = "function invokeCSharpAction(data){jsBridge.invokeAction(data);}";

    private JSBridge jsBridgeHandler;

    public HybridWebViewHandler() : base(HybridWebViewMapper)
    {
    }

    private void VirtualView_SourceChanged(object sender, SourceChangedEventArgs e)
    {
        LoadSource(e.Source, PlatformView);
    }

    protected override Android.Webkit.WebView CreatePlatformView()
    {
        var webView = new Android.Webkit.WebView(Context);
        jsBridgeHandler = new JSBridge(this);

        webView.Settings.JavaScriptEnabled = true;

        webView.SetWebViewClient(new JavascriptWebViewClient($"javascript: {JavascriptFunction}"));
        webView.AddJavascriptInterface(jsBridgeHandler, "jsBridge");

        return webView;
    }

    protected override void ConnectHandler(Android.Webkit.WebView platformView)
    {
        base.ConnectHandler(platformView);

        if (VirtualView.Source != null)
        {
            LoadSource(VirtualView.Source, PlatformView);
        }

        VirtualView.SourceChanged += VirtualView_SourceChanged;
    }

    protected override void DisconnectHandler(Android.Webkit.WebView platformView)
    {
        base.DisconnectHandler(platformView);

        VirtualView.SourceChanged -= VirtualView_SourceChanged;
        VirtualView.Cleanup();

        jsBridgeHandler?.Dispose();
        jsBridgeHandler = null;
    }

    private static void LoadSource(WebViewSource source, Android.Webkit.WebView control)
    {
        try
        {
            if (source is HtmlWebViewSource html)
            {
                control.LoadDataWithBaseURL(html.BaseUrl, html.Html, null, "charset=UTF-8", null);
            }
            else if (source is UrlWebViewSource url)
            {
                control.LoadUrl(url.Url);
            }
        }
        catch { }
    }
}

public class JavascriptWebViewClient : WebViewClient
{
    string _javascript;

    public JavascriptWebViewClient(string javascript)
    {
        _javascript = javascript;
    }

    public override void OnPageStarted(Android.Webkit.WebView view, string url, Bitmap favicon)
    {
        base.OnPageStarted(view, url, favicon);
        view.EvaluateJavascript(_javascript, null);
    }
}

public class JSBridge : Java.Lang.Object
{
    readonly WeakReference<HybridWebViewHandler> hybridWebViewRenderer;

    internal JSBridge(HybridWebViewHandler hybridRenderer)
    {
        hybridWebViewRenderer = new WeakReference<HybridWebViewHandler>(hybridRenderer);
    }

    [JavascriptInterface]
    [Export("invokeAction")]
    public void InvokeAction(string data)
    {
        HybridWebViewHandler hybridRenderer;

        if (hybridWebViewRenderer != null && hybridWebViewRenderer.TryGetTarget(out hybridRenderer))
        {
            hybridRenderer.VirtualView.InvokeAction(data);
        }
    }
}

iOS Implementation: iOS 实现:

public class HybridWebViewHandler : ViewHandler<IHybridWebView, WKWebView>
{
    public static PropertyMapper<IHybridWebView, HybridWebViewHandler> HybridWebViewMapper = new PropertyMapper<IHybridWebView, HybridWebViewHandler>(ViewHandler.ViewMapper);

    const string JavaScriptFunction = "function invokeCSharpAction(data){window.webkit.messageHandlers.invokeAction.postMessage(data);}";

    private WKUserContentController userController;
    private JSBridge jsBridgeHandler;

    public HybridWebViewHandler() : base(HybridWebViewMapper)
    {
    }

    private void VirtualView_SourceChanged(object sender, SourceChangedEventArgs e)
    {
        LoadSource(e.Source, PlatformView);
    }

    protected override WKWebView CreatePlatformView()
    {

        jsBridgeHandler = new JSBridge(this);
        userController = new WKUserContentController();

        var script = new WKUserScript(new NSString(JavaScriptFunction), WKUserScriptInjectionTime.AtDocumentEnd, false);

        userController.AddUserScript(script);
        userController.AddScriptMessageHandler(jsBridgeHandler, "invokeAction");

        var config = new WKWebViewConfiguration { UserContentController = userController };
        var webView = new WKWebView(CGRect.Empty, config);

        return webView;            
    }

    protected override void ConnectHandler(WKWebView platformView)
    {
        base.ConnectHandler(platformView);

        if (VirtualView.Source != null)
        {
            LoadSource(VirtualView.Source, PlatformView);
        }

        VirtualView.SourceChanged += VirtualView_SourceChanged;
    }

    protected override void DisconnectHandler(WKWebView platformView)
    {
        base.DisconnectHandler(platformView);

        VirtualView.SourceChanged -= VirtualView_SourceChanged;

        userController.RemoveAllUserScripts();
        userController.RemoveScriptMessageHandler("invokeAction");
    
        jsBridgeHandler?.Dispose();
        jsBridgeHandler = null;
    }


    private static void LoadSource(WebViewSource source, WKWebView control)
    {
        if (source is HtmlWebViewSource html)
        {
            control.LoadHtmlString(html.Html, new NSUrl(html.BaseUrl ?? "http://localhost", true));
        }
        else if (source is UrlWebViewSource url)
        {
            control.LoadRequest(new NSUrlRequest(new NSUrl(url.Url)));
        }

    }

}

public class JSBridge : NSObject, IWKScriptMessageHandler
{
    readonly WeakReference<HybridWebViewHandler> hybridWebViewRenderer;

    internal JSBridge(HybridWebViewHandler hybridRenderer)
    {
        hybridWebViewRenderer = new WeakReference<HybridWebViewHandler>(hybridRenderer);
    }

    public void DidReceiveScriptMessage(WKUserContentController userContentController, WKScriptMessage message)
    {
        HybridWebViewHandler hybridRenderer;

        if (hybridWebViewRenderer.TryGetTarget(out hybridRenderer))
        {
            hybridRenderer.VirtualView?.InvokeAction(message.Body.ToString());
        }
    }
}

As you can see, I'm listening for the event to change out the source, which will then perform the platform-specific steps necessary to change it.如您所见,我正在监听事件以更改源,然后执行更改它所需的特定于平台的步骤。

Also note that in both implementations of JSBridge I am using a WeakReference to track the control.另请注意,在JSBridge的两个实现中,我都使用 Wea kReference来跟踪控件。 I am not certain of any situations where disposal might deadlock, but I did this out of an abundance of caution.我不确定处置可能会陷入僵局的任何情况,但我这样做是出于非常谨慎的考虑。

Finally, you will need to initialize the MAUI application by adding ConfigureMauiHandlers to the app builder:最后,您需要通过将ConfigureMauiHandlers添加到应用程序构建器来初始化 MAUI 应用程序:

Initialize the MAUI Application in MauiProgram.cs在 MauiProgram.cs 中初始化 MAUI 应用程序

public static MauiApp CreateMauiApp()
{

    var builder = MauiApp.CreateBuilder();

    builder
        .UseMauiApp<App>()
        .ConfigureFonts(fonts =>
        {
            fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
            fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
        })
        .ConfigureMauiHandlers(handlers =>
        {
            handlers.AddHandler(typeof(HybridWebView), typeof(HybridWebViewHandler));
        });


    return builder.Build();
}

Add The Control To XAML将控件添加到 XAML

<controls:HybridWebView
            x:Name="MyWebView"
            HeightRequest="128"
            HorizontalOptions="Fill"
            Source="{Binding Source}"
            VerticalOptions="FillAndExpand"
            WidthRequest="512"
            />

Finally, I have added all of the above to a full example MAUI project in a repository on GitHub:最后,我将以上所有内容添加到 GitHub 存储库中的完整示例 MAUI 项目中:

https://github.com/nmoschkin/MAUIWebViewExample https://github.com/nmoschkin/MAUIWebViewExample

The GitHub repo example also includes a ViewModel that contains the WebViewSource to which the control is bound in markup. GitHub 存储库示例还包括一个 ViewModel,其中包含控件在标记中绑定到的WebViewSource

I solved it finnaly.我最终解决了它。 It's not completely solution however its work well.它不是完全的解决方案,但它工作得很好。

First, we define 2 variables globally on the javascipt side and define 2 functions waiting for each other.首先,我们在javascipt端全局定义2个变量,定义2个相互等待的函数。

var _flagformobiledata = false;
var _dataformobiledata = "";

const waitUntilMobileData = (condition, checkInterval = 100) => {
    return new Promise(resolve => {
        let interval = setInterval(() => {
            if (!condition()) return;
            clearInterval(interval);
            resolve();
        }, checkInterval)
    })
}


async function mobileajax(functionName, params) {
    window.location.href = "https://runcsharp." + functionName + "?" + params;
    await waitUntilMobileData(() => _flagformobiledata == true);
    _flagformobiledata = false;
    return _dataformobiledata;
}

function setmobiledata(aData) {

    _dataformobiledata = aData;
    _flagformobiledata = true;


}

Then in MainPage.xaml.cs file define a function named WebViewNavigation然后在MainPage.xaml.cs文件中定义一个名为WebViewNavigation的function

private async void WebViewNavigation(object sender, WebNavigatingEventArgs e)
{

    var urlParts = e.Url.Split("runcsharp.");
    if (urlParts.Length == 2)
    {
        Console.WriteLine(urlParts);
        var funcToCall = urlParts[1].Split("?");
        var methodName = funcToCall[0];
        var funcParams = funcToCall[1];

        e.Cancel = true;


        if (methodName.Contains("login"))
        {

            var phonenumber = funcParams.Split("=");
            var ActualPhoneNumber = "";
            if (phonenumber.Length == 2)
            {

                ActualPhoneNumber = Regex.Replace(phonenumber[1].Replace("%20", "").ToString(), @"[^\d]", "");
            }

            var response = _authService.GetMobileLicanceInfo(ActualPhoneNumber);
            if (response.status == 200)
            {

                PhoneGlobal = ActualPhoneNumber;
                string maui = "setmobiledata('" + "OK" + "')"; // this is function to set global return data

                await CustomWebView.EvaluateJavaScriptAsync(maui); // then run the script
            }

            else
            {
                await DisplayAlert("Error", response.message, "OK");
            }



        }

    }

}

Then your Mainpage.xaml file:然后是您的 Mainpage.xaml 文件:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="DinamoMobile.MainPage">
   <WebView Navigating="WebViewNavigation" x:Name="CustomWebView">


   </WebView>
</ContentPage>

After all that coding usage must be like:毕竟编码用法必须是这样的:

<script>const login = document.querySelector(".js-login-btnCls");
    login.addEventListener("click", logincallapi);
    async function logincallapi() {


        var phone = $("#phone").val();
        if (phone == "") {
            alert("Phone is required");
            return;
        }

        var isOK = await mobileajax("login", "phone=" + phone);

        if (isOK == "OK") {
            window.location.href = "verification.html";
        }
        else {
            alert("Invalid Phone Number.");
        }


    }</script>

Algorithm:算法:

  1. Write an asynchronous function that waits for data on the javascript side.写一个异步的function等待javascript这边的数据。
  2. Go with the URL of the desired function on the Javascript side. Go 与所需 function 的 URL 在 Javascript 侧。
  3. Set data to global variable with EvulateJavascript function.使用 EvulateJavascript function 将数据设置为全局变量。
  4. The function waiting for the data will continue in this way.等待数据的function就这样继续下去。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM