我正在将我们的应用程序从Xamarin迁移到MAUI,我有点挣扎于迁移在WebView和iOS上处理JS/.NET交互的代码。让我们集中精力在Android上。特别是关于从WebView中的JS调用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);
}
}
在带有毛伊岛的.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中,但这可能不是正确的方法。
发布于 2022-08-10 19:25:07
Windows :我为开发了一个工作环境。见下文.
TL;DR - https://github.com/nmoschkin/MAUIWebViewExample
我已经提出了一个同时适用于iOS和Android的MAUI解决方案,该解决方案使用了新的Handler模式,如下所述:
上面的文档有点差,并且没有为iOS版本提供一个实现。我提供这个给你。
此适配还使Source属性成为BindableProperty.与上面链接中的示例不同,我使用的是而不是,实际上是以传统的方式在平台处理程序中将属性添加到平台处理程序中。相反,我们将侦听由可绑定属性的属性更改通知方法触发的事件。
此示例实现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));
}
}
然后,您必须为每个平台声明处理程序,如下所示:
安卓实现:
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的实现中,我都使用一个JSBridge来跟踪控件。我不确定处理可能陷入僵局的任何情况,但我这样做是出于谨慎。
Windows实现:
所以。根据我所读过的各种文章,当前针对毛伊岛的WinUI3迭代WebView2还不允许我们调用AddHostObjectToScript。他们为将来的发行计划了这个。
但是,后来我想起了它是Windows的,所以我创建了一个解决方案--通过使用HttpListener.,它肯定会模仿相同的行为,并获得相同的结果:使用一个非正统的解决方案。
internal class HybridSocket
{
private HttpListener listener;
private HybridWebViewHandler handler;
bool token = false;
public HybridSocket(HybridWebViewHandler handler)
{
this.handler = handler;
CreateSocket();
}
private void CreateSocket()
{
listener = new HttpListener();
listener.Prefixes.Add("http://localhost:32000/");
}
public void StopListening()
{
token = false;
}
private void SendToNative(string json)
{
handler.VirtualView.InvokeAction(json);
}
public void Listen()
{
var s = listener;
try
{
token = true;
s.Start();
while (token)
{
HttpListenerContext ctx = listener.GetContext();
using HttpListenerResponse resp = ctx.Response;
resp.AddHeader("Access-Control-Allow-Origin", "null");
resp.AddHeader("Access-Control-Allow-Headers", "content-type");
var req = ctx.Request;
Stream body = req.InputStream;
Encoding encoding = req.ContentEncoding;
using (StreamReader reader = new StreamReader(body, encoding))
{
var json = reader.ReadToEnd();
if (ctx.Request.HttpMethod == "POST")
{
SendToNative(json);
}
}
resp.StatusCode = (int)HttpStatusCode.OK;
resp.StatusDescription = "Status OK";
}
CreateSocket();
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
}
public class HybridWebViewHandler : ViewHandler<IHybridWebView, WebView2>
{
public static PropertyMapper<IHybridWebView, HybridWebViewHandler> HybridWebViewMapper = new PropertyMapper<IHybridWebView, HybridWebViewHandler>(ViewHandler.ViewMapper);
const string JavascriptFunction = @"function invokeCSharpAction(data)
{
var http = new XMLHttpRequest();
var url = 'http://localhost:32000';
http.open('POST', url, true);
http.setRequestHeader('Content-type', 'application/json');
http.send(JSON.stringify(data));
}";
static SynchronizationContext sync;
private HybridSocket jssocket;
public HybridWebViewHandler() : base(HybridWebViewMapper)
{
sync = SynchronizationContext.Current;
jssocket = new HybridSocket(this);
Task.Run(() => jssocket.Listen());
}
~HybridWebViewHandler()
{
jssocket.StopListening();
}
private void OnWebSourceChanged(object sender, SourceChangedEventArgs e)
{
LoadSource(e.Source, PlatformView);
}
protected override WebView2 CreatePlatformView()
{
sync = sync ?? SynchronizationContext.Current;
var webView = new WebView2();
webView.NavigationCompleted += WebView_NavigationCompleted;
return webView;
}
private void WebView_NavigationCompleted(WebView2 sender, CoreWebView2NavigationCompletedEventArgs args)
{
var req = new EvaluateJavaScriptAsyncRequest(JavascriptFunction);
PlatformView.EvaluateJavaScript(req);
}
protected override void ConnectHandler(WebView2 platformView)
{
base.ConnectHandler(platformView);
if (VirtualView.Source != null)
{
LoadSource(VirtualView.Source, PlatformView);
}
VirtualView.SourceChanged += OnWebSourceChanged;
}
protected override void DisconnectHandler(WebView2 platformView)
{
base.DisconnectHandler(platformView);
VirtualView.SourceChanged -= OnWebSourceChanged;
VirtualView.Cleanup();
}
private static void LoadSource(WebViewSource source, WebView2 control)
{
try
{
if (control.CoreWebView2 == null)
{
control.EnsureCoreWebView2Async().AsTask().ContinueWith((t) =>
{
sync.Post((o) => LoadSource(source, control), null);
});
}
else
{
if (source is HtmlWebViewSource html)
{
control.CoreWebView2.NavigateToString(html.Html);
}
else if (source is UrlWebViewSource url)
{
control.CoreWebView2.Navigate(url.Url);
}
}
}
catch { }
}
}
最后,您需要通过将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 repo示例还包括一个ViewModel,该ViewModel包含将控件绑定到标记中的ViewModel。
https://stackoverflow.com/questions/73217992
复制相似问题