• 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结尾的文件),添加以下代码

    
    <ItemGroup Condition="$(TargetFramework.StartsWith('net7.0-android')) != true">
      <Compile Remove="**\**\*.Android.cs" />
      <None Include="**\**\*.Android.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
    ItemGroup>
    
    
    <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>
    
    
    <ItemGroup Condition="$(TargetFramework.StartsWith('net7.0-ios')) != true">
      <Compile Remove="**\**\*.iOS.cs" />
      <None Include="**\**\*.iOS.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
    ItemGroup>
    
    
    <ItemGroup Condition="$(TargetFramework.StartsWith('net7.0-maccatalyst')) != true">
      <Compile Remove="**\**\*.MacCatalyst.cs" />
      <None Include="**\**\*.MacCatalyst.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
    ItemGroup>
    
    
    <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

    image

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

    Hello, world!

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

    看一下效果

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

    image

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

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

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

    image

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

    看一下效果

    image

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

    image

    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

    image

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

    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

    image

    iOS

    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

  • 相关阅读:
    王道第二章算法部分代码总结-个人使用
    vs code 好用的插件
    nginx去除serve请求头 aarch64
    利用uni-app 开发的iOS app 发布到App Store全流程
    [附源码]Python计算机毕业设计Django家庭教育app
    使用 maven 自动将源码打包并发布
    GBase项目管理实践总结——项目立项前需要思考的关键问题
    insightface实战:画出嘴巴和眼睛的mask
    一级建造师从业者面试需要注意什么问题?
    36.cuBLAS开发指南中文版--cuBLAS中的Level-2函数hpmv()
  • 原文地址:https://www.cnblogs.com/Yu-Core/p/17571292.html