Angular 4.x LocationStrategy

12

在介绍 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 的路由模块。

参考文章


如果觉得我的文章对你有用,请随意赞赏

你可能感兴趣的

鱼肚 · 2018年06月10日

html5模式不错

回复

载入中...