【ASP.NET Core】標記幫助器——抽象層

東邪獨孤發表於2023-02-18

標記幫助器,即 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;
    }
}
[HtmlTargetElement("p")] 特性表示:我這個標記幫助器是專為<p>元素準備,它只作用於此元素。上述例子的意思是在<p>元素的內部文字呈現之前插入“<strong>”,在內部文字呈現之後插入“</strong>”。就是實現了讓段落中的文字加粗顯示的效果。PreContent 表示元素內容之前,PostContent 表示元素內容之後。
現在,我們們在 Razor 文件用 @addTagHelper 指令匯入一下。
@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。

ITagHelperComponentManager 類有個內部實現的類叫 TagHelperComponentManager,這個類沒有對外公開,但不影響我們使用。當我們在服務容器上開啟 MVC、RazorPages 等功能時,會自動向容器註冊 ITagHelperComponentManager 。因此,在 Razor 文件中,我們們可以透過依賴注入獲取它,然後把自己定義的 TagHelper 放進 Components 列表即可。
@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 樣式。

 

好了,今天我們們就聊到這兒吧。

 

相關文章