標記幫助器,即 Tag Helpers。這個嘛,就直接翻譯了,叫“標記幫助器”,雖然不好聽,但只能這樣了。當然你翻譯為“標記增強器”也行。
所謂標記幫助器,就是針對 HTML 標籤(不管是標準的還是自己命名的)進行擴充套件的做法。它是以 Razor 為基礎的,服務於開發人員的。在伺服器端用 C# 程式碼來實現一些需求,並生成 HTML 元素。在 Razor 文件中可以方便書寫,VS 、VS Code 等工具還有提示功能。
不太恰當的理解就是把某個 HTML 標記封裝為了一種元件,或者補充它原有的功能。不過,理解為一種元件也不算錯,只不過不像 Razor 元件那樣完整化的封裝(裡面是一大段HTML),Tag Helper 就是針對某個 HTML 元素的。
老周這篇水文不介紹常用的標記幫助器,畢竟這些大夥們都會用,就是在 Razor 文件中用 @addTagHeler 指令匯入的那些型別。如內建的 input、form 元素的幫助器。像我們們常用的像 asp-controller 、asp-action 這些HTML屬性就是透過幫助器來擴充的。
老周的想法是:我們們扒一下標記幫助器的底層知識,看能不能發現點啥樂子。生活不易,人世悲苦,“長太息以掩涕兮,哀民生之多艱”,所以得找點樂子充實一下人生。
我們們先聊最抽象的介面:ITagHelperComponent。咦?這貨還真是以“Component”結尾,看來確實把標記幫助器認定為一種小型 Razor 元件。看看這介面為我們規範了些啥。
Order 屬性:愚蠢的機器把它翻譯為【訂單】。這個錯誤很離譜,後果很嚴重。你要真按訂單去理解,那就完了。這個是叫【順序】,說直接點叫優先順序。數值越小就越先被執行,比如,0、3、5,那麼,Order 為0的先執行,Order為5的後執行。
Init 方法:看名字就知道這是初始化時被呼叫的。一般沒有特別需要,這方法裡不用寫什麼程式碼。方法有個 TagHelperContext 型別的引數。唯一能讓你修改的是 Items 屬性,它是個字典結構,用來存一些自定義資料。這些自定義資料可以在不同的 TagHelper 間傳遞。有點像 HttpContext.Items。
ProcessAsync 方法:這個是核核核心心心,重要的事延長三拍。各種為 HTML 元素新增屬性、生成內容等都在此方法中完成。
實現 ITagHelperComponent 介面的類,在 Razor 文件中是不能被 @addTagHelper 指令匯入的。我們們來做來試驗。
[HtmlTargetElement("p")] public class PragTagHelper : ITagHelperComponent { public int Order => 2; //這個優先順序可以隨意 public void Init(TagHelperContext context) { // 不用寫程式碼 } public Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { // 內容之前 output.PreContent.SetHtmlContent("<strong>"); // 內容之後 output.PostContent.SetHtmlContent("</strong>"); return Task.CompletedTask; } }
@page
@addTagHelper TestApp.PragTagHelper, TestApp
<p>孔明用槍打死了王司徒</p>
<p>孔明用手雷轟死了王朗</p>
執行程式後,發現不起作用。生成的 HTML 文件沒有插入<strong>元素。
然後,我把標記幫助器的程式碼改一改。這次我們們不實現 ITagHelperComponent 介面,而是 ITagHelper 介面。
[HtmlTargetElement("p")] public class PragTagHelper : ITagHelper { …… }
然後再次執行。喲西,這下起作用了。
嗯,看來 ITagHelper 介面裡面有文章,從宣告可以看到,這個介面是繼承 ITagHelperComponent 介面的。但這個介面是空的,沒定義新成員。
public interface ITagHelper : ITagHelperComponent { }
這樣就可以得出結論:ITagHelper 介面是一個標記介面,用來篩選出哪些型別可以用 @addTagHelper 指令引入——即哪些型別被認為是標記幫助器。
為了方便開發者定義自己的標記幫助器,ASP.NET Core 還提供了一個抽象類 TagHelper。
public abstract class TagHelper : ITagHelper, ITagHelperComponent { // 建構函式 protected TagHelper(); public virtual int Order { get; } public virtual void Init(TagHelperContext context); public virtual void Process(TagHelperContext context, TagHelperOutput output); public virtual Task ProcessAsync(TagHelperContext context, TagHelperOutput output); }
這個抽象類將介面的實現成員都宣告為虛方法,派生時開發者可以按需重寫。於是,我們們前面那個例子可以做以下修改:
[HtmlTargetElement("p")] public class PragTagHelper : TagHelper { public override void Process(TagHelperContext context, TagHelperOutput output) { // 內容之前 output.PreContent.SetHtmlContent("<strong>"); // 內容之後 output.PostContent.SetHtmlContent("</strong>"); } }
我們們前面試過,ITagHelperComponent 的實現類是不能被 @addTagHelper 指令發現的,那麼,這個介面還有沒有用呢?當然有用,只是直接實現這個介面的類,只針對<head>和<body>元素,通常用於大面積修改 HTML 的情形。比如,你要在 <body> 元素中插入一段 js 指令碼,插入一堆HTML元素,插入一段CSS樣式等。
來,我們們用例子來說明。
public class InsertStylesTagHelper : ITagHelperComponent { public int Order => 105; public void Init(TagHelperContext context) { // 空白 } public Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { // 要判斷一下是不是<head>元素 if(output.TagName.Equals("head", StringComparison.OrdinalIgnoreCase)) { // 插入以下CSS string css = """ <style> h5 { color: blue; } h3 { color: green; font-style: italic; } p[setfont] { font-family: '楷體'; } </style> """; output.PreContent.AppendHtml(css); } return Task.CompletedTask; } }
這個幫助器就是在 <head> 元素內容的前面插入一段 supper style,不,是 CSS。因為CSS是一大段文字,這裡老周用到了 C# 的原義文字塊(就是不轉義特殊字元),這個功能和 Python 中的差不多。只是C#要求左"""後要換行,右"""前也要換行。這樣規定可能是為了寫起程式碼來好看。
這種不實現 ITagHelper 的型別不能用 @addTagHelper 指令來引入,而是要新增到 ITagHelperComponentManager 介面的 Components 屬性中,此屬性是個列表物件,可以Add。
@page @using Microsoft.AspNetCore.Mvc.Razor.TagHelpers @using TestApp @inject ITagHelperComponentManager tagHelperManager @{ // 手動新增TagHelper元件 var mytaghelper = new InsertStylesTagHelper(); // 新增到元件列表中 tagHelperManager.Components.Add(mytaghelper); } <html> <head> <title>好看的例子</title> <meta charset="UTF-8" /> </head> <body> <h3>三號標題</h3> <h5>五號標題</h5> <h2>二號標題 - 此處不應用樣式</h2> <p>其他內容 - 不應用樣式</p> <p setfont>使用楷書字型</p> </body> </html>
這樣一弄,在執行程式後,自定義的 InsertStylesTagHelper 類就會自動應用到 head 標記上。
還沒完呢,接下來我們們偷窺一下 TagHelperComponentManager 類的原始碼。
internal sealed class TagHelperComponentManager : ITagHelperComponentManager { /// <summary> /// Creates a new <see cref="TagHelperComponentManager"/>. /// </summary> /// <param name="tagHelperComponents">The collection of <see cref="ITagHelperComponent"/>s.</param> public TagHelperComponentManager(IEnumerable<ITagHelperComponent> tagHelperComponents) { if (tagHelperComponents == null) { throw new ArgumentNullException(nameof(tagHelperComponents)); } Components = new List<ITagHelperComponent>(tagHelperComponents); } /// <inheritdoc /> public ICollection<ITagHelperComponent> Components { get; } }
其實這程式碼沒啥好看的,只要注意它的建構函式就行了。不知道你看到這個建構函式想到了啥,老周想到了依賴注入。什麼意思?就是說:你把實現 ITagHelperComponent 介面的類都註冊為服務,那麼,它就會自動起作用了,而且是面向整個應用程式的 Razor 程式碼。剛才我們們用依賴注入獲取 ITagHelperComponentManager,並手動新增標記幫助器物件的方法是區域性的,只對當前 Razor 文件有效。
所以,下面我們們把自己寫的 InsertStylesTagHelper 註冊為服務。
var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorPages(); builder.Services.AddTransient<ITagHelperComponent, InsertStylesTagHelper>(); var app = builder.Build();
最後回到 Razor 文件,打掃一下,手動新增到 Components 列表的程式碼現在不需要了。
@page <html> <head> <title>好看的例子</title> <meta charset="UTF-8" /> </head> <body> <h3>三號標題</h3> <h5>五號標題</h5> <h2>二號標題 - 此處不應用樣式</h2> <p>其他內容 - 不應用樣式</p> <p setfont>使用楷書字型</p> </body> </html>
再次執行一下,你會發現也是可行的,<head>元素內也插入了 CSS 樣式。
好了,今天我們們就聊到這兒吧。