我有类似问题中描述的要求,根据现有答案花了一段时间才弄清楚,所以我想分享我的最终解决方案。
要求
我的视图(组件,技术上)的状态可以由用户更改(过滤器设置,排序选项等)当状态发生变化时,即用户更改排序方向,我想:
- 在 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 observable 的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 对象仅限于平面对象(只有简单的字符串/布尔/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;
}
最后:一个现成的 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] 语句为组件创建新的服务实例。不要在应用模块上全局注册服务。