好久沒寫文章了,水一篇
關於MAUI Blazor 顯示本地圖片這個問題,有大佬發過了。
就是 token 大佬的那篇
Blazor Hybrid (Blazor混合開發)更好的讀取本地圖片
主要思路就是讀取本地圖片,通過C#與JS互操作,將byte[]傳給js,生成blob,圖片的src中填寫根據blob生成的url。
我之前一直使用這個辦法,簡單的優化了一下,無非也就是增加快取。
但是這種方法的弊端也是很明顯的:
img的src每一次並不固定,需要替換
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.cs
,MainPage.xaml.MaciOS.cs
,MainPage.xaml.Windows.cs
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)
{
}
}
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);
}
}
看一下效果
(下面偵錯工具這個截圖是後補的,所以路徑不一致,忽略這些細節)
由此可以看到,已經成功攔截,並且把響應換成了我們自己建立的。
換一張大一點的圖片,看看速度
特意選了一張13.28 MB的4k圖片,速度還可以
在之前那個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 WINDOWS
path = Uri.UnescapeDataString(path);
#endif
看一下效果
(這個截圖也是後補的,所以路徑不一致,忽略這些細節)
在這篇文章最開始寫的時候,筆者並沒有找到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
例子中的路徑也要處理一下
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
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