Angular 4 LocationStrategy

在介绍 LocationStrategy 策略之前,我们先来了解以下相关知识:

  • History 对象

  • Hash 模式和 HTML 5 模式

History 对象

属性

length

只读的,其值为一个整数,标志包括当前页面在内的会话历史中的记录数量,比如我们通常打开一个空白窗口,length 为 0,再访问一个页面,其 length 变为 1。

scrollRestoration

允许 Web 应用在会话历史导航时显式地设置默认滚动复原,其值为 auto 或 manual。

state

只读,返回代表会话历史堆栈顶部记录的任意可序列化类型数据值,我们可以以此来区别不同会话历史纪录。

方法

back()

返回会话历史记录中的上一个页面,等价于 window.history.go(-1) 和点击浏览器的后退按钮。

forward()

进入会话历史记录中的下一个页面,等价于 window.history.go(1) 和点击浏览器的前进按钮。

go()

加载会话历史记录中的某一个页面,通过该页面与当前页面在会话历史中的相对位置定位,如,-1 代表当前页面的上一个记录,1 代表当前页面的下一个页面。若不传参数或传入0,则会重新加载当前页面;若参数超出当前会话历史纪录数,则不进行操作。

pushState()

在会话历史堆栈顶部插入一条记录,该方法接收三个参数,一个 state 对象,一个页面标题,一个 URL:

  • 状态对象
    • 存储新添会话历史记录的状态信息对象,每次访问该条会话时,都会触发 popstate 事件,并且事件回调函数会接收一个参数,值为该事件对象的复制副本。
    • 状态对象可以是任何可序列化的数据,浏览器将状态对象存储在用户的磁盘以便用户再次重启浏览器时能恢复数据
    • 一个状态对象序列化后的最大长度是 640K,如果传递数据过大,则会抛出异常
  • 页面标题
    • 目前该参数值会被忽略,暂不被使用,可以传入空字符串
  • 页面 URL
    • 此参数声明新添会话记录的入口 URL
    • 在调用 pushState() 方法后,浏览器不会加载 URL 指向的页面,我们可以在 popstate 事件回调中处理页面是否加载
    • 此 URL 必须与当前页面 URL 同源,,否则会抛异常;其值可以是绝对地址,也可以是相对地址,相对地址会被基于当前页面 URL 解析得到绝对地址;若其值为空,则默认是当前页面 URL

replaceState()

更新会话历史堆栈顶部记录信息,支持的参数信息与 pushState() 一致。

pushState() 与 replaceState() 的区别:pushState()是在 history 栈中添加一个新的条目,replaceState() 是替换当前的记录值。此外这两个方法改变的只是浏览器关于当前页面的标题和 URL 的记录情况,并不会刷新或改变页面展示。

onpopstate 事件

window.onpopstate 是 popstate 事件在 window 对象上的事件句柄。每当处于激活状态的历史记录条目发生变化时,popstate 事件就会在对应 window 对象上触发。如果当前处于激活状态的历史记录条目是由 history.pushState() 方法创建,或者由 history.replaceState() 方法修改过的,则 popstate 事件对象的 state 属性包含了这个历史记录条目的 state 对象的一个拷贝。

调用 history.pushState() 或者 history.replaceState() 不会触发 popstate 事件。popstate 事件只会在浏览器某些行为下触发,比如点击后退、前进按钮 (或者在 JavaScript 中调用 history.back()、history.forward()、history.go() 方法)。

当网页加载时,各浏览器对 popstate 事件是否触发有不同的表现,Chrome 和 Safari 会触发 popstate 事件,而 Firefox 不会。

Hash 模式和 HTML 5 模式

Hash 模式

Hash 模式是基于锚点定位的内部链接机制,在 URL 加上 # ,然后在 # 后面加上 hash 标签,根据不同的标签做定位。示例如下:

https://segmentfault.com/u/angular4#user

开启 Hash 模式

导入 HashLocationStrategy 及 HashLocationStrategy

import { LocationStrategy, HashLocationStrategy } from '@angular/common';

配置 NgModule - providers

@NgModule({
  imports: [
    BrowserModule,
    RouterModule.forRoot(routes)
  ],
  ...,
  providers: [
    { provide: LocationStrategy, useClass: HashLocationStrategy }
  ]
})

友情提示:URL 中包含的 hash 信息是不会提交到服务端,所以若要使用 SSR (Server-Side Rendered) ,就不能使用 Hash 模式即不能使用 HashLocationStrategy 策略。

HTML 5 模式

HTML 5 模式则直接使用跟"真实"的 URL 一样,如上面的路径,在 HTML 5 模式地址如下:

https://segmentfault.com/u/angular4/user

HTML 5 模式下 URL 有两种访问方式:

  • 在浏览器地址栏直接输入 URL,这会向服务器请求加载页面。
  • 在 Angular 应用程序中,访问 HTML 5 模式下的 URL 地址,这不需要重新加载页面,可以直接切换到对应的视图。

在 HTML 5 模式下,Angular 使用了 HTML 5 的 pushState() API 来动态改变浏览器的 URL 而不用重新刷新页面。

开启 HTML 5 模式

导入 APP_BASE_HREF、LocationStrategy、PathLocationStrategy

import { APP_BASE_HREF, LocationStrategy, PathLocationStrategy } from '@angular/common';

配置 NgModule - providers

@NgModule({
  imports: [
    BrowserModule,
    RouterModule.forRoot(routes)
  ],
  ..,
  providers: [
    { provide: LocationStrategy, useClass: PathLocationStrategy },
    { provide: APP_BASE_HREF, useValue: '/' }
  ]
})

示例代码中的 APP_BASE_HREF,用于设置资源 (图片、脚本、样式) 加载的基础路径。除了在 NgModule 中配置 provider 外,我们也可以在入口文件,如 index.html 文件 <base> 标签中设置基础路径。

<base> 标签为页面上的所有链接规定默认地址或默认目标。通常情况下,浏览器会从当前文档的 URL 中提取相应的路径来补全相对 URL 中缺失的部分。使用 <base> 标签可以改变这一点。浏览器随后将不再使用当前文档的 URL,而使用指定的基本 URL 来解析所有的相对 URL。这其中包括<a><img><link><form> 标签中的 URL。具体使用示例如下:

<base href="/">

LocationStrategy

LocationStrategy 用于从浏览器 URL 中读取路由状态。Angular 中提供两种 LocationStrategy 策略:

  • HashLocationStrategy
  • PathLocationStrategy

以上两种策略都是继承于 LocationStrategy 抽象类,该类的具体定义如下:

LocationStrategy 抽象类

export abstract class LocationStrategy {
  // 获取path路径
  abstract path(includeHash?: boolean): string;
  // 生成完整的外部链接
  abstract prepareExternalUrl(internal: string): string;
  // 添加会话历史状态
  abstract pushState(state: any, title: string, url: string, 
    queryParams: string): void;
  // 修改会话历史状态
  abstract replaceState(state: any, title: string, url: string, 
    queryParams: string): void;
  // 进入会话历史记录中的下一个页面
  abstract forward(): void;
  // 返回会话历史记录中的上一个页面
  abstract back(): void;
  // 设置popstate监听
  abstract onPopState(fn: LocationChangeListener): void;
  // 获取base地址信息
  abstract getBaseHref(): string;
}

了解完 LocationStrategy 抽象类,接下来我们先来介绍 HashLocationStrategy 策略。

HashLocationStrategy

HashLocationStrategy 类继承于 LocationStrategy 抽象类,它的构造函数如下:

export class HashLocationStrategy extends LocationStrategy {
  constructor(
      private _platformLocation: PlatformLocation,
      @Optional() @Inject(APP_BASE_HREF) _baseHref?: string) {
      super();
      if (_baseHref != null) {
        this._baseHref = _baseHref;
      }
  }
}

该构造函数依赖 PlatformLocation 及 APP_BASE_HREF 关联的对象。APP_BASE_HREF 的作用,我们上面已经介绍过了,接下来我们来分析一下 PlatformLocation 对象。

PlatformLocation

// angular2/packages/platform-browser/src/browser.ts
export const INTERNAL_BROWSER_PLATFORM_PROVIDERS: Provider[] = [
  ...,
  {provide: PlatformLocation, useClass: BrowserPlatformLocation},
];

通过以上代码,我们可以知道在浏览器环境中,HashLocationStrategy 构造函数中注入的 PlatformLocation 对象是 BrowserPlatformLocation 类的实例。我们也先来看一下 BrowserPlatformLocation 类的构造函数:

// angular2/packages/platform-browser/src/browser/location/browser_platform_location.ts
export class BrowserPlatformLocation extends PlatformLocation {
  private _location: Location;
  private _history: History;

  constructor(@Inject(DOCUMENT) private _doc: any) {
    super();
    this._init();
  }

  _init() {
    this._location = getDOM().getLocation(); // 获取浏览器平台下Location对象
    this._history = getDOM().getHistory(); // 获取浏览器平台下的History对象
  }
}

在 BrowserPlatformLocation 构造函数中,我们调用 _init() 方法,在方法体中,我们调用 getDOM() 方法返回对象中的 getLocation()getHistory() 方法,分别获取 Location 对象和 History 对象。那 getDOM() 方法返回的是什么对象呢?其实该方法返回的是 DomAdapter 对象。

DomAdapter

let _DOM: DomAdapter = null !;

export function getDOM() {
  return _DOM;
}

export function setDOM(adapter: DomAdapter) {
  _DOM = adapter;
}

export function setRootDomAdapter(adapter: DomAdapter) {
  if (!_DOM) {
    _DOM = adapter;
  }
}

那什么时候会调用 setDOM()setRootDomAdapter() 方法呢?通过查看 Angular 源码,我们发现在浏览器平台初始化时,会调用 setRootDomAdapter() 方法。具体如下:

export const INTERNAL_BROWSER_PLATFORM_PROVIDERS: Provider[] = [
  {provide: PLATFORM_INITIALIZER, useValue: initDomAdapter, multi: true},
  ...
];

initDomAdapter() 方法

export function initDomAdapter() {
  BrowserDomAdapter.makeCurrent();
  BrowserGetTestability.init();
}

从上面代码中,可以看出在 initDomAdapter() 方法中,我们又调用了 BrowserDomAdapter 类提供的静态方法 makeCurrent() ,该方法的实现如下:

export class BrowserDomAdapter extends GenericBrowserDomAdapter {
    static makeCurrent() { setRootDomAdapter(new BrowserDomAdapter()); }
}

现在我们已经知道调用 getDom() 方法后,我们获得的是 BrowserDomAdapter 对象。该对象为我们提供 getLocation()getHistory() 方法,用于获取 Location 和 History 对象。以上两个方法的具体实现如下:

getHistory(): History { return window.history; }
getLocation(): Location { return window.location; }

此外该对象中还包含一个 getBaseHref() 方法,用于获取基础路径:

getBaseHref(doc: Document): string|null {
    const href = getBaseElementHref();
    return href == null ? null : relativePath(href);
}

// 获取入口文件中base元素的href属性值
function getBaseElementHref(): string|null {
  if (!baseElement) {
    baseElement = document.querySelector('base') !;
    if (!baseElement) {
      return null;
    }
  }
  return baseElement.getAttribute('href');
}

分析完 BrowserPlatformLocation 类的构造函数,我们再来分析该类中几个重要的方法:

getBaseHrefFromDOM()

// 用于获取base元素的href属性
getBaseHrefFromDOM(): string { return getDOM().getBaseHref(this._doc) !; }

onPopState()

// 设置popstate事件的监听函数
onPopState(fn: LocationChangeListener): void {
    getDOM().getGlobalEventTarget(this._doc, 'window')
      .addEventListener('popstate', fn, false);
}

interface LocationChangeListener { (e: LocationChangeEvent): any; }
interface LocationChangeEvent { type: string; }

onHashChange()

// 设置hashchange事件的监听函数
onHashChange(fn: LocationChangeListener): void {
    getDOM().getGlobalEventTarget(this._doc, 'window')
      .addEventListener('hashchange', fn, false);
}

pushState()

// 添加会话历史状态
pushState(state: any, title: string, url: string): void {
    if (supportsState()) {
      this._history.pushState(state, title, url);
    } else {
      this._location.hash = url;
    }
}

// 判断是否支持state相关API
export function supportsState(): boolean {
  return !!window.history.pushState;
}

replaceState()

// 修改会话历史状态
replaceState(state: any, title: string, url: string): void {
    if (supportsState()) {
      this._history.replaceState(state, title, url);
    } else {
      this._location.hash = url;
    }
}

forward()

// 进入会话历史记录中的下一个页面
forward(): void { this._history.forward(); }

back()

// 进入会话历史记录中的上一个页面
back(): void { this._history.back(); }

现在终于介绍完 PlatformLocation 对象,让我们回过头来继续分析我们的主角 - HashLocationStrategy 类。前面我们已经分析了该类的构造函数,我们再来看一下该类其它的方法:

// angular2/packages/common/src/location/hash_location_strategy.ts
export class HashLocationStrategy extends LocationStrategy {
  private _baseHref: string = ''; // 用于保存base URL地址

  onPopState(fn: LocationChangeListener): void {
    this._platformLocation.onPopState(fn);
    this._platformLocation.onHashChange(fn);
  }

  // 获取基础路径
  getBaseHref(): string { return this._baseHref; }
  
  // 获取hash路径
  path(includeHash: boolean = false): string {
    // the hash value is always prefixed with a `#`
    // and if it is empty then it will stay empty
    let path = this._platformLocation.hash;
    if (path == null) path = '#';

    return path.length > 0 ? path.substring(1) : path;
  }

  // 基于_baseHref及internal值,生成完整的URL地址
  prepareExternalUrl(internal: string): string {
    // joinWithSlash():该方法会判断_baseHref和internal是否含有'/'
    // 字符,然后自动帮我们拼接成合法的URL地址
    const url = Location.joinWithSlash(this._baseHref, internal);
    return url.length > 0 ? ('#' + url) : url;
  }

  // 添加会话历史状态
  pushState(state: any, title: string, path: string, queryParams: string) {
    // normalizeQueryParams():该方法会判断queryParams是否包含'?'
    // 字符,若不包含,则自动添加'?'字符。
    let url: string|null = this.prepareExternalUrl(path +
          Location.normalizeQueryParams(queryParams));
    if (url.length == 0) {
      url = this._platformLocation.pathname;
    }
    this._platformLocation.pushState(state, title, url);
  }

  // 更新会话历史状态
  replaceState(state: any, title: string, path: string, queryParams: string) {
    let url = this.prepareExternalUrl(path + 
          Location.normalizeQueryParams(queryParams));
    if (url.length == 0) {
      url = this._platformLocation.pathname;
    }
    this._platformLocation.replaceState(state, title, url);
  }

  // 进入会话历史记录中的下一个页面
  forward(): void { this._platformLocation.forward(); }

  // 进入会话历史记录中的上一个页面
  back(): void { this._platformLocation.back(); }  
}

到现在为止,我们已经完整分析了 HashLocationStrategy 策略。最后我们来分析 PathLocationStrategy 策略。

PathLocationStrategy

PathLocationStrategy 类也是继承于 LocationStrategy 抽象类,如果使用该策略,我们必须设置 APP_BASE_HREF 或在入口文件如 (index.html) 文件中设置 <base> 元素的 href 属性。我们也先来分析该类的构造函数:

// angular2/packages/common/src/location/path_location_strategy.ts
export class PathLocationStrategy extends LocationStrategy {
  private _baseHref: string;

  constructor(
      private _platformLocation: PlatformLocation,
      @Optional() @Inject(APP_BASE_HREF) href?: string) {
        super(); 
        if (href == null) {
          // 若未设置APP_BASE_HREF的值,则从base元素中
          href = this._platformLocation.getBaseHrefFromDOM();
        }
         
        // 若发现未设置基础路径,则会抛出异常。可能有一些初学者,会遇到这个问题
        if (href == null) {
          throw new Error(
              `No base href set. Please provide a value for the APP_BASE_HREF 
                 token or add a base element to the document.`);
        }
        this._baseHref = href;
  }
}

PathLocationStrategy 类其它的方法:

export class PathLocationStrategy extends LocationStrategy {
  // ...
  onPopState(fn: LocationChangeListener): void {
    this._platformLocation.onPopState(fn);
    this._platformLocation.onHashChange(fn);
  }

  // 获取基础路径
  getBaseHref(): string { return this._baseHref; }

  // 基于_baseHref及internal值,生成完整的URL地址
  prepareExternalUrl(internal: string): string {
    return Location.joinWithSlash(this._baseHref, internal);
  }

  // 根据传递的参数值,返回path(包含或不包含hash值)的路径
  path(includeHash: boolean = false): string {
    const pathname = this._platformLocation.pathname +
        Location.normalizeQueryParams(this._platformLocation.search);
    const hash = this._platformLocation.hash;
    return hash && includeHash ? `${pathname}${hash}` : pathname;
  }

  // 添加会话历史状态
  pushState(state: any, title: string, url: string, queryParams: string) {
    // normalizeQueryParams():该方法会判断queryParams是否包含'?'
    // 字符,若不包含,则自动添加'?'字符。
    const externalUrl = this.prepareExternalUrl(url + 
      Location.normalizeQueryParams(queryParams));
    this._platformLocation.pushState(state, title, externalUrl);
  }

  // 更新会话历史状态
  replaceState(state: any, title: string, url: string, queryParams: string) {
    const externalUrl = this.prepareExternalUrl(url +
       Location.normalizeQueryParams(queryParams));
    this._platformLocation.replaceState(state, title, externalUrl);
  }

  // 进入会话历史记录中的下一个页面
  forward(): void { this._platformLocation.forward(); }

  // 进入会话历史记录中的上一个页面
  back(): void { this._platformLocation.back(); }
}

终于介绍完 HashLocationStrategy 和 PathLocationStrategy 策略,后续的文章,我们会基于该基础,深入分析 Angular 的路由模块。

参考文章

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

推荐阅读更多精彩内容

  • 点击查看原文 Web SDK 开发手册 SDK 概述 网易云信 SDK 为 Web 应用提供一个完善的 IM 系统...
    layjoy阅读 13,634评论 0 15
  • 新增的API 1.语义: 能够让你更恰当地描述你的内容是什么。 2.连通性: 能够让你和服务器之间通过创新的新技术...
    红鲤鱼不理绿鲤鱼阅读 6,871评论 0 5
  • 本文由尚妆前端开发工程师欲休撰写本文发表于尚妆博客,欢迎订阅! 移动端开发在某些场景中有着特殊需求,如为了提高用户...
    尚妆产品技术刊读阅读 1,859评论 0 11
  • 雨像根根晶亮的银线,从天穹撒向人间。可是,对于走在路上没有打伞的林夕来说可是根根锃亮的银针,不禁令她想到容嬷...
    曹文和阅读 352评论 0 0
  • 2017年4月28日 相濡以沫 1、昨天背着小白出门,一无所获,今天依然负重出门,希望不负重望!很想不通为啥它叫小...
    滋滋味味阅读 254评论 2 2