当 AngularJS 完成加载时发送事件

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

已经存在任何事件吗? 我应该重载引导程序功能吗?

138719 次浏览

angular.Module的文件中,有一个描述 run函数的条目:

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

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

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

你可以在模块加载完成之后运行:

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

编辑: 手动初始化救援

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

我又看了一眼 手动初始化,似乎这个应该可以解决问题。

我做了一把小提琴来说明。

HTML 很简单:

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

注意缺少 ng-app。我有一个指令,它将执行一些 DOM 操作,这样我们就可以确定事情的顺序和时间。

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

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

And here's the directive:

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"); }
};
}
};
});

我们将用类 test-directivediv替换 test-directive标记,并将其内容包装在 h1中。

我已经添加了一个编译函数,它同时返回 pre 和 post 链接函数,这样我们就可以看到这些东西什么时候运行。

这是剩下的代码:

// 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;
}

在我们做任何事情之前,DOM 中应该没有 test-directive类的元素,完成之后应该有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

In fact, 如果将 ABC0函数附加到 app模块, you'll see it gets run before any of the compiling takes place.

If you run the fiddle and watch the console, you'll see the following:

before (should be 0): 0
Compiling test-directive
Prelink
Postlink
after (should be 1): 1 <--- success!

角度没有提供一种方式来信号当一个页面完成加载,也许是因为 “完成”取决于你的申请。例如,如果您有分部数据的分层树,一个加载另一个。“完成”意味着它们都被装载了。任何框架都很难分析您的代码并理解所有的事情都已经完成,或者仍然在等待。为此,您必须提供特定于应用程序的逻辑来检查和确定这一点。

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

指令是:

.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);
});
}
};
}]);

That can then just be added as an attribute to the body element and then listened for using $scope.$on('initialised', fn)

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

它显然不如 satchmorun 解决方案精确(因为摘要周期可能比超时时间更长) ,但是我的解决方案不需要您跟踪模块,这使得它更容易管理(特别是对于较大的项目)。不管怎样,看起来对我的要求足够准确了。希望能有帮助。

只是一个直觉: 为什么不看看 ngCloak 指令是如何做到的呢?显然,ngCloak 指令设法在加载之后显示内容。我打赌看看 ngCloak 就知道答案了。

EDIT 1 hour later: 好吧,我看了 英格斗篷,它真的很短。这显然意味着在\{\{ template }}表达式被求值之前不会执行编译函数(例如,它加载的模板) ,因此 ngCloak 指令具有很好的功能。

My educated guess would be to just make a directive with the same simplicity of ngCloak, then in your compile function do whatever you want to do. :) Place the directive on the root element of your app. You can call the directive something like myOnload and use it as an attribute my-onload. The compile function will execute once the template has been compiled (expressions evaluated and sub-templates loaded).

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

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

if you have this markup:

<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

By default a straight "link" function is a post-link, so your outer Directive1的 link 函数在内部 directive2's link function has ran. That's why we say that it's only 在后链接中做 DOM 操作是安全的 问题,应该没有问题访问子指令的 inner html from the outer directive's link function, though 必须编译动态插入的内容,如上所述。

From this we can conclude that we can simply make a directive to execute our code when everything is ready/compiled/linked/loaded:

    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>

只要确保您的函数(例如 body IsReady 和 divIsReady)定义在元素所在的作用域(在控制器中)中。

警告: 我说这将为 most的情况下工作。在使用某些指令时要小心,比如 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>

instead of this:

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

The ngElementReady directive will be compiled in the latter example, but it's link function will not be executed. Note: directives are always compiled, but their link functions are not always executed depending on certain scenarios like the above.

EDIT, a few minutes later:

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

<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);
}
});
}
};
});

EDIT, Oct 24 at 12:48am PST:

我改进了原来的 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);
}
}
}
}]);

例如,当一个元素被加载并且 \{\{placeholders}}还没有被替换时,像这样使用它来触发 someFunction:

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

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

计算任意多个表达式,最后一个表达式 true等待如下计算 \{\{placeholders}}:

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

someFunctionanotherFunction将在 \{\{placeholders}}被替换后发射。

这只在第一次加载元素时起作用,在将来的更改时不起作用。如果 $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是不错的。

我使用 JQuery 观察 DOM 对角度的操作,并且我为我的应用程序设置了一个完成度(一些预定义的和令人满意的情况,我需要我的应用程序-摘要)例如,我期望我的 ng 中继器产生7个结果,在那里我将设置一个观察函数,通过 setInterval 的帮助来实现这个目的。

$(document).ready(function(){


var interval = setInterval(function(){


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


},50);


});

If you are using 角形用户界面路由器, you can listen for the $viewContentLoadedevent.

“ $viewContentLoaded- 加载视图后触发,在呈现 DOM 之后。视图的‘ $scope’发出事件。”-Link

$scope.$on('$viewContentLoaded',
function(event){ ... });

这些都是伟大的解决方案,然而,如果你目前正在使用路由,那么我发现这个解决方案是最简单和最少的代码需要。在触发路由之前,使用“解析”属性等待承诺完成。例如:。

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

})

点击这里获得完整的文档-归功于 K · 斯科特 · 艾伦

根据 Angular 团队和这个 Github 的问题:

现在,我们分别在 ng-view 和 ng-include 中发出 $viewContentLoadedand$include。我认为这是我们能知道的最接近编译完成时的情况了。

基于这一点,似乎这是 目前不可能做在一个可靠的方式,否则,角度会提供了事件的盒子。

引导应用程序意味着在根范围上运行摘要循环,而且也没有一个摘要循环完成事件。

根据角度2 设计文件:

Because of multiple digests, it is impossible to determine and notify the component that the model is stable. This is because notification can further change data, which can restart the binding process.

根据这一点,这是不可能的事实之一是为什么决定以角度2重写。

如果没有使用 路线模块,也就是说没有 $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 的回答它的优先级最低。另外,$暂停将导致 Angular 在执行回调之前运行整个事件循环。

使用 $rootScope ,因为它允许在应用程序的任何作用域中放置指令,并且只通知必要的侦听器。

$rootScope.$emit will fire an event for all $rootScope.$on listeners only. The interesting part is that $rootScope.$broadcast will notify all $rootScope.$on as well as $scope.$on listeners 来源

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

在自定义的花哨的方块中,我显示内容与插值。

in the service, in the "open" fancybox method, i do

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

美元编译返回已编译的数据。 可以检查已编译的数据

如果您希望使用服务器端数据(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) {
});

我有一个片段,正在加载后/由主要部分,通过路由进来。

I needed to run a function after that subpartial loaded and I didn't want to write a new directive and figured out you could use a cheeky ngIf

家长部分控制器:

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

子部分的 HTML

<element ng-if="subIsLoaded()"><!-- more html --></element>