Angular 4.x LocationStrategy

semlinker發表於2019-02-22

在介紹 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 的路由模組。

參考文章

相關文章