在我的Angular 2应用中,当我向下滚动页面并单击页面底部的链接时,它确实会改变路由并将我带到下一页,但它不会滚动到页面顶部。因此,如果第一页很长,第二页内容很少,就会给人一种第二页缺乏内容的印象。因为只有当用户滚动到页面顶部时,内容才可见。

我可以滚动窗口到ngInit组件的页面顶部,但是,有没有更好的解决方案,可以自动处理我的应用程序中的所有路由?


当前回答

从Angular 6.1开始,路由器提供了一个名为scrollPositionRestoration的配置选项,这是为了满足这种情况而设计的。

imports: [
  RouterModule.forRoot(routes, {
    scrollPositionRestoration: 'enabled'
  }),
  ...
]

其他回答

如果您使用服务器端呈现,则应该注意不要在服务器上使用窗口运行代码,因为服务器上不存在该变量。这将导致代码被破坏。

export class AppComponent implements OnInit {
  routerSubscription: Subscription;

  constructor(private router: Router,
              @Inject(PLATFORM_ID) private platformId: any) {}

  ngOnInit() {
    if (isPlatformBrowser(this.platformId)) {
      this.routerSubscription = this.router.events
        .filter(event => event instanceof NavigationEnd)
        .subscribe(event => {
          window.scrollTo(0, 0);
        });
    }
  }

  ngOnDestroy() {
    this.routerSubscription.unsubscribe();
  }
}

isPlatformBrowser是一个函数,用于检查当前应用程序所呈现的平台是否是浏览器。我们给它注入platformId。

它也可以检查变量窗口的存在,为了安全起见,像这样:

if (typeof window != 'undefined')

对于iphone/ios safari,您可以使用setTimeout进行包装

setTimeout(function(){
    window.scrollTo(0, 1);
}, 0);

除了@Guilherme Meireles提供的完美答案, 您可以通过添加平滑滚动来调整实现,如下所示

 import { Component, OnInit } from '@angular/core';
    import { Router, NavigationEnd } from '@angular/router';

    @Component({
        selector: 'my-app',
        template: '<ng-content></ng-content>',
    })
    export class MyAppComponent implements OnInit {
        constructor(private router: Router) { }

        ngOnInit() {
            this.router.events.subscribe((evt) => {
                if (!(evt instanceof NavigationEnd)) {
                    return;
                }
                window.scrollTo(0, 0)
            });
        }
    }

然后添加下面的代码片段

 html {
      scroll-behavior: smooth;
    }

到你的样式。css

Angular 6.1及更高版本:

Angular 6.1(发布于2018-07-25)通过一个名为“路由器滚动位置恢复”的特性,添加了内置支持来处理这个问题。正如Angular官方博客中所描述的,你只需要在路由器配置中启用它,如下所示:

RouterModule.forRoot(routes, {scrollPositionRestoration: 'enabled'})

此外,该博客称“预计这将成为未来主要版本的默认设置”。到目前为止,这种情况还没有发生(从Angular 11.0开始),但最终你将不需要在代码中做任何事情,它将会正确地开箱工作。

你可以在官方文档中看到更多关于这个特性以及如何定制这个行为的细节。

Angular 6.0及更早版本:

虽然@GuilhermeMeireles的出色回答解决了最初的问题,但它引入了一个新问题,打破了向后或向前导航(使用浏览器按钮或通过代码中的位置)时预期的正常行为。预期的行为是,当您导航回页面时,它应该保持向下滚动到与单击链接时相同的位置,但在到达每个页面时滚动到顶部显然打破了这一预期。

下面的代码扩展了检测这种导航的逻辑,方法是订阅Location的PopStateEvent序列,如果新到达的页面是这种事件的结果,则跳过滚动到顶部的逻辑。

如果你返回的页面足够长,足以覆盖整个视口,滚动位置将自动恢复,但正如@JordanNelson正确指出的那样,如果页面较短,你需要跟踪原始的y轴滚动位置,并在返回页面时显式地恢复它。代码的更新版本也涵盖了这种情况,总是显式地恢复滚动位置。

import { Component, OnInit } from '@angular/core';
import { Router, NavigationStart, NavigationEnd } from '@angular/router';
import { Location, PopStateEvent } from "@angular/common";

@Component({
    selector: 'my-app',
    template: '<ng-content></ng-content>',
})
export class MyAppComponent implements OnInit {

    private lastPoppedUrl: string;
    private yScrollStack: number[] = [];

    constructor(private router: Router, private location: Location) { }

    ngOnInit() {
        this.location.subscribe((ev:PopStateEvent) => {
            this.lastPoppedUrl = ev.url;
        });
        this.router.events.subscribe((ev:any) => {
            if (ev instanceof NavigationStart) {
                if (ev.url != this.lastPoppedUrl)
                    this.yScrollStack.push(window.scrollY);
            } else if (ev instanceof NavigationEnd) {
                if (ev.url == this.lastPoppedUrl) {
                    this.lastPoppedUrl = undefined;
                    window.scrollTo(0, this.yScrollStack.pop());
                } else
                    window.scrollTo(0, 0);
            }
        });
    }
}

使用路由器本身会导致你无法完全克服的问题,以保持一致的浏览器体验。在我看来,最好的方法是使用一个自定义指令,让这重置滚动点击。这样做的好处是,如果你在相同的url,你点击,页面将滚动回顶部以及。这与一般的网站是一致的。基本指令可以是这样的:

import {Directive, HostListener} from '@angular/core';

@Directive({
    selector: '[linkToTop]'
})
export class LinkToTopDirective {

    @HostListener('click')
    onClick(): void {
        window.scrollTo(0, 0);
    }
}

具有以下用途:

<a routerLink="/" linkToTop></a>

这对于大多数用例来说已经足够了,但是我可以想象一些问题 由此产生:

由于window的使用,不能在通用上工作 对更改检测的速度影响很小,因为它是由每次单击触发的 没有办法禁用这个指令

克服这些问题其实很容易:

@Directive({
  selector: '[linkToTop]'
})
export class LinkToTopDirective implements OnInit, OnDestroy {

  @Input()
  set linkToTop(active: string | boolean) {
    this.active = typeof active === 'string' ? active.length === 0 : active;
  }

  private active: boolean = true;

  private onClick: EventListener = (event: MouseEvent) => {
    if (this.active) {
      window.scrollTo(0, 0);
    }
  };

  constructor(@Inject(PLATFORM_ID) private readonly platformId: Object,
              private readonly elementRef: ElementRef,
              private readonly ngZone: NgZone
  ) {}

  ngOnDestroy(): void {
    if (isPlatformBrowser(this.platformId)) {
      this.elementRef.nativeElement.removeEventListener('click', this.onClick, false);
    }
  }

  ngOnInit(): void {
    if (isPlatformBrowser(this.platformId)) {
      this.ngZone.runOutsideAngular(() => 
        this.elementRef.nativeElement.addEventListener('click', this.onClick, false)
      );
    }
  }
}

这考虑了大多数用例,使用方法与基本用例相同,优点是启用/禁用它:

<a routerLink="/" linkToTop></a> <!-- always active -->
<a routerLink="/" [linkToTop]="isActive"> <!-- active when `isActive` is true -->

如果你不想被广告吸引,就不要看广告

Another improvement could be made to check whether or not the browser supports passive events. This will complicate the code a bit more, and is a bit obscure if you want to implement all these in your custom directives/templates. That's why I wrote a little library which you can use to address these problems. To have the same functionality as above, and with the added passive event, you can change your directive to this, if you use the ng-event-options library. The logic is inside the click.pnb listener:

@Directive({
    selector: '[linkToTop]'
})
export class LinkToTopDirective {

    @Input()
    set linkToTop(active: string|boolean) {
        this.active = typeof active === 'string' ? active.length === 0 : active;
    }

    private active: boolean = true;

    @HostListener('click.pnb')
    onClick(): void {
      if (this.active) {
        window.scrollTo(0, 0);
      }        
    }
}