视图组件中的 Javascript

Posted

技术标签:

【中文标题】视图组件中的 Javascript【英文标题】:Javascript in a View Component 【发布时间】:2017-11-14 15:10:11 【问题描述】:

我有一个视图组件,它在 Razor (.cshtml) 文件中包含一些 jQuery。脚本本身非常特定于视图(处理一些第三方库的配置),所以为了组织起见,我想将脚本和 HTML 保存在同一个文件中。

问题是脚本没有在 _Layout Scripts 部分呈现。显然这只是关于 View Components 的 how MVC handles 脚本。

我可以通过将脚本放在 Razor 文件中来解决它,不是在脚本部分中

但后来我遇到了依赖问题——因为在引用库之前使用了 jQuery(对库的引用在 _Layout 文件的底部附近)。

除了在 Razor 代码中包含对 jQuery 的引用(这会阻碍组件所在位置的 HTML 渲染)之外,还有什么聪明的解决方案吗?

我目前不在代码前面,但如果有人需要查看它以更好地理解它,我当然可以提供它。

【问题讨论】:

【参考方案1】:

部分仅适用于 视图 不适用于 部分视图视图 组件Default.cshtml 独立于主ViewResult 执行,其Layout 值默认为null。)这是设计使然。

您可以将它用于render sections 以获取partial viewview component's 视图声明的Layout。但是,在局部和视图组件中定义的部分不会流回渲染视图或其布局。 如果您想在局部视图或视图组件中使用 jQuery 或其他库引用,则可以将库拉入 head 而不是 Layout 页面中的 body

示例:

查看组件:

public class ContactViewComponent : ViewComponent

    public IViewComponentResult Invoke()
    
        return View();
    

ViewComponent 的位置:

/Views/[CurrentController]/Components/[NameOfComponent]/Default.cshtml
/Views/Shared/Components/[NameOfComponent]/Default.cshtml

Default.cshtml:

@model ViewComponentTest.ViewModels.Contact.ContactViewModel

<form method="post" asp-action="contact" asp-controller="home">
    <fieldset>
        <legend>Contact</legend>
        Email: <input asp-for="Email" /><br />
        Name: <input asp-for="Name" /><br />
        Message: <textarea asp-for="Message"></textarea><br />
        <input type="submit" value="Submit" class="btn btn-default" />
    </fieldset>
</form>

_Layout.cshtml

    <!DOCTYPE html>
    <html>
    <head>
    ...
        @RenderSection("styles", required: false)
     <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
    </head>
    <body>
        @RenderBody()
    ...
    //script move to head
      @RenderSection("scripts", required: false)
    </body>
    </html>

View.cshtml:

@
    Layout =  "~/Views/Shared/_Layout.cshtml";     

..................
@Html.RenderPartial("YourPartialView");
..................
@await Component.InvokeAsync("Contact")
..................
@section Scripts 
    <script>
    //Do somthing
    </script>

通过grahamehorner查看组件:(您可以了解更多信息here)

@section 脚本不会在 ViewComponent、视图组件中呈现 应该能够在 @section 中包含脚本,不像 作为组件的部分视图可以在许多视图中重用,并且 组件负责它自己的功能。

【讨论】:

适用于场景。【参考方案2】:

我决定做的是编写一个提供“OnContentLoaded”属性的ScriptTagHelper。如果为真,那么我使用 javascript 函数包装脚本标记的内部内容,以便在文档准备好后执行。这避免了 ViewComponent 的脚本触发时 jQuery 库尚未加载的问题。

[HtmlTargetElement("script", Attributes = "on-content-loaded")]
public class ScriptTagHelper : TagHelper

  /// <summary>
  /// Execute script only once document is loaded.
  /// </summary>
  public bool OnContentLoaded  get; set;  = false;

  public override void Process(TagHelperContext context, TagHelperOutput output)
  
     if (!OnContentLoaded)
     
        base.Process(context, output);
      
     else
     
        var content = output.GetChildContentAsync().Result;
        var javascript = content.GetContent();

        var sb = new StringBuilder();
        sb.Append("document.addEventListener('DOMContentLoaded',");
        sb.Append("function() ");
        sb.Append(javascript);
        sb.Append(");");

        output.Content.SetHtmlContent(sb.ToString()); 
     
  

ViewComponent 中的示例用法:

<script type="text/javascript" on-content-loaded="true">
    $('.button-collapse').sideNav();
</script>

可能有比这更好的方法,但到目前为止这对我有用。

【讨论】:

你应该在调用 base 之后返回,否则,其他一切都会覆盖 base 实现:if (!OnContentLoaded) base.Process(context, output); return; 这里是 GitHub 上此问题的链接:GitHub TL;DR:Microsoft 计划在 .NET Core 3.0 中使用 Razor 组件解决此问题。 请注意,您需要确定标签助手的范围才能使@Jake 的解决方案发挥作用:***.com/questions/48271514/… 我应该在父组件中导入标签助手吗? 这解决了我的问题,但是我花了好几个小时才弄清楚我的代码写在ViewComponent...Duh 中的一个部分内时仍在测试我的代码 【参考方案3】:

这是我正在使用的解决方案。它支持外部脚本加载和自定义内联脚本。虽然一些设置代码需要进入您的布局文件(例如 _Layout.cshtml),但一切都是在视图组件中编排的。

布局文件:

在页面顶部附近添加这个(&lt;head&gt; 标签内是个好地方):

<script>
    MYCOMPANY = window.MYCOMPANY || ;
    MYCOMPANY.delayed = null;
    MYCOMPANY.delay = function (f)  var e = MYCOMPANY.delayed; MYCOMPANY.delayed = e ? function ()  e(); f();  : f; ;
</script>

在页面底部附近添加这个(就在结束 &lt;/body&gt; 标记之前是个好地方):

<script>MYCOMPANY.delay = function (f)  setTimeout(f, 1) ; if (MYCOMPANY.delayed)  MYCOMPANY.delay(MYCOMPANY.delayed); MYCOMPANY.delayed = null; </script>

查看组件:

现在在您的代码中的任何位置,包括您的视图组件,您都可以执行此操作并在页面底部执行脚本:

<script>
    MYCOMPANY.delay(function() 
        console.log('Hello from the bottom of the page');
    );
</script>

有外部脚本依赖

但有时这还不够。有时您需要加载外部 JavaScript 文件,并且只有在加载外部文件后才执行您的自定义脚本。您还希望在您的视图组件中编排这一切以实现完全封装。为此,我在布局页面(或布局页面加载的外部 JavaScript 文件)中添加了更多设置代码,以延迟加载外部脚本文件。看起来像这样:

布局文件:

延迟加载脚本

<script>
    MYCOMPANY.loadScript = (function () 
        var ie = null;
        if (/MSIE ([^;]+)/.test(navigator.userAgent)) 
            var version = parseInt(RegExp['$1'], 10);
            if (version)
                ie = 
                    version: parseInt(version, 10)
                ;
        

        var assets = ;

        return function (url, callback, attributes) 

            attributes || (attributes = );

            var onload = function (url) 
                assets[url].loaded = true;
                while (assets[url].callbacks.length > 0)
                    assets[url].callbacks.shift()();
            ;

            if (assets[url]) 

                if (assets[url].loaded)
                    callback();

                assets[url].callbacks.push(callback);

             else 

                assets[url] = 
                    loaded: false,
                    callbacks: [callback]
                ;

                var script = document.createElement('script');
                script.type = 'text/javascript';
                script.async = true;
                script.src = url;

                for (var attribute in attributes)
                    if (attributes.hasOwnProperty(attribute))
                        script.setAttribute(attribute, attributes[attribute]);

                // can't use feature detection, as script.readyState still exists in IE9
                if (ie && ie.version < 9)
                    script.onreadystatechange = function () 
                        if (/loaded|complete/.test(script.readyState))
                            onload(url);
                    ;
                else
                    script.onload = function () 
                        onload(url);
                    ;

                document.getElementsByTagName('head')[0].appendChild(script);
            
        ;
    ());
</script>

查看组件:

现在您可以在您的视图组件(或其他任何地方)中执行此操作:

<script>
    MYCOMPANY.delay(function () 
        MYCOMPANY.loadScript('/my_external_script.js', function () 
            console.log('This executes after my_external_script.js is loaded.');
        );
    );
</script>

为了稍微清理一下,这里有一个视图组件的标签助手(受@JakeShakesworth 的回答启发):

[HtmlTargetElement("script", Attributes = "delay")]
public class ScriptDelayTagHelper : TagHelper

    /// <summary>
    /// Delay script execution until the end of the page
    /// </summary>
    public bool Delay  get; set; 

    /// <summary>
    /// Execute only after this external script file is loaded
    /// </summary>
    [HtmlAttributeName("after")]
    public string After  get; set; 

    public override void Process(TagHelperContext context, TagHelperOutput output)
    
        if (!Delay) base.Process(context, output);

        var content = output.GetChildContentAsync().Result;
        var javascript = content.GetContent();

        var sb = new StringBuilder();
        sb.Append("if (!MYCOMPANY || !MYCOMPANY.delay) throw 'MYCOMPANY.delay missing.';");
        sb.Append("MYCOMPANY.delay(function() ");
        sb.Append(LoadFile(javascript));
        sb.Append(");");

        output.Content.SetHtmlContent(sb.ToString());
    

    string LoadFile(string javascript)
    
        if (After == NullOrWhiteSpace)
            return javascript;

        var sb = new StringBuilder();
        sb.Append("if (!MYCOMPANY || !MYCOMPANY.loadScript) throw 'MYCOMPANY.loadScript missing.';");
        sb.Append("MYCOMPANY.loadScript(");
        sb.Append($"'After',");
        sb.Append("function() ");
        sb.Append(javascript);
        sb.Append(");");

        return sb.ToString();
    

现在在我的视图组件中我可以这样做:

<script delay="true" after="/my_external_script.js"]">
  console.log('This executes after my_external_script.js is loaded.');
</script>

如果视图组件没有外部文件依赖项,您也可以省略after 属性。

【讨论】:

【参考方案4】:

此答案适用于 .netcore 2.1。简短的回答,注入一个 Microsoft.AspNetCore.Mvc.Razor.TagHelpers.ITagHelperComponentManager 的实例,它将组件 UI 功能所需的 javascript 作为正文元素的最后一个节点。

详细回答我是如何到达这里的以及什么对我有用。

我的应用程序使用了许多小部件,它们具有相同的 HTML 结构、样式和 UI 功能和逻辑,但使用不同的数据集。

我的应用使用身份声明(角色)来动态控制导航。这些角色还决定了为小部件提供数据的数据集,这些小部件可以组合起来在单个页面上提供该数据的不同视图。

我使用区域来分隔代码。为了便于进一步分离关注点,我的应用程序需要将小部件代码(数据检索、标记、样式和功能)分开,我选择使用 ViewComponents。

我遇到了同样的问题,因为我的功能依赖于我希望在 shared/_layout 文件中对整个应用程序可用的 javascript 库。

我的脚本渲染是 _layout 文件中 body 标记之前的最后一个元素。我的 body 渲染是结构上的几个节点,所以我需要以某种方式获取我需要的 javascript 附加到 body 元素中的子节点集合。否则,我的组件将无法创建必要的 javascript 对象来提供所需的功能。

这个解决方案的根源是TagHelperComponent 和TagHelperComponentManager。我创建了一个派生自该文件的 ITagHelperComponent 的抽象基类实现的类,并将其放在 ./Pages/Shared/

这是我的 ScriptTagHelper

public class ScriptTagHelper : TagHelperComponent

    private readonly string _javascript;
    public ScriptTagHelper(string javascript = "") 
        _javascript = javascript;
    
    public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    
        if (string.Equals(context.TagName, "body", StringComparison.Ordinal))
        
            output.PostContent.AppendHtml($"<script type='text/javascript'>_javascript</script>");
        
        return Task.CompletedTask;
    

代码将 javascript 字符串作为标签助手的构造函数参数。 ProcessAsync 任务被覆盖以查找正文标记并附加 javascript。

javascript 字符串包含在 Html 脚本标记中,并且完整的 Html 字符串附加到正文中已有内容的末尾。 (即渲染脚本在页面上插入的任何脚本部分,以及布局页面上的任何脚本)。

这个标签助手的使用方式是通过 ViewComponent 内部的依赖注入。这是一个连接以创建 ScriptTagHelper 实例的小部件示例。

这是我的数据使用分布小部件。它最终将在饼图中显示数据使用的分布,当浮动在图表的特定楔形上时,还将显示该特定数据段的更多数据亮点。目前,它只是我的所有小部件将用来帮助我设置为团队分配任务以开始构建资源的模板。

这个文件是 ./Pages/Shared/Components/DataDistribution/Default.cshtml

@model dynamic;
@using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
@inject ITagHelperComponentManager tagManager;

<div class="col-md-3 grid-margin stretch-card">
    <div class="card">
        <div class="card-body">
            <p class="card-title text-md-center text-xl-left">Data Usage Distribution</p>
            <div class="d-flex flex-wrap justify-content-between justify-content-md-center justify-content-xl-between align-items-center">
                <h3 class="mb-0 mb-md-2 mb-xl-0 order-md-1 order-xl-0">40016</h3>
                <i class="ti-agenda icon-md text-muted mb-0 mb-md-3 mb-xl-0"></i>
            </div>
            <p class="mb-0 mt-2 text-success">10.00%<span class="text-body ml-1"><small>(30 days)</small></span></p>
        </div>
    </div>
</div>


@
    Layout = null;

    string scriptOptions = Newtonsoft.Json.JsonConvert.SerializeObject(Model);

    string script = @"";

    script += $"console.log(scriptOptions);";

    tagManager.Components.Add(new ScriptTagHelper(javascript: script));

通过注入 ITagHelperComponentManager 的默认实现,我们可以访问标签助手组件的集合,并可以添加我们自己的标签助手的实现。这个标签助手的工作是在我们的body标签底部放置一个包含我们用于这个视图组件的javascript的脚本标签。

在这个简单的代码存根中,我现在可以安全可靠地期望在我开始渲染将使用这些库来修改我的 UI 的脚本之前加载我的所有支持 UI 库。在这里,我只是在控制台记录从我的视图组件返回的序列化模型。因此,运行此代码实际上并不能证明它有效,但是,如果您想确保 jquery 已加载到布局文件中,任何人都可以更改脚本变量以控制台记录 jquery 版本。

希望这对某人有所帮助。我听说 .netcore 3 中的支持部分即将发生变化,但人们遇到的这些问题已经在 2.1 中得到了本地和相当优雅的解决,正如我希望在这里展示的那样。

【讨论】:

【参考方案5】:

假设您有组件AB,您可以在组件文件夹中保留_Scripts 部分视图:

Views
  Shared
    Components
      A
        Default.cshtml
        _Scripts.cshtml
      B
        Default.cshtml
        _Scripts.cshtml

在您的页面(渲染组件的位置)中,您可以为您使用的每个组件执行此操作:

@await Component.InvokeAsync("A")
@await Component.InvokeAsync("B")

@section Scripts 
    @await Html.PartialAsync("~/Views/Shared/Components/A/_Scripts.cshtml", Model);
    @await Html.PartialAsync("~/Views/Shared/Components/B/_Scripts.cshtml", Model);

_Scripts 部分中,您可以在 Scripts 部分中放置您想要的任何内容。

我不建议您使用太多的复杂页面 成分。可能对少数组件有用,或者 正在加载的组件是动态的。

【讨论】:

以上是关于视图组件中的 Javascript的主要内容,如果未能解决你的问题,请参考以下文章

视图组件中的 Javascript

如何访问活动内部/从活动中的片段视图组件

调用时禁用视图组件中的所有内容

选择器视图选项填充标签

我在 Vuejs 中的组件没有在我的 Blade 视图中呈现

.net core razor 页面中的多个视图组件未正确绑定