在Angular 2中不重新加载页面的情况下更改路由参数

194

我正在使用Angular 2,Google Maps等技术制作一个房地产网站。当用户更改地图的中心位置时,我会向API执行搜索,指示地图当前位置以及半径。问题是,我想在不重新加载整个页面的情况下将这些值反映在URL中。这可能吗? 我找到了一些使用AngularJS 1.x的解决方案,但没有关于Angular 2的。


我认为如果你使用[routerLink]="['/route', { param1: value1 }]",它不会重新加载页面。 - Toolkit
但是我该如何添加另一个查询参数? - Toolkit
3
它会导致页面重新加载。 - LifterCoder
请注意,如果您使用SSR使您的网站与SEO兼容,那么这个问题就不存在了。 - Jonathan
@Jonathan,是吧?由于Angular一旦渲染静态页面就接管了路由,所以我认为即使使用SSR仍然是一个有效的问题。 - adam0101
只需将其推送到历史记录... window.history.pushState("http://example.ca", "示例标题", "/example/path.html"); - Dean Van Greunen
14个回答

169

从RC6开始,您可以执行以下操作来更改URL而不更改状态,从而保留您的路由历史记录。

import {OnInit} from '@angular/core';

import {Location} from '@angular/common'; 
// If you dont import this angular will import the wrong "Location"

@Component({
  selector: 'example-component',
  templateUrl: 'xxx.html'
})
export class ExampleComponent implements OnInit
{
  constructor( private location: Location )
  {}

  ngOnInit()
  {    
    this.location.replaceState("/some/newstate/");
  }
}

这对我不起作用。它仍然尝试加载路由。控制台错误:Error: Uncaught (in promise): Error: Cannot match any routes. URL Segment: 'some/newstate' - Inigo
8
将@golfadas建议的URL创建与此相结合,我们就获得了胜利! - crowmagnumb

126

您可以使用location.go(url),它会改变您的URL,而不会更改应用程序的路由。

注意:这可能会引起其他效果,例如从当前路由重定向到子路由。

相关问题描述了location.go,但不会通知Router进行更改。

如何操作

import { Location } from '@angular/common';

constructor(private _location: Location){ }

updateUrl(url: string) {
  this._location.go(url)
}

3
我的路由包含一个名为'search'的参数,用于接收序列化后的搜索字段。当列表状态首次加载时,我只需读取这些参数,使用this._routeParams.get('search')反序列化过滤器并执行搜索。如果用户通过地图或搜索选项更改了搜索字段,则使用路由器的generate方法构造正确的URL,例如:var instruction = this._router.generate(['Listing',{search: serializedFields}]),然后使用this._location.go(instruction.urlPath)修改URL,而不重新加载列表状态。 - Marcos Basualdo
24
如果有其他人想知道:import { Location } from 'angular2/platform/common'; - Kesarion
38
在Angular 4中,import { Location } from '@angular/common';表示从@angular/common模块导入Location - tehCivilian
5
你是否声明了像 constructor(private location: Location){ } 这样的构造函数? - Pankaj Parkar
4
问题很可能是您键入了 location.go(),而应该键入 this.location.go()。加上 this. 之后,您调用了 TypeScript 的 location 接口。 - georg-un
显示剩余6条评论

103

使用 location.go(url) 是可行的方式,但不要硬编码URL,考虑使用 router.createUrlTree() 生成URL。

如果您想进行以下路由调用:this.router.navigate([{param: 1}], {relativeTo: this.activatedRoute}),但不想重新加载组件,则可以重写为:

const url = this.router.createUrlTree([], {relativeTo: this.activatedRoute, queryParams: {param: 1}}).toString()

 this.location.go(url);

11
这个答案解决了我的问题。我有一个问题,上面生成的URL带有由“;”(分号)分隔的参数。我们应该怎么做才能用“&”将查询中的每个参数分开? - Amarnath
2
这是 createUrlTree(commands: any[], navigationExtras?: NavigationExtras) 的声明。您需要使用位于 navigationExtras 中的 queryParams,而不是 commandscreateUrlTree([], {relativeTo: this.activatedRoute, queryParams: {param: 1} }) - kit
1
只是为了澄清@kit所说的,做这个:this.router.createUrlTree([], {relativeTo: this.activatedRoute, queryParams: {param: 1}}).toString() - Eyad Arafat
这是一个好主意,但是使用 location.go 后,this.activatedRoute 不会改变,所以您也将在旧路由上添加参数... - Uko
这与路由参数有什么关系? - Captain Prinny

26
对于像我这样发现这个问题的人,以下内容可能会有用。
我遇到了类似的问题,最初尝试使用在其他答案中建议的location.go和location.replaceState。然而,当我必须导航到应用程序上的另一个页面时,我遇到了问题,因为导航是相对于当前路由的,并且当前路由没有被location.go或location.replaceState更新(路由器不知道它们对URL做了什么)。
实际上,我需要一种解决方案,当路由参数更改时不重新加载页面/组件,但在内部更新路由状态。
最终,我使用了查询参数。您可以在此处找到更多信息:https://angular-2-training-book.rangle.io/handout/routing/query_params.html 因此,如果您需要执行类似保存订单并获取订单ID之类的操作,可以按下面所示更新页面URL。更新地图上的中心位置和相关数据将类似。
// let's say we're saving an order. Initally the URL is just blah/orders
save(orderId) {
    // [Here we would call back-end to save the order in the database]

    this.router.navigate(['orders'], { queryParams: { id: orderId } });
    // now the URL is blah/orders?id:1234. We don't reload the orders
    // page or component so get desired behaviour of not seeing any 
    // flickers or resetting the page.
}

你可以在ngOnInit方法中跟踪它:

ngOnInit() {
    this.orderId = this.route
        .queryParamMap
        .map(params => params.get('id') || null);
    // orderID is up-to-date with what is saved in database now, or if
    // nothing is saved and hence no id query paramter the orderId variable
    // is simply null.
    // [You can load the order here from its ID if this suits your design]
}

如果您需要直接进入新订单的订单页面(未保存),可以执行以下操作:

this.router.navigate(['orders']);

如果您需要直接进入现有(已保存)订单的订单页面,可以执行以下操作:

this.router.navigate(['orders'], { queryParams: { id: '1234' } });

1
确实,这样可以正确更新路由(从Angular的角度),并且不会重建位于“orders”处的组件,这正是我想要的。 - yannick1976

17

我在angular2的RCx版本中遇到了一些大问题。Location包已经移动,而在constructor()内运行location.go()无法工作。它需要在ngOnInit()或生命周期的后面执行。以下是一些示例代码:

import {OnInit} from '@angular/core';
import {Location} from '@angular/common';

@Component({
  selector: 'example-component',
  templateUrl: 'xxx.html'
})
export class ExampleComponent implements OnInit
{
  constructor( private location: Location )
  {}

  ngOnInit()
  {    
    this.location.go( '/example;example_param=917' );
  }
}

以下是关于该问题的Angular资源: https://angular.io/docs/ts/latest/api/common/index/Location-class.html https://angular.io/docs/ts/latest/api/common/index/LocationStrategy-class.html


12

我曾经有过与问题描述类似的需求,但是根据现有答案花费了一段时间才找到解决方案,所以我想分享我的最终解决方案。

需求

我的视图状态(组件,在技术上)可以被用户更改(过滤设置、排序选项等)。当状态更改发生时,即用户更改排序方向时,我想要:

  • 在URL中反映状态更改
  • 处理状态更改,即进行API调用以接收新的结果集

此外,我还希望:

  • 根据情况指定是否将URL更改考虑在浏览器历史记录中(后退/前进)
  • 使用复杂对象作为状态参数,以提供更大的灵活性来处理状态更改(可选,但使生活更轻松,例如当某些状态更改触发后端/API调用而其他状态更改由前端内部处理时)

解决方案:在不重新加载组件的情况下更改状态

使用路由参数或查询参数进行状态更改时,状态更改不会导致组件重新加载。组件实例保持活动状态。我认为使用Location.go()location.replaceState()来搞乱路由器状态没有什么好处。

var state = { q: 'foo', sort: 'bar' }; 
var url = this.router.createUrlTree([], { relativeTo: this.activatedRoute, queryParams: state }).toString();
this.router.navigateByUrl(url);

state对象将被Angular的Router转换为URL查询参数:

https://localhost/some/route?q=foo&sort=bar

解决方案:处理状态更改以进行API调用

可以通过订阅ActivatedRoute.queryParams来处理上述触发的状态更改:

export class MyComponent implements OnInit {

   constructor(private activatedRoute: ActivatedRoute) { }

   ngOnInit()
   {
      this.activatedRoute.queryParams.subscribe((params) => {
         // params is the state object passed to the router on navigation 
         // Make API calls here
      });
   }

}

以上示例中的state对象将作为queryParams可观察参数的params参数传递。在处理程序中,如果必要,可以进行API调用。
但是:我更喜欢直接处理组件中的状态更改,避免通过ActivatedRoute.queryParams进行绕路。我的个人看法是,导航路由,让Angular执行路由魔法并处理queryParams更改以执行某些操作,会完全模糊掉我的组件中发生的情况,从而使我的代码难以维护和阅读。我所做的是:
将传递给queryParams可观察参数的状态与我的组件中的当前状态进行比较,如果没有更改,则不进行任何操作,并直接处理状态更改。
export class MyComponent implements OnInit {

   private _currentState;

   constructor(private activatedRoute: ActivatedRoute) { }

   ngOnInit()
   {
      this.activatedRoute.queryParams.subscribe((params) => {
         // Following comparison assumes, that property order doesn't change
         if (JSON.stringify(this._currentState) == JSON.stringify(params)) return;
         // The followig code will be executed only when the state changes externally, i.e. through navigating to a URL with params by the user
         this._currentState = params;
         this.makeApiCalls();
      });
   }

   updateView()
   {          
      this.makeApiCalls();
      this.updateUri();
   }    

   updateUri()
   {
      var url = this.router.createUrlTree([], { relativeTo: this.activatedRoute, queryParams: this._currentState }).toString();
this.router.navigateByUrl(url);
   }
}

解决方案:指定浏览器历史记录行为。
var createHistoryEntry = true // or false
var url = ... // see above
this.router.navigateByUrl(url, { replaceUrl : !createHistoryEntry});

解决方案:将复杂对象作为状态

这超出了原始问题的范围,但涉及常见场景,因此可能会有用:上面的state对象仅限于平面对象(仅具有简单的字符串/布尔/整数等属性但没有嵌套对象)。我发现这是有限制的,因为我需要区分需要通过后端调用处理的属性和其他仅由组件内部使用的属性。我想要一个像这样的状态对象:

var state = { filter: { something: '', foo: 'bar' }, viewSettings: { ... } };

为了将此状态用作路由器的queryParams对象,它需要被压平。我只需将对象的所有一级属性 JSON.stringify 即可:
private convertToParamsData(data) {
    var params = {};

    for (var prop in data) {
      if (Object.prototype.hasOwnProperty.call(data, prop)) {
        var value = data[prop];
        if (value == null || value == undefined) continue;
        params[prop] = JSON.stringify(value, (k, v) => {
          if (v !== null) return v
        });
      }
    }
    return params;
 }

同时,在处理路由器返回的查询参数时,请注意:

private convertFromParamsData(params) {
    var data = {};

    for (var prop in params) {
      if (Object.prototype.hasOwnProperty.call(params, prop)) {
        data[prop] = JSON.parse(params[prop]);
      }
    }
    return data;
}

最终:一个可直接使用的Angular服务

最终,所有这些都隔离在一个简单的服务中:

import { Injectable } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { Location } from '@angular/common';
import { map, filter, tap } from 'rxjs/operators';

@Injectable()
export class QueryParamsService {

  private currentParams: any;

  externalStateChange: Observable<any>;

  constructor(private activatedRoute: ActivatedRoute, private router: Router, private location: Location) {

    this.externalStateChange = this.activatedRoute.queryParams
      .pipe(map((flatParams) => {
        var params = this.convertFromParamsData(flatParams);
        return params
      }))
      .pipe(filter((params) => {
        return !this.equalsCurrentParams(params);
      }))
      .pipe(tap((params) => {
        this.currentParams = params;
      }));
  }

  setState(data: any, createHistoryEntry = false) {
    var flat = this.convertToParamsData(data);
    const url = this.router.createUrlTree([], { relativeTo: this.activatedRoute, queryParams: flat }).toString();
    this.currentParams = data;
    this.router.navigateByUrl(url, { replaceUrl: !createHistoryEntry });
  }

  private equalsCurrentParams(data) {
    var isEqual = JSON.stringify(data) == JSON.stringify(this.currentParams);
    return isEqual;
  }

  private convertToParamsData(data) {
    var params = {};

    for (var prop in data) {
      if (Object.prototype.hasOwnProperty.call(data, prop)) {
        var value = data[prop];
        if (value == null || value == undefined) continue;
        params[prop] = JSON.stringify(value, (k, v) => {
          if (v !== null) return v
        });
      }
    }
    return params;
  }

  private convertFromParamsData(params) {
    var data = {};

    for (var prop in params) {
      if (Object.prototype.hasOwnProperty.call(params, prop)) {
        data[prop] = JSON.parse(params[prop]);
      }
    }
    return data;
  }
}

可用于:

@Component({
  selector: "app-search",
  templateUrl: "./search.component.html",
  styleUrls: ["./search.component.scss"],
  providers: [QueryParamsService]
})
export class ProjectSearchComponent implements OnInit {

    filter : any;

    viewSettings : any;

    constructor(private queryParamsService: QueryParamsService) { }

    ngOnInit(): void {

        this.queryParamsService.externalStateChange
          .pipe(debounce(() => interval(500))) // Debounce optional
          .subscribe(params => {
           // Set state from params, i.e.
           if (params.filter) this.filter = params.filter;
           if (params.viewSettings) this.viewSettings = params.viewSettings;

           // You might want to init this.filter, ... with default values here
           // If you want to write default values to URL, you can call setState here
            this.queryParamsService.setState(params, false); // false = no history entry

            this.initializeView(); //i.e. make API calls        
         });
     }

     updateView() {

       var data = {
         filter: this.filter,
         viewSettings: this.viewSettings
       };

       this.queryParamsService.setState(data, true);

       // Do whatever to update your view
     }

  // ...

}

不要忘记在组件级别上添加 providers: [QueryParamsService] 语句以创建该组件的新服务实例。不要在应用程序模块上全局注册该服务。


9
我用以下方式获取它:
const queryParamsObj = {foo: 1, bar: 2, andThis: 'text'};

this.location.replaceState(
  this.router.createUrlTree(
    [this.locationStrategy.path().split('?')[0]], // Get uri
    {queryParams: queryParamsObj} // Pass all parameters inside queryParamsObj
  ).toString()
);

--编辑--

我认为我应该添加更多信息。

如果您使用 this.location.replaceState(),则应用程序的路由器不会更新,因此,如果您稍后使用路由器信息,则与浏览器中的信息不相等。例如,如果您使用 localizeService 更改语言,在切换语言后,您的应用程序会返回到您在使用 this.location.replaceState() 更改之前的上一个 URL。

如果您不想要这种行为,可以选择其他方法来更新 URL,例如:

this.router.navigate(
  [this.locationStrategy.path().split('?')[0]],
  {queryParams: queryParamsObj}
);

在这个选项中,你的浏览器不会刷新,但是你URL变化也被注入到你应用程序的Router中,所以当你切换语言时,你就不会像在this.location.replaceState()中遇到的问题一样。
当然,你可以根据自己的需求选择方法。第一种方法更轻量级,因为你只需要改变浏览器中的URL,而不会使你的应用程序更加复杂。

5
在更改url时,请使用属性queryParamsHandling:'merge'。
this.router.navigate([], {
        queryParams: this.queryParams,
        queryParamsHandling: 'merge',
        replaceUrl: true,
});

11
这会导致当前路由的组件重新加载。 - btx

4

对于我来说,使用Angular 4.4.5时出现了一个混合的情况。

使用router.navigate时,它不尊重相对于activatedRoute的部分,从而摧毁了我的URL。

最后我采用了以下方式:

this._location.go(this._router.createUrlTree([this._router.url], { queryParams: { profile: value.id } }).toString())

3
在2021年,我使用以下解决方案。使用createUrlTree创建URL树,并使用location导航到路由。
//Build URL Tree
    const urlTree = this.router.createUrlTree(["/employee/"+this.employeeId],{
      relativeTo: this.route,
      queryParams: params,
      queryParamsHandling: 'merge'
    });

//Update the URL 
this.location.go(urlTree.toString());

在 Angular 12 中进行了测试,效果非常好。我只是使用了 replaceState 方法而不是 go 方法,这样它就会替换先前的状态而不是添加到它之后。当您有以 /new 结尾的 URL,然后想要在将其保存到数据库后将 new 替换为实体 ID 时,这个方法非常方便。 - Kirill G.

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