117

想知道当所有指令都完成编译/链接时,检测页面加载/引导完成的最佳方法是什么。

有什么活动吗?我应该重载引导功能吗?

4

12 回答 12

209

只是一个预感:为什么不看看 ngCloak 指令是如何做到的呢?显然,ngCloak 指令会在加载后显示内容。我敢打赌看 ngCloak 会得出确切的答案……

1 小时后编辑: 好的,好吧,我查看了ngCloak,它真的很短。这显然意味着编译函数在 {{template}} 表达式被评估(即它加载的模板)之前不会被执行,因此 ngCloak 指令的功能很好。

我有根据的猜测是只用与 ngCloak 相同的简单性制作一个指令,然后在你的编译函数中做你想做的任何事情。:) 将指令放在应用程序的根元素上。您可以调用类似 myOnload 的指令并将其用作属性 my-onload。一旦模板被编译(表达式评估和子模板加载),编译函数将执行。

编辑,23 小时后: 好的,所以我做了一些研究,我也问了我自己的问题。我问的问题与这个问题间接相关,但它巧合地引导我找到解决这个问题的答案。

答案是您可以创建一个简单的指令并将代码放入指令的链接函数中,该函数(对于大多数用例,如下所述)将在您的元素准备好/加载时运行。根据Josh 对编译和链接函数执行顺序的描述

如果你有这个标记:

<div directive1>
  <div directive2>
    <!-- ... -->
  </div>
</div>

然后 AngularJS 将通过按特定顺序运行指令函数来创建指令:

directive1: compile
  directive2: compile
directive1: controller
directive1: pre-link
  directive2: controller
  directive2: pre-link
  directive2: post-link
directive1: post-link

默认情况下,直接的“链接”函数是后链接,因此外部指令 1 的链接函数在内部指令 2 的链接函数运行之前不会运行。这就是为什么我们说只有在 post-link 中进行 DOM 操作才是安全的。因此,对于最初的问题,从外部指令的链接函数访问子指令的内部 html 应该没有问题,尽管必须编译动态插入的内容,如上所述。

由此我们可以得出结论,当一切准备就绪/编译/链接/加载时,我们可以简单地创建一个指令来执行我们的代码:

    app.directive('ngElementReady', [function() {
        return {
            priority: -1000, // a low number so this directive loads after all other directives have loaded. 
            restrict: "A", // attribute only
            link: function($scope, $element, $attributes) {
                console.log(" -- Element ready!");
                // do what you want here.
            }
        };
    }]);

现在你可以做的是把 ngElementReady 指令放到应用程序的根元素上,console.log当它被加载时会触发:

<body data-ng-app="MyApp" data-ng-element-ready="">
   ...
   ...
</body>

就是这么简单!只需制作一个简单的指令并使用它。;)

您可以进一步自定义它,以便它可以通过添加来执行表达式(即函数)$scope.$eval($attributes.ngElementReady);

    app.directive('ngElementReady', [function() {
        return {
            priority: Number.MIN_SAFE_INTEGER, // execute last, after all other directives if any.
            restrict: "A",
            link: function($scope, $element, $attributes) {
                $scope.$eval($attributes.ngElementReady); // execute the expression in the attribute.
            }
        };
    }]);

然后你可以在任何元素上使用它:

<body data-ng-app="MyApp" data-ng-controller="BodyCtrl" data-ng-element-ready="bodyIsReady()">
    ...
    <div data-ng-element-ready="divIsReady()">...<div>
</body>

只需确保在元素所在的范围(在控制器中)中定义了函数(例如 bodyIsReady 和 divIsReady)。

警告:我说这适用于大多数情况。使用某些指令(如 ngRepeat 和 ngIf)时要小心。他们创建自己的范围,您的指令可能不会触发。例如,如果您将我们的新 ngElementReady 指令放在也具有 ngIf 的元素上,并且 ngIf 的条件评估为 false,那么我们的 ngElementReady 指令将不会被加载。或者,例如,如果您将我们的新 ngElementReady 指令放在也具有 ngInclude 指令的元素上,如果 ngInclude 的模板不存在,我们的指令将不会被加载。您可以通过确保嵌套指令而不是将它们全部放在同一个元素上来解决其中的一些问题。例如,通过这样做:

<div data-ng-element-ready="divIsReady()">
    <div data-ng-include="non-existent-template.html"></div>
<div>

而不是这个:

<div data-ng-element-ready="divIsReady()" data-ng-include="non-existent-template.html"></div>

ngElementReady 指令将在后面的示例中编译,但不会执行它的链接函数。注意:指令总是被编译,但它们的链接函数并不总是根据上面的某些场景执行。

编辑,几分钟后:

哦,为了完全回答这个问题,您现在可以从属性中执行的表达式$emit$broadcast函数中获取事件。ng-element-ready:) 例如:

<div data-ng-element-ready="$emit('someEvent')">
    ...
<div>

编辑,几分钟后:

@satchmorun 的答案也有效,但仅适用于初始加载。这是一个非常有用的 SO 问题,它描述了执行的顺序,包括链接函数、app.run等等。因此,根据您的用例,app.run可能会很好,但不适用于特定元素,在这种情况下链接功能会更好。

编辑,五个月后,10 月 17 日太平洋标准时间 8:11:

这不适用于异步加载的部分。您需要将簿记添加到您的部分中(例如,一种方法是让每个部分跟踪其内容何时完成加载,然后发出一个事件,以便父范围可以计算已加载的部分数量并最终执行所需的操作在加载所有部分后执行)。

编辑,太平洋标准时间 10 月 23 日晚上 10:52:

我做了一个简单的指令,用于在加载图像时触发一些代码:

/*
 * This img directive makes it so that if you put a loaded="" attribute on any
 * img element in your app, the expression of that attribute will be evaluated
 * after the images has finished loading. Use this to, for example, remove
 * loading animations after images have finished loading.
 */
  app.directive('img', function() {
    return {
      restrict: 'E',
      link: function($scope, $element, $attributes) {
        $element.bind('load', function() {
          if ($attributes.loaded) {
            $scope.$eval($attributes.loaded);
          }
        });
      }
    };
  });

编辑,太平洋标准时间 10 月 24 日上午 12:48:

我改进了原始ngElementReady指令并将其重命名为whenReady.

/*
 * The whenReady directive allows you to execute the content of a when-ready
 * attribute after the element is ready (i.e. done loading all sub directives and DOM
 * content except for things that load asynchronously like partials and images).
 *
 * Execute multiple expressions by delimiting them with a semi-colon. If there
 * is more than one expression, and the last expression evaluates to true, then
 * all expressions prior will be evaluated after all text nodes in the element
 * have been interpolated (i.e. {{placeholders}} replaced with actual values). 
 *
 * Caveats: if other directives exists on the same element as this directive
 * and destroy the element thus preventing other directives from loading, using
 * this directive won't work. The optimal way to use this is to put this
 * directive on an outer element.
 */
app.directive('whenReady', ['$interpolate', function($interpolate) {
  return {
    restrict: 'A',
    priority: Number.MIN_SAFE_INTEGER, // execute last, after all other directives if any.
    link: function($scope, $element, $attributes) {
      var expressions = $attributes.whenReady.split(';');
      var waitForInterpolation = false;

      function evalExpressions(expressions) {
        expressions.forEach(function(expression) {
          $scope.$eval(expression);
        });
      }

      if ($attributes.whenReady.trim().length == 0) { return; }

      if (expressions.length > 1) {
        if ($scope.$eval(expressions.pop())) {
          waitForInterpolation = true;
        }
      }

      if (waitForInterpolation) {
        requestAnimationFrame(function checkIfInterpolated() {
          if ($element.text().indexOf($interpolate.startSymbol()) >= 0) { // if the text still has {{placeholders}}
            requestAnimationFrame(checkIfInterpolated);
          }
          else {
            evalExpressions(expressions);
          }
        });
      }
      else {
        evalExpressions(expressions);
      }
    }
  }
}]);

例如,someFunction当一个元素被加载{{placeholders}}但尚未被替换时,像这样使用它来触发:

<div when-ready="someFunction()">
  <span ng-repeat="item in items">{{item.property}}</span>
</div>

someFunction将在所有item.property占位符被替换之前调用。

评估尽可能多的表达式,并使最后一个表达式true等待{{placeholders}}被评估,如下所示:

<div when-ready="someFunction(); anotherFunction(); true">
  <span ng-repeat="item in items">{{item.property}}</span>
</div>

someFunction并在被替换anotherFunction后被解雇。{{placeholders}}

这仅适用于第一次加载元素时,不适用于将来的更改。如果在最初替换占位符后继续发生,它可能无法按预期工作$digest($digest 最多可能发生 10 次,直到数据停止更改)。它将适用于绝大多数用例。

编辑,太平洋标准时间 10 月 31 日晚上 7:26:

好吧,这可能是我最后一次也是最后一次更新了。这可能适用于 99.999 个用例:

/*
 * The whenReady directive allows you to execute the content of a when-ready
 * attribute after the element is ready (i.e. when it's done loading all sub directives and DOM
 * content). See: https://stackoverflow.com/questions/14968690/sending-event-when-angular-js-finished-loading
 *
 * Execute multiple expressions in the when-ready attribute by delimiting them
 * with a semi-colon. when-ready="doThis(); doThat()"
 *
 * Optional: If the value of a wait-for-interpolation attribute on the
 * element evaluates to true, then the expressions in when-ready will be
 * evaluated after all text nodes in the element have been interpolated (i.e.
 * {{placeholders}} have been replaced with actual values).
 *
 * Optional: Use a ready-check attribute to write an expression that
 * specifies what condition is true at any given moment in time when the
 * element is ready. The expression will be evaluated repeatedly until the
 * condition is finally true. The expression is executed with
 * requestAnimationFrame so that it fires at a moment when it is least likely
 * to block rendering of the page.
 *
 * If wait-for-interpolation and ready-check are both supplied, then the
 * when-ready expressions will fire after interpolation is done *and* after
 * the ready-check condition evaluates to true.
 *
 * Caveats: if other directives exists on the same element as this directive
 * and destroy the element thus preventing other directives from loading, using
 * this directive won't work. The optimal way to use this is to put this
 * directive on an outer element.
 */
app.directive('whenReady', ['$interpolate', function($interpolate) {
  return {
    restrict: 'A',
    priority: Number.MIN_SAFE_INTEGER, // execute last, after all other directives if any.
    link: function($scope, $element, $attributes) {
      var expressions = $attributes.whenReady.split(';');
      var waitForInterpolation = false;
      var hasReadyCheckExpression = false;

      function evalExpressions(expressions) {
        expressions.forEach(function(expression) {
          $scope.$eval(expression);
        });
      }

      if ($attributes.whenReady.trim().length === 0) { return; }

    if ($attributes.waitForInterpolation && $scope.$eval($attributes.waitForInterpolation)) {
        waitForInterpolation = true;
    }

      if ($attributes.readyCheck) {
        hasReadyCheckExpression = true;
      }

      if (waitForInterpolation || hasReadyCheckExpression) {
        requestAnimationFrame(function checkIfReady() {
          var isInterpolated = false;
          var isReadyCheckTrue = false;

          if (waitForInterpolation && $element.text().indexOf($interpolate.startSymbol()) >= 0) { // if the text still has {{placeholders}}
            isInterpolated = false;
          }
          else {
            isInterpolated = true;
          }

          if (hasReadyCheckExpression && !$scope.$eval($attributes.readyCheck)) { // if the ready check expression returns false
            isReadyCheckTrue = false;
          }
          else {
            isReadyCheckTrue = true;
          }

          if (isInterpolated && isReadyCheckTrue) { evalExpressions(expressions); }
          else { requestAnimationFrame(checkIfReady); }

        });
      }
      else {
        evalExpressions(expressions);
      }
    }
  };
}]);

像这样使用它

<div when-ready="isReady()" ready-check="checkIfReady()" wait-for-interpolation="true">
   isReady will fire when this {{placeholder}} has been evaluated
   and when checkIfReady finally returns true. checkIfReady might
   contain code like `$('.some-element').length`.
</div>

当然,它可能会被优化,但我就这样吧。requestAnimationFrame很好。

于 2014-05-18T01:57:45.030 回答
38

在 的文档中angular.Modulerun,有一个描述该函数的条目:

使用此方法注册在注入器完成加载所有模块时应执行的工作。

因此,如果您有一些模块是您的应用程序:

var app = angular.module('app', [/* module dependencies */]);

您可以在模块加载后运行东西:

app.run(function() {
  // Do post-load initialization stuff here
});

编辑:手动初始化救援

所以有人指出,run当 DOM 准备好并链接起来时,不会调用 。$injector当被引用的模块加载了它的所有依赖项时,它会被调用ng-app,这与 DOM 编译步骤是分开的。

我又看了看手动初始化,看来这应该可以解决问题。

我做了一个小提琴来说明

HTML 很简单:

<html>
    <body>
        <test-directive>This is a test</test-directive>
    </body>
</html>

请注意缺少ng-app. 我有一个指令会做一些 DOM 操作,所以我们可以确保事情的顺序和时间。

像往常一样,创建一个模块:

var app = angular.module('app', []);

这是指令:

app.directive('testDirective', function() {
    return {
        restrict: 'E',
        template: '<div class="test-directive"><h1><div ng-transclude></div></h1></div>',
        replace: true,
        transclude: true,
        compile: function() {
            console.log("Compiling test-directive");
            return {
                pre: function() { console.log("Prelink"); },
                post: function() { console.log("Postlink"); }
            };
        }
    };
});

我们将用一个of class替换test-directive标签,并将其内容包装在一个.divtest-directiveh1

我添加了一个编译函数,它返回前链接函数和后链接函数,这样我们就可以看到这些东西何时运行。

这是其余的代码:

// The bootstrapping process

var body = document.getElementsByTagName('body')[0];

// Check that our directive hasn't been compiled

function howmany(classname) {
    return document.getElementsByClassName(classname).length;
}

在我们做任何事情之前,test-directive在 DOM 中应该没有具有 class of 的元素,而在我们完成之后应该有 1。

console.log('before (should be 0):', howmany('test-directive'));

angular.element(document).ready(function() {
    // Bootstrap the body, which loades the specified modules
    // and compiled the DOM.
    angular.bootstrap(body, ['app']);

    // Our app is loaded and the DOM is compiled
    console.log('after (should be 1):', howmany('test-directive'));
});

这很简单。文档准备好后,angular.bootstrap使用应用程序的根元素和一组模块名称进行调用。

实际上,如果您将run函数附加到app模块,您会看到它在任何编译发生之前运行。

如果您运行小提琴并观察控制台,您将看到以下内容:

before (should be 0): 0 
Compiling test-directive 
Prelink
Postlink
after (should be 1): 1 <--- success!
于 2013-02-19T22:27:55.493 回答
17

Angular 没有提供一种方法来指示页面何时完成加载,可能是因为“完成”取决于您的应用程序。例如,如果您有部分分层树,则一个加载其他部分。“完成”意味着所有这些都已加载。任何框架都很难分析您的代码并理解一切都已完成或仍在等待。为此,您必须提供特定于应用程序的逻辑来检查和确定。

于 2013-03-27T18:22:56.520 回答
15

我想出了一个在评估角度初始化何时完成时相对准确的解决方案。

该指令是:

.directive('initialisation',['$rootScope',function($rootScope) {
            return {
                restrict: 'A',
                link: function($scope) {
                    var to;
                    var listener = $scope.$watch(function() {
                        clearTimeout(to);
                        to = setTimeout(function () {
                            console.log('initialised');
                            listener();
                            $rootScope.$broadcast('initialised');
                        }, 50);
                    });
                }
            };
        }]);

然后可以将其作为属性添加到body元素中,然后监听使用$scope.$on('initialised', fn)

它的工作原理是假设应用程序在没有更多 $digest 循环时被初始化。$watch 在每个摘要周期都被调用,因此会启动一个计时器(setTimeout 不是 $timeout,因此不会触发新的摘要周期)。如果在超时时间内没有出现摘要循环,则假定应用程序已初始化。

它显然不如 satchmoruns 解决方案准确(因为消化周期可能比超时时间长),但我的解决方案不需要您跟踪模块,这使得管理更容易(尤其是对于大型项目)。无论如何,似乎对我的要求足够准确。希望能帮助到你。

于 2013-10-30T15:21:38.143 回答
12

如果你使用Angular UI Router,你可以监听$viewContentLoaded事件。

“$viewContentLoaded - 在视图加载后触发,在 DOM 渲染后触发。视图的 '$scope' 会发出事件。” -链接

$scope.$on('$viewContentLoaded', 
function(event){ ... });
于 2014-12-23T20:46:00.483 回答
3

我用 JQuery 观察了 Angular 的 DOM 操作,我确实为我的应用程序设置了一个完成(某种预定义和令人满意的情况,我需要我的应用程序摘要),例如我希望我的 ng-repeater 产生 7 个结果,并且在那里为我为此目的,将在 setInterval 的帮助下设置一个观察函数。

$(document).ready(function(){

  var interval = setInterval(function(){

  if($("article").size() == 7){
     myFunction();
     clearInterval(interval);
  }

  },50);

});
于 2014-05-24T22:01:21.140 回答
3

如果您不使用ngRoute模块,即您没有$viewContentLoaded事件。

您可以使用另一种指令方法:

    angular.module('someModule')
        .directive('someDirective', someDirective);

    someDirective.$inject = ['$rootScope', '$timeout']; //Inject services

    function someDirective($rootScope, $timeout){
        return {
            restrict: "A",
            priority: Number.MIN_SAFE_INTEGER, //Lowest priority
            link    : function(scope, element, attr){
                $timeout(
                    function(){
                        $rootScope.$emit("Some:event");
                    }
                );
            }
        };
    }

因此,对于trusktr 的回答,它的优先级最低。加上$timeout将导致 Angular 在回调执行之前运行整个事件循环。

使用$rootScope,因为它允许在应用程序的任何范围内放置指令并仅通知必要的侦听器。

$rootScope.$emit 将为所有 $rootScope.$on 监听器触发一个事件。有趣的部分是 $rootScope.$broadcast 将通知所有 $rootScope.$on 以及 $scope.$on 侦听器

于 2015-07-31T14:17:03.063 回答
2

根据 Angular 团队和这个Github 问题

我们现在有分别在 ng-view 和 ng-include 中发出的 $viewContentLoaded 和 $includeContentLoaded 事件。我认为这与我们何时完成编译一样接近。

基于此,目前似乎无法以可靠的方式做到这一点,否则 Angular 会提供开箱即用的事件。

引导应用程序意味着在根范围上运行摘要周期,并且也没有摘要周期完成事件。

根据 Angular 2设计文档

由于有多个摘要,无法确定并通知组件模型是稳定的。这是因为通知可以进一步更改数据,从而重新启动绑定过程。

据此,这是不可能的事实是决定在 Angular 2 中进行重写的原因之一。

于 2015-04-21T20:02:25.097 回答
2

我有一个片段在通过路由进入的主要部分之后/被加载。

我需要在加载该子部分后运行一个函数,我不想编写新指令并发现你可以使用厚脸皮ngIf

父部分的控制器:

$scope.subIsLoaded = function() { /*do stuff*/; return true; };

子部分的 HTML

<element ng-if="subIsLoaded()"><!-- more html --></element>
于 2015-12-03T00:56:40.713 回答
1

如果您想使用服务器端数据(JSP、PHP)生成 JS,您可以将您的逻辑添加到服务中,该服务将在您的控制器加载时自动加载。

此外,如果您想在所有指令完成编译/链接时做出反应,您可以在初始化逻辑中添加上面提出的适当解决方案。

module.factory('YourControllerInitService', function() {

    // add your initialization logic here

    // return empty service, because it will not be used
    return {};
});


module.controller('YourController', function (YourControllerInitService) {
});
于 2015-08-27T09:05:09.383 回答
0

这些都是很好的解决方案,但是,如果您当前正在使用路由,那么我发现此解决方案是最简单且所需代码量最少的解决方案。在触发路由之前,使用 'resolve' 属性等待 Promise 完成。例如

$routeProvider
.when("/news", {
    templateUrl: "newsView.html",
    controller: "newsController",
    resolve: {
        message: function(messageService){
            return messageService.getMessage();
    }
}

})

单击此处获取完整文档 - 感谢 K. Scott Allen

于 2015-04-02T06:25:13.373 回答
0

也许我可以通过这个例子来帮助你

在自定义花式框中,我显示带有插值的内容。

在服务中,在“打开”fancybox 方法中,我愿意

open: function(html, $compile) {
        var el = angular.element(html);
     var compiledEl = $compile(el);
        $.fancybox.open(el); 
      }

$compile 返回编译后的数据。你可以检查编译的数据

于 2015-08-13T13:45:33.623 回答