栈+单向链表实现Angular 11访客浏览脚印

应用中需要浏览脚印功能实现导航条的后退,登录成功后的跳转,404页面中的:返回上一页功能。当浏览时(非后退操作时)将数据压入栈, 后退时弹出栈顶; 用单向链表来存储数据,使用:ngx-webstorage-service将数据存储在客户端。数据结据为:

//单向链表
export interface TrackItem {
  //上一页的连接
  previous: string;
  //当前页的连接
  value: string;
}

A: 保存

import { ChangeDetectionStrategy, Component, OnInit} from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styles: [``],
  changeDetection: ChangeDetectionStrategy.Default
})
export class AppComponent implements OnInit {
  constructor(
    private router: Router,
    private footMark: FootmarkTrackService) {
    this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe((event: any) => {
      //console.log('[App]prev url:', event.url);
      this.footMark.save(event.url);
    });
  }
}

FootmarkTrackService的代码在后面附上

B: 导航的后退

后退通过指令实现,只要a元素的class定义中含有: historyBack, 404映射的模板示例:

<a href="javascript:;" role="button" class="btn historyBack">上一页</a>

指令定义如下:

import { Directive, HostListener } from '@angular/core';
import { Params, Router } from '@angular/router';
@Directive({
  selector: 'a.historyBack'
})
export class HistoryBackDirective {
  private currentURL: string;

  constructor(private router: Router, private footMark: FootmarkTrackService) {
    this.currentURL = router.url;
  }

  @HostListener('click', ['$event.target'])
  public backHistory($event: Event): void {
    let previousURL: string | null = this.footMark.getPrevious();
    let data: { path: string, queryParams: Params } = this.footMark.processURL(previousURL || '/home');
    this.router.navigate([data.path], { queryParams: data.queryParams });
  }
}

C: 登录时获取来源: Referer

import { Component, OnInit } from '@angular/core';
import { Params, Router } from '@angular/router';
@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styles: [``]
})
export class LoginComponent implements OnInit {
  public member: { names: string, pswd: string, redirect: string } = {
    names: '',
    pswd: '',
    redirect: ''
  };
  private previousUrl!: string | null;
  
  constructor(private router: Router, private footMark: FootmarkTrackService) {}

  ngOnInit(): void {
      this.previousUrl = this.footMark.getReferer();
  }
  //登录成功后的回调函数
  private storeMember(): void {
     //ETC
     //处理完后跳转
     this.processRedirectURL(this.member.redirect || this.previousUrl);
  }
  private processRedirectURL(argDedirectURL: string | null): void {
    //是否有参数
    let redirectURL: string = argDedirectURL || '/home';
    let data: { path: string, queryParams: Params } = this.footMark.processURL(redirectURL);
    this.router.navigate([data.path], { queryParams: data.queryParams });
  }
}

D: FootmarkTrackService

import { Injectable, Inject } from '@angular/core';
import { Params } from '@angular/router';
import { StorageService, SESSION_STORAGE } from 'ngx-webstorage-service';
@Injectable({
  providedIn: 'root'
})
export class FootmarkTrackService {
  private ftKey: string = 'ftStack';

  //存储会员浏览地址的路线图
  //用途: 1)后退功能.正向压栈,后退弹栈; 2)获取当前地址的referer 
  constructor(@Inject(SESSION_STORAGE) private storage: StorageService) { }

  /**
   * 保存/压栈
   * @param url 
   */
  public save(url: string): void {
    let data: TrackItem[] = [];
    let lastEle: TrackItem | undefined = undefined;
    if (this.exist()) {
      data = this.get();
      lastEle = data[data.length - 1];
    }
    //不存在 或 存在但一样
    let previousURL: string = lastEle?.value ?? '';
    if (previousURL === url) { //后退时会发生;
      return;
    }
    let pr: TrackItem = { previous: previousURL, value: url };
    data.push(pr);
    this.storage.set(this.ftKey, data);
  }

  /**
   * 是否忽略地址
   * :/member/login(|register|offline); :/404
   * @param url 
   * @returns
   */
  private isIgnoreURL(url: string): boolean {
    return url.startsWith('/member/login') || url.startsWith('/member/register') || url.startsWith('/member/offline') || url.startsWith('/404');
  }

  /**
   * 是否是第一次保存/栈是否存在
   * @returns
   */
  private exist(): boolean {
    return this.storage.has(this.ftKey);
  }

  /**
   * (2)获取当前地址的Referer
   * 注意:LoginComponent.ngOnInit方法中调用;若在constructor方法中调用会取到错误的值
   * @returns
   */
  public getReferer(): string | null {
    if (!this.exist()) {
      return null;
    }
    //
    let data: TrackItem[] = this.get();
    //栈顶
    let lastEle: TrackItem | undefined = data[data.length - 1];
    return lastEle?.previous ?? null;
  }

  /**
   * 返回存储的数组
   * @returns
   */
  private get(): TrackItem[] {
    return this.storage.get(this.ftKey);
  }

  /**
   * (1)返回前一个地址
   * 注意:方法存在一个缺陷, 例:1>A->login, 2>login->A 此时调用又回到了A,产生在A(2>)上调用回退无作用的假象.getPreviousRef方法修复此缺陷
   * @returns
   */
  public getPrevious(): string | null {
    if (!this.exist()) {
      return null;
    }
    let data: TrackItem[] = this.get();
    //弹栈
    let result: string | null = null;
    do {
      let lastEle: TrackItem | undefined = data.pop();
      if (lastEle && typeof (lastEle.previous) !== 'undefined') {
        result = lastEle.previous;
        if (this.isIgnoreURL(result)) {
          result = null;
        }
      }
    } while (result === null);
    //覆盖掉
    this.storage.set(this.ftKey, data);
    return result;
  }

  /**
   * (1)查看以参考地址为界的前一个地址
   * 修复getPrevious方法在忽略地址前后调用后退无作用的假像
   * @param refUrl 
   * @returns
   */
  public getPreviousRef(refUrl: string): string | null {
    if (!this.exist()) {
      return null;
    }
    let data: TrackItem[] = this.get();
    //地址最后一次出现在哪
    let lastShowIndex: number = -1;
    for (let i: number = data.length - 1; i >= 0; i--) {
      if (data[i].previous === refUrl) {
        lastShowIndex = i;
        break;
      }
    }
    //出现过后,开始截取前部分
    if(lastShowIndex > 0){
      data = data.slice(0, lastShowIndex);
    }
    //往前推一级
    let lastEle: TrackItem | undefined = data.pop();
    let result: string | null = lastEle?.previous ?? null;
    //若是忽略的地址再往上推一层
    if (result !== null && this.isIgnoreURL(result)) {
      result = data.pop()?.previous ?? null;
    }
    //覆盖掉
    this.storage.set(this.ftKey, data);
    return result;
  }

  //处理redirect
  public processURL(redirectURL: string): { path: string, queryParams: Params } {
    let p: any;
    let qs: Params = {};
    if (redirectURL.indexOf('?') == -1) {
      p = redirectURL;
    } else {
      p = redirectURL.substring(0, redirectURL.indexOf('?'));
      let queryString = redirectURL.substring(redirectURL.indexOf('?') + 1);
      if (queryString) {
        let segment: string[] = queryString.split('&');
        segment.forEach(ele => {
          let kv: string[] = ele.split('=');
          if (kv.length == 2) {
            qs[kv[0]] = kv[1];
          }
        });
      }
    }

    return { path: p, queryParams: qs };
  }
}
//单向链表
export interface TrackItem {
  //上一页的连接
  previous: string;
  //当前页的连接
  value: string;
}

附图:


2021-01-16 190944.jpg
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容

  • AngularJS是什么?AngularJs(后面就简称ng了)是一个用于设计动态web应用的结构框架。首先,它是...
    200813阅读 1,602评论 0 3
  • 前端常见的一些问题 1.前端性能优化手段? 1. 尽可能使用雪碧图2. 使用字体图标代替图片3. 对HTML,cs...
    十八人言阅读 1,124评论 0 1
  • css相关 1. 万能居中 1.margin: 0 auto;水平2.text-align: center;水平3...
    宁_Yi阅读 3,347评论 2 39
  • css相关 1. 万能居中 1.margin: 0 auto;水平 2.text-align: center;水平...
    chaocc阅读 962评论 0 2
  • 夜莺2517阅读 127,718评论 1 9