返回

通过 EmbededFileProvider 实现 Blazor 的静态文件访问

AI 摘要
博主计划使用 Blazor 重构其基于 Hexo 的个人博客,探讨了 Blazor 访问静态文件的方法。文章首先介绍了内嵌资源的概念,演示了如何在 Visual Studio 中设置内嵌资源,并通过 Assembly类提供的接口读取内嵌资源。接着,文章讨论了 ASP.NET Core 中的 IFileProvider 接口和 EmbededFileProvider 类,展示了如何使用 EmbededFileProvider 访问内嵌资源。此外,文章还提到了 Blazor 在处理配置文件时遇到的问题,以及如何通过依赖注入和 HttpClient 实现数据加载。博主认为 Blazor 是一个舒适的框架,但其未来取决于 WebAssembly 的接受程度。最后,博主强调了从一个问题扩展到整个系统的思考方式的重要性。

重构我的 独立博客 ,是博主今年的计划之一,这个基于 Hexo 的静态博客,最早搭建于2014年,可以说是比女朋友更亲密的存在,陪伴着博主走过了毕业、求职以及此刻的而立之年。其间虽然尝试过像 JekyllHugo 这样的静态博客生成器,可是考虑到模板、插件等周边生态,这个想法一直被搁置下来。直到最近,突然涌现出通过 Blazor 重写博客的想法,尤其是它对于 WebAssembly 的支持,而类似 VueReact的组件化开发模式,在开发体验上有着同样不错的表现。所以,今天这篇博客就来聊聊在重写博客过程中的一点收获,即如何让 Blazor 访问本地的静态文件。

从内嵌资源说起

首先,我们要引入一个概念,即:内嵌资源。我们平时接触的更多的是本地文件系统,或者是 FTP 、对象存储这类运行在远程服务器上的文件系统,这些都是非内嵌资源,所以,内嵌资源主要是指那些没有目录层级的文件资源,因为它会在编译的时候“嵌入”到动态链接库(DLL)中。一个典型的例子是Swagger,它在.NET Core平台下的实现是Swashbuckle.AspNetCore,它允许使用自定义的HTML页面。这里可以注意到,它使用到了GetManifestResourceStream()方法:

app.UseSwaggerUI(c =>
{
    // requires file to be added as an embedded resource
    c.IndexStream = () => GetType().Assembly
        .GetManifestResourceStream("CustomUIIndex.Swagger.index.html"); 
});

其实,这里使用的就是一个内嵌资源。关于内嵌资源,我们有两种方式来定义它:

  • 在 Visual Studio 中选中指定文件,在其属性窗口中选择生成操作为嵌入的资源:

如何定义一个文件资源为内嵌资源
如何定义一个文件资源为内嵌资源

  • 在项目文件(.csproj)中修改对应ItemGroup节点,参考示例如下:
<Project Sdk="Microsoft.NET.Sdk.Web">
<!-- ... -->
  <ItemGroup>
    <EmbeddedResource Include="_config.yml">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </EmbeddedResource>
  </ItemGroup>
<!-- ... -->
</Project>

这样,我们就完成了内嵌资源的定义。而定义内嵌资源,本质上还是为了在运行时期间去读取和使用,那么,自然而然地,我们不禁要问,该怎么读取这些内嵌资源呢?在Assembly类中,微软为我们提供了下列接口来处理内嵌资源:

public virtual ManifestResourceInfo GetManifestResourceInfo(string resourceName);
public virtual string[] GetManifestResourceNames();
public virtual Stream GetManifestResourceStream(Type type, string name);
public virtual Stream GetManifestResourceStream(string name);

其中,GetManifestResourceNames()方法用来返回所有内嵌资源的名称,GetManifestResourceInfo()方法用来返回指定内嵌资源的描述信息,GetManifestResourceStream()方法用来返回指定内嵌资源的文件流。为了方便大家理解,这里我们准备了一个简单的示例:

var assembly = Assembly.GetExecutingAssembly();
var resources = assembly.GetManifestResourceNames();
resources.ToList().ForEach(x => Console.WriteLine(x));
//ConsoleApp.A.B.示例文档.txt
//ConsoleApp.A._config.yml
var fileInfo = assembly.GetManifestResourceInfo(resources[0]);
var fileStream = assembly.GetManifestResourceStream(resources[0]);

此时,我们会发现,内嵌资源都是使用类似A.B.C.D这样的形式来表示资源路径的,因为内嵌资源本身是没有目录层级的。现在,如果我们再回过头去看Swagger的示例,就不难理解为什么会有CustomUIIndex.Swagger.index.html这样一个奇怪的值,因为它对应着实际的物理文件路径,如下图所示,示例代码中输出的资源路径和实际的物理路径存在着对应关系:

项目中的物理路径与内嵌资源路径对照
项目中的物理路径与内嵌资源路径对照

EmbededFileProvider

OK,那么在了解了内嵌资源以后,接下来,我们需要关注的是EmbededFileProvider。需要说明的是,在ASP.NET Core中,微软是通过IFileProvider这个接口来解决文件读取问题的,典型的使用场景有静态文件中间件、Rozar模板引擎以及WWWRoot目录定位等等,通常情况下,我们使用PhysicalFileProvider更多一点,它和EmbededFileProvider一样,都实现了IFileProvider接口,所以,ASP.NET Core可以从不同的来源访问文件信息。

显然,EmbededFileProvider正是为了内嵌资源而生,它在内部使用到了Assembly类中和内嵌资源相关的接口.所以,除了上面的方式,我们还可以通过下面的方式来访问内嵌资源,需要注意的是,使用EmbededFileProvider需要引用Microsoft.Extensions.FileProviders.Embedded,大家可以比较一下这两种方式地差异:

var assembly = Assembly.GetExecutingAssembly();
var provider = new EmbeddedFileProvider(assembly);
//注意,这里写"."或者""都可以
var resouces = provider.GetDirectoryContents(".").ToList();
var fileInfo = provider.GetFileInfo(resouces[0]);
var fileStream = fileInfo.CreateReadStream();

除此以外,IFileProvider还有一个最重要的功能,即Watch()方法,它可以监听文件的变化,并返回一个IChangeToken。有没有一种似曾相识燕归来的感觉?没错,博主曾经在 基于选项模式实现.NET Core的配置热更新 这篇文章中介绍过它,它是实现配置热更新的关键。事实上,FileConfigurationSource这个类中有一个Provider属性,而它对应的类型恰好是IFileProvider,这难道是巧合吗?不,仔细顺着这条线,我们大概就能明白微软的良苦用心,我们的配置文件自然是来自文件系统,而考虑到内嵌资源的存在,我们面对的文件系统其实是一个广义的文件系统,它可以是物理文件、内嵌文件、Glob、对象存储(OSS)等等

Blazor的奇妙缘分

好了,千呼万唤始出来,现在终于要讨论 Blazor 这个话题啦!众所周知,静态博客生成器里主要存在着两种配置,即站点配置和主题配置,Hexo 里甚至还支持从特定文件夹里加载自定义的数据。所以,对于静态博客而言,它需要有从外部加载数据这个特性。我们知道,Blazor 分为服务器和客户端两个版本,两者的区别主要在于 Rozar 模板由谁来渲染,前者相当于服务端渲染(SSR) + SignalR,而后者则是基于 WebAssembly,它可以直接在浏览器中加载。显然,后者更接近我们静态博客生成器的想法。由于 Hexo 使用 Yaml 作为配置语言,所以,为了读取原来 Hexo 博客的配置,参考 实现自己的.NET Core配置Provider之Yaml 这篇博客实现了一个YamlConfigurationProvider。

在使用的过程中,遇到的问题是,它无法识别配置文件的路径。原因很简单,经过编译的 Blazor 会被打包为 WebAssembly ,而 WebAssembly 在前端加载以后,原来的目录层级早已荡然无存。此时,基于物理文件的 PhysicalFileProvider 将无法工作。解决方案其实大家都能想到,换一种IFileProvider的实现就好了啊!至此,奇妙的缘分产生了:

class YamlConfigurationProvider : FileConfigurationProvider
{
    private readonly FileConfigurationSource _source;
    public YamlConfigurationProvider(FileConfigurationSource source) : base(source)
    {
        _source = source;
    }

    public override void Load()
    {
        var path = _source.Path;
        var provider = _source.FileProvider;
        using (var stream = provider.GetFileInfo(path).CreateReadStream())
        {
            //核心问题就是这个Stream的来源发生了变化
            var parser = new YamlConfigurationFileParser();
            Data = parser.Parse(stream);
        }
    }

其实,官方文档中提到过,Blazor 的配置文件默认从 WWWRoot 下的appsettings.json加载,所以,对于像JSON这类静态文件,可以注入HttpClient,以API的方式进行访问。例如,官方文档中推荐的加载配置文件的方式为:

var httpClient = new HttpClient()
{
    BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
};

builder.Services.AddScoped(sp => httpClient);

//前方有语法糖,高甜:)
using var response = await http.GetAsync("cars.json");
using var stream = await response.Content.ReadAsStreamAsync();

builder.Configuration.AddJsonStream(stream);

而经过我们这样改造以后,我们还可以这样加载配置:

builder.Configuration.AddYamlFile(
    provider:new EmbeddedFileProvider(Assembly.GetExecutingAssembly()),
    path: "_config.yml",
    optional:false,
    reloadOnChange:true
);

一旦这些配置注入到 IoC 容器里,我们就可以纵享无所不在的依赖注入,这里以某个组件为例:

@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration

<div class="mdui-container-fluid">
    <div class="mdui-row DreamCat-content-header">
        <div class="mdui-container fade-scale in">
            <h1 class="title">@Configuration["title"]</h1>
            <h5 class="subtitle">@Configuration["subtitle"]</h5>
        </div>
    </div>
</div>

同样地,对于组件内的数据,在大多数场景下,我们可以这样来处理,还是因为有无所不在的依赖注入:

@page "/"
@layout MainLayout

@inject HttpClient httpClient
@using BlazorBlog.Core.Domain.Blog;
@using BlazorBlog.Web.Shared.Partials;
@if (posts != null && posts.Any())
{
    foreach (var post in posts)
    {
        //这是一个自定义组件
        <PostItem Model=post></PostItem>
    }
}

@code
{
    private List<Post> posts { get; set; }
    protected override async Task OnInitializedAsync()
    {
        posts = await httpClient.GetFromJsonAsync<List<Post>>("content.json");
        await base.OnInitializedAsync();
    }
}

这里可以给大家展示下尚在开发中的静态博客:

基于 Balzor 的静态博客
基于 Balzor 的静态博客

理论上任何文件都可以这样做,主要是考虑到配置这种信息,用依赖注入会更好一点,这样每一个组件都可以使用这些配置,而如果是以 API 的形式集成,以目前 Blazor 打包以后加载的效果来看,页面会有比较大的“空白期”。我更加疑惑的是,如果 Blazor 打包后的体积过大,那么浏览器自带的存储空间是否够用呢?一句话总结的话, Blazor 是一个写起来非常舒服的框架,可未来是否会像当年的 Sliverlight 一样,这还要看大家对 WebAssembly 的接受程度,可谓是“路漫漫其修远兮”啊……

本文小结

这篇博客,是博主由一个个“闪念”而串联起来的脑洞,作为一个实验性质的尝试,希望通过 Blazor 的客户端模式(WebAssembly) 实现一个静态博客,而在这个过程中,需要解决 Balzor 读取本地文件的问题,由此,我们引入了这篇博客的主题之一,即:EmbededFileProvider。顺着这条线索,我们梳理了内嵌的文件资源、IFileProvider接口、FileConfigurationProviderFileConfigurationSource等等一系列看起来毫无关联的概念。事实上,“冥冥之中自有天意”,这一切怎么会毫无关联呢?我们最终从文件系统看到了配置系统,聊到了 Blazor 中的配置问题,这里我们熟悉的依赖注入、配置系统都得以延续下来。其实,单单就解决这个问题而言,完全不值得专门写一篇博客,可从一个点辐射到整个面的这种感悟,在人生的成长中更显得弥足珍贵,希望我们每一个人都能多多跳脱出自己的视角,去努力的看一看这个丰富多彩的世界,在多样性与多元化中去寻找整体上的统一,这是作为技术人员的我,一生都想去探索的哲学。好了,以上就是这篇博客的全部内容啦,欢迎大家在评论中留下你的想法或者建议,谢谢大家!

Built with Hugo v0.126.1
Theme Stack designed by Jimmy
已创作 274 篇文章,共计 1038462 字