你能在AngularJS中不重新加载控制器的情况下更改路径吗?

196

这个问题之前已经有人问过了,从答案来看情况并不乐观。考虑到下面的示例代码,我想提出我的问题...

我的应用程序会加载当前服务中提供的项目。有几个控制器可以在不重新加载该项目的情况下操纵项目数据。

如果尚未设置该项目,则我的控制器将重新加载该项目,否则它将在控制器之间使用当前加载的项目。

问题:我想为每个控制器使用不同的路径,而不重新加载Item.html。

1)这可能吗?

2)如果不可能,请问有没有更好的方法来为每个控制器设置路径,而不是我在这里想出来的方法?

app.js

var app = angular.module('myModule', []).
  config(['$routeProvider', function($routeProvider) {
    $routeProvider.
      when('/items', {templateUrl: 'partials/items.html',   controller: ItemsCtrl}).
      when('/items/:itemId/foo', {templateUrl: 'partials/item.html', controller: ItemFooCtrl}).
      when('/items/:itemId/bar', {templateUrl: 'partials/item.html', controller: ItemBarCtrl}).
      otherwise({redirectTo: '/items'});
    }]);

物品.html

<!-- Menu -->
<a id="fooTab" my-active-directive="view.name" href="#/item/{{item.id}}/foo">Foo</a>
<a id="barTab" my-active-directive="view.name" href="#/item/{{item.id}}/bar">Bar</a>
<!-- Content -->
<div class="content" ng-include="" src="view.template"></div>

控制器.js

// Helper function to load $scope.item if refresh or directly linked
function itemCtrlInit($scope, $routeParams, MyService) {
  $scope.item = MyService.currentItem;
  if (!$scope.item) {
    MyService.currentItem = MyService.get({itemId: $routeParams.itemId});
    $scope.item = MyService.currentItem;
  }
}
function itemFooCtrl($scope, $routeParams, MyService) {
  $scope.view = {name: 'foo', template: 'partials/itemFoo.html'};
  itemCtrlInit($scope, $routeParams, MyService);
}
function itemBarCtrl($scope, $routeParams, MyService) {
  $scope.view = {name: 'bar', template: 'partials/itemBar.html'};
  itemCtrlInit($scope, $routeParams, MyService);
}

解决方案。

状态:使用接受答案中推荐的搜索查询方法,使我能够在不重新加载主控制器的情况下提供不同的url。

app.js

var app = angular.module('myModule', []).
  config(['$routeProvider', function($routeProvider) {
    $routeProvider.
      when('/items', {templateUrl: 'partials/items.html',   controller: ItemsCtrl}).
      when('/item/:itemId/', {templateUrl: 'partials/item.html', controller: ItemCtrl, reloadOnSearch: false}).
      otherwise({redirectTo: '/items'});
    }]);

物品.html

<!-- Menu -->
<dd id="fooTab" item-tab="view.name" ng-click="view = views.foo;"><a href="#/item/{{item.id}}/?view=foo">Foo</a></dd>
<dd id="barTab" item-tab="view.name" ng-click="view = views.bar;"><a href="#/item/{{item.id}}/?view=foo">Bar</a></dd>

<!-- Content -->
<div class="content" ng-include="" src="view.template"></div>

控制器.js

function ItemCtrl($scope, $routeParams, Appts) {
  $scope.views = {
    foo: {name: 'foo', template: 'partials/itemFoo.html'},
    bar: {name: 'bar', template: 'partials/itemBar.html'},
  }
  $scope.view = $scope.views[$routeParams.view];
}

指令.js

app.directive('itemTab', function(){
  return function(scope, elem, attrs) {
    scope.$watch(attrs.itemTab, function(val) {
      if (val+'Tab' == attrs.id) {
        elem.addClass('active');
      } else {
        elem.removeClass('active');
      }
    });
  }
});

我的局部内容被包含在 ng-controller=... 中。


找到了这个答案:https://dev59.com/y2Ml5IYBdhLWcg3wgnSl#18551525 - 它使用了reloadOnSearch: false,但也检查了搜索栏中URL的更新(例如,如果用户点击返回按钮并且URL更改)。 - Rusty Rob
12个回答

121
如果你不必使用像#/item/{{item.id}}/foo#/item/{{item.id}}/bar这样的URL,而是改用#/item/{{item.id}}/?foo#/item/{{item.id}}/?bar,那么你可以设置路由/item/{{item.id}}/reloadOnSearchfalse(参考:https://docs.angularjs.org/api/ngRoute/provider/$routeProvider)。这告诉AngularJS如果url的搜索部分发生变化,不要重新加载视图。

明白了。我在视图数组中添加了一个控制器参数,然后在 ng-include 指令中添加了 ng-controller="view.controller" - Coder1
如果你能在 jsFiddle/plnkr 等网站上隔离出你的问题,我相信我们可以帮助你解决它。 - Anders Ekdahl
我不确定如何在jsFiddle中使用多个partials。 我已经留下了我的进展,它确实解决了能够在不重新加载根控制器的情况下使用网址的初始问题。今天稍后我将不得不开始处理作用域问题。你肯定让我走上了正确的轨道,谢谢! - Coder1
1
需要注意的是,实际上您不需要重新格式化您的URL。这可能是在发布响应时的情况,但现在文档(http://docs.angularjs.org/api/ngRoute.$routeProvider)中已经说明:[reloadOnSearch=true] - {boolean=} - 当$location.search()或$location.hash()更改时重新加载路由。 - Rhys van der Waerden
对于任何感兴趣的人,UI路由器存在相同的参数 - 您可以在状态上设置“reloadOnSearch:false”。 - Illidan
显示剩余5条评论

94
如果您需要更改路径,请在应用程序文件中的.config后面添加以下内容。然后,您可以执行$location.path('/sampleurl', false);以防止重新加载。
app.run(['$route', '$rootScope', '$location', function ($route, $rootScope, $location) {
    var original = $location.path;
    $location.path = function (path, reload) {
        if (reload === false) {
            var lastRoute = $route.current;
            var un = $rootScope.$on('$locationChangeSuccess', function () {
                $route.current = lastRoute;
                un();
            });
        }
        return original.apply($location, [path]);
    };
}])

最优雅的解决方案来自于https://www.consolelog.io/angularjs-change-path-without-reloading,所有的功劳归属于他们。


6
好的解决方案。有没有办法让浏览器的返回按钮“尊重”这个解决方案? - Mark B
6
请注意,这种解决方案保留了旧路由,如果您请求 $route.current,您将得到先前的路径而不是当前的路径。否则,这是一个非常好的小技巧。 - jornare
4
想说一下,原始解决方案实际上是由"EvanWinstanley"在这里发布的:https://github.com/angular/angular.js/issues/1699#issuecomment-34841248 - chipit24
2
我已经使用这个解决方案一段时间了,只是注意到在某些路径更改时,即使传递了一个真值,重新加载仍然会注册为假。还有其他人遇到过类似的问题吗? - Will Hitchcock
2
有时候在执行apply后,$locationChangeSuccess事件没有被触发(即使路由确实需要改变),因此我不得不在return语句之前添加$timeout(un, 200);。我的解决方案可以在调用path(..., false)后清除这些问题。 - snowindy
显示剩余6条评论

18

为什么不将ng-controller放到更高的层级?

<body ng-controller="ProjectController">
    <div ng-view><div>

不要在路由中设置控制器,

.when('/', { templateUrl: "abc.html" })

对我来说它有效。


非常好的提示,它对我的情况也完美适用!非常感谢你! :) - daveoncode
4
这是一个很好的“俄国人使用铅笔”的回答,对我来说很有效 :) - inolasco
1
我说得太早了。这个解决方法有一个问题,如果你需要在控制器中从 $routeParams 中加载值,它们将不会加载,默认为 {}。 - inolasco
如果你在$routeChangeSuccess处理程序中读取$routeParams,那么它就能工作。往往情况下,这可能是你想要的。仅在控制器中读取将只获取最初加载的URL的值。 - plong0
@plong0 很好的建议,应该可行。但在我的情况下,我只需要在控制器在页面加载时获取URL值,以初始化某些值。最终我采用了vigrond提出的解决方案,使用$locationChangeSuccess,但你的方法也是另一个有效的选择。 - inolasco

13

10
尽管这篇文章很旧并且已经有了一个被接受的答案,但对于我们中需要更改实际路径而不仅仅是参数的人来说,使用reloadOnSeach=false并不能解决问题。这里有一个简单的解决方案可供考虑:使用ng-include代替ng-view,并在模板中分配控制器。
<!-- In your index.html - instead of using ng-view -->
<div ng-include="templateUrl"></div>

<!-- In your template specified by app.config -->
<div ng-controller="MyController">{{variableInMyController}}</div>

//in config
$routeProvider
  .when('/my/page/route/:id', { 
    templateUrl: 'myPage.html', 
  })

//in top level controller with $route injected
$scope.templateUrl = ''

$scope.$on('$routeChangeSuccess',function(){
  $scope.templateUrl = $route.current.templateUrl;
})

//in controller that doesn't reload
$scope.$on('$routeChangeSuccess',function(){
  //update your scope based on new $routeParams
})

唯一的缺点是您不能使用resolve属性,但这很容易解决。 另外,您必须管理控制器的状态,例如根据$routeParams的逻辑来更改相应URL后控制器内路由的更改。
以下是示例: http://plnkr.co/edit/WtAOm59CFcjafMmxBVOP?p=preview

1
不确定在plnkr中返回按钮是否会正确地工作...正如我所提到的,当路由改变时,你必须自己管理一些东西...这不是最值得编写代码的解决方案,但它确实可以工作。 - Lukus

2
我使用这个解决方案。
angular.module('reload-service.module', [])
.factory('reloadService', function($route,$timeout, $location) {
  return {
     preventReload: function($scope, navigateCallback) {
        var lastRoute = $route.current;

        $scope.$on('$locationChangeSuccess', function() {
           if (lastRoute.$$route.templateUrl === $route.current.$$route.templateUrl) {
              var routeParams = angular.copy($route.current.params);
              $route.current = lastRoute;
              navigateCallback(routeParams);
           }
        });
     }
  };
})

//usage
.controller('noReloadController', function($scope, $routeParams, reloadService) {
     $scope.routeParams = $routeParams;

     reloadService.preventReload($scope, function(newParams) {
        $scope.routeParams = newParams;
     });
});

这种方法保留了后退按钮的功能,而且模板中始终有当前的routeParams,与我见过的其他方法不同。


1
上面的答案,包括 GitHub 上的答案,在我的情况下存在一些问题,而且从浏览器返回按钮或直接更改 URL 会重新加载控制器,这不是我想要的。最终,我采用了以下方法:
1. 在路由定义中定义一个属性,称为“noReload”,用于那些您不希望控制器在路由更改时重新加载的路由。
.when('/:param1/:param2?/:param3?', {
    templateUrl: 'home.html',
    controller: 'HomeController',
    controllerAs: 'vm',
    noReload: true
})

2. 在你的模块的运行函数中,放置检查这些路由的逻辑。只有当noReloadtrue且前一个路由控制器相同时,才会防止重新加载。

fooRun.$inject = ['$rootScope', '$route', '$routeParams'];

function fooRun($rootScope, $route, $routeParams) {
    $rootScope.$on('$routeChangeStart', function (event, nextRoute, lastRoute) {
        if (lastRoute && nextRoute.noReload 
         && lastRoute.controller === nextRoute.controller) {
            var un = $rootScope.$on('$locationChangeSuccess', function () {
                un();
                // Broadcast routeUpdate if params changed. Also update
                // $routeParams accordingly
                if (!angular.equals($route.current.params, lastRoute.params)) {
                    lastRoute.params = nextRoute.params;
                    angular.copy(lastRoute.params, $routeParams);
                    $rootScope.$broadcast('$routeUpdate', lastRoute);
                }
                // Prevent reload of controller by setting current
                // route to the previous one.
                $route.current = lastRoute;
            });
        }
    });
}

3. 最后,在控制器中,监听$routeUpdate事件,这样当路由参数改变时,您就可以执行任何需要执行的操作。

HomeController.$inject = ['$scope', '$routeParams'];

function HomeController($scope, $routeParams) {
    //(...)

    $scope.$on("$routeUpdate", function handler(route) {
        // Do whatever you need to do with new $routeParams
        // You can also access the route from the parameter passed
        // to the event
    });

    //(...)
}

请记住,使用这种方法,您不会先更改控制器中的内容,然后相应地更新路径。它是反过来的。您首先更改路径,然后侦听$routeUpdate事件,在路由参数更改时更改控制器中的内容。
这样做可以保持简单和一致性,因为您可以在仅更改路径(但如果需要,不进行昂贵的$http请求)和完全重新加载浏览器时使用相同的逻辑。

1

自版本1.2起,您可以使用{{link1:$location.replace()}}:

$location.path('/items');
$location.replace();

0

有一种简单的方法可以在不重新加载页面的情况下更改路径

URL is - http://localhost:9000/#/edit_draft_inbox/1457

使用此代码更改URL,页面将不会重定向

第二个参数“false”非常重要。

$location.path('/edit_draft_inbox/'+id, false);

1
这不是AngularJS的正确用法。请参考文档:https://docs.angularjs.org/api/ng/service/$location。 - steampowered

0

这是我的完整解决方案,解决了@Vigrond和@rahilwazir错过的一些问题:

  • 当搜索参数更改时,会阻止广播$routeUpdate
  • 当路由实际上未更改时,$locationChangeSuccess从未触发,这导致防止下一个路由更新。
  • 如果在同一次脏检查周期中有另一个更新请求,这次请求希望重新加载,则事件处理程序将取消该重新加载。

    app.run(['$rootScope', '$route', '$location', '$timeout', function ($rootScope, $route, $location, $timeout) {
        ['url', 'path'].forEach(function (method) {
            var original = $location[method];
            var requestId = 0;
            $location[method] = function (param, reload) {
                // getter
                if (!param) return original.call($location);
    
                # 只允许最后一次调用在一个脏检查周期内执行操作
                var currentRequestId = ++requestId;
                if (reload === false) {
                    var lastRoute = $route.current;
                    // 拦截仅下一个$locateChangeSuccess
                    var un = $rootScope.$on('$locationChangeSuccess', function () {
                        un();
                        if (requestId !== currentRequestId) return;
    
                        if (!angular.equals($route.current.params, lastRoute.params)) {
                            // 当参数更改时,应始终广播此消息
                            $rootScope.$broadcast('$routeUpdate');
                        }
                        var current = $route.current;
                        $route.current = lastRoute;
                        // 使先前的路由更改工作
                        $timeout(function() {
                            if (requestId !== currentRequestId) return;
                            $route.current = current;
                        });
                    });
                    // 如果由于某种原因未触发,则不要拦截下一个
                    $timeout(un);
                }
                return original.call($location, param);
            };
        });
    }]);
    

错别字在末尾应该是“param”,而且这不能与哈希一起使用,我的地址栏中的URL也不会更新。 - deltree
已修复。嗯,奇怪的是它在我的 HTML5 URL 上可以工作。我想这可能仍然对某些人有所帮助... - Oded Niv

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接