项目

Scriban 集成

安装

建议使用 ABP CLI 来安装此包。

使用 ABP CLI

在项目文件夹(.csproj 文件所在目录)中打开命令行窗口,输入以下命令:

abp add-package Volo.Abp.TextTemplating.Scriban

手动安装

如果你想手动安装:

  1. Volo.Abp.TextTemplating.Scriban NuGet 包添加到你的项目:

    dotnet add package Volo.Abp.TextTemplating.Scriban
    
  2. AbpTextTemplatingScribanModule 添加到模块的依赖列表中:

    [DependsOn(
        //...其他依赖
        typeof(AbpTextTemplatingScribanModule) //添加新模块依赖
        )]
    public class YourModule : AbpModule
    {
    }
    

定义模板

在渲染模板之前,你应该先定义它。创建一个继承自 TemplateDefinitionProvider 基类的类:

public class DemoTemplateDefinitionProvider : TemplateDefinitionProvider
{
    public override void Define(ITemplateDefinitionContext context)
    {
        context.Add(
            new TemplateDefinition("Hello") //模板名称:"Hello"
                .WithVirtualFilePath(
                    "/Demos/Hello/Hello.tpl", //模板内容路径
                    isInlineLocalized: true
                )
                .WithScribanEngine()
        );
    }
}
  • context 对象用于添加新模板或获取依赖模块定义的模板。使用 context.Add(...) 来定义新模板。
  • TemplateDefinition 类代表一个模板。每个模板必须有一个唯一的名称(在渲染模板时会用到)。
  • /Demos/Hello/Hello.tpl 是模板文件的路径。
  • isInlineLocalized 用于声明你是对所有语言使用单个模板(true)还是为每种语言使用不同的模板(false)。详见下文本地化部分。
  • WithRenderEngine 方法用于设置模板的渲染引擎。

模板内容

WithVirtualFilePath 表明我们使用 虚拟文件系统 来存储模板内容。在你的项目中创建一个Hello.tpl 文件,并在属性窗口中将其标记为“嵌入式资源”: hello-template

Hello.tpl 内容示例如下:

Hello { { { model.name } } } :)

虚拟文件系统 要求在你的 模块 类的 ConfigureServices 方法中添加你的文件:

Configure<AbpVirtualFileSystemOptions>(options =>
{
    options.FileSets.AddEmbedded<TextTemplateDemoModule>("TextTemplateDemo");
});
  • TextTemplateDemoModule 是你在其中定义模板的模块类。
  • TextTemplateDemo 是你项目的根命名空间。

渲染模板

ITemplateRenderer服务用于渲染模板内容。

示例:渲染简单模板

public class HelloDemo : ITransientDependency
{
    private readonly ITemplateRenderer _templateRenderer;

    public HelloDemo(ITemplateRenderer templateRenderer)
    {
        _templateRenderer = templateRenderer;
    }

    public async Task RunAsync()
    {
        var result = await _templateRenderer.RenderAsync(
            "Hello", //模板名称
            new HelloModel
            {
                Name = "John"
            }
        );

        Console.WriteLine(result);
    }
}
  • HelloDemo 是一个简单的类,在其构造函数中注入 ITemplateRenderer 并在 RunAsync 方法中使用它。
  • RenderAsync 接收两个基本参数:
    • templateName:要渲染的模板名称(本例中为 Hello)。
    • model:在模板内部用作 model 的对象(本例中为 HelloModel 对象)。

此示例的结果如下:

Hello John : )

匿名模型

虽然建议为模板创建模型类,但在简单情况下使用匿名对象会更实用(也是可能的):

var result = await _templateRenderer.RenderAsync(
    "Hello",
    new
    {
        Name = "John"
    }
);

在这种情况下,我们没有创建模型类,而是创建了一个匿名对象作为模型。

PascalCase 与 snake_case

PascalCase属性名(如UserName)在模板中用作snake_case(如user_name)。

本地化

可以根据当前文化对模板内容进行本地化。有两种本地化选项,在以下章节中描述。

内联本地化

内联本地化使用本地化系统来本地化模板内的文本。

示例:重置密码链接

假设你需要向用户发送电子邮件以重置他/她的密码。这里是模板内容:

[{ { { L "ResetMyPassword" model.name } } }](/docs/zh-Hans/abp-docs/master/framework/infrastructure/text-templating/{ { { model.link } } })

L函数用于根据当前用户文化本地化给定的键。你需要在本地化文件中定义ResetMyPassword键:

"ResetMyPasswordTitle": "重置我的密码",
"ResetMyPassword": "你好 {0},点击这里重置你的密码"

你还需要在模板定义提供者类中声明与此模板一起使用的本地化资源:

context.Add(
    new TemplateDefinition(
            "PasswordReset", //模板名称
            typeof(DemoResource) //本地化资源
        )
        .WithScribanEngine()
        .WithVirtualFilePath(
            "/Demos/PasswordReset/PasswordReset.tpl", //模板内容路径
            isInlineLocalized: true
        )
);

这样就可以了。当你像这样渲染此模板时:

var result = await _templateRenderer.RenderAsync(
    "PasswordReset", //模板名称
    new PasswordResetModel
    {
        Name = "john",
        Link = "https://abp.io/example-link?userId=123&token=ABC"
    }
);

你将看到本地化的结果:

<a title="重置我的密码" href="https://abp.io/example-link?userId=123&token=ABC">你好 john,点击这里重置你的密码</a>

如果你为应用程序定义了默认本地化资源,则无需为模板定义声明资源类型。

多内容本地化

与其使用本地化系统来本地化单个模板,你可能希望为每种语言创建不同的模板文件。如果模板对于特定文化需要完全不同(而不仅仅是简单的文本本地化),则可能需要这样做。

示例:欢迎邮件模板

假设你想向用户发送欢迎邮件,但希望根据用户文化定义完全不同的模板。

首先,创建一个文件夹并将你的模板放入其中,例如en.tpltr.tpl... 为你支持的每种文化创建一个: multiple-file-template

然后在模板定义提供者类中添加你的模板定义:

context.Add(
    new TemplateDefinition(
            name: "WelcomeEmail",
            defaultCultureName: "en"
        )
        .WithScribanEngine()
        .WithVirtualFilePath(
            "/Demos/WelcomeEmail/Templates", //模板内容文件夹
            isInlineLocalized: false
        )
);
  • 设置默认文化名称,以便在没有所需文化的模板时回退到默认文化。
  • 指定模板文件夹而不是单个模板文件。
  • 对于这种情况,将isInlineLocalized设置为false

这样就可以了,你可以为当前文化渲染模板:

var result = await _templateRenderer.RenderAsync("WelcomeEmail");

为保持示例简洁,此处省略了模型,但你可以像之前解释的那样使用模型。

指定文化

如果未指定,ITemplateRenderer服务使用当前文化(CultureInfo.CurrentUICulture)。如果需要,你可以将文化指定为cultureName参数:

var result = await _templateRenderer.RenderAsync(
    "WelcomeEmail",
    cultureName: "en"
);

布局模板

布局模板用于在其他模板之间创建共享布局。它类似于 ASP.NET Core MVC / Razor Pages 中的布局系统。

示例:电子邮件 HTML 布局模板

例如,你可能希望为所有电子邮件模板创建一个统一的布局。

首先,像之前一样创建一个模板文件:

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
</head>
<body>
    { { { content } } }
</body>
</html>
  • 布局模板必须有一个**{ { } }**部分作为渲染子内容的占位符。

然后在模板定义提供者中注册你的模板:

context.Add(
    new TemplateDefinition(
        "EmailLayout",
        isLayout: true //设置 isLayout!
    )
    .WithScribanEngine()
    .WithVirtualFilePath(
        "/Demos/EmailLayout/EmailLayout.tpl",
        isInlineLocalized: true
    )
);

现在,你可以将此模板用作任何其他模板的布局:

context.Add(
    new TemplateDefinition(
            name: "WelcomeEmail",
            defaultCultureName: "en",
            layout: "EmailLayout" //设置 LAYOUT
        )
        .WithScribanEngine()
        .WithVirtualFilePath(
            "/Demos/WelcomeEmail/Templates",
            isInlineLocalized: false
        )
);

全局上下文

ABP传递 model,该模型可以在模板内部访问。如果需要,你可以传递更多全局变量。

模板内容示例:

A global object value: { { { myGlobalObject } } }

此模板假设模板渲染上下文中存在一个myGlobalObject对象。你可以如下所示提供它:

var result = await _templateRenderer.RenderAsync(
    "GlobalContextUsage",
    globalContext: new Dictionary<string, object>
    {
        {"myGlobalObject", "TEST VALUE"}
    }
);

渲染结果将是:

A global object value: TEST VALUE

替换现有模板

可以替换应用程序中使用的模块定义的模板。这样,你可以根据自己的需求自定义模板,而无需更改模块代码。

选项-1:使用虚拟文件系统

虚拟文件系统允许你通过将相同文件放置到项目中的相同路径来覆盖任何文件。

示例:替换标准电子邮件布局模板

ABP提供了一个电子邮件发送系统,该系统内部使用文本模板来渲染电子邮件内容。它在/Volo/Abp/Emailing/Templates/Layout.tpl路径定义了一个标准的电子邮件布局模板。该模板的唯一名称是Abp.StandardEmailTemplates.Layout,此字符串定义为Volo.Abp.Emailing.Templates.StandardEmailTemplates静态类上的常量。

按照以下步骤用你自己的模板文件替换该模板:

1) 在你的项目中相同位置(/Volo/Abp/Emailing/Templates/Layout.tpl)添加一个新文件: replace-email-layout

2) 准备你的电子邮件布局模板:

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
</head>
<body>
    <h1>这是我的页眉</h1>

    { { { content } } }

    <footer>
      这是我的页脚...
    </footer>
</body>
</html>

此示例简单地向模板添加了页眉和页脚,并在它们之间渲染内容(参见上面的布局模板部分以理解)。

3).csproj文件中配置嵌入式资源

  • Microsoft.Extensions.FileProviders.Embedded NuGet包添加到项目。

  • .csproj文件的<PropertyGroup>...</PropertyGroup>部分添加<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>

  • 将以下代码添加到你的.csproj文件:

    <ItemGroup>
      <None Remove="Volo\Abp\Emailing\Templates\*.tpl" />
      <EmbeddedResource Include="Volo\Abp\Emailing\Templates\*.tpl" />
    </ItemGroup>
    

    这使模板文件成为“嵌入式资源”。

4) 配置虚拟文件系统

在你的模块ConfigureServices方法中配置AbpVirtualFileSystemOptions,将嵌入式文件添加到虚拟文件系统:

Configure<AbpVirtualFileSystemOptions>(options =>
{
    options.FileSets.AddEmbedded<BookStoreDomainModule>();
});

在此示例代码中,BookStoreDomainModule应是你的模块名称。

请确保你的模块(直接或间接)依赖于AbpEmailingModule。因为VFS可以根据依赖顺序覆盖文件。

现在,当你想要渲染电子邮件布局模板时,将使用你的模板。

选项-2:使用模板定义提供者

你可以创建一个模板定义提供者类,该类获取电子邮件布局模板并更改该模板的虚拟文件路径。

示例:使用/MyTemplates/EmailLayout.tpl文件代替标准模板

using Volo.Abp.DependencyInjection;
using Volo.Abp.Emailing.Templates;
using Volo.Abp.TextTemplating;

namespace MyProject
{
    public class MyTemplateDefinitionProvider
        : TemplateDefinitionProvider, ITransientDependency
    {
        public override void Define(ITemplateDefinitionContext context)
        {
            var emailLayoutTemplate = context.GetOrNull(StandardEmailTemplates.Layout);

            emailLayoutTemplate
                .WithVirtualFilePath(
                    "/MyTemplates/EmailLayout.tpl",
                    isInlineLocalized: true
                );
        }
    }
}

你仍然需要如前所述将文件/MyTemplates/EmailLayout.tpl添加到虚拟文件系统中。这种方法允许你将模板定位在任何文件夹中,而不是依赖模块定义的文件夹。

除了模板内容,你还可以操作模板定义属性,如DisplayNameLayoutLocalizationSource

高级特性

本节介绍文本模板系统的一些内部原理和更高级的用法。

模板内容提供者

ITemplateRenderer用于渲染模板,在大多数情况下这正是你想要的。但是,你可以使用ITemplateContentProvider来获取原始(未渲染的)模板内容。

ITemplateContentProvider在内部被ITemplateRenderer用来获取原始模板内容。

示例:

public class TemplateContentDemo : ITransientDependency
{
    private readonly ITemplateContentProvider _templateContentProvider;

    public TemplateContentDemo(ITemplateContentProvider templateContentProvider)
    {
        _templateContentProvider = templateContentProvider;
    }

    public async Task RunAsync()
    {
        var result = await _templateContentProvider
            .GetContentOrNullAsync("Hello");

        Console.WriteLine(result);
    }
}

结果将是原始模板内容:

Hello { { { model.name } } } : )
  • GetContentOrNullAsync如果请求的模板没有定义内容,则返回null
  • 它可以接收一个cultureName参数,如果模板针对不同文化有不同的文件,则会使用该参数(参见上文多内容本地化部分)。

模板内容贡献者

ITemplateContentProvider服务使用ITemplateContentContributor实现来查找模板内容。有一个预实现的内容贡献者VirtualFileTemplateContentContributor,它从虚拟文件系统中获取模板内容,如上所述。

你可以实现ITemplateContentContributor以从另一个源读取原始模板内容。

示例:

public class MyTemplateContentProvider
    : ITemplateContentContributor, ITransientDependency
{
    public async Task<string> GetOrNullAsync(TemplateContentContributorContext context)
    {
        var templateName = context.TemplateDefinition.Name;

        //TODO: 尝试从另一个源查找内容
        return null;
    }
}

如果你的源找不到内容,则返回null,这样ITemplateContentProvider就会回退到下一个贡献者。

模板定义管理器

ITemplateDefinitionManager服务可用于获取模板定义(由模板定义提供者创建)。

另请参阅

在本文档中