Angular-ui.router:更新URL而不刷新视图

24

我有一个Angular SPA,它展示了基于一些餐厅数据的不同分类(请参阅m.amsterdamfoodie.nl),包括各种推荐列表和谷歌地图上的位置。我希望每个列表都有自己的URL地址。为了让Google能够爬取这些不同的列表,我在离屏导航中使用了<a>标签。

目前,<a>标签会导致视图刷新,这在地图上非常明显。

  • 我可以使用ng-click$event.preventDefault()来防止这种情况(请参见下面的代码片段),但随之而来的是需要实现一种更新浏览器URL的方法。
  • 但是,在尝试使用Angular的$state或浏览器的history.pushstate时,我最终触发了状态更改和视图刷新...!

因此,我的问题是如何更新模型和URL,但不刷新视图?(另请参见Angular/UI-Router - How Can I Update The URL Without Refreshing Everything?

我已经尝试了很多方法,并且目前有这个HTML:

<a href="criteria/price/1" class="btn btn-default" ng-click="main.action($event)">Budget</a>

在控制器中:

this.action = ($event) ->
    $event.preventDefault()
    params = $event.target.href.match(/criteria\/(.*)\/(.*)$/)

    # seems to cause a view refresh
    # history.pushState({}, "page 2", "criteria/"+params[1]+"/"+params[2]);

    # seems to cause a view refresh
    # $state.transitionTo 'criteria', {criteria:params[1], q:params[2]}, {inherit:false}

    updateModel(...)

我认为正在发生的是,我正在触发$stateProvider代码:

angular.module 'afmnewApp'
.config ($stateProvider) ->
  $stateProvider
  .state 'main',
    url: '/'
    templateUrl: 'app/main/main.html'
    controller: 'MainCtrl'
    controllerAs: 'main'
  .state 'criteria',
    url: '/criteria/:criteria/:q'
    templateUrl: 'app/main/main.html'
    controller: 'MainCtrl'
    controllerAs: 'main'

可能的一个线索是,如果我加载以下代码,例如 http://afmnew.herokuapp.com/criteria/cuisine/italian,则在导航时视图会刷新,而如果我加载 http://afmnew.herokuapp.com/,则没有刷新,但 URL 也没有更新。我完全不明白为什么会发生这种情况。


我还没有深入研究这篇文章,但你可能会觉得它很有趣。https://weluse.de/blog/angularjs-seo-finally-a-piece-of-cake.html - isherwood
不幸的是,这涉及到预渲染,而我的问题是如何以对SEO友好的方式导航SPA。 - Simon H
你看过ui-sref吗?还有状态配置中的reloadOnSearch参数吗? - shaunhusain
reloadOnSearch 看起来可能有帮助,但它是 ui.router 的一部分吗? - Simon H
4个回答

16

3
太棒了,谢谢John。顺便说一下,这在旧路由器上不起作用,但在我升级到angular-ui-router/0.2.18(之前是0.2.13)后,它就像魔法一样奏效了。 - ErichBSchulz
没问题。看起来有一些“次要”的问题,你可以在我回答中指出的 Github 问题中进行检查。但如果我没有错的话,这个问题应该已经在 UI-Router 的 1.x 版本中得到解决了。 - John Bernardsson
这个链接在点击时可以正常工作,但如果我在浏览器上向前/向后导航,它会重新加载视图。 - phazei
是的,这正是我在评论中提到的问题之一。我没有机会检查它是否仍然存在于1.0 alpha版本(截至今天最新版本)...那是这种情况吗? - John Bernardsson

11

基于我们之前的讨论,我想给你一些使用 UI-Router 的想法。我相信我正确地理解了你的挑战... 这里有一个可工作的例子。如果这个例子不完全适合,请将其视为灵感。

免责声明:我无法通过plunker实现这个:http://m.amsterdamfoodie.nl/,但在那个示例中,原则应该是类似的。

所以,有一个状态定义(我们只有两个状态)

  $stateProvider
    .state('main', {
        url: '/',
        views: {
          '@' : {
            templateUrl: 'tpl.layout.html',
            controller: 'MainCtrl',
          },
          'right@main' : { templateUrl: 'tpl.right.html',}, 
          'map@main' : {
            templateUrl: 'tpl.map.html',
            controller: 'MapCtrl',
          },
          'list@main' : {
            templateUrl: 'tpl.list.html',
            controller: 'ListCtrl',
          },
        },
      })
    .state('main.criteria', {
        url: '^/criteria/:criteria/:value',
        views: {
          'map' : {
            templateUrl: 'tpl.map.html',
            controller: 'MapCtrl',
          },
          'list' : {
            templateUrl: 'tpl.list.html',
            controller: 'ListCtrl',
          },
        },
      })
}];

这将是我们的主要tpl.layout.html

<div>

  <section class="main">

    <section class="map">
      <div ui-view="map"></div>
    </section>

    <section class="list">
      <div ui-view="list"></div>
    </section>

  </section>

  <section class="right">
    <div ui-view="right"></div>
  </section>

</div>

正如我们所看到的,主状态确实针对这些嵌套视图进行了定位:'viewName@main',例如 'right@main'

此外,子视图main.criteria会注入到布局视图中。

其URL以符号^开头(url : '^/criteria/:criteria/:value'),这允许主要部分使用/斜杠而不是儿童使用双斜杠

还有控制器,它们有点天真,但它们应该表明在后台可能会加载真正的数据(基于标准)。

这里最重要的东西是,父级MainCtrl创建了$scope.Model = {}。这个属性将(由于继承)在父级和子级之间共享。这就是为什么所有这些都能够工作:

app.controller('MainCtrl', function($scope)
{
  $scope.Model = {};
  $scope.Model.data = ['Rest1', 'Rest2', 'Rest3', 'Rest4', 'Rest5'];  
  $scope.Model.randOrd = function (){ return (Math.round(Math.random())-0.5); };
})
.controller('ListCtrl', function($scope, $stateParams)
{
  $scope.Model.list = []
  $scope.Model.data
    .sort( $scope.Model.randOrd )
    .forEach(function(i) {$scope.Model.list.push(i + " - " + $stateParams.value || "root")})
  $scope.Model.selected = $scope.Model.list[0];
  $scope.Model.select = function(index){
    $scope.Model.selected = $scope.Model.list[index];  
  }
})

以下是如何使用UI-Router提供的功能的一些想法:

您可以在此处查看上述摘录,并在工作示例中进行检查。

扩展:新的plunker可以在此处找到。

如果我们不想重建地图视图,我们可以从子状态定义中省略该定义:

.state('main.criteria', {
    url: '^/criteria/:criteria/:value',
    views: {
      // 'map' : {
      //  templateUrl: 'tpl.map.html',
      //  controller: 'MapCtrl',
      //},
      'list' : {
        templateUrl: 'tpl.list.html',
        controller: 'ListCtrl',
      },
    },
  })
现在我们的地图视图 VIEW 将只接收模型中的更改(可以被监视),但视图和控制器不会重新呈现。
此外,还有另一个 Plunker http://plnkr.co/edit/y0GzHv?p=preview,它使用了 controllerAs。
.state('main', {
    url: '/',
    views: {
      '@' : {
        templateUrl: 'tpl.layout.html',
        controller: 'MainCtrl',
        controllerAs: 'main',        // here
      },
      ...
    },
  })
.state('main.criteria', {
    url: '^/criteria/:criteria/:value',
    views: {
      'list' : {
        templateUrl: 'tpl.list.html',
        controller: 'ListCtrl',
        controllerAs: 'list',      // here
      },
    },
  })

并且可以像这样使用:

<h4>{{main.hello()}}</h4>
<h4>{{list.hello()}}</h4>

最后一个 Plunker 在这里


快速问题:在MainCtrl中创建模型是否至关重要 - 到目前为止我一直在使用共享工厂? - Simon H
1
Simon,请你认真对待这个问题。我想帮忙,但是你给我的链接是压缩过的JavaScript版本?那样是行不通的。你应该知道我是想要帮忙的。所以请尝试调整和扩展我的plunker来重现这个问题。我也可以重新设计你的life app,但是...(这有点超出了在SO上的问答范围)...无论如何,我想给你一个承诺。我创建了一个相当复杂的plunker。如果你想让我做更多的事情,请让它变得容易。如果你不再需要我的帮助,我也理解...这有意义吗? - Radim Köhler
我目前正在探讨的想法是将地图本身放入主视图中,但是由子视图创建标记。 - Simon H
非常抱歉,现在无法回复,稍后会有空... 这个想法对我来说是正确的。将更新 Plunker,不会在子级上使用 map... - Radim Köhler
让我们在聊天中继续这个讨论 - Simon H
显示剩余4条评论

2

您可以使用作用域继承在不刷新视图的情况下更新 URL。

$stateProvider
            .state('itemList', {
                url: '/itemlist',
                templateUrl: 'Scripts/app/item/ItemListTemplate.html',
                controller: 'ItemListController as itemList'
                //abstract: true //abstract maybe?
            }).state('itemList.itemDetail', {
                url: '/:itemName/:itemID',
                templateUrl: 'Scripts/app/item/ItemDetailTemplate.html',
                controller: 'ItemDetailController as itemDetail',
                resolve: {
                    'CurrentItemID': ['$stateParams',function ($stateParams) {
                        return $stateParams['itemID'];
                    }]
                }
            })

如果子视图在父视图内部,则两个控制器共享相同的作用域。 因此,您可以在父视图内放置一个虚拟(或必要的)ui-view,该视图将由子视图填充。
$scope.loadChildData = function(itemID){..blabla..};

父控制器中的函数将在子控制器加载时被调用。因此,当用户单击时...
<a ui-sref="childState({itemID: 12})">bla</a> 

只有子控制器和子视图将被刷新。然后,您可以使用必要的参数调用父作用域函数。

谢谢。实际上,我最终将地图包装在一个带有控制器但未链接到视图的 div 中。我没有使用作用域继承,而是将所有关键数据放在一个服务中,控制器当然可以访问该服务。 - Simon H

0
简短的答案是不要将地图放在一个会变化的视图中。接受的答案提供了更多如何构建带有子视图的页面的细节,但关键点是不要将地图作为视图的一部分,而是将其行为与会改变的视图连接起来,并使用控制器更新市场图标。

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