.NET MAUI 使用 WebView 实现 JS 与 C# 交互和数据传输,解决访问 URL 白屏与 A JavaScript exception occurred 问题

参考

环境

软件/系统 版本 说明
macOS macOS 15.6.1 Apple M2 芯片
Safari 18.6
.NET 10.0.100
Visual Studio Code 1.107.1
Visual Studio Code 扩展: .NET MAUI 1.12.15
Visual Studio Code 扩展: C# 2.110.4
Visual Studio Code 扩展: .NET Install Tool 3.0.0
Visual Studio Code 扩展: C# Dev Kit 1.90.2

注意

iOS 和 Mac Catalyst:使用 WKWebView,它基于 Safari WebKit 浏览器引擎。 这是 iOS 和 macOS 上的 Safari 浏览器使用的相同引擎。 -- https://learn.microsoft.com/zh-cn/dotnet/maui/user-interface/controls/webview

  1. WKWebView 存在 JS 兼容问题,建议先在 Safari 中进行验证与测试。
  2. WKWebView 注入自定义函数时出现下面的错误, 是多行JS直接作为字符串传递可能会出现问题,因为里面有换行符。多行必须转换为一行
    Error Domain=WKErrorDomain Code=4 "A JavaScript exception occurred" UserInfo={WKJavaScriptExceptionLineNumber=1, WKJavaScriptExceptionMessage=SyntaxError: Unexpected EOF, WKJavaScriptExceptionColumnNumber=0, WKJavaScriptExceptionSourceURL=https://www.xxxx.com/, NSLocalizedDescription=A JavaScript exception occurred}
    
  3. 在 iOS 和 Mac Catalyst 上配置应用传输安全性(允许 http 请求),在 项目/Platforms/MacCatalyst/Info.plist 添加以下代码:
    <!-- 允许不安全的请求 https://learn.microsoft.com/zh-cn/dotnet/maui/user-interface/controls/webview?view=net-maui-10.0&pivots=devices-maccatalyst#inspect-a-webview-on-mac-catalyst -->
    <key>NSAppTransportSecurity</key>
    <dict>
    	<key>NSAllowsArbitraryLoads</key>
    	<true/>
    </dict>
    

正文

客户端代码

image

JS->C# 通过JS触发 Navigating 事件进行解析数据传递相关的 URL进行数据传输。
C#->JS 通过 webView.EvaluateJavaScriptAsync(js) 进行执行JS。

  1. MainPage.xaml.cs
    using System.Diagnostics;
    using System.Web;
    
    namespace xqc_test_maui_app;
    
    public partial class MainPage : ContentPage
    {
    	public MainPage()
    	{
    		InitializeComponent();
    	}
    
    	private void AddLog(string msg)
    	{
    		if (string.IsNullOrEmpty(msg))
    		{
    			return;
    		}
    		Debug.WriteLine(msg);
    		MainThread.InvokeOnMainThreadAsync(() =>
    		{
    			logEditor.Text = $"[{DateTime.Now}]:" + msg + "\r\n" + logEditor.Text;
    		});
    	}
    	/// <summary>
    	/// 导航完成事件
    	/// </summary>
    	/// <param name="sender"></param>
    	/// <param name="e"></param>
    	private void webView_Navigated(object sender, WebNavigatedEventArgs e)
    	{
    		Debug.WriteLine("导航完毕");
    	}
    	/// <summary>
    	/// 监听窗口刷新
    	/// </summary>
    	/// <param name="sender"></param>
    	/// <param name="e"></param>
    	private async void WebView_Navigating(object sender, WebNavigatingEventArgs e)
    	{
    		var url = e.Url;
    		if (!url.StartsWith("maui://"))
    		{
    			return;
    		}
    		//
    		AddLog(url);
    		// 阻止网页跳转
    		e.Cancel = true;
    		// 当前URL
    		Uri uri = new Uri(url);
    		var function = uri.Host;
    		var paramses = HttpUtility.ParseQueryString(uri.Query);
    		//
    		if (function == "JsSendTestMessage")
    		{
    			AddLog($"JS发送测试消息,{paramses["message"]}");
    		}
    		else
    		{
    			AddLog($"JS调用:{function} (暂未生效),数据:{paramses.ToString()}");
    		}
    	}
    	private void Go_Clicked(object sender, EventArgs e)
    	{
    		webView.Source = inputUrl.Text;
    	}
    	private void Ref_Clicked(object sender, EventArgs e)
    	{
    		webView.Reload();
    	}
    	private void Back_Clicked(object sender, EventArgs e)
    	{
    		webView.GoBack();
    	}
    	/// <summary>
    	/// 执行js
    	/// </summary>
    	/// <param name="sender"></param>
    	/// <param name="e"></param>
    	private async void EvaluateJavaScriptAsync_Clicked(object sender, EventArgs e)
    	{
    		if (string.IsNullOrEmpty(jsEditor.Text))
    		{
    			return;
    		}
    		try
    		{
    			AddLog(await webView.EvaluateJavaScriptAsync(jsEditor.Text));
    		}
    		catch (Exception ex)
    		{
    			AddLog(ex.Message);
    		}
    	}
    	/// <summary>
    	/// 测试js调用C#
    	/// </summary>
    	/// <param name="sender"></param>
    	/// <param name="e"></param>
    	private async void TestJsCallCSharp_Clicked(object sender, EventArgs e)
    	{
    		var msg = await webView.EvaluateJavaScriptAsync(@"window.location.href = ""maui://callcs?msg=我是JS的简单消息"" + new Date().getTime();");
    		AddLog(msg);
    	}
    }
    
    
  2. 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="xqc_test_maui_app.MainPage">
    	<ContentPage.Resources>
    		<Style TargetType="Entry"
    			   x:Key="webViewEntry">
    			<Setter Property="HeightRequest"
    					Value="50"></Setter>
    			<Setter Property="WidthRequest"
    					Value="300"></Setter>
    		</Style>
    		<Style TargetType="Button"
    			   x:Key="webViewBtn">
    			<Setter Property="HeightRequest"
    					Value="50"></Setter>
    			<Setter Property="Margin"
    					Value="0, 0, 10, 0"></Setter>
    		</Style>
    	</ContentPage.Resources>
    	<Grid RowDefinitions="80,*"
    		  Padding="10"
    		  ColumnDefinitions="*,300">
    		<Border Stroke="Gray"
    				Padding="10"
    				Grid.Row="0"
    				Grid.Column="0"
    				Grid.ColumnSpan="2 ">
    			<StackLayout Orientation="Horizontal"
    						 Spacing="10">
    				<StackLayout Orientation="Horizontal"
    							 Spacing="10">
    					<Label Text="URL"
    							HeightRequest="20"></Label>
    					<Entry x:Name="inputUrl"
    						   Style="{StaticResource webViewEntry}"></Entry>
    				</StackLayout>
    				<Button Clicked="Go_Clicked"
    						Text="前往"
    						Style="{StaticResource webViewBtn}"></Button>
    				<Button Clicked="Ref_Clicked"
    						Text="刷新"
    						Style="{StaticResource webViewBtn}"></Button>
    				<Button Clicked="Back_Clicked"
    						Text="返回"
    						Style="{StaticResource webViewBtn}"></Button>
    			</StackLayout>
    		</Border>
    		<Border Stroke="Gray"
    				Padding="10"
    				Grid.Row="1"
    				Grid.Column="0">
    			<WebView x:Name="webView"
    					Source="https://www.cnblogs.com/xiaqiuchu"
    					Navigated="WebView_Navigated"
    					Navigating="WebView_Navigating"></WebView>
    		</Border>
    		<Border Stroke="Gray"
    				Padding="10"
    				Grid.Row="1"
    				Grid.Column="1">
    			<StackLayout Padding="10"
    						 Spacing="10"
    					VerticalOptions="Center">
    				<Border Stroke="Gray"
    						HeightRequest="300">
    					<Editor x:Name="jsEditor"></Editor>
    				</Border>
    				<Button Clicked="EvaluateJavaScriptAsync_Clicked"
    						Text="执行JS"></Button>
    				<Button Clicked="TestJsCallCSharp_Clicked"
    						Text="测试通信JS->C#"></Button>
    				<Border Stroke="Gray"
    						HeightRequest="300">
    					<Editor x:Name="logEditor"
    							IsReadOnly="True"></Editor>
    				</Border>
    			</StackLayout>
    		</Border>
    	</Grid>
    
    
    </ContentPage>
    
    

注入JS示例

  • 压缩前
    // alert(JSON.stringify(CSharpDocumentJsObject))
    
    // C#调用
    function setDataAndStart(callFunctionName) {
    	callFunctionName && callFunctionName();
    }
    // JS 调用
    function sendData(paramers = {}) {
    	callCSharp('JsSendCurrentData', paramers)
    }
    function sendTestMessage(message = 'sendTestMessage') {
    	callCSharp('JsSendTestMessage', {
    		message: message
    	})
    }
    function callCSharp(functionName, paramers = {}) {
    	location.href = 'maui://' + functionName + objectToQueryString(paramers)
    }
    function objectToQueryString(obj = {}) {
    	return '?' + Object.keys(obj).map(key => {
    		return encodeURIComponent(key) + '=' + encodeURIComponent(obj[key])
    	}).join('&')
    }
    function waitForElement(selector, callback, interval = 100, timeout = 10000) {
    	const startTime = Date.now();
    	const checkElement = () => {
    		const element = document.querySelector(selector);
    		if (element) {
    			callback(element);
    			return true
    		}
    		if (Date.now() - startTime > timeout) {
    			console.warn(`等待元素${selector}超时`);
    			return false
    		}
    		setTimeout(checkElement, interval)
    	};
    	checkElement()
    }
    
  • 压缩后
    function setDataAndStart(callFunctionName){callFunctionName&&callFunctionName()}function sendData(paramers={}){callCSharp('JsSendCurrentData',paramers)}function sendTestMessage(message='sendTestMessage'){callCSharp('JsSendTestMessage',{message:message})}function callCSharp(functionName,paramers={}){location.href='maui://'+functionName+objectToQueryString(paramers)}function objectToQueryString(obj={}){return'?'+Object.keys(obj).map(key=>{return encodeURIComponent(key)+'='+encodeURIComponent(obj[key])}).join('&')}function waitForElement(selector,callback,interval=100,timeout=10000){const startTime=Date.now();const checkElement=()=>{const element=document.querySelector(selector);if(element){callback(element);return true}if(Date.now()-startTime>timeout){console.warn(`等待元素${selector}超时`);return false}setTimeout(checkElement,interval)};checkElement()}
    
posted @ 2025-12-31 16:19  夏秋初  阅读(3)  评论(0)    收藏  举报