Liquid 是一門開源的模板語言,由 Shopify 創造并用 Ruby 實作,它是 Shopify 主題的主要構成部分,并且被用于加載店鋪系統的動態內容,它是一種安全的模板語言,對于非程式員的受眾來說也非常容易理解,
Fluid 是一個基于 Liquid 模板語言的開源 .NET 模板引擎,由 Sébastien Ros 開發并發布在 GitHub 上,NuGet 上的參考地址是: https://www.nuget.org/packages/Fluid.Core ,
Liquid 模板語言
如果你對 Liquid 模板語言還不了解,可以先行查看筆者翻譯的 Liquid 模板語言中文檔案: https://www.coderbusy.com/archives/1219.html ,Liquid 模板的檔案擴展名為 .liquid ,假如我們有以下 Liquid 模板:
<ul id="products"> {% for product in products %} <li> <h2>{{product.name}}</h2> Only {{product.price | price }} {{product.description | prettyprint | paragraph }} </li> {% endfor %} </ul>
該模板被渲染后將會產生以下輸出:
<ul id="products"> <li> <h2>Apple</h2> $329 Flat-out fun. </li> <li> <h2>Orange</h2> $25 Colorful. </li> <li> <h2>Banana</h2> $99 Peel it. </li> </ul>
在專案中使用 Fluid
你可以直接在專案中參考 NuGet 包,
Hello World
C# 代碼:
var parser = new FluidParser(); var model = new { Firstname = "Bill", Lastname = "Gates" }; var source = "Hello {{ Firstname }} {{ Lastname }}"; if (parser.TryParse(source, out var template, out var error)) { var context = new TemplateContext(model); Console.WriteLine(template.Render(context)); } else { Console.WriteLine($"Error: {error}"); }
運行結果:
Hello Bill Gates
執行緒安全
FluidParser 型別是執行緒安全的,可以被整個應用程式共享,常規做法是將其定義為一個本地的靜態變數:
private static readonly FluidParser _parser = new FluidParser();
IFluidTemplate 型別也是執行緒安全的,其實體可以被快取起來,并被多個執行緒并發使用,
TemplateContext 不是執行緒安全的,每次使用時都應該新建一個實體,
過濾器
過濾器 改變 Liquid 物件的輸出,通過一個 | 符號分隔,
{{ "/my/fancy/url" | append: ".html" }}
/my/fancy/url.html
多個過濾器可以共同作用于同一個輸出,并按照從左到右的順序執行,
{{ "adam!" | capitalize | prepend: "Hello " }}
Hello Adam!
Fluid 實作了 Liquid 所有的標準過濾器,同時支持自定義過濾器,
自定義的過濾器可以是同步的,也可以是異步的,過濾器被定義為一個委托,該委托接收一個輸入,一個引數集合和當前的渲染背景關系,以下是一個實作文字轉小寫過濾器的代碼:
public static ValueTask<FluidValue> Downcase(FluidValue input, FilterArguments arguments, TemplateContext context) { return new StringValue(input.ToStringValue().ToLower()); }
過濾器需要注冊在 TemplateOptions 物件上,該 Options 物件可以被重用,
var options = new TemplateOptions(); options.Filters.AddFilter('downcase', Downcase); var context = new TemplateContext(options);
成員屬性白名單
Liquid 是一種安全的模板語言,它只允許白名單中的成員屬性被訪問,并且成員屬性不能被改變,白名單成員需要被加入到 TemplateOptions.MemberAccessStrategy 中,
另外,MemberAccessStrategy 可以被設定為 UnsafeMemberAccessStrategy ,這將允許模板語言訪問所有成員屬性,
將特定型別加入白名單
下面的代碼會將 Person 型別加入白名單,這意味著該型別下所有公開的欄位和屬性都可以被模板讀取:
var options = new TemplateOptions(); options.MemberAccessStrategy.Register<Person>();
注意:當用 new TemplateContext(model) 傳遞一個模型時,模型物件會被自動加入白名單,該行為可以通過呼叫 new TemplateContext(model, false) 來禁用,
將特定成員加入白名單
下面的代碼只允許模板讀取特定的成員:
var options = new TemplateOptions(); options.MemberAccessStrategy.Register<Person>("Firstname", "Lastname");
訪問攔截
Fluid 提供了一種可以在運行時攔截屬性訪問的方式,通過該方式你可以允許訪問成員并回傳自定義值,或者阻止訪問,
下面的代碼演示了如何攔截對 JObject 的呼叫并回傳相應的屬性:
var options = new TemplateOptions(); options.MemberAccessStrategy.Register<JObject, object>((obj, name) => obj[name]);
繼承處理
當被注冊到白名單中的型別包含繼承關系時,情況將變得復雜:默認情況下被注冊型別的父類實體成員將不能被訪問,子類實體中的派生成員可以被訪問,
型別定義
public class Animal { public string Type { get; set; } } public class Human : Animal { public string Name { get; set; } public Int32 Age { get; set; } } public class Boy : Human { public string Toys { get; set; } }
測驗代碼
var parser = new FluidParser(); var model = new { }; var source = @" Animal=Type:{{Animal.Type}} Human=Type:{{Human.Type}},Name:{{Human.Name}},Age:{{Human.Age}} Boy=Type:{{Boy.Type}},Name:{{Boy.Name}},Age:{{Boy.Age}},Toys:{{Boy.Toys}} "; var options = new Fluid.TemplateOptions { }; options.MemberAccessStrategy.Register(typeof(Human)); if (parser.TryParse(source, out var template, out var error)) { var context = new TemplateContext(model, options); context.SetValue("Animal", new Animal { Type = "Human" }); context.SetValue("Human", new Human { Type = "Human", Name = "碼農很忙", Age = 30 }); context.SetValue("Boy", new Boy { Type = "Human", Name = "小明", Age = 10, Toys = "小汽車" }); Console.WriteLine(template.Render(context)); } else { Console.WriteLine($"Error: {error}"); }
輸出結果
Animal=Type: Human=Type:Human,Name:碼農很忙,Age:30 Boy=Type:Human,Name:小明,Age:10,Toys:
成員名稱風格
默認情況下,注冊物件的屬性是區分大小寫的,并按照其源代碼中的內容進行注冊,例如,屬性 FirstName 將使用 {{ p.FirstName }} 標簽訪問,
同時,也可以配置使用不同的名稱風格,比如小駝峰(firstName)或者蛇形(first_name)風格,
以下代碼可以配置為使用小駝峰風格:
var options = new TemplateOptions(); options.MemberAccessStrategy.MemberNameStrategy = MemberNameStrategies.CamelCase;
執行限制
限制模板遞回
當呼叫 {% include 'sub-template' %} 陳述句時,有些模板可能會產生無限的遞回,從而阻塞服務器,為了防止這種情況,TemplateOptions 類定義了一個默認的 MaxRecursion = 100 ,防止模板的深度超過100 ,
限制模板執行
模板可能會不經意地創建無限回圈,這可能會使服務器無限期地運行而堵塞,為了防止這種情況,TemplateOptions 類定義了一個默認的 MaxSteps,默認情況下,這個值沒有被設定,
轉換 CLR 型別
當一個物件在模板中被操作時,它會被轉換為一個特定的 FluidValue 實體,該機制與 JavaScript 中的動態型別系統有些類似,
在Liquid中,它們可以是數字、字串、布林值、陣列或字典,Fluid會自動將CLR型別轉換為相應的Liquid型別,同時也提供專門的型別,
為了能夠定制這種轉換,你可以添加自定義的轉換器,
添加一個值轉換器
當轉換邏輯不能直接從一個物件的型別中推斷出來時,可以使用一個值轉換器,
值轉換器可以回傳:
- null 代表值不能被轉換,
- 一個 FluidValue 實體,代表停止進一步的轉換,并使用這個值,
- 其他物件實體,代表需要繼續使用自定義和內部型別映射進行轉換,
以下的代碼演示了如何將實作介面的任意實體轉換為自定義字串值:
var options = new TemplateOptions(); options.ValueConverters.Add((value) => value is IUser user ? user.Name : null);
注意:型別映射的定義是全域的,對整個程式都生效,
在模型中使用 Json.NET 物件
Json.NET 中使用的類并不像類那樣有直接命名的屬性,這使得它們在 Liquid 模板中無法開箱使用,
為了彌補這一點,我們可以配置 Fluid,將名稱映射為 JObject 屬性,并將 JValue 物件轉換為 Fluid 所使用的物件,
var options = new TemplateOptions(); // When a property of a JObject value is accessed, try to look into its properties options.MemberAccessStrategy.Register<JObject, object>((source, name) => source[name]); // Convert JToken to FluidValue options.ValueConverters.Add(x => x is JObject o ? new ObjectValue(o) : null); options.ValueConverters.Add(x => x is JValue v ? v.Value : null); var model = JObject.Parse("{\"Name\": \"Bill\"}"); var parser = new FluidParser(); parser.TryParse("His name is {{ Name }}", out var template); var context = new TemplateContext(model, options); Console.WriteLine(template.Render(context));
編碼
默認情況下,Fluid 不會對輸出進行編碼,在模板上呼叫 Render() 或 RenderAsync() 時可以指定編碼器,
HTML 編碼
可以使用 System.Text.Encodings.Web.HtmlEncoder.Default 實體來渲染 HTML 編碼的模板,
該編碼被 MVC View engine 作為默認編碼使用,
在背景關系中禁用編碼
當一個編碼器被定義后,你可以使用一個特殊的 raw 過濾器或 {% raw %} … {% endraw %} 標簽來阻止一個值被編碼,例如,如果你知道這個內容是 HTML 并且是安全的:
代碼
{% assign html = '<em>This is some html</em>' %}
Encoded: {{ html }}
Not encoded: {{ html | raw }
結果
<em%gt;This is some html</em%gt; <em>This is some html</em>
Capture 塊不會被二次編碼
當使用 capture 塊時,內部內容被標記為預編碼,如果在 {{ }} 標簽中使用,就不會被再次編碼,
代碼
{% capture breaktag %}<br />{% endcapture %}
{{ breaktag }}
結果
<br />
本地化
默認情況下,模板使用不變的文化( Invariant culture ,對應 CultureInfo.InvariantCulture ,)進行渲染,這樣在不同的系統中可以得到一致的結果,這項設定在輸出日期、時間和數字時很重要,
即便如此,也可以使用 TemplateContext.CultureInfo 屬性來定義渲染模板時使用的文化資訊(你也可以稱之為多語言資訊),
代碼
var options = new TemplateOptions(); options.CultureInfo = new CultureInfo("en-US"); var context = new TemplateContext(options); var result = template.Render(context);
模板
{{ 1234.56 }}
{{ "now" | date: "%v" }}
結果
1234.56 Tuesday, August 1, 2017
時區
系統時區
TemplateOptions 和 TemplateContext 提供了一個定義默認時區的屬性,以便在決議日期和時間時使用,該屬性的默認值是當前系統的時區,當日期和時間被決議而沒有指定時區時,將會使用默認時區,設定一個自定義的時區可以防止在不同環境(資料中心)時產生不同的結果,
注意:date 過濾器符合 Ruby 的日期和時間格式: https://ruby-doc.org/core-3.0.0/Time.html#method-i-strftime ,要使用 .NET 標準的日期格式,請使用 format_date 過濾器,
代碼
var context = new TemplateContext { TimeZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time") } ; var result = template.Render(context);
模板
{{ '1970-01-01 00:00:00' | date: '%c' }}
結果
Wed Dec 31 19:00:00 -08:00 1969
時區轉換
日期和時間可以使用 time_zone 標簽轉換為特定的時區,格式為:time_zone:<iana> ,
代碼
var context = new TemplateContext(); context.SetValue("published", DateTime.UtcNow);
模板
{{ published | time_zone: 'America/New_York' | date: '%+' }}
結果
Tue Aug 1 17:04:36 -05:00 2017
自定義標簽和塊
Fluid 的語法可以被修改,以使其接受任何新的標記(tag)和帶有任何自定義引數的塊(block),Fluid 使用了 Parlot 作為語法分析器,這使得 Fluid 完全可擴展,
與塊(block)不同,標記(tag)沒有結束元素(例如:回圈,自增),當把一個模板的某個部分作為一組陳述句來操作時,塊很有用,
Fluid 提供了用于注冊常見標簽和塊的幫助方法,所有的標簽和塊總是以他們的名稱作為識別符號開始,
自定義標簽時需要提供一個委托(delegate),該委托會在標簽被匹配時執行,該委托可以使用使用以下三個屬性:
- writer,TextWriter的實體,用于渲染文字,
- encode,TextEncoder的實體,例如 HtmlEncoder 或者 NullEncoder,由模板的呼叫者定義,
- context,TemplateContext 的實體,
注冊自定義標簽
自定義標簽可以分為三種型別:
- Empty:空白標簽,沒有任何引數,例如 {% renderbody %} ,
- Identifier:識別符號,將識別符號作為標簽引數,例如 {% increment my_variable %} ,
- Expression:運算式,以運算式作為引數,例如 {% layout 'home' | append: '.liquid' %} ,
代碼
parser.RegisterIdentifierTag("hello", (identifier, writer, encoder, context) => { writer.Write("Hello "); writer.Write(identifier); });
模板
{% hello you %}
結果
Hello you
注冊自定義塊
塊的創建方式與標記相同,可以在委托中訪問塊內的陳述句串列,
原始碼
parser.RegisterExpressionBlock("repeat", (value, statements, writer, encoder, context) => { for (var i = 0; i < value.ToNumber(); i++) { await return statements.RenderStatementsAsync(writer, encoder, context); } return Completion.Normal; });
模板
{% repeat 1 | plus: 2 %}Hi! {% endrepeat %}
結果
Hi! Hi! Hi!
自定義模板決議
如果 identifier、 empty 和 expression 決議器不能滿足你的要求,RegisterParserBlock 和 RegisterParserTag 方法可以接受自定義的決議結構,這些結構可以是 FluidParser 中定義的標準決議器,例如 Primary 或者其他任意組合,
例如,RegisterParseTag(Primary.AndSkip(Comma).And(Primary), …) 將期望兩個 Primary 元素用逗號隔開,然后,該委托將被呼叫,使用 ValueTuple<Expression, Expression> 代表這兩個 Primary 運算式,
注冊自定義運算子
運算子是用來比較數值的,比如 > 或 contains ,如果需要提供特殊的比較,可以定義自定義運算子,
自定義 xor 運算子
下面的例子創建了一個自定義的 xor 運算子,如果左或右運算式被轉換為布爾時只有一個是真的,它將為真,
using Fluid.Ast; using Fluid.Values; using System.Threading.Tasks; namespace Fluid.Tests.Extensibility { public class XorBinaryExpression : BinaryExpression { public XorBinaryExpression(Expression left, Expression right) : base(left, right) { } public override async ValueTask<FluidValue> EvaluateAsync(TemplateContext context) { var leftValue = https://www.cnblogs.com/Soar1991/archive/2021/05/02/await Left.EvaluateAsync(context); var rightValue = https://www.cnblogs.com/Soar1991/archive/2021/05/02/await Right.EvaluateAsync(context); return BooleanValue.Create(leftValue.ToBooleanValue() ^ rightValue.ToBooleanValue()); } } }
配置決議器
parser.RegisteredOperators["xor"] = (a, b) => new XorBinaryExpression(a, b);
模板
{% if true xor false %}Hello{% endif %}
結果
Hello
空白控制
Liquid 在支持空白方面遵循嚴格的規則,默認情況下,所有的空格和新行都從模板中保留下來,Liquid 的語法和一些 Fluid 選項允許自定義這種行為,
通過連字符控制空白輸出
例如有以下模板:
{% assign name = "Bill" %}
{{ name }}
在 assign 標簽之后的換行將被保留下來,輸出如下:
Bill
標簽和值可以使用連字符來剝離空白,
{% assign name = "Bill" -%}
{{ name }}
這將輸出:
Bill
模板中的 -%} 將 assign 標簽右側的空白部分剝離,
通過模板選項控制空白輸出
Fluid 提供了 TemplateOptions.Triming 屬性,可以用預定義的偏好來設定何時應該自動剝離空白,即使標簽和輸出值中不存在連字符,
貪婪模式
當 TemplateOptions.Greedy 中的貪婪模式被禁用時,只有第一個新行之前的空格被剝離,貪婪模式默認啟用,這是 Liquid 語言的標準行為,
自定義過濾器
Fliud 默認提供了一些非標準過濾器,
format_date
使用標準的 .NET 日期和時間格式來格式化日期和時間,它使用系統當前的多語言資訊,
輸入
"now" | format_date: "G"
輸出
6/15/2009 1:45:30 PM
詳細的檔案可以看這里: https://docs.microsoft.com/zh-cn/dotnet/standard/base-types/standard-date-and-time-format-strings
format_number
使用 .NET 數字格式來格式化數字,
輸入
123 | format_number: "N"
輸出
123.00
詳細的檔案可以看這里:https://docs.microsoft.com/zh-cn/dotnet/standard/base-types/standard-numeric-format-strings
format_string
格式化字串
輸入
"hello {0} {1:C}" | format_string: "world" 123
輸出
hello world $123.00
詳細的檔案可以看這里:https://docs.microsoft.com/zh-cn/dotnet/api/system.string.format?view=net-5.0
性能測驗
快取
如果你在渲染之前對決議過的模板進行快取,你的應用程式可以獲得一些性能提升,決議是記憶體安全的,因為它不會引起任何編譯(意味著如果你決定決議大量的模板,所有的記憶體都可以被收集),你可以通過存盤和重用 FluidTemplate 實體來跳過決議步驟,
只要每次對 Render() 的呼叫使用一個獨立的 TemplateContext 實體,這些物件就是執行緒安全的,
基準測驗
Fluid 專案的源代碼中提供了一個基準測驗應用程式,用于比較 Fluid、Scriban、DotLiquid 和 Liquid.NET ,在本地運行該專案,分析執行特定模板所需的時間,
Results
Fluid 比所有其他知名的 .NET Liquid 模板分析器更快,分配的記憶體更少,對于決議,Fluid 比 Scriban快30%,分配的記憶體少 3 倍,對于渲染,Fluid 比 Scriban 快 3 倍,分配的記憶體少 5 倍,與 DotLiquid 相比,Fluid 的渲染速度快 10 倍,分配的記憶體少 40 倍,
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 Intel Core i7-1065G7 CPU 1.30GHz, 1 CPU, 8 logical and 4 physical cores .NET Core SDK=5.0.201 [Host] : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT ShortRun : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT Job=ShortRun IterationCount=3 LaunchCount=1 WarmupCount=3 | Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | |------------------- |--------------:|-------------:|------------:|-------:|--------:|----------:|---------:|--------:|------------:| | Fluid_Parse | 7.056 us | 1.081 us | 0.0592 us | 1.00 | 0.00 | 0.6714 | - | - | 2.77 KB | | Scriban_Parse | 9.209 us | 2.989 us | 0.1638 us | 1.31 | 0.03 | 1.8005 | - | - | 7.41 KB | | DotLiquid_Parse | 38.978 us | 13.704 us | 0.7512 us | 5.52 | 0.14 | 2.6855 | - | - | 11.17 KB | | LiquidNet_Parse | 73.198 us | 25.888 us | 1.4190 us | 10.37 | 0.29 | 15.1367 | 0.1221 | - | 62.08 KB | | | | | | | | | | | | | Fluid_ParseBig | 38.725 us | 11.771 us | 0.6452 us | 1.00 | 0.00 | 2.9907 | 0.1831 | - | 12.34 KB | | Scriban_ParseBig | 49.139 us | 8.313 us | 0.4557 us | 1.27 | 0.02 | 7.8125 | 1.0986 | - | 32.05 KB | | DotLiquid_ParseBig | 208.644 us | 45.839 us | 2.5126 us | 5.39 | 0.15 | 13.1836 | 0.2441 | - | 54.39 KB | | LiquidNet_ParseBig | 24,211.719 us | 3,862.113 us | 211.6955 us | 625.30 | 8.32 | 6843.7500 | 375.0000 | - | 28557.49 KB | | | | | | | | | | | | | Fluid_Render | 414.462 us | 12.612 us | 0.6913 us | 1.00 | 0.00 | 22.9492 | 5.3711 | - | 95.75 KB | | Scriban_Render | 1,141.302 us | 114.127 us | 6.2557 us | 2.75 | 0.02 | 99.6094 | 66.4063 | 66.4063 | 487.64 KB | | DotLiquid_Render | 5,753.263 us | 7,420.054 us | 406.7182 us | 13.88 | 0.96 | 867.1875 | 125.0000 | 23.4375 | 3879.18 KB | | LiquidNet_Render | 3,262.545 us | 1,245.387 us | 68.2639 us | 7.87 | 0.18 | 1000.0000 | 390.6250 | - | 5324.5 KB |
以上結果的測驗時間是 2021年3月26 日,使用的組件詳情如下:
- Scriban 3.6.0
- DotLiquid 2.1.405
- Liquid.NET 0.10.0
測驗專案說明
Parse:決議一個包含過濾器和屬性的簡單 HTML 模板,
ParseBig:決議一個博客文章模板,
Render:使用 500 個產品渲染一個包含過濾器和屬性的簡單 HTML 模板,
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/282215.html
標籤:.NET技术
上一篇:C#,WPF
下一篇:vb net 學習筆記
