在介紹 LocationStrategy 策略以前,咱們先來了解如下相關知識:html
History 對象typescript
Hash 模式和 HTML 5 模式segmentfault
只讀的,其值爲一個整數,標誌包括當前頁面在內的會話歷史中的記錄數量,好比咱們一般打開一個空白窗口,length 爲 0,再訪問一個頁面,其 length 變爲 1。瀏覽器
容許 Web 應用在會話歷史導航時顯式地設置默認滾動復原,其值爲 auto 或 manual。服務器
只讀,返回表明會話歷史堆棧頂部記錄的任意可序列化類型數據值,咱們能夠以此來區別不一樣會話歷史紀錄。angular2
返回會話歷史記錄中的上一個頁面,等價於 window.history.go(-1) 和點擊瀏覽器的後退按鈕。angular4
進入會話歷史記錄中的下一個頁面,等價於 window.history.go(1) 和點擊瀏覽器的前進按鈕。ide
加載會話歷史記錄中的某一個頁面,經過該頁面與當前頁面在會話歷史中的相對位置定位,如,-1
表明當前頁面的上一個記錄,1
表明當前頁面的下一個頁面。若不傳參數或傳入0,則會從新加載當前頁面;若參數超出當前會話歷史紀錄數,則不進行操做。函數
在會話歷史堆棧頂部插入一條記錄,該方法接收三個參數,一個 state 對象,一個頁面標題,一個 URL:post
狀態對象
存儲新添會話歷史記錄的狀態信息對象,每次訪問該條會話時,都會觸發 popstate 事件,而且事件回調函數會接收一個參數,值爲該事件對象的複製副本。
狀態對象能夠是任何可序列化的數據,瀏覽器將狀態對象存儲在用戶的磁盤以便用戶再次重啓瀏覽器時能恢復數據
一個狀態對象序列化後的最大長度是 640K,若是傳遞數據過大,則會拋出異常
頁面標題
目前該參數值會被忽略,暫不被使用,能夠傳入空字符串
頁面 URL
此參數聲明新添會話記錄的入口 URL
在調用 pushState() 方法後,瀏覽器不會加載 URL 指向的頁面,咱們能夠在 popstate 事件回調中處理頁面是否加載
此 URL 必須與當前頁面 URL 同源,,不然會拋異常;其值能夠是絕對地址,也能夠是相對地址,相對地址會被基於當前頁面 URL 解析獲得絕對地址;若其值爲空,則默認是當前頁面 URL
更新會話歷史堆棧頂部記錄信息,支持的參數信息與 pushState()
一致。
pushState() 與 replaceState() 的區別:pushState()是在 history 棧中添加一個新的條目,replaceState() 是替換當前的記錄值。此外這兩個方法改變的只是瀏覽器關於當前頁面的標題和 URL 的記錄狀況,並不會刷新或改變頁面展現。
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 模式是基於錨點定位的內部連接機制,在 URL 加上 #
,而後在 #
後面加上 hash 標籤,根據不一樣的標籤作定位。示例以下:
https://segmentfault.com/u/angular4#user
導入 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 模式則直接使用跟"真實"的 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 而不用從新刷新頁面。
導入 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 用於從瀏覽器 URL 中讀取路由狀態。Angular 中提供兩種 LocationStrategy 策略:
HashLocationStrategy
PathLocationStrategy
以上兩種策略都是繼承於 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 類繼承於 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 對象。
// 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
對象。
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}, ... ];
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 類的構造函數,咱們再來分析該類中幾個重要的方法:
// 用於獲取base元素的href屬性 getBaseHrefFromDOM(): string { return getDOM().getBaseHref(this._doc) !; }
// 設置popstate事件的監聽函數 onPopState(fn: LocationChangeListener): void { getDOM().getGlobalEventTarget(this._doc, 'window') .addEventListener('popstate', fn, false); } interface LocationChangeListener { (e: LocationChangeEvent): any; } interface LocationChangeEvent { type: string; }
// 設置hashchange事件的監聽函數 onHashChange(fn: LocationChangeListener): void { getDOM().getGlobalEventTarget(this._doc, 'window') .addEventListener('hashchange', fn, false); }
// 添加會話歷史狀態 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(state: any, title: string, url: string): void { if (supportsState()) { this._history.replaceState(state, title, url); } else { this._location.hash = url; } }
// 進入會話歷史記錄中的下一個頁面 forward(): void { this._history.forward(); }
// 進入會話歷史記錄中的上一個頁面 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 類也是繼承於 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 的路由模塊。