【问题标题】:How to check if $compile has been completed?如何检查 $compile 是否已完成?
【发布时间】:2017-11-13 13:10:57
【问题描述】:

我正在编写一个函数,该函数可以从 HTML 模板和给出的一些信息创建电子邮件模板。为此,我使用了 Angular 的 $compile 函数。

似乎只有一个问题我无法解决。该模板包含一个基本模板,其中包含无限数量的ng-include。当我使用“最佳实践”$timeout (advised here) 删除所有ng-include 时,它会起作用。所以这不是我想要的。

$timeout 示例:

return this.$http.get(templatePath)
    .then((response) => {
       let template = response.data;
       let scope = this.$rootScope.$new();
       angular.extend(scope, processScope);

       let generatedTemplate = this.$compile(jQuery(template))(scope);
       return this.$timeout(() => {
           return generatedTemplate[0].innerHTML;
       });
    })
    .catch((exception) => {
        this.logger.error(
           TemplateParser.getOnderdeel(process),
           "Email template creation",
           (<Error>exception).message
        );
        return null;
     });

当我开始将ng-include's 添加到模板中时,此函数开始返回尚未完全编译的模板(一个解决方法是嵌套$timeout 函数)。我相信这是因为ng-include 的异步性质。


工作代码

此代码在完成渲染后返回 html 模板(现在可以重用函数,see this question for the problem)。但是这个解决方案是一个很大的问题,因为它使用 angular private $$phase 来检查是否有任何正在进行的$digest。所以我想知道是否还有其他解决方案?

return this.$http.get(templatePath)
   .then((response) => {
       let template = response.data;
       let scope = this.$rootScope.$new();
       angular.extend(scope, processScope);

       let generatedTemplate = this.$compile(jQuery(template))(scope);
       let waitForRenderAndPrint = () => {
           if (scope.$$phase || this.$http.pendingRequests.length) {
               return this.$timeout(waitForRenderAndPrint);
           } else {
               return generatedTemplate[0].innerHTML;
           }
        };
        return waitForRenderAndPrint();
    })
    .catch((exception) => {
        this.logger.error(
           TemplateParser.getOnderdeel(process),
           "Email template creation",
           (<Error>exception).message
         );
         return null;
     });

我想要什么

我希望有一个功能可以处理无限数量的ng-inlude,并且仅在成功创建模板后返回。我没有渲染这个模板,需要返回完全编译的模板。


解决方案

在尝试了@estus answer之后,我终于找到了另一种检查 $compile 何时完成的方法。这导致了下面的代码。我使用$q.defer() 的原因是模板在事件中被解析。因此,我不能像正常的承诺那样返回结果(我不能这样做return scope.$on())。这段代码唯一的问题是它严重依赖ng-include。如果您为函数提供没有ng-include 的模板,则$q.defer 永远不会被解析。

/**
 * Using the $compile function, this function generates a full HTML page based on the given process and template
 * It does this by binding the given process to the template $scope and uses $compile to generate a HTML page
 * @param {Process} process - The data that can bind to the template
 * @param {string} templatePath - The location of the template that should be used
 * @param {boolean} [useCtrlCall=true] - Whether or not the process should be a sub part of a $ctrl object. If the template is used
 * for more then only an email template this could be the case (EXAMPLE: $ctrl.<process name>.timestamp)
 * @return {IPromise<string>} A full HTML page
*/
public parseHTMLTemplate(process: Process, templatePath: string, useCtrlCall = true): ng.IPromise<string> {
   let scope = this.$rootScope.$new(); //Do NOT use angular.extend. This breaks the events

   if (useCtrlCall) {
       const controller = "$ctrl"; //Create scope object | Most templates are called with $ctrl.<process name>
       scope[controller] = {};
       scope[controller][process.__className.toLowerCase()] = process;
    } else {
       scope[process.__className.toLowerCase()] = process;
    }

    let defer = this.$q.defer(); //use defer since events cannot be returned as promises
    this.$http.get(templatePath)
       .then((response) => {
          let template = response.data;
          let includeCounts = {};
          let generatedTemplate = this.$compile(jQuery(template))(scope); //Compile the template

           scope.$on('$includeContentRequested', (e, currentTemplateUrl) => {
                        includeCounts[currentTemplateUrl] = includeCounts[currentTemplateUrl] || 0;
                        includeCounts[currentTemplateUrl]++; //On request add "template is loading" indicator
                    });
           scope.$on('$includeContentLoaded', (e, currentTemplateUrl) => {
                        includeCounts[currentTemplateUrl]--; //On load remove the "template is loading" indicator

            //Wait for the Angular bindings to be resolved
            this.$timeout(() => {
               let totalCount = Object.keys(includeCounts) //Count the number of templates that are still loading/requested
                   .map(templateUrl => includeCounts[templateUrl])
                   .reduce((counts, count) => counts + count);

                if (!totalCount) { //If no requests are left the template compiling is done.
                    defer.resolve(generatedTemplate.html());
                 }
              });
          });
       })
       .catch((exception) => {                
          defer.reject(exception);
       });

   return defer.promise;
}

【问题讨论】:

    标签: javascript angularjs typescript asynchronous


    【解决方案1】:

    我认为你被链式承诺和编译事件卡住了。我关注了您的一系列问题,这可能是您正在寻找的,带有递归 ng-include 的编译模板字符串。

    首先,我们需要定义自己的函数来检测编译何时完成,有几种方法可以实现,但持续时间检查是我最好的选择。

    // pass searchNode, this will search the children node by elementPath, 
    // for every 0.5s, it will do the search again until find the element
    function waitUntilElementLoaded(searchNode, elementPath, callBack){
    
        $timeout(function(){
    
            if(searchNode.find(elementPath).length){
              callBack(elementPath, $(elementPath));
          }else{
            waitUntilElementLoaded(searchNode, elementPath, callBack);
          }
          },500)
    
    
      }
    

    在下面的示例中,directive-one 是包装我需要的所有输出模板的容器元素,因此您可以将其更改为您喜欢的任何元素。通过使用 Angular 的 $q,我将公开 promise 函数来捕获输出模板,因为它是异步工​​作的。

    $scope.getOutput = function(templatePath){
    
    
      var deferred = $q.defer();
        $http.get(templatePath).then(function(templateResult){
          var templateString = templateResult.data;
          var result = $compile(templateString)($scope) 
    
    
         waitUntilElementLoaded($(result), 'directive-one', function() {
    
           var compiledStr = $(result).find('directive-one').eq(0).html();
            deferred.resolve(compiledStr);
         })
    
        })
    
      return deferred.promise;
    
    
      }
    
    
    
      // usage
    
      $scope.getOutput("template-path.html").then(function(output){
          console.log(output)
        })
    

    TL;DR; My Demo plunker

    另外,如果您使用的是 TypeScript 2.1,您可以使用 async/await 来使代码看起来更干净,而不是使用回调。会是这样的

    var myOutput = await $scope.getOutput('template-path')
    

    【讨论】:

    • 你是在暗示 $compile 函数是异步的,但没有实现任何类型的“完成”回调?
    • @EricMORAND $compile 是一个异步函数,它没有任何可以告诉您何时完成的钩子。这与模板中的元素也是异步的(例如:ng-include)并且也没有任何挂钩的事实有关。由于这个 $compile 不能告诉你什么时候完成。建议使用 $timeout,因为它会在浏览器堆栈的末尾添加一个事件。大多数时候 $compile 是在 $timeout 执行时完成的。不幸的是,ng-include 破坏了这一点,因为它也是异步的并在浏览器堆栈的末尾创建事件。
    • @Telvin Nguyen,谢谢您的回答。然而这个例子对我不起作用,因为我不知道模板中导入了什么(有多少 ng-includes)。因此,我无法确定将告诉我的函数已完成编译的 ID 放置在哪里。它也使用jQuery。在这个项目中我无权访问的库。
    • @Mr.wiseguy,感谢您的确认。这就是我所害怕的。这是 Angular 团队的一个巨大错误。
    • @Mr.wiseguy:我的例子是使用 jQuery 和容器元素的 ID,它们不是必需的。实际上,该演示正在表达如何解决此问题的想法。你完全可以在没有 jQuery 的情况下做同样的事情。还有各种方法可以捕获编译后的内部 HTML,无论您知道内部是什么(像您正在做的那样获取第一个元素 innerHTML)。通过您的问题,我看到您几乎完成了,只是多了一点点。对不起,如果这不能帮助你,但我没有足够的时间重写另一个版本来再次说明它:)
    【解决方案2】:

    $compile同步函数。它只是同步编译给定的 DOM,并不关心嵌套指令中发生了什么。如果嵌套指令有异步加载的模板或其他阻止其内容在同一滴答中可用的东西,则这不是父指令的问题。

    由于数据绑定和 Angular 编译器的工作方式,没有明确的时刻可以认为 DOM 肯定是“完整的”,因为任何地方、任何时间都可能发生变化。 ng-include 也可能涉及绑定,并且包含的​​模板可以随时更改和加载。

    这里的实际问题是没有考虑到以后如何管理的决定。 ng-include 带有随机模板可以用于原型制作,但会导致设计问题,这就是其中之一。

    处理这种情况的一种方法是确定涉及哪些模板;设计良好的应用程序不能让它的部分过于松散。实际的解决方案取决于此模板的来源以及它包含随机嵌套模板的原因。但这个想法是,使用过的模板应该在使用之前放入缓存的模板中。这可以使用gulp-angular-templates 等构建工具来完成。或者通过在 ng-include 编译之前使用 $templateRequest 进行请求(这实际上是在执行 $http 请求并将其发送到 $templateCache) - 执行 $templateRequest 基本上就是 ng-include 所做的。

    虽然在缓存模板时$compile$templateRequest 是同步的,但ng-include 不是-它在下一个tick 时完全编译,即$timeout 零延迟(plunk):

    var templateUrls = ['foo.html', 'bar.html', 'baz.html'];
    
    $q.all(templateUrls.map(templateUrl => $templateRequest(templateUrl)))
    .then(templates => {
      var fooElement = $compile('<div><ng-include src="\'foo.html\'"></ng-include></div>')($scope);
    
      $timeout(() => {
       console.log(fooElement.html());
      })
    });
    

    一般来说,将模板用于缓存是摆脱 Angular 模板给编译生命周期带来的异步性的更好方法——不仅适用于 ng-include,而且适用于任何指令。

    另一种方法是使用ng-include events。这样,应用程序变得更加松散和基于事件(有时这是一件好事,但大多数时候不是)。由于每个ng-include 都会发出一个事件,因此需要对事件进行计数,当它们发生时,这意味着ng-include 指令的层次结构已经完全编译(plunk):

    var includeCounts = {};
    
    var fooElement = $compile('<div><ng-include src="\'foo.html\'"></ng-include></div>')($scope);
    
    $scope.$on('$includeContentRequested', (e, currentTemplateUrl) => {
      includeCounts[currentTemplateUrl] = includeCounts[currentTemplateUrl] || 0;
      includeCounts[currentTemplateUrl]++;
    })
    // should be done for $includeContentError as well
    $scope.$on('$includeContentLoaded', (e, currentTemplateUrl) => {
      includeCounts[currentTemplateUrl]--;
    
      // wait for a nested template to begin a request
      $timeout(() => {
        var totalCount = Object.keys(includeCounts)
        .map(templateUrl => includeCounts[templateUrl])
        .reduce((counts, count) => counts + count);
    
        if (!totalCount) {
          console.log(fooElement.html());
        }
      });
    })
    

    请注意,这两个选项都只会处理由异步模板请求引起的异步。

    【讨论】:

    • 感谢您的回答。但是我似乎找不到将第二种解决方案集成到我的功能中的方法(请参阅我的主题问题)。问题是当我在我创建的范围对象上设置事件监视时,任何事件都不会触发该事件。你有一个例子我应该如何将它整合到我的功能中?哦,你的 plunkr 不起作用。它没有给我任何 html 输出。
    • 笨拙的作品。它有console.log 语句。检查控制台。我不确定你所说的集成是什么意思。您需要在作用域上设置观察者并调用 $compile,仅此而已。顺序在这里应该无关紧要,但首先尝试设置观察者。如果这对您不起作用,请考虑提供一个可以重现问题的 plunk。无论如何,ng-include 是自 1.0 以来的遗留指令,应尽可能避免使用,因为它不符合当前的 Angular 最佳实践。
    • 我刚刚发现,由于我使用的是 $rootScope.$new() (我在服务中没有任何范围)事件没有被触发。你知道为什么,如果 $rootScope 导致它,你知道任何解决方案吗?见plnkr.co/edit/ZEVSG7TBpYirR77UDxcF?p=preview
    • 我不确定你的意思。 $rootScope.$new() 为子作用域提供了适当的层次结构,因此事件应该向上传播。这实际上取决于幕后代码中究竟发生了什么。您还可以尝试使用$rootScope.$on 而不是scope.$on 在根范围上设置事件侦听器,如果事情变得混乱,这可能会有所帮助。如果不了解这在您的情况下是如何工作的,很难说其他任何事情。
    • $rootScope.$on() 成功了。 plunkr 现在可以工作了 (plnkr.co/edit/ZEVSG7TBpYirR77UDxcF?p=preview)。然而它在我的项目中不起作用。这是由于 angular.extend。它似乎与 $scope 事件混淆(在我从代码中删除 angular.extend 之前不会触发事件)。你知道有什么解决办法吗?
    猜你喜欢
    • 2014-07-22
    • 1970-01-01
    • 2015-08-14
    • 2014-10-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-28
    相关资源
    最近更新 更多