在 Angular 2 中更改路由参数而不重新加载

Change route params without reloading in Angular 2

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

您可以使用 location.go(url),这基本上会改变您的 url,而不会改变申请途径。

NOTE this could cause other effect like redirect to child route from the current route.

描述location.go不会让Router发生变化。

我很难让它在 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

从 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/");
        }
    }

对于像我这样发现这个问题的人来说,以下内容可能会有用。

我遇到了类似的问题,最初尝试按照此处其他答案中的建议使用 location.go 和 location.replaceState。但是,当我不得不导航到应用程序的另一个页面时,我 运行 遇到了问题,因为导航是相对于当前路线的,而当前路线没有被 location.go 或 location.replaceState 更新(路由器不知道这些对 URL 做了什么)

本质上,我需要一个解决方案,当路由参数更改时,我不会重新加载 page/component,但会在内部更新路由状态。

我最终使用了查询参数。您可以在这里找到更多相关信息: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' } });

使用 location.go(url) 是可行的方法,但与其对 url 进行硬编码,不如考虑使用 router.createUrlTree().

生成它

鉴于你想执行以下路由器调用:this.router.navigate([{param: 1}], {relativeTo: this.activatedRoute}) 但无需重新加载组件,它可以重写为:

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

 this.location.go(url);

对我来说,它实际上是 Angular 4.4.5 的混合体。

不尊重 realtiveTo:activatedRoute 部分,使用 router.navigate 不断破坏我的 url。

我得到了:

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

我是这样获取的:

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 更多地参与您的应用程序。

在更改 url.

时使用属性 queryParamsHandling: 'merge'
this.router.navigate([], {
        queryParams: this.queryParams,
        queryParamsHandling: 'merge',
        replaceUrl: true,
});

在我的例子中,我需要删除 url 的查询参数以防止用户看到它。

我发现 replaceState 比 location.go 更安全,因为带有旧查询参数的路径从堆栈中消失了,用户可以重做与此查询相关的查询。所以,我更喜欢这样做:

this.location.replaceState(this.router.url.split('?')[0]);

惠特 location.go,返回浏览器将 return 使用查询参数返回您的旧路径,并将其保留在导航堆栈中。

this.location.go(this.router.url.split('?')[0]);

我有过与问题中描述的类似需求,根据现有答案花了一段时间才弄明白,所以我想分享我的最终解决方案。

要求

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

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

另外,我想:

  • 指定是否根据情况在浏览器历史记录 (back/forward) 中考虑 URL 更改
  • 使用复杂对象作为状态参数以在处理状态变化时提供更大的灵活性(可选,但会使生活更轻松,例如当某些状态变化触发 backend/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。 IMO,导航路由器,让 Angular 执行路由魔术并处理 queryParams 更改以执行某些操作,完全混淆了我的组件中发生的关于我的代码的可维护性和可读性的事情。我做了什么:

将传递给 queryParams observable 的状态与我组件中的当前状态进行比较,如果它在那里没有发生变化,则什么也不做,而是直接处理状态变化:

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 对象仅限于平面对象(只有简单的 string/bool/int/... 属性的对象但没有嵌套对象)。我发现了这个限制,因为我需要区分需要通过后端调用处理的属性和仅供组件内部使用的其他属性。我想要一个像这样的状态对象:

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;
 }

返回,处理路由传入的queryParams时:

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;
}

最后:一个ready-to-useAngular服务

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

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] 语句为组件创建新的服务实例。不要在应用程序模块上全局注册服务。

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());

最好使用 activatedRoute.navigate() 更改 URL 参数并使用快照(不订阅)调用 API 如果您不想调用 API 当 URL 参数改变时。

export class MyComponent implements OnInit {

   constructor(private activatedRoute: ActivatedRoute) { }

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

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

@Component({
    selector: 'child-component',
    templateUrl: 'child.component.html',
    styleUrls: ['child.component.scss']
})
export class ChildComponent implements OnInit {
    
    constructor(
       private location: Location
    ) {}

    ngOnInit() {
        // you can put 'this.location.go()' method call in any another method
        this.location.go('parentRoute/anotherChildRoute');
    }
}

对我来说,它改变了浏览器中的子路由,没有任何当前组件重新加载。