前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Razor Page Library:开发独立通用RPL(内嵌wwwroot资源文件夹)

Razor Page Library:开发独立通用RPL(内嵌wwwroot资源文件夹)

作者头像
圣杰
发布2018-08-01 10:13:28
1.1K0
发布2018-08-01 10:13:28
举报
文章被收录于专栏:圣杰的专栏圣杰的专栏

Demo路径:GitHub-RPL.Demo

1. Introduction

Razor Page Library 是ASP.NET Core 2.1引入的新类库项目,属于新特性之一,用于创建通用页面公用类库。也就意味着可以将多个Web项目中通用的Web页面提取出来,封装成RPL,以进行代码重用。 官方文档Create reusable UI using the Razor Class Library project in ASP.NET Core中,仅简单介绍了如何创建RPL,但要想开发出一个独立通用的RPL远远没有那么简单,容我娓娓道来。

2. Hello RPL

老规矩,从Hello World 开始,我们创建一个Demo项目。 记住开始之前请确认已安装.NET Core 2.1 SDK!!! 我们这次使用命令行来创建项目:

代码语言:javascript
复制
>dotnet --version
2.1.300
>dotnet new razorclasslib --name RPL.CommonUI
已成功创建模板“Razor Class Library”。

正在处理创建后操作...
正在 RPL.CommonUI\RPL.CommonUI.csproj 上运行 "dotnet restore"...
  正在还原 F:\Coding\Demo\RPL.CommonUI\RPL.CommonUI.csproj 的包...
  正在生成 MSBuild 文件 F:\Coding\Demo\RPL.CommonUI\obj\RPL.CommonUI.csproj.nuge
t.g.props。
  正在生成 MSBuild 文件 F:\Coding\Demo\RPL.CommonUI\obj\RPL.CommonUI.csproj.nuge
t.g.targets。
  F:\Coding\Demo\RPL.CommonUI\RPL.CommonUI.csproj 的还原在 1.34 sec 内完成。

还原成功。
>dotnet new mvc --name RPL.Web
已成功创建模板“ASP.NET Core Web App (Model-View-Controller)”。
此模板包含非 Microsoft 的各方的技术,有关详细信息,请参阅 https://aka.ms/aspnetc
ore-template-3pn-210。

正在处理创建后操作...
正在 RPL.Web\RPL.Web.csproj 上运行 "dotnet restore"...
  正在还原 F:\Coding\Demo\RPL.Web\RPL.Web.csproj 的包...
  正在生成 MSBuild 文件 F:\Coding\Demo\RPL.Web\obj\RPL.Web.csproj.nuget.g.props
。
  正在生成 MSBuild 文件 F:\Coding\Demo\RPL.Web\obj\RPL.Web.csproj.nuget.g.target
s。
  F:\Coding\Demo\RPL.Web\RPL.Web.csproj 的还原在 2 sec 内完成。

还原成功。
>dotnet new sln --name RPL.Demo
已成功创建模板“Solution File”。
>dotnet sln RPL.Demo.sln add RPL.CommonUI/RPL.CommonUI.csproj
已将项目“RPL.CommonUI\RPL.CommonUI.csproj”添加到解决方案中。
>dotnet sln RPL.Demo.sln add RPL.Web/RPL.Web.csproj
已将项目“RPL.Web\RPL.Web.csproj”添加到解决方案中。

创建完毕后,双击RPL.Demo.sln打开解决方案,如下图:

  1. 修改Page1.cshtml,body内添加<h1>This is from CommonUI.Page1</h1>
  2. RPL.Web添加引用项目【RPL.CommonUI】
  3. 设置RPL为启动项目。
  4. CTRL+F5运行。

我们观察到RPL.CommonUI中预置了一个Razor Page,因为Razor Page是基于文件系统路由,所以直接https://localhost:<port>/myfeature/page1即可访问。

到这一步,我们就可以笃定RPL正确生效。

3. Keep Going

以上只是简单的HTML页面,如果要想加以润色,就需要写CSS来处理。 两种处理方式:

  1. 使用内联样式
  2. 引用外部样式文件

内联样式,很简单,就不加以赘述。 我们来定义样式文件来处理。仿照RPL.Web项目,创建一个wwwroot根目录,然后再添加一个css文件夹,再添加一个demo.css的样式文件。

代码语言:javascript
复制
h1 {
    color: red;
}

然后将demo.css引用添加到page1.cshtml中。

代码语言:javascript
复制
<head>
    <meta name="viewport" content="width=device-width" />
    <link rel="stylesheet" href="~/css/demo.css" />
    <title>Page1</title>
</head>

CTRL+F5重新运行,运行结果如下图:

可以清晰的看到,定义的样式并未生效。从浏览器F12 Developer Tool中可以清晰的看到,无法请求demo.css样式文件。 到这里,也就抛出了本文所要解决的问题:如何开发独立通用的RPL? 如果RPL中无法引用项目中定义一些静态资源文件(CSS、JS、Image等),那RPL将无法有效的组织View。

4. Analyze

要想访问RPL中的静态资源文件,首先我们要弄明白.NET Core Web项目中wwwroot文件夹的资源是如何访问的。 这一切得从应用程序启动说起,为了方便查阅,使用Code Map将相关代码显示如下:

Program.cs
Program.cs

从中可以看出在构建WebHost的业务逻辑中会去初始化IHostingEnvironment对象。该对象主要用来描述应用程序运行的web宿主环境的相关信息,主要包含以下几个属性:

代码语言:javascript
复制
string EnvironmentName { get; set; }
string ApplicationName { get; set; }
string WebRootPath { get; set; }
IFileProvider WebRootFileProvider { get; set; }
string ContentRootPath { get; set; }
IFileProvider ContentRootFileProvider { get; set; }

从上图的注释代码中可以看到,其初始化逻辑正是去指定WebRootPathWebRootFileProvider。 如果我们在应用程序未手动通过webHostBuilder.UseWebRoot("your web root path");指定自定义的Web Root路径,那么将会默认指定为wwwroot文件夹。 同时注意下面这段代码:

代码语言:javascript
复制
hostingEnvironment.WebRootFileProvider = new
PhysicalFileProvider(hostingEnvironment.WebRootPath);

其指定的IFileProvider的类型为PhysicalFileProvider。 到这里,是不是就豁然开朗了,Web 应用启动时,指定的WebRootFileProvider仅仅映射了Web应用的wwwroot目录,自然是访问不了我们RPL项目指定的wwwroot目录啊。

到这里,其实我们离问题就很近了。但是只要指定了WebRootFileProvider就可以访问WebRoot目录的资源了吗?并不是。

我们知道,ASP.NET Core是通过由一系列中间件组装而成的请求管道来处理请求的。不管是View视图也好,还是静态资源文件也好,都是通过Http Request来请求的。HTTP Request流入请求管道后,根据请求类型,不同的中间件负责处理不同的请求。那对于静态资源文件,ASP.NET Core中是借助StaticFileMiddleware中间件来处理的。这也就是为什么在启动类StartupConfigure方法中需要指定app.UseStaticFiles();来启用StaticFileMiddleware中间件。

在ASP.NET Core 官方文档中Static files in ASP.NET Core,介绍了如何访问自定义目录的静态资源文件。

如果需要访问自定义路径目录的资源,需要添加类似以下代码:

代码语言:javascript
复制
app.UseStaticFiles(new StaticFileOptions
    {
        FileProvider = new PhysicalFileProvider(
            Path.Combine(Directory.GetCurrentDirectory(), "MyStaticFiles")),
        RequestPath = "/StaticFiles"
    });

但这似乎并不能满足我们的需求。Why?看标题,开发独立通用的RPL。怎么理解独立通用?也就意味着RPL中的资源文件最好能够通过程序集打包。这样才能完全独立。否则,在发布RPL时,还需要输出静态资源文件,显然增加了使用的难度。而如何将资源文件打包进程序集呢?——内嵌资源。

5. Embedded Resource

一个程序集主要由两种类型的文件构成,它们分别是承载IL代码的托管模块文件和编译时内嵌的资源文件。那在.NET Core中如何定义内嵌资源呢?

  1. 编辑RPL.CommonUI.csproj文件,添加wwwroot为内嵌资源。 <ItemGroup> <EmbeddedResource Include="wwwroot\**\*" /> </ItemGroup>
  2. 添加GenerateEmbeddedFilesManifest节点,指定生成内嵌资源清单。 <GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
  3. 添加Microsoft.Extensions.FileProviders.EmbeddedNuget包引用。

修改完后的RPL.CommonUI.csproj,如下所示:

代码语言:javascript
复制
<Project Sdk="Microsoft.NET.Sdk.Razor">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.0" />
    <PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="2.1.0" />
  </ItemGroup>
  <ItemGroup>
    <EmbeddedResource Include="wwwroot\**\*" />
  </ItemGroup>
</Project>

我们用ildasm.exe反编译RPL.CommonUI.dll,查看下其程序集清单:

Manifest
Manifest

从图中可以看出内嵌的demo.css文件,是以{程序集名称}.{文件路径}命名的。

那内嵌资源如何访问呢?可以借助EmbeddedFileProvider,我们仿照上面的例子,在Startup.csConfigure方法中添加以下代码:

代码语言:javascript
复制
app.UseStaticFiles();

var dllPath = Path.Join(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "RPL.CommonUI.dll");
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new ManifestEmbeddedFileProvider(Assembly.LoadFrom(dllPath), "wwwroot")
});

CTRL+F5,运行。Perfect!

当然这也不是最好的解决方案,因为你肯定不想所有调用这个RPL的地方,添加这么几句代码,因为这段代码有很强的侵入性,且不可隔离变化。

5. Final Solution

  • 编辑RPL.CommonUI.csproj文件,添加wwwroot为内嵌资源。 <ItemGroup> <EmbeddedResource Include="wwwroot\**\*" /> </ItemGroup>
  • 添加GenerateEmbeddedFilesManifest节点,指定生成内嵌资源清单。 <GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
  • 添加Microsoft.AspNetCore.StaticFilesMicrosoft.Extensions.FileProviders.EmbeddedNuget包引用。

修改完后的RPL.CommonUI.csproj,如下所示:

代码语言:javascript
复制
<Project Sdk="Microsoft.NET.Sdk.Razor">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.1.0" />
    <PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="2.1.0" />
  </ItemGroup>
  <ItemGroup>
    <EmbeddedResource Include="wwwroot\**\*" />
  </ItemGroup>
</Project>
  • 接下来添加CommonUIConfigureOptions.cs,定义如下:
代码语言:javascript
复制
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Options;
using System;

namespace RPL.CommonUI
{
    internal class CommonUIConfigureOptions: IPostConfigureOptions<StaticFileOptions>
    {
        public CommonUIConfigureOptions(IHostingEnvironment environment)
        {
            Environment = environment;
        }
        public IHostingEnvironment Environment { get; }

        public void PostConfigure(string name, StaticFileOptions options)
        {
            name = name ?? throw new ArgumentNullException(nameof(name));
            options = options ?? throw new ArgumentNullException(nameof(options));

            // Basic initialization in case the options weren't initialized by any other component
            options.ContentTypeProvider = options.ContentTypeProvider ?? new FileExtensionContentTypeProvider();
            if (options.FileProvider == null && Environment.WebRootFileProvider == null)
            {
                throw new InvalidOperationException("Missing FileProvider.");
            }

            options.FileProvider = options.FileProvider ?? Environment.WebRootFileProvider;

            // Add our provider
            var filesProvider = new ManifestEmbeddedFileProvider(GetType().Assembly, "wwwroot");
            options.FileProvider = new CompositeFileProvider(options.FileProvider, filesProvider);
        }
    }
}
  • 然后添加CommonUIServiceCollectionExtensions.cs,代码如下:
代码语言:javascript
复制
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Text;

namespace RPL.CommonUI
{
    public static class CommonUIServiceCollectionExtensions
    {
        public static void AddCommonUI(this IServiceCollection services)
        {
            services.ConfigureOptions(typeof(CommonUIConfigureOptions));
        }
    }
}
  • 修改RPL.Web启动类startup.cs,在services.AddMvc()之前添加services.AddCommonUI();即可。
  • CTRL+F5重新运行,我们发现H1被成功设置为红色,检查发现demo.css也能正确被请求,检查network也可以看到其Request URL为:https://localhost:44379/css/demo.css
Request URL
Request URL

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018-06-11 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. Introduction
  • 2. Hello RPL
  • 3. Keep Going
  • 4. Analyze
  • 5. Embedded Resource
  • 5. Final Solution
相关产品与服务
消息队列 TDMQ
消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档