前言

上一篇文章 MAUI Blazor 显示本地图片的新思路 中, 提出了通过webview拦截,从而在前端中显示本地图片的思路。不过当时还不完善,随后也发现了很多问题。比如,

  1. 不同平台上的url不统一。这对于需要存储图片路径并且多端互通的需求来说,并不友好。至少 FileSystem.AppDataDirectoryFileSystem.CacheDirectory 下的文件生成的url应该统一。
  2. 音频文件和视频文件无法使用。理论上可以用于各种文件,但是音频和视频不能播放,应该是需要相应的处理
  3. Windows上有限制。大于9~10M的图片不显示
  4. iOS/ Mac有跨域问题。尤其是调用用于截图的js库,图片会由于跨域不出现在截图中

所以,在这篇文章中,对这个思路进行完善,使之成为一个可行的方案。

例如 <img src='appdata/Image/image1.jpg' > 会显示 FileSystem.AppDataDirectory 文件夹下的 Image 文件夹下的 image1.jpg 这个图片

<video src='cache/Video/video1.mp4' controls > 会播放 FileSystem.CacheDirectory 文件夹下的 Video 文件夹下的 video1.mp4 这个视频

对于其他路径的文件来说,url设为 file/ 加上转义后的完整路径

正文

准备工作

新建一个MAUI Blazor项目

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

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

添加一个处理ContentType的静态类

用来获取文件的ContentType

没找到什么太好的方法,偶然看到Maui的源码中的一段,还不错,不过是internal修饰的,就直接抄来了

新建Utilities/MimeType文件夹,在里面添加 StaticContentProvider.cs

代码如下:

#nullable disable
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Diagnostics.CodeAnalysis; namespace MauiBlazorLocalMediaFile.Utilities
{
internal partial class StaticContentProvider
{
private static readonly FileExtensionContentTypeProvider ContentTypeProvider = new(); internal static string GetResponseContentTypeOrDefault(string path)
=> ContentTypeProvider.TryGetContentType(path, out var matchedContentType)
? matchedContentType
: "application/octet-stream"; internal static IDictionary<string, string> GetResponseHeaders(string contentType)
=> new Dictionary<string, string>(StringComparer.Ordinal)
{
{ "Content-Type", contentType },
{ "Cache-Control", "no-cache, max-age=0, must-revalidate, no-store" },
}; internal class FileExtensionContentTypeProvider
{
// Notes:
// - This table was initially copied from IIS and has many legacy entries we will maintain for backwards compatibility.
// - We only plan to add new entries where we expect them to be applicable to a majority of developers such as being
// used in the project templates.
#region Extension mapping table
/// <summary>
/// Creates a new provider with a set of default mappings.
/// </summary>
public FileExtensionContentTypeProvider()
: this(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ ".323", "text/h323" },
{ ".3g2", "video/3gpp2" },
{ ".3gp2", "video/3gpp2" },
{ ".3gp", "video/3gpp" },
{ ".3gpp", "video/3gpp" },
{ ".aac", "audio/aac" },
{ ".aaf", "application/octet-stream" },
{ ".aca", "application/octet-stream" },
{ ".accdb", "application/msaccess" },
{ ".accde", "application/msaccess" },
{ ".accdt", "application/msaccess" },
{ ".acx", "application/internet-property-stream" },
{ ".adt", "audio/vnd.dlna.adts" },
{ ".adts", "audio/vnd.dlna.adts" },
{ ".afm", "application/octet-stream" },
{ ".ai", "application/postscript" },
{ ".aif", "audio/x-aiff" },
{ ".aifc", "audio/aiff" },
{ ".aiff", "audio/aiff" },
{ ".appcache", "text/cache-manifest" },
{ ".application", "application/x-ms-application" },
{ ".art", "image/x-jg" },
{ ".asd", "application/octet-stream" },
{ ".asf", "video/x-ms-asf" },
{ ".asi", "application/octet-stream" },
{ ".asm", "text/plain" },
{ ".asr", "video/x-ms-asf" },
{ ".asx", "video/x-ms-asf" },
{ ".atom", "application/atom+xml" },
{ ".au", "audio/basic" },
{ ".avi", "video/x-msvideo" },
{ ".axs", "application/olescript" },
{ ".bas", "text/plain" },
{ ".bcpio", "application/x-bcpio" },
{ ".bin", "application/octet-stream" },
{ ".bmp", "image/bmp" },
{ ".c", "text/plain" },
{ ".cab", "application/vnd.ms-cab-compressed" },
{ ".calx", "application/vnd.ms-office.calx" },
{ ".cat", "application/vnd.ms-pki.seccat" },
{ ".cdf", "application/x-cdf" },
{ ".chm", "application/octet-stream" },
{ ".class", "application/x-java-applet" },
{ ".clp", "application/x-msclip" },
{ ".cmx", "image/x-cmx" },
{ ".cnf", "text/plain" },
{ ".cod", "image/cis-cod" },
{ ".cpio", "application/x-cpio" },
{ ".cpp", "text/plain" },
{ ".crd", "application/x-mscardfile" },
{ ".crl", "application/pkix-crl" },
{ ".crt", "application/x-x509-ca-cert" },
{ ".csh", "application/x-csh" },
{ ".css", "text/css" },
{ ".csv", "text/csv" }, // https://tools.ietf.org/html/rfc7111#section-5.1
{ ".cur", "application/octet-stream" },
{ ".dcr", "application/x-director" },
{ ".deploy", "application/octet-stream" },
{ ".der", "application/x-x509-ca-cert" },
{ ".dib", "image/bmp" },
{ ".dir", "application/x-director" },
{ ".disco", "text/xml" },
{ ".dlm", "text/dlm" },
{ ".doc", "application/msword" },
{ ".docm", "application/vnd.ms-word.document.macroEnabled.12" },
{ ".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" },
{ ".dot", "application/msword" },
{ ".dotm", "application/vnd.ms-word.template.macroEnabled.12" },
{ ".dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template" },
{ ".dsp", "application/octet-stream" },
{ ".dtd", "text/xml" },
{ ".dvi", "application/x-dvi" },
{ ".dvr-ms", "video/x-ms-dvr" },
{ ".dwf", "drawing/x-dwf" },
{ ".dwp", "application/octet-stream" },
{ ".dxr", "application/x-director" },
{ ".eml", "message/rfc822" },
{ ".emz", "application/octet-stream" },
{ ".eot", "application/vnd.ms-fontobject" },
{ ".eps", "application/postscript" },
{ ".etx", "text/x-setext" },
{ ".evy", "application/envoy" },
{ ".exe", "application/vnd.microsoft.portable-executable" }, // https://www.iana.org/assignments/media-types/application/vnd.microsoft.portable-executable
{ ".fdf", "application/vnd.fdf" },
{ ".fif", "application/fractals" },
{ ".fla", "application/octet-stream" },
{ ".flr", "x-world/x-vrml" },
{ ".flv", "video/x-flv" },
{ ".gif", "image/gif" },
{ ".gtar", "application/x-gtar" },
{ ".gz", "application/x-gzip" },
{ ".h", "text/plain" },
{ ".hdf", "application/x-hdf" },
{ ".hdml", "text/x-hdml" },
{ ".hhc", "application/x-oleobject" },
{ ".hhk", "application/octet-stream" },
{ ".hhp", "application/octet-stream" },
{ ".hlp", "application/winhlp" },
{ ".hqx", "application/mac-binhex40" },
{ ".hta", "application/hta" },
{ ".htc", "text/x-component" },
{ ".htm", "text/html" },
{ ".html", "text/html" },
{ ".htt", "text/webviewhtml" },
{ ".hxt", "text/html" },
{ ".ical", "text/calendar" },
{ ".icalendar", "text/calendar" },
{ ".ico", "image/x-icon" },
{ ".ics", "text/calendar" },
{ ".ief", "image/ief" },
{ ".ifb", "text/calendar" },
{ ".iii", "application/x-iphone" },
{ ".inf", "application/octet-stream" },
{ ".ins", "application/x-internet-signup" },
{ ".isp", "application/x-internet-signup" },
{ ".IVF", "video/x-ivf" },
{ ".jar", "application/java-archive" },
{ ".java", "application/octet-stream" },
{ ".jck", "application/liquidmotion" },
{ ".jcz", "application/liquidmotion" },
{ ".jfif", "image/pjpeg" },
{ ".jpb", "application/octet-stream" },
{ ".jpe", "image/jpeg" },
{ ".jpeg", "image/jpeg" },
{ ".jpg", "image/jpeg" },
{ ".js", "application/javascript" },
{ ".json", "application/json" },
{ ".jsx", "text/jscript" },
{ ".latex", "application/x-latex" },
{ ".lit", "application/x-ms-reader" },
{ ".lpk", "application/octet-stream" },
{ ".lsf", "video/x-la-asf" },
{ ".lsx", "video/x-la-asf" },
{ ".lzh", "application/octet-stream" },
{ ".m13", "application/x-msmediaview" },
{ ".m14", "application/x-msmediaview" },
{ ".m1v", "video/mpeg" },
{ ".m2ts", "video/vnd.dlna.mpeg-tts" },
{ ".m3u", "audio/x-mpegurl" },
{ ".m4a", "audio/mp4" },
{ ".m4v", "video/mp4" },
{ ".man", "application/x-troff-man" },
{ ".manifest", "application/x-ms-manifest" },
{ ".map", "text/plain" },
{ ".markdown", "text/markdown" },
{ ".md", "text/markdown" },
{ ".mdb", "application/x-msaccess" },
{ ".mdp", "application/octet-stream" },
{ ".me", "application/x-troff-me" },
{ ".mht", "message/rfc822" },
{ ".mhtml", "message/rfc822" },
{ ".mid", "audio/mid" },
{ ".midi", "audio/mid" },
{ ".mix", "application/octet-stream" },
{ ".mmf", "application/x-smaf" },
{ ".mno", "text/xml" },
{ ".mny", "application/x-msmoney" },
{ ".mov", "video/quicktime" },
{ ".movie", "video/x-sgi-movie" },
{ ".mp2", "video/mpeg" },
{ ".mp3", "audio/mpeg" },
{ ".mp4", "video/mp4" },
{ ".mp4v", "video/mp4" },
{ ".mpa", "video/mpeg" },
{ ".mpe", "video/mpeg" },
{ ".mpeg", "video/mpeg" },
{ ".mpg", "video/mpeg" },
{ ".mpp", "application/vnd.ms-project" },
{ ".mpv2", "video/mpeg" },
{ ".ms", "application/x-troff-ms" },
{ ".msi", "application/octet-stream" },
{ ".mso", "application/octet-stream" },
{ ".mvb", "application/x-msmediaview" },
{ ".mvc", "application/x-miva-compiled" },
{ ".nc", "application/x-netcdf" },
{ ".nsc", "video/x-ms-asf" },
{ ".nws", "message/rfc822" },
{ ".ocx", "application/octet-stream" },
{ ".oda", "application/oda" },
{ ".odc", "text/x-ms-odc" },
{ ".ods", "application/oleobject" },
{ ".oga", "audio/ogg" },
{ ".ogg", "video/ogg" },
{ ".ogv", "video/ogg" },
{ ".ogx", "application/ogg" },
{ ".one", "application/onenote" },
{ ".onea", "application/onenote" },
{ ".onetoc", "application/onenote" },
{ ".onetoc2", "application/onenote" },
{ ".onetmp", "application/onenote" },
{ ".onepkg", "application/onenote" },
{ ".osdx", "application/opensearchdescription+xml" },
{ ".otf", "font/otf" },
{ ".p10", "application/pkcs10" },
{ ".p12", "application/x-pkcs12" },
{ ".p7b", "application/x-pkcs7-certificates" },
{ ".p7c", "application/pkcs7-mime" },
{ ".p7m", "application/pkcs7-mime" },
{ ".p7r", "application/x-pkcs7-certreqresp" },
{ ".p7s", "application/pkcs7-signature" },
{ ".pbm", "image/x-portable-bitmap" },
{ ".pcx", "application/octet-stream" },
{ ".pcz", "application/octet-stream" },
{ ".pdf", "application/pdf" },
{ ".pfb", "application/octet-stream" },
{ ".pfm", "application/octet-stream" },
{ ".pfx", "application/x-pkcs12" },
{ ".pgm", "image/x-portable-graymap" },
{ ".pko", "application/vnd.ms-pki.pko" },
{ ".pma", "application/x-perfmon" },
{ ".pmc", "application/x-perfmon" },
{ ".pml", "application/x-perfmon" },
{ ".pmr", "application/x-perfmon" },
{ ".pmw", "application/x-perfmon" },
{ ".png", "image/png" },
{ ".pnm", "image/x-portable-anymap" },
{ ".pnz", "image/png" },
{ ".pot", "application/vnd.ms-powerpoint" },
{ ".potm", "application/vnd.ms-powerpoint.template.macroEnabled.12" },
{ ".potx", "application/vnd.openxmlformats-officedocument.presentationml.template" },
{ ".ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12" },
{ ".ppm", "image/x-portable-pixmap" },
{ ".pps", "application/vnd.ms-powerpoint" },
{ ".ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12" },
{ ".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow" },
{ ".ppt", "application/vnd.ms-powerpoint" },
{ ".pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12" },
{ ".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" },
{ ".prf", "application/pics-rules" },
{ ".prm", "application/octet-stream" },
{ ".prx", "application/octet-stream" },
{ ".ps", "application/postscript" },
{ ".psd", "application/octet-stream" },
{ ".psm", "application/octet-stream" },
{ ".psp", "application/octet-stream" },
{ ".pub", "application/x-mspublisher" },
{ ".qt", "video/quicktime" },
{ ".qtl", "application/x-quicktimeplayer" },
{ ".qxd", "application/octet-stream" },
{ ".ra", "audio/x-pn-realaudio" },
{ ".ram", "audio/x-pn-realaudio" },
{ ".rar", "application/octet-stream" },
{ ".ras", "image/x-cmu-raster" },
{ ".rf", "image/vnd.rn-realflash" },
{ ".rgb", "image/x-rgb" },
{ ".rm", "application/vnd.rn-realmedia" },
{ ".rmi", "audio/mid" },
{ ".roff", "application/x-troff" },
{ ".rpm", "audio/x-pn-realaudio-plugin" },
{ ".rtf", "application/rtf" },
{ ".rtx", "text/richtext" },
{ ".scd", "application/x-msschedule" },
{ ".sct", "text/scriptlet" },
{ ".sea", "application/octet-stream" },
{ ".setpay", "application/set-payment-initiation" },
{ ".setreg", "application/set-registration-initiation" },
{ ".sgml", "text/sgml" },
{ ".sh", "application/x-sh" },
{ ".shar", "application/x-shar" },
{ ".sit", "application/x-stuffit" },
{ ".sldm", "application/vnd.ms-powerpoint.slide.macroEnabled.12" },
{ ".sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide" },
{ ".smd", "audio/x-smd" },
{ ".smi", "application/octet-stream" },
{ ".smx", "audio/x-smd" },
{ ".smz", "audio/x-smd" },
{ ".snd", "audio/basic" },
{ ".snp", "application/octet-stream" },
{ ".spc", "application/x-pkcs7-certificates" },
{ ".spl", "application/futuresplash" },
{ ".spx", "audio/ogg" },
{ ".src", "application/x-wais-source" },
{ ".ssm", "application/streamingmedia" },
{ ".sst", "application/vnd.ms-pki.certstore" },
{ ".stl", "application/vnd.ms-pki.stl" },
{ ".sv4cpio", "application/x-sv4cpio" },
{ ".sv4crc", "application/x-sv4crc" },
{ ".svg", "image/svg+xml" },
{ ".svgz", "image/svg+xml" },
{ ".swf", "application/x-shockwave-flash" },
{ ".t", "application/x-troff" },
{ ".tar", "application/x-tar" },
{ ".tcl", "application/x-tcl" },
{ ".tex", "application/x-tex" },
{ ".texi", "application/x-texinfo" },
{ ".texinfo", "application/x-texinfo" },
{ ".tgz", "application/x-compressed" },
{ ".thmx", "application/vnd.ms-officetheme" },
{ ".thn", "application/octet-stream" },
{ ".tif", "image/tiff" },
{ ".tiff", "image/tiff" },
{ ".toc", "application/octet-stream" },
{ ".tr", "application/x-troff" },
{ ".trm", "application/x-msterminal" },
{ ".ts", "video/vnd.dlna.mpeg-tts" },
{ ".tsv", "text/tab-separated-values" },
{ ".ttc", "application/x-font-ttf" },
{ ".ttf", "application/x-font-ttf" },
{ ".tts", "video/vnd.dlna.mpeg-tts" },
{ ".txt", "text/plain" },
{ ".u32", "application/octet-stream" },
{ ".uls", "text/iuls" },
{ ".ustar", "application/x-ustar" },
{ ".vbs", "text/vbscript" },
{ ".vcf", "text/x-vcard" },
{ ".vcs", "text/plain" },
{ ".vdx", "application/vnd.ms-visio.viewer" },
{ ".vml", "text/xml" },
{ ".vsd", "application/vnd.visio" },
{ ".vss", "application/vnd.visio" },
{ ".vst", "application/vnd.visio" },
{ ".vsto", "application/x-ms-vsto" },
{ ".vsw", "application/vnd.visio" },
{ ".vsx", "application/vnd.visio" },
{ ".vtx", "application/vnd.visio" },
{ ".wasm", "application/wasm" },
{ ".wav", "audio/wav" },
{ ".wax", "audio/x-ms-wax" },
{ ".wbmp", "image/vnd.wap.wbmp" },
{ ".wcm", "application/vnd.ms-works" },
{ ".wdb", "application/vnd.ms-works" },
{ ".webm", "video/webm" },
{ ".webmanifest", "application/manifest+json" }, // https://w3c.github.io/manifest/#media-type-registration
{ ".webp", "image/webp" },
{ ".wks", "application/vnd.ms-works" },
{ ".wm", "video/x-ms-wm" },
{ ".wma", "audio/x-ms-wma" },
{ ".wmd", "application/x-ms-wmd" },
{ ".wmf", "application/x-msmetafile" },
{ ".wml", "text/vnd.wap.wml" },
{ ".wmlc", "application/vnd.wap.wmlc" },
{ ".wmls", "text/vnd.wap.wmlscript" },
{ ".wmlsc", "application/vnd.wap.wmlscriptc" },
{ ".wmp", "video/x-ms-wmp" },
{ ".wmv", "video/x-ms-wmv" },
{ ".wmx", "video/x-ms-wmx" },
{ ".wmz", "application/x-ms-wmz" },
{ ".woff", "application/font-woff" }, // https://www.w3.org/TR/WOFF/#appendix-b
{ ".woff2", "font/woff2" }, // https://www.w3.org/TR/WOFF2/#IMT
{ ".wps", "application/vnd.ms-works" },
{ ".wri", "application/x-mswrite" },
{ ".wrl", "x-world/x-vrml" },
{ ".wrz", "x-world/x-vrml" },
{ ".wsdl", "text/xml" },
{ ".wtv", "video/x-ms-wtv" },
{ ".wvx", "video/x-ms-wvx" },
{ ".x", "application/directx" },
{ ".xaf", "x-world/x-vrml" },
{ ".xaml", "application/xaml+xml" },
{ ".xap", "application/x-silverlight-app" },
{ ".xbap", "application/x-ms-xbap" },
{ ".xbm", "image/x-xbitmap" },
{ ".xdr", "text/plain" },
{ ".xht", "application/xhtml+xml" },
{ ".xhtml", "application/xhtml+xml" },
{ ".xla", "application/vnd.ms-excel" },
{ ".xlam", "application/vnd.ms-excel.addin.macroEnabled.12" },
{ ".xlc", "application/vnd.ms-excel" },
{ ".xlm", "application/vnd.ms-excel" },
{ ".xls", "application/vnd.ms-excel" },
{ ".xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12" },
{ ".xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12" },
{ ".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" },
{ ".xlt", "application/vnd.ms-excel" },
{ ".xltm", "application/vnd.ms-excel.template.macroEnabled.12" },
{ ".xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template" },
{ ".xlw", "application/vnd.ms-excel" },
{ ".xml", "text/xml" },
{ ".xof", "x-world/x-vrml" },
{ ".xpm", "image/x-xpixmap" },
{ ".xps", "application/vnd.ms-xpsdocument" },
{ ".xsd", "text/xml" },
{ ".xsf", "text/xml" },
{ ".xsl", "text/xml" },
{ ".xslt", "text/xml" },
{ ".xsn", "application/octet-stream" },
{ ".xtp", "application/octet-stream" },
{ ".xwd", "image/x-xwindowdump" },
{ ".z", "application/x-compress" },
{ ".zip", "application/x-zip-compressed" },
})
{
}
#endregion /// <summary>
/// Creates a lookup engine using the provided mapping.
/// It is recommended that the IDictionary instance use StringComparer.OrdinalIgnoreCase.
/// </summary>
/// <param name="mapping"></param>
public FileExtensionContentTypeProvider(IDictionary<string, string> mapping)
{
if (mapping == null)
{
throw new ArgumentNullException(nameof(mapping));
}
Mappings = mapping;
} /// <summary>
/// The cross reference table of file extensions and content-types.
/// </summary>
public IDictionary<string, string> Mappings { get; private set; } /// <summary>
/// Given a file path, determine the MIME type
/// </summary>
/// <param name="subpath">A file path</param>
/// <param name="contentType">The resulting MIME type</param>
/// <returns>True if MIME type could be determined</returns>
public bool TryGetContentType(string subpath, [MaybeNullWhen(false)] out string contentType)
{
var extension = GetExtension(subpath);
if (extension == null)
{
contentType = null;
return false;
}
return Mappings.TryGetValue(extension, out contentType);
} private static string GetExtension(string path)
{
// Don't use Path.GetExtension as that may throw an exception if there are
// invalid characters in the path. Invalid characters should be handled
// by the FileProviders if (string.IsNullOrWhiteSpace(path))
{
return null;
} int index = path.LastIndexOf('.');
if (index < 0)
{
return null;
} return path.Substring(index);
}
}
}
}

创建自定义的BlazorWebViewHandler类

BlazorWebViewHandler是MAUI Blazor中处理BlazorWebView相关的一个类,我们自定义一个类替换它,添加自己需要的一些处理逻辑

Maui Blazor中iOS / Mac和其他平台的baseUrl是不统一的,iOS / Mac是 app://0.0.0.0 (原因在Maui源码的注释中有写到,iOS WKWebView doesn't allow handling 'http'/'https' schemes, so we use the fake 'app' scheme),其他平台是 https://0.0.0.0 ,所以我们的url设为相对路径才能统一,而且与页面同源,不会有跨域问题(笔者之前在iOS / Mac上的做法就是注册自定义协议,使用html2canvas截图时,结果发生了跨域问题)。

添加MauiBlazorWebViewHandler.cs

代码如下

using Microsoft.AspNetCore.Components.WebView.Maui;

namespace MauiBlazorLocalMediaFile
{
public partial class MauiBlazorWebViewHandler : BlazorWebViewHandler
{
private const string AppHostAddress = "0.0.0.0";
#if IOS || MACCATALYST
public const string BaseUri = $"app://{AppHostAddress}/";
#else
public const string BaseUri = $"https://{AppHostAddress}/";
#endif
public readonly static Dictionary<string, string> AppFilePathMap = new()
{
{ FileSystem.AppDataDirectory, "appdata" },
{ FileSystem.CacheDirectory, "cache" },
}; private static readonly string OtherFileMapPath = "file"; //把真实的文件路径转化为url相对路径
public static string FilePathToUrlRelativePath(string filePath)
{
foreach (var item in AppFilePathMap)
{
if (filePath.StartsWith(item.Key))
{
return item.Value + filePath[item.Key.Length..].Replace(Path.DirectorySeparatorChar, '/');
}
} return OtherFileMapPath + "/" + Uri.EscapeDataString(filePath);
} //把url相对路径转化为真实的文件路径
public static string UrlRelativePathToFilePath(string urlRelativePath)
{
UrlRelativePathToFilePath(urlRelativePath, out string path);
return path;
} private static bool Intercept(string uri, out string path)
{
if (!uri.StartsWith(BaseUri))
{
path = string.Empty;
return false;
} var urlRelativePath = uri[BaseUri.Length..];
return UrlRelativePathToFilePath(urlRelativePath, out path);
} private static bool UrlRelativePathToFilePath(string urlRelativePath, out string path)
{
if (string.IsNullOrEmpty(urlRelativePath))
{
path = string.Empty;
return false;
} urlRelativePath = Uri.UnescapeDataString(urlRelativePath); foreach (var item in AppFilePathMap)
{
if (urlRelativePath.StartsWith(item.Value + '/'))
{
string urlRelativePathSub = urlRelativePath[(item.Value.Length + 1)..];
path = Path.Combine(item.Key, urlRelativePathSub.Replace('/', Path.DirectorySeparatorChar));
if (File.Exists(path))
{
return true;
}
}
} if (urlRelativePath.StartsWith(OtherFileMapPath + '/'))
{
string urlRelativePathSub = urlRelativePath[(OtherFileMapPath.Length + 1)..];
path = urlRelativePathSub.Replace('/', Path.DirectorySeparatorChar);
if (File.Exists(path))
{
return true;
}
} path = string.Empty;
return false;
}
}
}
Android

添加MauiBlazorWebViewHandler.Android.cs

代码如下

using Android.Webkit;
using MauiBlazorLocalMediaFile.Utilities;
using WebView = Android.Webkit.WebView; namespace MauiBlazorLocalMediaFile
{
public partial class MauiBlazorWebViewHandler
{
#pragma warning disable CA1416 // 验证平台兼容性
protected override void ConnectHandler(WebView platformView)
{
base.ConnectHandler(platformView);
platformView.SetWebViewClient(new MyWebViewClient(platformView.WebViewClient));
} #nullable disable
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 intercept = InterceptCustomPathRequest(request, out WebResourceResponse webResourceResponse);
if (intercept)
{
return webResourceResponse;
} return WebViewClient.ShouldInterceptRequest(view, request);
} 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();
} private static bool InterceptCustomPathRequest(IWebResourceRequest request, out WebResourceResponse webResourceResponse)
{
webResourceResponse = null; var uri = request.Url.ToString();
if (!Intercept(uri, out string path))
{
return false;
} if (!File.Exists(path))
{
return false;
} webResourceResponse = CreateWebResourceResponse(request, path);
return true;
} private static WebResourceResponse CreateWebResourceResponse(IWebResourceRequest request, string path)
{
string contentType = StaticContentProvider.GetResponseContentTypeOrDefault(path);
var headers = StaticContentProvider.GetResponseHeaders(contentType);
FileStream stream = File.OpenRead(path);
var length = stream.Length;
long rangeStart = 0;
long rangeEnd = length - 1; string encoding = "UTF-8";
int stateCode = 200;
string reasonPhrase = "OK"; //适用于音频视频文件资源的响应
bool partial = request.RequestHeaders.TryGetValue("Range", out string rangeString);
if (partial)
{
//206,可断点续传
stateCode = 206;
reasonPhrase = "Partial Content"; var ranges = rangeString.Split('=');
if (ranges.Length > 1 && !string.IsNullOrEmpty(ranges[1]))
{
string[] rangeDatas = ranges[1].Split("-");
rangeStart = Convert.ToInt64(rangeDatas[0]);
if (rangeDatas.Length > 1 && !string.IsNullOrEmpty(rangeDatas[1]))
{
rangeEnd = Convert.ToInt64(rangeDatas[1]);
}
} headers.Add("Accept-Ranges", "bytes");
headers.Add("Content-Range", $"bytes {rangeStart}-{rangeEnd}/{length}");
} //这一行删去似乎也不影响
headers.Add("Content-Length", (rangeEnd - rangeStart + 1).ToString()); var response = new WebResourceResponse(contentType, encoding, stateCode, reasonPhrase, headers, stream);
return response;
} }
}
}
iOS / Mac

iOS / Mac中我们要替换Maui对于app://自定义协议的注册,但是很多类、方法、属性、字段是不公开的,也就是internal和private,所以我们的代码用了很多反射。

添加 MauiBlazorWebViewHandler.MaciOS.cs

代码如下

using Foundation;
using Microsoft.AspNetCore.Components.WebView;
using Microsoft.AspNetCore.Components.WebView.Maui;
using Microsoft.Extensions.Logging;
using MauiBlazorLocalMediaFile.Utilities;
using System.Globalization;
using System.Reflection;
using System.Runtime.Versioning;
using UIKit;
using WebKit;
using RectangleF = CoreGraphics.CGRect; namespace MauiBlazorLocalMediaFile
{
#nullable disable
public partial class MauiBlazorWebViewHandler
{
private BlazorWebViewHandlerReflection _base; private BlazorWebViewHandlerReflection Base => _base ??= new(this); [SupportedOSPlatform("ios11.0")]
protected override WKWebView CreatePlatformView()
{
Base.LoggerCreatingWebKitWKWebView(); var config = new WKWebViewConfiguration(); // By default, setting inline media playback to allowed, including autoplay
// and picture in picture, since these things MUST be set during the webview
// creation, and have no effect if set afterwards.
// A custom handler factory delegate could be set to disable these defaults
// but if we do not set them here, they cannot be changed once the
// handler's platform view is created, so erring on the side of wanting this
// capability by default.
if (OperatingSystem.IsMacCatalystVersionAtLeast(10) || OperatingSystem.IsIOSVersionAtLeast(10))
{
config.AllowsPictureInPictureMediaPlayback = true;
config.AllowsInlineMediaPlayback = true;
config.MediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypes.None;
} VirtualView.BlazorWebViewInitializing(new BlazorWebViewInitializingEventArgs()
{
Configuration = config
}); // Legacy Developer Extras setting.
config.Preferences.SetValueForKey(NSObject.FromObject(Base.DeveloperToolsEnabled), new NSString("developerExtrasEnabled")); config.UserContentController.AddScriptMessageHandler(Base.CreateWebViewScriptMessageHandler(), "webwindowinterop");
config.UserContentController.AddUserScript(new WKUserScript(
new NSString(Base.BlazorInitScript), WKUserScriptInjectionTime.AtDocumentEnd, true)); // iOS WKWebView doesn't allow handling 'http'/'https' schemes, so we use the fake 'app' scheme
config.SetUrlSchemeHandler(new SchemeHandler(this), urlScheme: "app"); var webview = new WKWebView(RectangleF.Empty, config)
{
BackgroundColor = UIColor.Clear,
AutosizesSubviews = true
}; if (OperatingSystem.IsIOSVersionAtLeast(16, 4) || OperatingSystem.IsMacCatalystVersionAtLeast(13, 3))
{
// Enable Developer Extras for Catalyst/iOS builds for 16.4+
webview.SetValueForKey(NSObject.FromObject(Base.DeveloperToolsEnabled), new NSString("inspectable"));
} VirtualView.BlazorWebViewInitialized(Base.CreateBlazorWebViewInitializedEventArgs(webview)); Base.LoggerCreatedWebKitWKWebView(); return webview;
} private class SchemeHandler : NSObject, IWKUrlSchemeHandler
{
private readonly MauiBlazorWebViewHandler _webViewHandler; public SchemeHandler(MauiBlazorWebViewHandler webViewHandler)
{
_webViewHandler = webViewHandler;
} [Export("webView:startURLSchemeTask:")]
[SupportedOSPlatform("ios11.0")]
public void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSchemeTask)
{
var intercept = InterceptCustomPathRequest(urlSchemeTask);
if (intercept)
{
return;
} var responseBytes = GetResponseBytes(urlSchemeTask.Request.Url?.AbsoluteString ?? "", out var contentType, statusCode: out var statusCode);
if (statusCode == 200)
{
using (var dic = new NSMutableDictionary<NSString, NSString>())
{
dic.Add((NSString)"Content-Length", (NSString)(responseBytes.Length.ToString(CultureInfo.InvariantCulture)));
dic.Add((NSString)"Content-Type", (NSString)contentType);
// Disable local caching. This will prevent user scripts from executing correctly.
dic.Add((NSString)"Cache-Control", (NSString)"no-cache, max-age=0, must-revalidate, no-store");
if (urlSchemeTask.Request.Url != null)
{
using var response = new NSHttpUrlResponse(urlSchemeTask.Request.Url, statusCode, "HTTP/1.1", dic);
urlSchemeTask.DidReceiveResponse(response);
} }
urlSchemeTask.DidReceiveData(NSData.FromArray(responseBytes));
urlSchemeTask.DidFinish();
}
} private byte[] GetResponseBytes(string? url, out string contentType, out int statusCode)
{
var allowFallbackOnHostPage = _webViewHandler.Base.IsBaseOfPage(_webViewHandler.Base.AppOriginUri, url);
url = _webViewHandler.Base.QueryStringHelperRemovePossibleQueryString(url); _webViewHandler.Base.LoggerHandlingWebRequest(url); if (_webViewHandler.Base.TryGetResponseContentInternal(url, allowFallbackOnHostPage, out statusCode, out var statusMessage, out var content, out var headers))
{
statusCode = 200;
using var ms = new MemoryStream(); content.CopyTo(ms);
content.Dispose(); contentType = headers["Content-Type"]; _webViewHandler?.Base.LoggerResponseContentBeingSent(url, statusCode); return ms.ToArray();
}
else
{
_webViewHandler?.Base.LoggerReponseContentNotFound(url); statusCode = 404;
contentType = string.Empty;
return Array.Empty<byte>();
}
} [Export("webView:stopURLSchemeTask:")]
public void StopUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSchemeTask)
{
} private static bool InterceptCustomPathRequest(IWKUrlSchemeTask urlSchemeTask)
{
var uri = urlSchemeTask.Request.Url.ToString();
if (uri == null)
{
return false;
} if (!Intercept(uri, out string path))
{
return false;
} if (!File.Exists(path))
{
return false;
} long length = new FileInfo(path).Length;
string contentType = StaticContentProvider.GetResponseContentTypeOrDefault(path);
using (var dic = new NSMutableDictionary<NSString, NSString>())
{
dic.Add((NSString)"Content-Length", (NSString)(length.ToString(CultureInfo.InvariantCulture)));
dic.Add((NSString)"Content-Type", (NSString)contentType);
// Disable local caching. This will prevent user scripts from executing correctly.
dic.Add((NSString)"Cache-Control", (NSString)"no-cache, max-age=0, must-revalidate, no-store");
using var response = new NSHttpUrlResponse(urlSchemeTask.Request.Url, 200, "HTTP/1.1", dic);
urlSchemeTask.DidReceiveResponse(response);
} urlSchemeTask.DidReceiveData(NSData.FromFile(path));
urlSchemeTask.DidFinish();
return true;
}
}
} public class BlazorWebViewHandlerReflection
{
public BlazorWebViewHandlerReflection(BlazorWebViewHandler blazorWebViewHandler)
{
_blazorWebViewHandler = blazorWebViewHandler;
_logger = new(() =>
{
var property = Type.GetProperty("Logger", BindingFlags.NonPublic | BindingFlags.Instance);
return (ILogger)property?.GetValue(_blazorWebViewHandler);
});
_blazorInitScript = new(() =>
{
var property = Type.GetField("BlazorInitScript", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
return (string)property?.GetValue(_blazorWebViewHandler);
});
_appOriginUri = new(() =>
{
var property = Type.GetField("AppOriginUri", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
return (Uri)property?.GetValue(_blazorWebViewHandler);
});
} private readonly BlazorWebViewHandler _blazorWebViewHandler; private static readonly Type Type = typeof(BlazorWebViewHandler); private static readonly Assembly Assembly = Type.Assembly; private static readonly Type TypeLog = Assembly.GetType("Microsoft.AspNetCore.Components.WebView.Log")!; private readonly Lazy<ILogger> _logger; private readonly Lazy<string> _blazorInitScript; private readonly Lazy<Uri> _appOriginUri; private object WebviewManager; private MethodInfo MethodTryGetResponseContentInternal; private MethodInfo MethodIsBaseOfPage; private MethodInfo MethodQueryStringHelperRemovePossibleQueryString; public ILogger Logger => _logger.Value; public string BlazorInitScript => _blazorInitScript.Value; public Uri AppOriginUri => _appOriginUri.Value; public bool DeveloperToolsEnabled => GetDeveloperToolsEnabled(); public void LoggerCreatingWebKitWKWebView()
{
var method = TypeLog.GetMethod("CreatingWebKitWKWebView");
method?.Invoke(null, new object[] { Logger });
} public void LoggerCreatedWebKitWKWebView()
{
var method = TypeLog.GetMethod("CreatedWebKitWKWebView");
method?.Invoke(null, new object[] { Logger });
} public void LoggerHandlingWebRequest(string url)
{
var method = TypeLog.GetMethod("HandlingWebRequest");
method?.Invoke(null, new object[] { Logger, url });
} public void LoggerResponseContentBeingSent(string url, int statusCode)
{
var method = TypeLog.GetMethod("ResponseContentBeingSent");
method?.Invoke(null, new object[] { Logger, url, statusCode });
} public void LoggerReponseContentNotFound(string url)
{
var method = TypeLog.GetMethod("ReponseContentNotFound");
method?.Invoke(null, new object[] { Logger, url });
} private bool GetDeveloperToolsEnabled()
{
var PropertyDeveloperTools = Type.GetProperty("DeveloperTools", BindingFlags.NonPublic | BindingFlags.Instance);
var DeveloperTools = PropertyDeveloperTools.GetValue(_blazorWebViewHandler); var type = DeveloperTools.GetType();
var Enabled = type.GetProperty("Enabled", BindingFlags.Public | BindingFlags.Instance);
return (bool)Enabled?.GetValue(DeveloperTools);
} public IWKScriptMessageHandler CreateWebViewScriptMessageHandler()
{
Type webViewScriptMessageHandlerType = Type.GetNestedType("WebViewScriptMessageHandler", BindingFlags.NonPublic); if (webViewScriptMessageHandlerType != null)
{
// 获取 MessageReceived 方法信息
MethodInfo messageReceivedMethod = Type.GetMethod("MessageReceived", BindingFlags.Instance | BindingFlags.NonPublic); if (messageReceivedMethod != null)
{
// 创建 WebViewScriptMessageHandler 实例
object webViewScriptMessageHandlerInstance = Activator.CreateInstance(webViewScriptMessageHandlerType, new object[] { Delegate.CreateDelegate(typeof(Action<Uri, string>), _blazorWebViewHandler, messageReceivedMethod) });
return (IWKScriptMessageHandler)webViewScriptMessageHandlerInstance;
}
} return null;
} public BlazorWebViewInitializedEventArgs CreateBlazorWebViewInitializedEventArgs(WKWebView wKWebView)
{
var blazorWebViewInitializedEventArgs = new BlazorWebViewInitializedEventArgs();
PropertyInfo property = typeof(BlazorWebViewInitializedEventArgs).GetProperty("WebView", BindingFlags.Public | BindingFlags.Instance);
property.SetValue(blazorWebViewInitializedEventArgs, wKWebView);
return blazorWebViewInitializedEventArgs;
} public bool TryGetResponseContentInternal(string uri, bool allowFallbackOnHostPage, out int statusCode, out string statusMessage, out Stream content, out IDictionary<string, string> headers)
{
if (MethodTryGetResponseContentInternal == null)
{
var Field_webviewManager = Type.GetField("_webviewManager", BindingFlags.NonPublic | BindingFlags.Instance);
WebviewManager = Field_webviewManager.GetValue(_blazorWebViewHandler); MethodTryGetResponseContentInternal = WebviewManager.GetType().GetMethod("TryGetResponseContentInternal", BindingFlags.NonPublic | BindingFlags.Instance);
}
// 定义参数
object[] parameters = new object[] { uri, allowFallbackOnHostPage, 0, null, null, null }; bool result = (bool)MethodTryGetResponseContentInternal.Invoke(WebviewManager, parameters); // 获取返回值和输出参数
statusCode = (int)parameters[2];
statusMessage = (string)parameters[3];
content = (Stream)parameters[4];
headers = (IDictionary<string, string>)parameters[5];
return result;
} public bool IsBaseOfPage(Uri baseUri, string? uriString)
{
if (MethodIsBaseOfPage == null)
{
var type = Assembly.GetType("Microsoft.AspNetCore.Components.WebView.Maui.UriExtensions")!;
MethodIsBaseOfPage = type.GetMethod("IsBaseOfPage", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
} return (bool)MethodIsBaseOfPage.Invoke(null, new object[] { baseUri, uriString });
} public string QueryStringHelperRemovePossibleQueryString(string? url)
{
if (MethodQueryStringHelperRemovePossibleQueryString == null)
{
var type = Assembly.GetType("Microsoft.AspNetCore.Components.WebView.QueryStringHelper")!;
MethodQueryStringHelperRemovePossibleQueryString = type.GetMethod("RemovePossibleQueryString", BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance);
} return (string)MethodQueryStringHelperRemovePossibleQueryString.Invoke(null, new object[] { url });
}
}
}
Windows

添加MauiBlazorWebViewHandler.Windows.cs

代码如下

using MauiBlazorLocalMediaFile.Utilities;
using Microsoft.Web.WebView2.Core;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Storage.Streams;
using WebView2Control = Microsoft.UI.Xaml.Controls.WebView2; namespace MauiBlazorLocalMediaFile
{
public partial class MauiBlazorWebViewHandler
{
protected override void ConnectHandler(WebView2Control platformView)
{
base.ConnectHandler(platformView);
platformView.CoreWebView2Initialized += CoreWebView2Initialized;
} protected override void DisconnectHandler(WebView2Control platformView)
{
platformView.CoreWebView2Initialized -= CoreWebView2Initialized;
base.DisconnectHandler(platformView);
} private void CoreWebView2Initialized(WebView2Control sender, Microsoft.UI.Xaml.Controls.CoreWebView2InitializedEventArgs args)
{
var webview2 = sender.CoreWebView2;
webview2.WebResourceRequested += WebView2WebResourceRequested;
} async void WebView2WebResourceRequested(CoreWebView2 webview2, CoreWebView2WebResourceRequestedEventArgs args)
{
await InterceptCustomPathRequest(webview2, args);
} static async Task<bool> InterceptCustomPathRequest(CoreWebView2 webview2, CoreWebView2WebResourceRequestedEventArgs args)
{
string uri = args.Request.Uri;
if (!Intercept(uri, out string filePath))
{
return false;
} if (File.Exists(filePath))
{
args.Response = await CreateWebResourceResponse(webview2, args, filePath);
}
else
{
args.Response = webview2.Environment.CreateWebResourceResponse(null, 404, "Not Found", string.Empty);
} return true; static string GetHeaderString(IDictionary<string, string> headers) =>
string.Join(Environment.NewLine, headers.Select(kvp => $"{kvp.Key}: {kvp.Value}")); static async Task<CoreWebView2WebResourceResponse> CreateWebResourceResponse(CoreWebView2 webview2, CoreWebView2WebResourceRequestedEventArgs args, string filePath)
{
var contentType = StaticContentProvider.GetResponseContentTypeOrDefault(filePath);
var headers = StaticContentProvider.GetResponseHeaders(contentType);
using var contentStream = File.OpenRead(filePath);
var length = contentStream.Length;
long rangeStart = 0;
long rangeEnd = length - 1; int statusCode = 200;
string reasonPhrase = "OK"; //适用于音频视频文件资源的响应
bool partial = args.Request.Headers.Contains("Range");
if (partial)
{
statusCode = 206;
reasonPhrase = "Partial Content"; var rangeString = args.Request.Headers.GetHeader("Range");
var ranges = rangeString.Split('=');
if (ranges.Length > 1 && !string.IsNullOrEmpty(ranges[1]))
{
string[] rangeDatas = ranges[1].Split("-");
rangeStart = Convert.ToInt64(rangeDatas[0]);
if (rangeDatas.Length > 1 && !string.IsNullOrEmpty(rangeDatas[1]))
{
rangeEnd = Convert.ToInt64(rangeDatas[1]);
}
else
{
//每次加载4Mb,不能设置太多
rangeEnd = Math.Min(rangeEnd, rangeStart + 4 * 1024 * 1024);
}
} headers.Add("Accept-Ranges", "bytes");
headers.Add("Content-Range", $"bytes {rangeStart}-{rangeEnd}/{length}");
} headers.Add("Content-Length", (rangeEnd - rangeStart + 1).ToString());
var headerString = GetHeaderString(headers);
IRandomAccessStream stream = await ReadStreamRange(contentStream, rangeStart, rangeEnd);
return webview2.Environment.CreateWebResourceResponse(stream, statusCode, reasonPhrase, headerString);
} static async Task<IRandomAccessStream> ReadStreamRange(Stream contentStream, long start, long end)
{
long length = end - start + 1;
contentStream.Position = start; using var memoryStream = new MemoryStream(); StreamCopy(contentStream, memoryStream, length);
// 将内存流的位置重置为起始位置
memoryStream.Seek(0, SeekOrigin.Begin); var randomAccessStream = new InMemoryRandomAccessStream();
await randomAccessStream.WriteAsync(memoryStream.GetWindowsRuntimeBuffer()); return randomAccessStream;
} // 辅助方法,用于限制StreamCopy复制的数据长度
static void StreamCopy(Stream source, Stream destination, long length)
{
//缓冲区设为1Mb,应该是够了
byte[] buffer = new byte[1024 * 1024];
int bytesRead; while (length > 0 && (bytesRead = source.Read(buffer, 0, (int)Math.Min(buffer.Length, length))) > 0)
{
destination.Write(buffer, 0, bytesRead);
length -= bytesRead;
}
}
}
}
}

在MauiProgram.cs中添加

添加在builder.Services.AddMauiBlazorWebView();的下面

builder.Services.ConfigureMauiHandlers(delegate (IMauiHandlersCollection handlers)
{
handlers.AddHandler<IBlazorWebView>((IServiceProvider _) => new MauiBlazorWebViewHandler());
});

试验一下

添加一个复制文件的静态类

简单写一个方法,用于把选中的文件复制到指定目录,并且返回所需要的url相对路径

为了防止文件被重复复制,我们以md5作为文件名

添加文件夹Utilities/File,在里面添加一个静态类MediaResourceFile.cs

代码如下

using System.Security.Cryptography;

namespace MauiBlazorLocalMediaFile.Utilities
{
public static class MediaResourceFile
{
public static async Task<string?> CreateMediaResourceFileAsync(string targetDirectoryPath, string? sourceFilePath)
{
if (string.IsNullOrEmpty(sourceFilePath))
{
return null;
} using Stream stream = File.OpenRead(sourceFilePath);
//新的文件以文件的md5为文件名,确保文件不会重复存在
//获取文件的md5有一点耗时,暂时没想到更好的方案
var fn = stream.CreateMD5() + Path.GetExtension(sourceFilePath);
var targetFilePath = Path.Combine(targetDirectoryPath, fn);
//如果文件存在就不用复制了
if (!File.Exists(targetFilePath))
{
if (sourceFilePath.StartsWith(FileSystem.CacheDirectory))
{
stream.Close();
await FileMoveAsync(sourceFilePath, targetFilePath);
}
else
{
//将流的位置重置为起始位置
stream.Seek(0, SeekOrigin.Begin);
await FileCopyAsync(targetFilePath, stream);
}
} return MauiBlazorWebViewHandler.FilePathToUrlRelativePath(targetFilePath);
} private static async Task FileCopyAsync(string targetFilePath, Stream sourceStream)
{
CreateFileDirectory(targetFilePath); using (FileStream localFileStream = File.OpenWrite(targetFilePath))
{
await sourceStream.CopyToAsync(localFileStream, 1024 * 1024);
};
} private static Task FileMoveAsync(string sourceFilePath, string targetFilePath)
{
CreateFileDirectory(targetFilePath);
File.Move(sourceFilePath, targetFilePath);
return Task.CompletedTask;
} private static void CreateFileDirectory(string filePath)
{
string? directoryPath = Path.GetDirectoryName(filePath);
if (!Directory.Exists(directoryPath))
{
Directory.CreateDirectory(directoryPath!);
}
} private static string CreateMD5(this Stream stream, int bufferSize = 1024 * 1024)
{
using MD5 md5 = MD5.Create();
byte[] buffer = new byte[bufferSize];
int bytesRead; while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
{
md5.TransformBlock(buffer, 0, bytesRead, buffer, 0);
} md5.TransformFinalBlock(buffer, 0, 0); byte[] hash = md5.Hash ?? [];
return BitConverter.ToString(hash).Replace("-", "").ToLower();
}
}
}
写一个选中视频文件并显示的示例页面
@page "/video"
@using MauiBlazorLocalMediaFile.Utilities <h1>Video</h1> <video src="@Src" controls style="max-width:100%;"></video> <div style="word-break: break-all;">Src="@Src"</div> <button class="btn btn-primary" @onclick="()=>Pick(true)">选中并复制到AppDataDirectory</button>
<button class="btn btn-primary" @onclick="()=>Pick(false)">选中不复制(仅限Windows)</button> @code {
private string? Src; private async void Pick(bool copy)
{
#if !WINDOWS
if (!copy)
{
return;
}
#endif var result = await MediaPicker.Default.PickVideoAsync();
var path = result?.FullPath;
if (path is null)
{
return;
} if (copy)
{
var targetDirectoryPath = Path.Combine(FileSystem.AppDataDirectory, "Video");
Src = await MediaResourceFile.CreateMediaResourceFileAsync(targetDirectoryPath, path);
}
else
{
Src = MauiBlazorWebViewHandler.FilePathToUrlRelativePath(path);
} await InvokeAsync(StateHasChanged);
}
}
截图

用笔者比较喜欢的动画电影《魁拔》作为视频文件,大约500MB多一些

Windows

Android

iOS / Mac选中视频会被压缩,特别慢,所以就用一个短的视频了

iOS

Mac

后来补的一张音频的截图,用的原始路径

后记

这篇文章改了又改,总觉得有不妥之处。实在改不动了,就这么地吧,可能写的还是不够详细。

笔者水平有限,性能上可能还存在优化的空间,希望各位大佬不吝赐教,提出宝贵意见

源码

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

有需要的可以去看一下

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

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

MAUI Blazor 如何通过url使用本地文件的更多相关文章

  1. Bootstrap Blazor Viewer 图片浏览器 组件更新, 支持流转图片(ImageFromStream), 用于本地项目例如 MAUI Blazor,Blazor hybrid

    示例: https://blazor.app1.es/viewer 使用方法: 1.nuget包 BootstrapBlazor.Viewer 2._Imports.razor 文件 或者页面添加 添 ...

  2. 爬虫任务二:爬取(用到htmlunit和jsoup)通过百度搜索引擎关键字搜取到的新闻标题和url,并保存在本地文件中(主体借鉴了网上的资料)

    采用maven工程,免着到处找依赖jar包 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi=&quo ...

  3. 学习.NET MAUI Blazor(三)、创建.NET MAUI Blazor应用并使用AntDesignBlazor

    大致了解了Blazor和MAUI之后,尝试创建一个.NET MAUI Blazor应用. 需要注意的是: 虽然都叫MAUI,但.NET MAUI与.NET MAUI Blazor 并不相同,MAUI还 ...

  4. 学习.NET MAUI Blazor(四)、路由

    Web应用程序的可以通过URL将多个页面串联起来,并且可以互相跳转.Web应用主要是使用a标签或者是服务端redirect来跳转.而现在流行的单页应用程序 (SPA) ,则通过路由(Router)来实 ...

  5. Maui Blazor 使用摄像头实现

    Maui Blazor 使用摄像头实现 由于Maui Blazor中界面是由WebView渲染,所以再使用Android的摄像头时无法去获取,因为原生的摄像头需要绑定界面组件 所以我找到了其他的实现方 ...

  6. .NET 读取本地文件绑定到GridViewRow

    wjgl.aspx.cs: using System; using System.Collections; using System.Configuration; using System.Data; ...

  7. Google调用explorer.exe打开本地文件

    给IE浏览器地址栏输个本地文件路径,会自动用explorer.exe打开,这个挺好的,但是IE对jQuery稍微高点的版本不怎么待见,只好自己给Google折腾一个调用explorer的功能----- ...

  8. 用java 代码下载Samba服务器上的文件到本地目录以及上传本地文件到Samba服务器

    引入: 在我们昨天架设好了Samba服务器上并且创建了一个 Samba 账户后,我们就迫不及待的想用JAVA去操作Samba服务器了,我们找到了一个框架叫 jcifs,可以高效的完成我们工作. 实践: ...

  9. 头疼:为什么chrome不能访问本地文件(带--disable-web-security --allow-file-access-from-files )

    如题,寻求帮助! chrome 带参数启动 --disable-web-security  --allow-file-access-from-files 照理应该可以加载本地文件,找遍google和英 ...

  10. iOS5可能会删除本地文件储存 - Caches 也不安全

    转自:http://blog.163.com/ray_jun/blog/static/1670536422011101225132544/ 出处:http://superman474.blog.163 ...

随机推荐

  1. Java学习之进制之间的转换

    import java.util.*; public class Jinzhi { public static void main(String[] args) { /** * 总结: * 二进制转换 ...

  2. valgrind 配合 gdb 调试程序

    在实际研发过程中,可能会遇到过这样的问题:测试通过 valgrind 验证当前代码存在变量未初始化的问题,但仅通过 valgrind 测试报告,研发无法确认具体的应用场景.本文将通过 valgrind ...

  3. [ABC150F] Xor Shift

    2023-03-10 题目 题目传送门 翻译 翻译 难度&重要性(1~10):6 题目来源 AtCoder 题目算法 KMP,Z函数 解题思路 首先是按位确定,令 \(t(i,j)\) 表示 ...

  4. 三维模型OSGB格式轻量化的纹理压缩和质量保持分析

    三维模型OSGB格式轻量化的纹理压缩和质量保持分析 在三维模型应用中,纹理数据是一个重要的部分,可以为模型增加更多的真实感和细节.但是,由于纹理数据通常会占用大量的存储空间和传输带宽,因此,在OSGB ...

  5. 文心一言(ERNIE Bot)初体验

    引言 几个月前向百度提交了文心一言的体验申请,这两天收到了可以体验的通知,立马体验了一把.总体来说,文心一言基本上能做到有问必答,但是一些奇葩的问题还是会难住这位初出茅庐的 AI. 分享体验 我先后问 ...

  6. EtherCAT转Modbus网关做为 MODBUS 从站配置

    EtherCAT转Modbus网关做为 MODBUS 从站配置案例 兴达易控EtherCAT转Modbus网关可以用作MODBUS从站的配置.这种网关允许将Modbus协议与EtherCAT协议进行转 ...

  7. 模块化打包工具-初识Webpack

    1. 为什么需要模块化打包工具 在上一篇文章中提到的ES Module可以帮助开发者更好地组织代码,完成js文件的模块化,基本解决了模块化的问题,但是实际开发中仅仅完成js文件的模块化是不够的,尤其是 ...

  8. Python常用模块-20个常用模块总结

    目录 time模块 datetime模块 random 模块 os 模块 sys 模块 json 和 pickle 模块 hashlib和hmac 模块 logging 模块 numpy 模块 pan ...

  9. Go语言常用标准库——context

    文章目录 为什么需要Context 基本示例 全局变量方式 通道方式 官方版的方案 Context初识 Context接口 Background()和TODO() With系列函数 WithCance ...

  10. SpringBoot整合XXLJob

    目录 XXLJob简介 特性 模块 安装调度中心 初始化数据库 配置 启动 整合执行器 pom yml XxlJobConfig 启动执行器 实践 简单的定时任务 在执行器创建任务 在调度中心创建执行 ...