如何重写AngularJS指令的$scope函数以扩展现有指令
AngularJS指令装饰器重写$scope.init()不生效的问题解决
我来帮你分析下你遇到的问题:你成功用$provide.decorator修改了指令的templateUrl,但重写$scope.init()后,装饰后的调试日志没出现,原指令的init()却正常执行了。先看一下你的代码和控制台输出:
原指令代码
angular.module('app').directive("postWidgetDir", function() { console.log('postWidgetDir()'); return { restrict: 'E', priority: 9999, scope: {}, templateUrl: 'app/views/postWidgetDir.view.html', controller: ['$scope', 'apiService', 'alertService', function($scope, apiService, alertService) { console.log('postWidgetDirController()'); $scope.init = function() { console.log('postWidgetDirController().init()'); }; // Initialize $scope.init(); } ], link: function(scope, element, attrs) { console.log("link() in postWidgetDir()"); element.on('$destroy', function() { console.log('$destroy() in postWidgetDir()'); }); } }; });
装饰器代码
angular.module('app').config(function($provide) { return $provide.decorator('postWidgetDirDirective', function($delegate, $controller) { console.log("$provide.decorator('postWidgetDirDirective')"); // Original directive var directive = $delegate[0]; // Override original directive properties directive.templateUrl = 'app/views/postWidgetDir_custom.view.html'; // Extend original directive controller var controller = directive.controller; directive.controller = function($scope, $timeout) { console.log("directive.controller() in $provide.decorator('postWidgetDirDirective')"); angular.extend(this, $controller(controller, { $scope: $scope })); // 这里的代码有两个问题:递归调用+时机太晚 $scope.init = function() { console.log("$scope.init() in directive.controller() in $provide.decorator('postWidgetDirDirective')"); $scope.init(); // 无限递归! }; }; var link = directive.link; directive.compile = function() { console.log("directive.compile() in $provide.decorator('postWidgetDirDirective')"); return function(scope, element, attrs) { console.log("link() return in directive.compile() in $provide.decorator('postWidgetDirDirective')"); link.apply(this, arguments); // 同样的问题:递归+时机太晚(原init已经执行过了) scope.init = function() { console.log("scope.init() in link() return in directive.compile() in $provide.decorator('postWidgetDirDirective')"); scope.init(); // 无限递归! }; }; }; return $delegate; }); });
实际控制台输出
postWidgetDir() $provide.decorator('postWidgetDirDirective') directive.compile() in $provide.decorator('postWidgetDirDirective') directive.controller() in $provide.decorator('postWidgetDirDirective') postWidgetDirController() postWidgetDirController().init() link() return in directive.compile() in $provide.decorator('postWidgetDirDirective') link() in postWidgetDir()
问题核心分析
- 时机问题:原指令的控制器在实例化时直接调用了
$scope.init(),而你是在原控制器实例化完成之后才重写的init函数,所以原init已经执行完毕,新的init根本没机会被调用。 - 递归陷阱:你在重写
init时直接调用$scope.init(),这会导致无限递归调用新的init函数,即使时机对了也会报错。
解决方案
我们需要调整重写init的时机,同时避免递归调用。这里提供两种可行的修改方案:
方案一:拦截原控制器的init调用,替换为自定义逻辑
通过临时替换$scope.init为空函数,让原控制器的初始化调用不执行原逻辑,之后再重写init并手动调用新逻辑:
angular.module('app').config(function($provide) { return $provide.decorator('postWidgetDirDirective', function($delegate, $controller) { console.log("$provide.decorator('postWidgetDirDirective')"); var directive = $delegate[0]; // 替换templateUrl directive.templateUrl = 'app/views/postWidgetDir_custom.view.html'; var originalController = directive.controller; directive.controller = ['$scope', 'apiService', 'alertService', function($scope, apiService, alertService) { console.log("directive.controller() in $provide.decorator('postWidgetDirDirective')"); // 1. 先把$scope.init替换为空函数,避免原控制器调用原init var tempInit = $scope.init; $scope.init = angular.noop; // 2. 实例化原控制器 var ctrlInstance = $controller(originalController, { $scope: $scope, apiService: apiService, alertService: alertService }); angular.extend(this, ctrlInstance); // 3. 保存原控制器定义的init函数 var originalInit = $scope.init; // 4. 重写init函数,避免递归 $scope.init = function() { console.log("$scope.init() in directive.controller() in $provide.decorator('postWidgetDirDirective')"); // 调用原init逻辑 originalInit.call($scope); }; // 5. 手动调用新的init函数 $scope.init(); }]; var originalLink = directive.link; directive.compile = function() { console.log("directive.compile() in $provide.decorator('postWidgetDirDirective')"); return function(scope, element, attrs) { console.log("link() return in directive.compile() in $provide.decorator('postWidgetDirDirective')"); originalLink.apply(this, arguments); // 如果需要在link阶段再扩展init,可以这样写(同样避免递归) var originalLinkInit = scope.init; scope.init = function() { console.log("scope.init() in link() return in directive.compile() in $provide.decorator('postWidgetDirDirective')"); originalLinkInit.call(scope); }; // 可选:如果需要在这里再次调用init // scope.init(); }; }; return $delegate; }); });
方案二:利用$timeout延迟重写(适合不需要拦截原init执行的场景)
如果你只想在原init执行后,替换init函数供后续调用使用,可以用$timeout延迟重写,避免递归:
directive.controller = ['$scope', '$timeout', 'apiService', 'alertService', function($scope, $timeout, apiService, alertService) { console.log("directive.controller() in $provide.decorator('postWidgetDirDirective')"); var ctrlInstance = $controller(originalController, { $scope: $scope, apiService: apiService, alertService: alertService }); angular.extend(this, ctrlInstance); // 保存原init var originalInit = $scope.init; // 用$timeout延迟重写,确保原init已经执行完毕 $timeout(function() { $scope.init = function() { console.log("$scope.init() in directive.controller() in $provide.decorator('postWidgetDirDirective')"); originalInit.call($scope); }; }); }];
修改后的预期控制台输出
postWidgetDir() $provide.decorator('postWidgetDirDirective') directive.compile() in $provide.decorator('postWidgetDirDirective') directive.controller() in $provide.decorator('postWidgetDirDirective') postWidgetDirController() $scope.init() in directive.controller() in $provide.decorator('postWidgetDirDirective') postWidgetDirController().init() link() return in directive.compile() in $provide.decorator('postWidgetDirDirective') link() in postWidgetDir()
内容的提问来源于stack exchange,提问作者Shovas
相关产品推荐
相关产品推荐

