繁体   English   中英

MAUI上JS/.NET交互 WebView

[英]JS/.NET interact on MAUI WebView

我正在将我们的应用程序从 Xamarin 迁移到 MAUI,我有点难以迁移处理 WebView 中 Android 和 iOS 上的 JS/.NET 交互的代码。让我们专注于 Android 中的 code6038340988。 WebView。

在 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");
    }
}

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);
    }
}

在带有 MAUI 的 .NET 6 中,这已被弃用。 我尝试使用处理程序构建它,但从未调用过OnPageFinished 缺乏例子让我很难弄清楚我错过了什么。

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
  });

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);
  }
}

我应该把这段代码放在哪里? 这是正确的方法吗? 我错过了什么? 我现在把它放在子类 WebView 中,但可能这不是正确的方法。

好的,想通了。 为那些寻找相同问题的人添加信息。

你需要做什么:

  1. 覆盖 WebView 客户端。

通用的:

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:

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. 将此自定义视图添加到您的 XAML
<views:CustomWebView x:Name="CustomWebViewName"/>
  1. 修改 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

我提出了一个适用于 iOS 和 Android 的 MAUI 解决方案,使用如下所述的新处理程序模式:

将自定义渲染器移植到处理程序

上面的文档有点差,并且没有针对 iOS 版本的实现。 我在这里提供。

这种改编还使 Source 属性成为BindableProperty 与上面链接中的示例不同,我实际上并没有以传统方式将属性添加到平台处理程序中的PropertyMapper 相反,我们将监听由可绑定属性的属性更改通知方法触发的事件。

此示例实现了 100% 自定义 WebView。 如果您想从本机组件移植其他属性和方法,则必须自己添加这些附加功能。

共享代码:

在共享代码文件中,您希望通过以下方式实现上述链接中描述的类和接口来创建自定义视图(为我们将提供给消费者的事件提供额外的类):

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));
    }
}

然后您必须为每个平台声明处理程序,如下所示:

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 实现:

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());
        }
    }
}

如您所见,我正在监听事件以更改源,然后执行更改它所需的特定于平台的步骤。

另请注意,在JSBridge的两个实现中,我都使用 Wea kReference来跟踪控件。 我不确定处置可能会陷入僵局的任何情况,但我这样做是出于非常谨慎的考虑。

最后,您需要通过将ConfigureMauiHandlers添加到应用程序构建器来初始化 MAUI 应用程序:

在 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();
}

将控件添加到 XAML

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

最后,我将以上所有内容添加到 GitHub 存储库中的完整示例 MAUI 项目中:

https://github.com/nmoschkin/MAUIWebViewExample

GitHub 存储库示例还包括一个 ViewModel,其中包含控件在标记中绑定到的WebViewSource

我最终解决了它。 它不是完全的解决方案,但它工作得很好。

首先,我们在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;


}

然后在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");
            }



        }

    }

}

然后是您的 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>

毕竟编码用法必须是这样的:

<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>

算法:

  1. 写一个异步的function等待javascript这边的数据。
  2. Go 与所需 function 的 URL 在 Javascript 侧。
  3. 使用 EvulateJavascript function 将数据设置为全局变量。
  4. 等待数据的function就这样继续下去。

暂无
暂无

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

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