Blazor Webassembly:如何将组件插入字符串

Posted

技术标签:

【中文标题】Blazor Webassembly:如何将组件插入字符串【英文标题】:Blazor Webassembly: How to insert components into a string 【发布时间】:2022-01-21 04:29:39 【问题描述】:

我有这个显示通用消息的组件:

<span>@message</span>

消息由id 标识,来自资源文件中的字符串表(多种语言)。消息的示例是:

"Hello user! Welcome to site!"

所以在基本情况下,我只需解析字符串并将user 替换为“John Doe”,将site 替换为“MySiteName”。结果设置为message,然后正确(且安全)地呈现。

但我实际上想做的是用我创建的 component 替换 site,该组件显示具有特殊字体和样式的站点名称。我还有其他情况,我想用 components 替换特殊的 markings

您将如何解决这个问题?有没有办法将组件“插入”到字符串中,然后“安全地”插入字符串以进行渲染?我说“安全”是因为最终字符串的某些部分可能来自数据库并且本质上是不安全的(例如用户名),因此插入带有 @((MarkupString)message) 之类的字符串似乎并不安全。

编辑: 感谢 MrC aka Shaun Curtis,这个最终解决方案受到了极大的启发。我将他的答案标记为最佳答案。

所以我最终选择了一个范围服务,它从资源文件中获取字符串,解析它们并返回它从组件的静态表中获取的 RenderFragments 列表。我使用动态对象在需要时将特定参数发送到 RenderFragments。

我现在基本上可以通过这种集中机制获取我的应用程序的所有文本。

以下是资源文件字符串表中的条目示例:

Name: "welcome"; Value: "Welcome to site:name 0!"

这是它在组件中的使用方式:

<h3><Localizer Key="notif:welcome" Data="@(new List<string>()  NotifModel.UserNames.First )"/></h3>

您可以在下面看到简化的组件和服务代码。为简单起见,我明确省略了验证和错误检查代码。

@using MySite.Client.Services.Localizer
@inject ILocalizerService Loc 

@foreach (var fragment in _fragments)

  @fragment.Renderer(fragment.Item)


@code

  private List<ILocalizerService.Fragment> _fragments;

  public enum RendererTypes
  
    Default,
    SiteName,
    SiteLink,
  

  public static Dictionary<RendererTypes, RenderFragment<dynamic>> Renderers = new Dictionary<RendererTypes, RenderFragment<dynamic>>()
  
    // NOTE: For each of the following items, do NOT insert a space between the end of the markup and the closing curly brace otherwise it will be rendered !!!
    //                                                       Like here ↓↓
     RendererTypes.Default, (model) => @<span>@(model as string)</span>,
     RendererTypes.SiteName, (model) => @<MySiteNameComponent />,
     RendererTypes.SiteLink, (model) => @<a href="@model.LinkUrl">@model.LinkTxt</a>
  ;

  [Parameter]
  public string Key  get; set; 

  [Parameter]
  public List<string> Data  get; set; 

  protected override void OnParametersSet()
  
    _fragments = Loc.GetFragments(Key, Data);
  


interface ILocalizerService

  public struct Fragment
  
    public Fragment(RenderFragment<dynamic> renderer)
      : this(renderer, default)
    
    

    public Fragment(RenderFragment<dynamic> renderer, dynamic item)
    
      Renderer = renderer;
      Item = item;
    

    public RenderFragment<dynamic> Renderer  get; set; 
    public dynamic Item  get; set; 
  

  List<Fragment> GetFragments(string key, List<string> parameters);


internal sealed class LocalizerService : ILocalizerService

  private readonly Dictionary<string, IStringLocalizer> _strLoc = new Dictionary<string, IStringLocalizer>();

  public LocalizerService(IStringLocalizer<MySite.Shared.Resources.App> appLoc,
                          IStringLocalizer<MySite.Shared.Resources.Connection> connLoc,
                          IStringLocalizer<MySite.Shared.Resources.Notifications> notifLoc)
  
    // Keep string localizers
    _strLoc.Add("app", appLoc);
    _strLoc.Add("conn", connLoc);
    _strLoc.Add("notif", notifLoc);
  

  public List<Fragment> GetFragments(string key, List<string> parameters)
  
    var list = new List<Fragment>();

    GetFragments(list, key, parameters);

    return list;
  

  private void GetFragments(List<Fragment> list, string key, List<string> parameters)
  
    // First, get key tokens
    var tokens = key.Split(':');

    // Analyze first token
    switch (tokens[0])
    
      case "site":
        // Format : site:...
        ProcessSite(list, tokens, parameters);
        break;

      default:
        // Format : 0|1|2|...
        if (uint.TryParse(tokens[0], out var paramIndex))
        
          ProcessParam(list, paramIndex, parameters);
        
        // Format : app|conn|notif|...
        else if (_strLoc.ContainsKey(tokens[0]))
        
          ProcessStringLocalizer(list, tokens, parameters);
        
        break;
    

  

  private void ProcessSite(List<Fragment> list, string[] tokens, List<string> parameters)
  
    // Analyze second token
    switch (tokens[1])
    
      case "name":
        // Format site:name
        // Add name component
        list.Add(new Fragment(Shared.Localizer.Renderers[Shared.Localizer.RendererTypes.SiteName]));
        break;

      case "link":
        // Format site:link:...
        ProcessLink(list, tokens, parameters);
        break;
    
  

  private void ProcessLink(List<Fragment> list, string[] tokens, List<string> parameters)
  
    // Analyze third token
    switch (tokens[2])
    
      case "user":
        // Format: site:link:user:...
        ProcessLinkUser(list, tokens, parameters);
        break;
    
  

  private void ProcessLinkUser(List<Fragment> list, string[] tokens, List<string> parameters)
  
    // Check length
    var length = tokens.Length;
    if (length >= 4)
    
      string linkUrl;
      string linkTxt;

      // URL
      // Format: site:link:user:0|1|2|...
      // Retrieve handle from param
      if (!uint.TryParse(tokens[3], out var paramIndex))
      
        throw new ApplicationException("Invalid token!");
      
      var userHandle = GetParam(paramIndex, parameters);
      linkUrl = $"/user/userHandle";

      // Text
      if (length >= 5)
      
        if (tokens[4].Equals("t"))
        
          // Format: site:link:user:0|1|2|...:t
          // Use token directly as text
          linkTxt = tokens[4];
        
        else if (uint.TryParse(tokens[4], out paramIndex))
        
          // Format: site:link:user:0|1|2|...:0|1|2|...
          // Use specified param as text
          linkTxt = GetParam(paramIndex, parameters);
        
      
      else
      
        // Format: site:link:user:0|1|2|...
        // Use handle as text
        linkTxt = userHandle;
      

      // Add link component
      list.Add(new Fragment(Shared.Localizer.Renderers[Shared.Localizer.RendererTypes.SiteLink], new  LinkUrl = linkUrl, LinkTxt = linkTxt ));
    
  

  private void ProcessParam(List<Fragment> list, uint paramIndex, List<string> parameters)
  
    // Add text component
    list.Add(new Fragment(Shared.Localizer.Renderers[Shared.Localizer.RendererTypes.Default], GetParam(paramIndex, parameters)));
  

  private string GetParam(uint paramIndex, List<string> parameters)
  
    // Proceed
    if (paramIndex < parameters.Length)
    
      return parameters[paramIndex];
    
  

  private void ProcessStringLocalizer(List<Fragment> list, string[] tokens, List<string> parameters)
  
    // Format loc:str
    // Retrieve string localizer
    var strLoc = _strLoc[tokens[0]];

    // Retrieve string
    var str = strLoc[tokens[1]].Value;

    // Split the string in parts to see if it needs formatting
    // NOTE:  str is in the form "...xxx key0 yyy key1 zzz...".
    //        This means that once split, the keys are always at odd indexes (even if key starts or ends the string)
    var strParts = str.Split('', '');
    for (int i = 0; i < strParts.Length; i += 2)
    
      // Get parts
      var evenPart = strParts[i];
      var oddPart = ((i + 1) < strParts.Length) ? strParts[i + 1] : null;

      // Even parts are always regular text. If not null or empty, we add directly
      if (!string.IsNullOrEmpty(evenPart))
      
        list.Add(new Fragment(Shared.Localizer.Renderers[Shared.Localizer.RendererTypes.Default], evenPart));
      

      // Odd parts are always keys. If not null or empty, get fragments recursively
      if (!string.IsNullOrEmpty(oddPart))
      
        GetFragments(list, oddPart, parameters);
      
    
  

【问题讨论】:

在我整理答案之前检查一下:“用组件替换 site”中的组件是 Blazor 组件吗? “安全”问题建议您将 html 存储在后端并进行渲染。请详细说明输入/输出的范围。 @MrCakaShaunCurtis 是的,blazor 组件。例如 @HenkHolterman 不,我没有在后端存储 HTML。只需用户提供字符串,例如他们的名字。我提到安全性只是因为我担心如果我的问题的解决方案意味着使用“MarkupString”(或类似的东西),那么用户提供的字符串最终可能会在未经验证的情况下被渲染。例如,如果用户说他们的名字是“”,只是想确保可能的解决方案考虑到我显然不希望它成为实际的 HTML 的事实! 好的,那只是显示文本。例如“用户”字段应该有多“可配置”? 【参考方案1】:

您不一定需要构建组件。组件是发出 RenderFragment 的 C# 类。

您可以简单地为 site 构建RenderFragments,...这是一个简单的静态类,它显示了两种方法:

namespace ***Answers;

public static class RenderFragements

    public static RenderFragment SiteName => (builder) =>
    
        // Get the content from a service that's accessing a database and checking the culture info for language
        builder.OpenElement(0, "div");
        builder.AddAttribute(1, "class", "p-2 bg-primary text-white");
        builder.AddContent(2, "My Site");
        builder.CloseElement();
    ;

    public static RenderFragment GetSiteName(string sitename) => (builder) =>
   
       // parse to make sure you're happy with the string
       builder.OpenElement(0, "span");
       builder.AddAttribute(1, "class", "p-2 bg-dark text-white");
       builder.AddContent(2, sitename);
       builder.CloseElement();
   ;

这是一个使用它们的索引页面:

@page "/"
@using ***Answers

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

<div class=m-2>
The site name for this site is @(RenderFragements.GetSiteName("this site"))
</div>

@(RenderFragements.SiteName)

使用RenderFragment 编写 c# 代码。您可以在渲染之前运行解析器来检查字符串。

您可以有一个作用域服务,它从数据库中为用户获取信息并公开一组RenderFragments,然后您可以在您的页面/组件中使用。

【讨论】:

感谢您的回答。 RenderFragments 确实为我指明了正确的方向。我的最终解决方案(参见 OP 中的编辑)受到您的解决方案的极大启发,我将其标记为答案。但是,我没有像您的回答那样使用构建器;相反,我直接将渲染片段存储在静态表中。然后我的服务使用这个表来构建一个渲染片段列表。【参考方案2】:

我使用正则表达式在TokenMappings 中配置的标记处拆分源。例如,令牌映射可以很容易地从 json 源加载。要配置更多“markings”,只需在TokenMappings 中添加更多行。

<StringParser Source="Hello user! Welcome to site!" />

StringParser.razor

@foreach (var subString in substrings)

    if (tokens.Contains(subString))
    
        var key = StripCurlyBrackets(subString);
        <DynamicComponent Type=@(TokenMappings[key].Item1) 
                          Parameters=@(TokenMappings[key].Item2) />
    
    else
    
        @subString
    


@code 
    private Dictionary<string, (Type, Dictionary<string, object>?)> TokenMappings;
    private string[] substrings;
    private string[] tokens;

    [Parameter]
    public string Source  get; set; 

    protected override void OnParametersSet()
    
        var user = "John Doe";       // I would expect these are supplied via a signin context.
        var site = "MySiteName"; //  

        TokenMappings = new Dictionary<string, (Type, Dictionary<string, object>?)>
        
             "user", ( typeof(UserComponent), new Dictionary<string, object>  "User", user   ) ,
             "site", ( typeof(SiteComponent), new Dictionary<string, object>  "Site", site   ) 
        ;

        var keys = TokenMappings.Keys.Select(a => a);
        var pattern = keys.Select(key => $"((?:key))").Aggregate((a, b) => a + "|" + b);
        this.substrings = System.Text.RegularExpressions.Regex.Split(Source, pattern);
        this.tokens = TokenMappings!.Keys.Select(key => $"key").ToArray();
        base.OnParametersSet();
    

    private string StripCurlyBrackets(string source)
    
        return source
            .Replace(oldValue: "", newValue: string.Empty)
            .Replace(oldValue: "", newValue: string.Empty);
    

是的,MarkupString 允许您呈现 html。

substrings :

【讨论】:

感谢您的回答!尽管我并没有完全这样做,但您的回答为我指明了正确的方向,将字符串拆分为标记并单独处理每个标记。但是从那里,我去了@MrC aka Shaun Curtis 的回答中指出的 RenderFragments。您可以在 OP 的编辑中看到我的最终解决方案。

以上是关于Blazor Webassembly:如何将组件插入字符串的主要内容,如果未能解决你的问题,请参考以下文章

重定向到 Blazor WebAssembly 中的 403 禁止组件

我们如何使用 State 容器 Blazor Webassembly 从子级调用方法

Blazor WebAssembly身份认证与授权

Blazor WebAssembly身份认证与授权

Blazor WebAssembly身份认证与授权

深入浅出Blazor webassembly之razor组件的C#代码组织形式