Angular 4.x LocationStrategy

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

  • History 对象typescript

  • Hash 模式和 HTML 5 模式segmentfault

History 对象

属性

length

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

scrollRestoration

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

state

只读,返回表明会话历史堆栈顶部记录的任意可序列化类型数据值,咱们能够以此来区别不一样会话历史纪录。angular2

方法

back()

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

forward()

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

go()

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

pushState()

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

  • 状态对象

    • 存储新添会话历史记录的状态信息对象,每次访问该条会话时,都会触发 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 的路由模块。

参考文章