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

ASP.NET Core知多少系列:整體介紹及目錄
Demo路徑:GitHub-RPL.Democss

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遠遠沒有那麼簡單,容我娓娓道來。html

2. Hello RPL

老規矩,從Hello World 開始,咱們建立一個Demo項目。
記住開始以前請確認已安裝.NET Core 2.1 SDK!!!
咱們此次使用命令行來建立項目:git

>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打開解決方案,以下圖:github

  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便可訪問。
web

到這一步,咱們就能夠篤定RPL正確生效。api

3. Keep Going

以上只是簡單的HTML頁面,若是要想加以潤色,就須要寫CSS來處理。
兩種處理方式:瀏覽器

  1. 使用內聯樣式
  2. 引用外部樣式文件

內聯樣式,很簡單,就不加以贅述。
咱們來定義樣式文件來處理。仿照RPL.Web項目,建立一個wwwroot根目錄,而後再添加一個css文件夾,再添加一個demo.css的樣式文件。mvc

h1 {
    color: red;
}

而後將demo.css引用添加到page1.cshtml中。app

<head>
    <meta name="viewport" content="width=device-width" />
    <link rel="stylesheet" href="~/css/demo.css" />
    <title>Page1</title>
</head>

CTRL+F5從新運行,運行結果以下圖:
ide

能夠清晰的看到,定義的樣式並未生效。從瀏覽器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構建的內嵌(資源)文件系統
相關文章
相關標籤/搜索