MAUI Blazor 显示本地图片的新思路

前言

好久没写文章了,水一篇

关于MAUI Blazor 显示本地图片这个问题,有大佬发过了。

就是 token 大佬的那篇

Blazor Hybrid (Blazor混合开发)更好的读取本地图片

主要思路就是读取本地图片,通过C#与JS互操作,将byte[]传给js,生成blob,图片的src中填写根据blob生成的url。

我之前一直使用这个办法,简单的优化了一下,无非也就是增加缓存。

但是这种方法的弊端也是很明显的:

  1. img的src每一次并不固定,需要替换

  2. Android端加载体积比较大的图片的速度,特别特别慢

所以有没有一种办法能够解决这两个问题

思考了很久,终于有了思路,

拦截网络请求/响应,读取本地文件并返回响应

搜索了一下,C#/MAUI中没有太好的拦截办法,只能从Webview下手

理论已有,实践开始

准备工作

新建一个MAUI Blazor项目

参考 配置基于文件名的多目标 ,更改项目文件(以.csproj结尾的文件),添加以下代码

<!-- Android --> <ItemGroup Condition="$(TargetFramework.StartsWith('net7.0-android')) != true">   <Compile Remove="*****.Android.cs" />   <None Include="*****.Android.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" /> </ItemGroup>  <!-- Both iOS and Mac Catalyst --> <ItemGroup Condition="$(TargetFramework.StartsWith('net7.0-ios')) != true AND $(TargetFramework.StartsWith('net7.0-maccatalyst')) != true">   <Compile Remove="*****.MaciOS.cs" />   <None Include="*****.MaciOS.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" /> </ItemGroup>  <!-- iOS --> <ItemGroup Condition="$(TargetFramework.StartsWith('net7.0-ios')) != true">   <Compile Remove="*****.iOS.cs" />   <None Include="*****.iOS.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" /> </ItemGroup>  <!-- Mac Catalyst --> <ItemGroup Condition="$(TargetFramework.StartsWith('net7.0-maccatalyst')) != true">   <Compile Remove="*****.MacCatalyst.cs" />   <None Include="*****.MacCatalyst.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" /> </ItemGroup>  <!-- Windows --> <ItemGroup Condition="$(TargetFramework.Contains('-windows')) != true">   <Compile Remove="***.Windows.cs" />   <None Include="***.Windows.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" /> </ItemGroup> 

分别添加MainPage.xaml.Android.csMainPage.xaml.MaciOS.csMainPage.xaml.Windows.cs

MAUI Blazor 显示本地图片的新思路

MainPage.xaml.cs

public partial class MainPage : ContentPage { 	public MainPage() 	{ 		InitializeComponent();          blazorWebView.BlazorWebViewInitializing += BlazorWebViewInitializing;         blazorWebView.BlazorWebViewInitialized -= BlazorWebViewInitialized;     }      private partial void BlazorWebViewInitializing(object sender, BlazorWebViewInitializingEventArgs e);     private partial void BlazorWebViewInitialized(object sender, BlazorWebViewInitializedEventArgs e); } 

MainPage.xaml.Android.cs,MainPage.xaml.MaciOS.cs,MainPage.xaml.Windows.cs

public partial class MainPage     {         private partial void BlazorWebViewInitializing(object sender, BlazorWebViewInitializingEventArgs e)         {         }          private partial void BlazorWebViewInitialized(object sender, BlazorWebViewInitializedEventArgs e)         {         }     } 

Android

https://github.com/dotnet/maui/issues/11382

从这个issue中找到了拦截请求的办法

在ShouldInterceptRequest中添加请求不到时的一些处理。

因为这里填写的,是图片文件的本机绝对路径,安卓中的文件路径是符合浏览器url格式的,所以会被视为基于 https://0.0.0.0 这个基地址的相对路径去发起请求。

当然,它是请求不到的,因为压根就不存在。

所以我们去判断该路径的文件是否存在,存在就读取文件,返回一个新的响应。

注意,不是任意文件都可以的,你的App要对这个文件有访问权限。

MainPage.xaml.Android.cs

using Android.Webkit; using Microsoft.AspNetCore.Components.WebView; using Microsoft.AspNetCore.Components.WebView.Maui;  namespace MauiBlazorLocalImage {     public partial class MainPage     {         private partial void BlazorWebViewInitializing(object sender, BlazorWebViewInitializingEventArgs e)         {         }          private partial void BlazorWebViewInitialized(object sender, BlazorWebViewInitializedEventArgs e)         {                         e.WebView.SetWebViewClient(new MyWebViewClient(e.WebView.WebViewClient));         }          private class MyWebViewClient : WebViewClient         {             private WebViewClient WebViewClient { get; }              public MyWebViewClient(WebViewClient webViewClient)             {                 WebViewClient = webViewClient;             }              public override bool ShouldOverrideUrlLoading(Android.Webkit.WebView view, IWebResourceRequest request)             {                 return WebViewClient.ShouldOverrideUrlLoading(view, request);             }              public override WebResourceResponse ShouldInterceptRequest(Android.Webkit.WebView view, IWebResourceRequest request)             {                 var resourceResponse = WebViewClient.ShouldInterceptRequest(view, request);                 if (resourceResponse == null)                     return null;                 if (resourceResponse.StatusCode == 404)                 {                     var path = request.Url.Path;                     if (File.Exists(path))                     {                         string mime = MimeTypeMap.Singleton.GetMimeTypeFromExtension(Path.GetExtension(path));                         string encoding = "UTF-8";                         Stream stream = File.OpenRead(path);                         return new(mime, encoding, stream);                     }                 }                 //Debug.WriteLine("路径:" + request.Url.ToString());                 return resourceResponse;             }              public override void OnPageFinished(Android.Webkit.WebView view, string url)             => WebViewClient.OnPageFinished(view, url);              protected override void Dispose(bool disposing)             {                 if (!disposing)                     return;                  WebViewClient.Dispose();             }         }     } }  

下面做一个小例子

用MAUI的 MediaPicker.Default.PickPhotoAsync 去选择图片

这里不做过多的处理,Android中选择图片得到的路径实际上是复制到App的Cache文件夹下的图片文件路径

App对自己的FileSystem.Current.AppDataDirectory和FileSystem.Current.CacheDirectory这两个文件夹是有完全的读写权限的。

这里不做过多解释

Pages/Index.razor

@page "/"  <h1>Hello, world!</h1>  Welcome to your new app.  <SurveyPrompt Title="How is Blazor working for you?" />  <div>     <img src="@photoPath" style="max-width: 100%;" /> </div>  <div style="word-wrap: break-word;">     @photoPath </div>  <div>     <button @onclick="PickPhoto">PickPhoto</button> </div>  @code {     string photoPath;     private async Task PickPhoto()     {         var fileResult = await MediaPicker.Default.PickPhotoAsync();         var path = fileResult?.FullPath;         if (path is null)         {             return;         }          photoPath = path;         await InvokeAsync(StateHasChanged);     } } 

看一下效果

MAUI Blazor 显示本地图片的新思路

(下面调试工具这个截图是后补的,所以路径不一致,忽略这些细节)

MAUI Blazor 显示本地图片的新思路

由此可以看到,已经成功拦截,并且把响应换成了我们自己创建的。

换一张大一点的图片,看看速度

特意选了一张13.28 MB的4k图片,速度还可以

MAUI Blazor 显示本地图片的新思路

Windows

在之前那个issue https://github.com/dotnet/maui/issues/11382 中,并没有关于Windows如何拦截Webview请求的方法。

Windows上的Webview是使用的微软自家的WebView2。

于是我在Webview2的官方文档中找到了这个

重写响应,以主动替换它

但有个难题,Windows上的文件路径不符合浏览器url格式,它会被视为文件请求自动变成file:///开头的路径

file:///开头的路径是请求不到的,这里不过多解释。

所以我们在使用Windows上的文件路径之前,先把它转义一下 Uri.EscapeDataString()

等到拦截请求后,再变回去 Uri.UnescapeDataString()

MainPage.xaml.Windows.cs

using Microsoft.AspNetCore.Components.WebView; using System.Runtime.InteropServices.WindowsRuntime; using Windows.Storage.Streams;  namespace MauiBlazorLocalImage {     public partial class MainPage     {         private partial void BlazorWebViewInitializing(object sender, BlazorWebViewInitializingEventArgs e)         {         }          private partial void BlazorWebViewInitialized(object sender, BlazorWebViewInitializedEventArgs e)         {             var webview2 = e.WebView.CoreWebView2;              webview2.WebResourceRequested += async (sender, args) =>             {                 string path = new Uri(args.Request.Uri).AbsolutePath.TrimStart('/');                 path = Uri.UnescapeDataString(path);                 if (File.Exists(path))                 {                     using var contentStream = File.OpenRead(path);                     IRandomAccessStream stream = await CopyContentToRandomAccessStreamAsync(contentStream);                     var response = webview2.Environment.CreateWebResourceResponse(stream, 200, "OK", null);                     args.Response = response;                 }             };              //为什么这么写?我也不知道,Maui源码就是这么写的             async Task<IRandomAccessStream> CopyContentToRandomAccessStreamAsync(Stream content)             {                 using var memStream = new MemoryStream();                 await content.CopyToAsync(memStream);                 var randomAccessStream = new InMemoryRandomAccessStream();                 await randomAccessStream.WriteAsync(memStream.GetWindowsRuntimeBuffer());                 return randomAccessStream;             }         }     } }  

例子中的路径也要处理一下

Pages/Index.razor

      var fileResult = await MediaPicker.Default.PickPhotoAsync();       var path = fileResult?.FullPath;        if (path is null)       {           return;       }  #if WINDOWS      path = Uri.EscapeDataString(path); #endif 

看一下效果

MAUI Blazor 显示本地图片的新思路

(这个截图也是后补的,所以路径不一致,忽略这些细节)

MAUI Blazor 显示本地图片的新思路

iOS / mac OS

在这篇文章最开始写的时候,笔者并没有找到iOS / mac OS中如何拦截请求

本来已经要放弃了,但天无绝人之路

抱着严谨的态度,又做了一些努力,看 Maui 源码,看 issue

克服了种种困难之后,终于有办法了

MainPage.xaml.Windows.cs

using Foundation; using Microsoft.AspNetCore.Components.WebView; using System.Runtime.Versioning; using WebKit;  namespace MauiBlazorLocalImage {     public partial class MainPage     {         private partial void BlazorWebViewInitializing(object sender, BlazorWebViewInitializingEventArgs e)         {             e.Configuration.SetUrlSchemeHandler(new MySchemeHandler(), "myfile");         }          private partial void BlazorWebViewInitialized(object sender, BlazorWebViewInitializedEventArgs e)         {         }          private class MySchemeHandler : NSObject, IWKUrlSchemeHandler         {             [Export("webView:startURLSchemeTask:")]             [SupportedOSPlatform("ios11.0")]             public void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSchemeTask)             {                 if (urlSchemeTask.Request.Url == null)                 {                     return;                 }                  var path = urlSchemeTask.Request.Url?.Path ?? "";                 if (File.Exists(path))                 {                     byte[] bytes = File.ReadAllBytes(path);                     using var response = new NSHttpUrlResponse(urlSchemeTask.Request.Url, 200, "HTTP/1.1", null);                     urlSchemeTask.DidReceiveResponse(response);                     urlSchemeTask.DidReceiveData(NSData.FromArray(bytes));                     urlSchemeTask.DidFinish();                 }             }              [Export("webView:stopURLSchemeTask:")]             public void StopUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSchemeTask)             {             }         }     } }  

iOS / mac OS中不能拦截 http 和 https 协议,但是可以拦截自定义协议

所以我们这里添加一个自定义协议 myfile

(不能用file,因为已经被注册过了,被注册过的协议在这里是不能设置的)

实际上,iOS / mac OS中,页面的协议头也是自定义的 app协议,而不是像windows或Android中的https

MAUI Blazor 显示本地图片的新思路

例子中的路径也要处理一下

Pages/Index.razor

        var fileResult = await MediaPicker.Default.PickPhotoAsync();         var path = fileResult?.FullPath;          if (path is null)         {             return;         }  #if WINDOWS         path = Uri.EscapeDataString(path); #elif IOS || MACCATALYST         path = "myfile://" + path; #endif 

看一下效果

mac OS

MAUI Blazor 显示本地图片的新思路

iOS

MAUI Blazor 显示本地图片的新思路

mac上的浏览器开发者工具最近有bug,用不了,所以就没有开发者工具的截图了

cannot use developer tools to debug blazor hybrid MAUI application in Mac OS

后记

虽然已经基本实现了最开始的目标,不过受限于笔者水平,可能还是不够完美。

文章到这里就结束了,感谢你的阅读

源码地址

本文中的例子的源码放到 Github 和 Gitee 了

有需要的可以去看一下

Github: https://github.com/Yu-Core/MauiBlazorLocalImage

Gitee: https://gitee.com/Yu-core/MauiBlazorLocalImage

发表评论

评论已关闭。

相关文章