【发布时间】:2013-02-19 22:22:35
【问题描述】:
想知道当所有指令都完成编译/链接时,检测页面加载/引导完成的最佳方法是什么。
已经有什么活动了吗?我应该重载引导函数吗?
【问题讨论】:
想知道当所有指令都完成编译/链接时,检测页面加载/引导完成的最佳方法是什么。
已经有什么活动了吗?我应该重载引导函数吗?
【问题讨论】:
只是一个预感:为什么不看看 ngCloak 指令是如何做到的呢?显然,ngCloak 指令会在加载后显示内容。我敢打赌看 ngCloak 会得出确切的答案...
1 小时后编辑: 好吧,我看了ngCloak,真的很短。这显然意味着编译函数在 {{template}} 表达式被评估(即它加载的模板)之前不会被执行,因此 ngCloak 指令的功能很好。
我有根据的猜测是只用与 ngCloak 相同的简单性创建一个指令,然后在你的编译函数中做任何你想做的事情。 :) 将指令放在应用程序的根元素上。您可以调用类似 myOnload 的指令并将其用作属性 my-onload。一旦模板被编译(表达式评估和子模板加载),编译函数就会执行。
编辑,23 小时后: 好的,所以我做了一些研究,我也asked my own question。我问的问题与这个问题间接相关,但它巧合地引导我找到了解决这个问题的答案。
答案是您可以创建一个简单的指令并将代码放在指令的链接函数中,该函数(对于大多数用例,如下所述)将在您的元素准备好/加载时运行。基于Josh's description of the order in which compile and link functions are executed,
如果你有这个标记:
<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 指令在后面的例子中会被编译,但是它的链接函数不会被执行。注意:指令总是被编译,但它们的链接函数并不总是根据上面的某些场景执行。
几分钟后编辑:
哦,要完整回答这个问题,您现在可以通过ng-element-ready 属性中执行的表达式或函数来$emit 或$broadcast 您的事件。 :) 例如:
<div data-ng-element-ready="$emit('someEvent')">
...
<div>
编辑,再过几分钟:
@satchmorun 的答案也有效,但仅适用于初始加载。这是一个very useful SO question,描述了执行的顺序,包括链接函数、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);
}
}
}
}]);
例如,当元素已加载且 {{placeholders}} 尚未替换时,像这样使用它来触发 someFunction:
<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 不错。
【讨论】:
在docs for 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 编译步骤是分开的。
我又看了一下manual initialization,看来这应该可以解决问题。
I've made a fiddle to illustrate.
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"); }
};
}
};
});
我们将用test-directive 类的div 替换test-directive 标记,并将其内容包装在h1 中。
我添加了一个编译函数,它返回前链接和后链接函数,以便我们可以看到这些东西何时运行。
下面是剩下的代码:
// 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 并使用应用的根元素和一组模块名称。
事实上,if you attach a run function to the app module,你会看到它在任何编译发生之前运行。
如果您运行 fiddle 并观察控制台,您将看到以下内容:
before (should be 0): 0
Compiling test-directive
Prelink
Postlink
after (should be 1): 1 <--- success!
【讨论】:
run 在指令之前触发,当运行触发时,html 并不全部
$timeout( initMyPlugins,0) 有效,我需要的所有html都在那里
Angular 没有提供在页面加载完成时发出信号的方法,可能是因为“完成”取决于您的应用程序。例如,如果您有部分分层树,则一个加载其他部分。 “完成”意味着所有这些都已加载。任何框架都很难分析您的代码并理解一切都已完成或仍在等待。为此,您必须提供特定于应用程序的逻辑来检查和确定。
【讨论】:
我想出了一个在评估角度初始化何时完成时相对准确的解决方案。
指令是:
.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 解决方案准确(因为消化周期可能比超时时间更长),但我的解决方案不需要您跟踪模块,这使得它更容易管理(特别是对于较大的项目)。无论如何,似乎对我的要求足够准确。希望对您有所帮助。
【讨论】:
如果您使用Angular UI Router,您可以监听$viewContentLoaded事件。
"$viewContentLoaded - 在视图加载后触发,在 DOM 渲染后。视图的 '$scope' 会发出事件。" - Link
$scope.$on('$viewContentLoaded',
function(event){ ... });
【讨论】:
我观察到 DOM 使用 JQuery 对 Angular 进行操作,并且我确实为我的应用程序设置了一个完成(我的应用程序摘要需要某种预定义且令人满意的情况),例如我希望我的 ng-repeater 产生 7 个结果和为此,我将在 setInterval 的帮助下设置一个观察函数。
$(document).ready(function(){
var interval = setInterval(function(){
if($("article").size() == 7){
myFunction();
clearInterval(interval);
}
},50);
});
【讨论】:
如果您不使用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's answer,它的优先级最低。加上$timeout 将导致 Angular 在回调执行之前运行整个事件循环。
使用$rootScope,因为它允许在应用程序的任何范围内放置指令并仅通知必要的侦听器。
$rootScope.$emit 将为所有 $rootScope.$on 监听器触发一个事件。有趣的是 $rootScope.$broadcast 将通知所有 $rootScope.$on 以及 $scope.$on 监听器 Source
【讨论】:
根据 Angular 团队和Github issue:
我们现在有分别在 ng-view 和 ng-include 中发出的 $viewContentLoaded 和 $includeContentLoaded 事件。我认为这是尽可能接近我们何时完成编译。
基于此,似乎目前不可能以可靠的方式做到这一点,否则 Angular 会开箱即用地提供事件。
引导应用程序意味着在根范围内运行摘要周期,并且也没有摘要周期完成事件。
根据 Angular 2 design docs:
由于有多个摘要,无法确定并通知组件模型是稳定的。这是因为通知可以进一步更改数据,从而可以重新启动绑定过程。
据此,这是不可能的,这是决定在 Angular 2 中重写的原因之一。
【讨论】:
我有一个片段在通过路由进入的主要部分之后/之后被加载。
我需要在加载该子部分后运行一个函数,我不想编写新指令并发现你可以使用厚脸皮的ngIf
父部分的控制器:
$scope.subIsLoaded = function() { /*do stuff*/; return true; };
子部分的HTML
<element ng-if="subIsLoaded()"><!-- more html --></element>
【讨论】:
如果您想使用服务器端数据(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) {
});
【讨论】:
这些都是很棒的解决方案,但是,如果您当前正在使用路由,那么我发现此解决方案是最简单且所需代码量最少的解决方案。在触发路由之前,使用 'resolve' 属性等待 Promise 完成。例如
$routeProvider
.when("/news", {
templateUrl: "newsView.html",
controller: "newsController",
resolve: {
message: function(messageService){
return messageService.getMessage();
}
}
})
【讨论】:
也许我可以通过这个例子帮助你
在自定义花式框中,我用插值显示内容。
在服务中,在“打开”fancybox 方法中,我愿意
open: function(html, $compile) {
var el = angular.element(html);
var compiledEl = $compile(el);
$.fancybox.open(el);
}
$compile 返回编译后的数据。 可以查看编译后的数据
【讨论】: