ASP.NET Core知多少(11):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構建的內嵌(資源)文件系統
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,002評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,400評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,136評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,714評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,452評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,818評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,812評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,997評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,552評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,292評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,510評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,721評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,121評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,429評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,235評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,480評論 2 379