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打開解決方案,如下圖:
- 修改Page1.cshtml,body內添加
<h1>This is from CommonUI.Page1</h1>
- RPL.Web添加引用項目【RPL.CommonUI】
- 設置RPL為啟動項目。
- CTRL+F5運行。
我們觀察到RPL.CommonUI中預置了一個Razor Page,因為Razor Page是基於文件系統路由,所以直接https://localhost:<port>/myfeature/page1
即可訪問。
到這一步,我們就可以篤定RPL正確生效。
3. Keep Going
以上只是簡單的HTML頁面,如果要想加以潤色,就需要寫CSS來處理。
兩種處理方式:
- 使用內聯樣式
- 引用外部樣式文件
內聯樣式,很簡單,就不加以贅述。
我們來定義樣式文件來處理。仿照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將相關代碼顯示如下:
從中可以看出在構建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; }
從上圖的注釋代碼中可以看到,其初始化邏輯正是去指定WebRootPath
和WebRootFileProvider
。
如果我們在應用程序未手動通過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
中間件來處理的。這也就是為什么在啟動類Startup
的Configure
方法中需要指定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中如何定義內嵌資源呢?
- 編輯RPL.CommonUI.csproj文件,添加wwwroot為內嵌資源。
<ItemGroup>
<EmbeddedResource Include="wwwroot\**\*" />
</ItemGroup>
- 添加
GenerateEmbeddedFilesManifest
節點,指定生成內嵌資源清單。
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
- 添加
Microsoft.Extensions.FileProviders.Embedded
Nuget包引用。
修改完后的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,查看下其程序集清單:
從圖中可以看出內嵌的demo.css文件,是以{程序集名稱}.{文件路徑}命名的。
那內嵌資源如何訪問呢?可以借助EmbeddedFileProvider
,我們仿照上面的例子,在Startup.cs
的Configure
方法中添加以下代碼:
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.StaticFiles
和Microsoft.Extensions.FileProviders.Embedded
Nuget包引用。
修改完后的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>
- 接下來添加
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);
}
}
}
- 然后添加
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));
}
}
}
-
修改RPL.Web啟動類startup.cs,在
services.AddMvc()
之前添加services.AddCommonUI();
即可。 -
CTRL+F5重新運行,我們發現H1被成功設置為紅色,檢查發現demo.css也能正確被請求,檢查network也可以看到其Request URL為:https://localhost:44379/css/demo.css
6. Case Study
Demonstrate how to use Razor class library to create reusable email template.
這個鏈接是一個進階demo,演示了如何使用RPL去創建可重用的郵件模板,感興趣的不妨一看。