Pattern for moving javascript out of the markup

时间:2016-04-04 18:33:02

标签: knockout.js refactoring rendering episerver

tl;dr How do I refactor a typical .NET+KnockoutJS site using lots of server generated blocks (controllers) into one where all js is loaded separately from the markup?

Up until now all my foray into front-end development has been new projects, built from the ground using modern tooling and workflows, such as React, Redux, isomorphic rendering, etc. Yesterday a legacy project (ok, made three years ago) landed on my lap, and it features "blocks" (an EPi Server term basically meaning templates) such as this:

MySuperSlider.ascx

<!-- ko foreach: products() -->
<div class="my-super-slider">
    <a data-bind="attr: { href: ProductUrl }">
        <h1  data-bind="text: name"></h1>
        <img src="" data-bind="attr: { src : image.uri }"
    </a>
</div>
<!-- /ko -->

<script type="text/javascript"> 
// Make a call to the back-end to retrieve the product data used 
// in the KO template above and apply it
ko.applyBindings(new SliderBlock(
            '<%# ProductsController.GetServiceUrl(ProductsController.BlockUrl, Request.Url)%>',
            '<%# CurrentPage.PageLink.ID %>',
            '<%# GetBlockId() %>',
            '<%# (CurrentPage as SitePageDataBase) == null?string.Empty:(CurrentPage as SitePageDataBase).ProductPageUrl %>?id=',
            '<%# ContentLanguageCode %>',
            "<%# SliderContentBlock.ClientID %>"), document.getElementById("<%# SliderContentBlock.ClientID %>"));

</script>

Blocks, such as this one, are being reused all over the site, but with different settings (ref SliderContentBlock.ClientID which would fetch data for that current block). The fact that the referenced variables are unique for each block instance and rendered at runtime makes it impossible/hard to split the js out. Except possibly using some data attributes that could later be picked up?

Problem: <script> tags cannot be moved out of <head>

It is currently impossible to move <script> tags out of <head>, thus blocking the rendering pipeline. Neither moving after them after <body> or using async or defer attributes is possible as this would make any inline <script> blocks referring to the ko variable break.

So what is a good strategy for refactoring the above code into something that will work regardless of whether the knockout library has been loaded or not?

Hacky solution

The fastest (and probably most brittle) solution would be to create an array to hold initialization functions that could then be executed later on. Something like

<head>
    <script>window.initFuncs = [];</script>
</head>
<body>
    ...
    <!--  inline code pushing init code in the queue -->
    ...
    <script>initFuncs.forEach( fn => fn() );</script>
</body>

Then all I would need to do to defer execution of the above javascript would be to wrap it in a window.initFuncs.push( () => { ko.applyBindings(/* js goes here */) })

This would work, but does not exactly seem like the way to go. But this problem must have been solved a million times before (?). How does other Knockout developers do this?

I cannot simply move all of the javascript (as it is) out into separate files, as variables are being inserted into the markup during server rendering.

Attempt at better solution

I am not a .NET developer, but I feel the logic in the template with all the variable substitution would be better done in the backing code, instead of pushing all the details into the front-end. Not sure how that would be done, though.

3 个答案:

答案 0 :(得分:1)

一些建议:

  1. 将.net控制器更改为输出json,并从html中<script>标记中引用的单独js文件发出ajax请求。
  2. -OR -

    1. 如果您仍然希望您的.net模板直接将数据呈现给html,而没有单独的ajax请求,那么很好,但是没有它写出js函数或类似的。这将难以维持。相反,设置一个javascript对象,类似于你的建议,但它只包含数据。然后在html中<script>标记中引用的单独js文件中,读取该对象中的数据以初始化您的前端视图模型或其他js函数。

      e.g。

      <强>的.ascx

      <!-- ko foreach: products() -->
      <div class="my-super-slider">
          <a data-bind="attr: { href: ProductUrl }">
              <h1  data-bind="text: Name"></h1>
          </a>
      </div>
      <!-- /ko -->
      <script>
        MyPageContext = {};
        MyPageContext.ProductId = '<%# (CurrentPage as SitePageDataBase) == null?string.Empty:(CurrentPage as SitePageDataBase).ProductId %>';
        MyPageContext.OtherData = '...';        
      </script>
      <script src="productpage.js"></script>
      

      <强> productpage.js

       ko.applyBindings(new SliderBlock(MyPageContext.ProductId, MyPageContext.OtherData, ...));
        ...
      

      此外,没有理由这些脚本不能出现在您的内容的最后。在您致电ko.applyBindings

    2. 之前,Knockout不会尝试运行

答案 1 :(得分:1)

虽然不在EPi中,但这是我倾向于做的事情:

products = items.map do |row|
  row.map do |k, v|
    if k == "Color" 
      # go do something
      # add transformed k, v to products
    else
      puts "SKEY:#{k} VALUE: #{v}"
      k => v # add k, v to products
    end    
  end
end.flatten

在我的标记中:

public class MyPageViewModel
{
   ...

   public ClientSideModel ClientSideModel {get;set;}
}

public class ClientSideModel
{
    public string ProductId {get;set;}

    ....

    public HtmlString ToJson()
    {
        return new HtmlString(JsonConvert.SerializeObject(this));
    }
}

答案 2 :(得分:0)

我自己在这里发布了一个额外的答案,详细介绍了迄今为止我发现最好的方法。在这个来自Node开发人员的similar question中,它也涉及删除内联js,最好的答案似乎是让服务器将各个块所需的数据呈现为数据属性。然后,您可以稍后检索dom节点,从数据属性中提取数据,并执行使其工作所需的任何js。

因此,对于上面的示例,可能省略了最后一个<script>块。

相反,我们会在滑块中插入一个或多个数据标记,例如:

<div 
    class="my-super-slider" 
    data-model='{ 
        "url" : "<%# ProductsController.GetServiceUrl(ProductsController.BlockUrl, Request.Url)%>",
        "pageLinkId" : "<%# CurrentPage.PageLink.ID %>",
        "blockId" : "<%# GetBlockId() %>",
        "lang" : "<%# ContentLanguageCode %>",
        "clientId" : "<%# SliderContentBlock.ClientID %>"
    }'
>

然后我们可以加载任何查询.my-super-slider元素的js异步,并使用数据属性开始做任何相关的魔术。这解决了所有同步问题,并为性能优化的全新维度开辟了道路。

(数据属性方法与@ MaciejGrzyb的答案相似,但创建数据的EPiServer控制器超出了我的范围。)