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 = newFluidParser();
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 对象能够被重用。 varoptions =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<erson>();
重视:当用 new TemplateContext(model) 传递一个模型时,模型对象会被自动加入白名单。该行径能够经过调用 new TemplateContext(model, false) 来禁用。
将特定成员加入白名单
下面的代码只准许模板读取特定的成员: var options = new TemplateOptions();options.MemberAccessStrategy.Register<erson>("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 = newTemplateOptions();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 isIUser 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 propertiesoptions.MemberAccessStrategy.Register<JObject, object>((source, name) => source[name]);// Convert JToken to FluidValueoptions.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 = newTemplateContext(options);var result = template.Render(context);
模板 {{ 1234.56 }}{{ "now" | date: "%v" }}
结果 1234.56Tuesday, 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 = newTemplateContext { 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 = await Left.EvaluateAsync(context); var rightValue = await Right.EvaluateAsync(context); returnBooleanValue.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 中的贪婪模式被禁用时,仅有第1个新行之前的空格被剥离。贪婪模式默认启用,这是 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 。在本地运行该项目,分析执行特定模板所需的时间。
Fluid 比所有其他知名的 .NET Liquid 模板分析器更快,分配的内存更少。针对解析,Fluid 比 Scriban快30%,分配的内存少 3 倍。针对渲染,Fluid 比 Scriban 快 3 倍,分配的内存少 5 倍。与 DotLiquid 相比,Fluid 的渲染速度快 10 倍,分配的内存少 40 倍。 BenchmarkDotNet=v0.12.1, OS=Windows10.0.19042Intel 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 RyuJITJob=ShortRun IterationCount=3 LaunchCount=1WarmupCount=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 模板。
|