Razor Page Library:開發獨立通用RPL(內嵌wwwroot資源文件夾)


ASP.NET Core知多少系列:總體介紹及目錄
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!!!
我們這次使用命令行來創建項目:

>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的樣式文件。

h1 {
    color: red;
}

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

<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

從中可以看出在構建WebHost的業務邏輯中會去初始化IHostingEnvironment對象。該對象主要用來描述應用程序運行的web宿主環境的相關信息,主要包含以下幾個屬性:

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文件夾。
同時注意下面這段代碼:

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,介紹了如何訪問自定義目錄的靜態資源文件。

如果需要訪問自定義路徑目錄的資源,需要添加類似以下代碼:

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>
  1. 添加GenerateEmbeddedFilesManifest節點,指定生成內嵌資源清單。
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
  1. 添加Microsoft.Extensions.FileProviders.EmbeddedNuget包引用。

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

<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

從圖中可以看出內嵌的demo.css文件,是以{程序集名稱}.{文件路徑}命名的。

那內嵌資源如何訪問呢?可以借助EmbeddedFileProvider,我們仿照上面的例子,在Startup.csConfigure方法中添加以下代碼:

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

  1. 編輯RPL.CommonUI.csproj文件,添加wwwroot為內嵌資源。
  <ItemGroup>
    <EmbeddedResource Include="wwwroot\**\*" />
  </ItemGroup>
  1. 添加GenerateEmbeddedFilesManifest節點,指定生成內嵌資源清單。
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
  1. 添加Microsoft.AspNetCore.StaticFilesMicrosoft.Extensions.FileProviders.EmbeddedNuget包引用。

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

<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>
  1. 接下來添加CommonUIConfigureOptions.cs,定義如下:
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);
        }
    }
}

  1. 然后添加CommonUIServiceCollectionExtensions.cs,代碼如下:
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));
        }
    }
}

  1. 修改RPL.Web啟動類startup.cs,在services.AddMvc()之前添加services.AddCommonUI();即可。

  2. CTRL+F5重新運行,我們發現H1被成功設置為紅色,檢查發現demo.css也能正確被請求,檢查network也可以看到其Request URL為:https://localhost:44379/css/demo.css

    Request URL

6. Case Study

Demonstrate how to use Razor class library to create reusable email template.
這個鏈接是一個進階demo,演示了如何使用RPL去創建可重用的郵件模板,感興趣的不妨一看。

7. References

  1. Static files in ASP.NET Core
  2. File Providers in ASP.NET Core
  3. ManifestEmbeddedFileProvider Class
  4. Make it easier to use static assets that are part of a RCL project
  5. .NET Core的文件系統[4]:由EmbeddedFileProvider構建的內嵌(資源)文件系統


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM