在介紹 LocationStrategy 策略之前释液,我們先來了解以下相關(guān)知識:
History 對象
Hash 模式和 HTML 5 模式
History 對象
屬性
length
只讀的全释,其值為一個整數(shù),標志包括當前頁面在內(nèi)的會話歷史中的記錄數(shù)量误债,比如我們通常打開一個空白窗口浸船,length 為 0,再訪問一個頁面找前,其 length 變?yōu)?1糟袁。
scrollRestoration
允許 Web 應(yīng)用在會話歷史導航時顯式地設(shè)置默認滾動復(fù)原,其值為 auto 或 manual躺盛。
state
只讀项戴,返回代表會話歷史堆棧頂部記錄的任意可序列化類型數(shù)據(jù)值,我們可以以此來區(qū)別不同會話歷史紀錄槽惫。
方法
back()
返回會話歷史記錄中的上一個頁面周叮,等價于 window.history.go(-1) 和點擊瀏覽器的后退按鈕。
forward()
進入會話歷史記錄中的下一個頁面界斜,等價于 window.history.go(1) 和點擊瀏覽器的前進按鈕仿耽。
go()
加載會話歷史記錄中的某一個頁面,通過該頁面與當前頁面在會話歷史中的相對位置定位各薇,如项贺,-1
代表當前頁面的上一個記錄君躺,1
代表當前頁面的下一個頁面。若不傳參數(shù)或傳入0开缎,則會重新加載當前頁面棕叫;若參數(shù)超出當前會話歷史紀錄數(shù),則不進行操作奕删。
pushState()
在會話歷史堆棧頂部插入一條記錄俺泣,該方法接收三個參數(shù),一個 state 對象完残,一個頁面標題伏钠,一個 URL:
- 狀態(tài)對象
- 存儲新添會話歷史記錄的狀態(tài)信息對象,每次訪問該條會話時谨设,都會觸發(fā) popstate 事件熟掂,并且事件回調(diào)函數(shù)會接收一個參數(shù),值為該事件對象的復(fù)制副本扎拣。
- 狀態(tài)對象可以是任何可序列化的數(shù)據(jù)打掘,瀏覽器將狀態(tài)對象存儲在用戶的磁盤以便用戶再次重啟瀏覽器時能恢復(fù)數(shù)據(jù)
- 一個狀態(tài)對象序列化后的最大長度是 640K,如果傳遞數(shù)據(jù)過大鹏秋,則會拋出異常
- 頁面標題
- 目前該參數(shù)值會被忽略,暫不被使用亡笑,可以傳入空字符串
- 頁面 URL
- 此參數(shù)聲明新添會話記錄的入口 URL
- 在調(diào)用 pushState() 方法后侣夷,瀏覽器不會加載 URL 指向的頁面,我們可以在 popstate 事件回調(diào)中處理頁面是否加載
- 此 URL 必須與當前頁面 URL 同源,仑乌,否則會拋異常百拓;其值可以是絕對地址,也可以是相對地址晰甚,相對地址會被基于當前頁面 URL 解析得到絕對地址衙传;若其值為空,則默認是當前頁面 URL
replaceState()
更新會話歷史堆棧頂部記錄信息厕九,支持的參數(shù)信息與 pushState()
一致蓖捶。
pushState() 與 replaceState() 的區(qū)別:pushState()是在 history 棧中添加一個新的條目,replaceState() 是替換當前的記錄值扁远。此外這兩個方法改變的只是瀏覽器關(guān)于當前頁面的標題和 URL 的記錄情況俊鱼,并不會刷新或改變頁面展示。
onpopstate 事件
window.onpopstate 是 popstate
事件在 window 對象上的事件句柄畅买。每當處于激活狀態(tài)的歷史記錄條目發(fā)生變化時并闲,popstate 事件就會在對應(yīng) window 對象上觸發(fā)。如果當前處于激活狀態(tài)的歷史記錄條目是由 history.pushState() 方法創(chuàng)建谷羞,或者由 history.replaceState() 方法修改過的帝火,則 popstate 事件對象的 state 屬性包含了這個歷史記錄條目的 state 對象的一個拷貝。
調(diào)用 history.pushState() 或者 history.replaceState() 不會觸發(fā) popstate 事件。popstate 事件只會在瀏覽器某些行為下觸發(fā)犀填,比如點擊后退蠢壹、前進按鈕 (或者在 JavaScript 中調(diào)用 history.back()、history.forward()宏浩、history.go() 方法)知残。
當網(wǎng)頁加載時,各瀏覽器對 popstate 事件是否觸發(fā)有不同的表現(xiàn)比庄,Chrome 和 Safari 會觸發(fā) popstate 事件求妹,而 Firefox 不會。
Hash 模式和 HTML 5 模式
Hash 模式
Hash 模式是基于錨點定位的內(nèi)部鏈接機制佳窑,在 URL 加上 #
制恍,然后在 #
后面加上 hash 標簽,根據(jù)不同的標簽做定位神凑。示例如下:
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 信息是不會提交到服務(wù)端净神,所以若要使用 SSR (Server-Side Rendered) ,就不能使用 Hash 模式即不能使用 HashLocationStrategy 策略溉委。
HTML 5 模式
HTML 5 模式則直接使用跟"真實"的 URL 一樣鹃唯,如上面的路徑,在 HTML 5 模式地址如下:
https://segmentfault.com/u/angular4/user
HTML 5 模式下 URL 有兩種訪問方式:
- 在瀏覽器地址欄直接輸入 URL瓣喊,這會向服務(wù)器請求加載頁面坡慌。
- 在 Angular 應(yīng)用程序中,訪問 HTML 5 模式下的 URL 地址藻三,這不需要重新加載頁面洪橘,可以直接切換到對應(yīng)的視圖。
在 HTML 5 模式下棵帽,Angular 使用了 HTML 5 的 pushState()
API 來動態(tài)改變?yōu)g覽器的 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
弟晚,用于設(shè)置資源 (圖片、腳本逾苫、樣式) 加載的基礎(chǔ)路徑指巡。除了在 NgModule 中配置 provider
外,我們也可以在入口文件隶垮,如 index.html
文件 <base>
標簽中設(shè)置基礎(chǔ)路徑藻雪。
<base>
標簽為頁面上的所有鏈接規(guī)定默認地址或默認目標。通常情況下狸吞,瀏覽器會從當前文檔的 URL 中提取相應(yīng)的路徑來補全相對 URL 中缺失的部分勉耀。使用 <base>
標簽可以改變這一點指煎。瀏覽器隨后將不再使用當前文檔的 URL,而使用指定的基本 URL 來解析所有的相對 URL便斥。這其中包括<a>
至壤、<img>
、<link>
枢纠、<form>
標簽中的 URL像街。具體使用示例如下:
<base href="/">
LocationStrategy
LocationStrategy 用于從瀏覽器 URL 中讀取路由狀態(tài)。Angular 中提供兩種 LocationStrategy 策略:
- HashLocationStrategy
- PathLocationStrategy
以上兩種策略都是繼承于 LocationStrategy 抽象類晋渺,該類的具體定義如下:
LocationStrategy 抽象類
export abstract class LocationStrategy {
// 獲取path路徑
abstract path(includeHash?: boolean): string;
// 生成完整的外部鏈接
abstract prepareExternalUrl(internal: string): string;
// 添加會話歷史狀態(tài)
abstract pushState(state: any, title: string, url: string,
queryParams: string): void;
// 修改會話歷史狀態(tài)
abstract replaceState(state: any, title: string, url: string,
queryParams: string): void;
// 進入會話歷史記錄中的下一個頁面
abstract forward(): void;
// 返回會話歷史記錄中的上一個頁面
abstract back(): void;
// 設(shè)置popstate監(jiān)聽
abstract onPopState(fn: LocationChangeListener): void;
// 獲取base地址信息
abstract getBaseHref(): string;
}
了解完 LocationStrategy 抽象類镰绎,接下來我們先來介紹 HashLocationStrategy 策略。
HashLocationStrategy
HashLocationStrategy 類繼承于 LocationStrategy 抽象類木西,它的構(gòu)造函數(shù)如下:
export class HashLocationStrategy extends LocationStrategy {
constructor(
private _platformLocation: PlatformLocation,
@Optional() @Inject(APP_BASE_HREF) _baseHref?: string) {
super();
if (_baseHref != null) {
this._baseHref = _baseHref;
}
}
}
該構(gòu)造函數(shù)依賴 PlatformLocation 及 APP_BASE_HREF 關(guān)聯(lián)的對象畴栖。APP_BASE_HREF
的作用,我們上面已經(jīng)介紹過了八千,接下來我們來分析一下 PlatformLocation 對象吗讶。
PlatformLocation
// angular2/packages/platform-browser/src/browser.ts
export const INTERNAL_BROWSER_PLATFORM_PROVIDERS: Provider[] = [
...,
{provide: PlatformLocation, useClass: BrowserPlatformLocation},
];
通過以上代碼,我們可以知道在瀏覽器環(huán)境中恋捆,HashLocationStrategy 構(gòu)造函數(shù)中注入的 PlatformLocation 對象是 BrowserPlatformLocation 類的實例照皆。我們也先來看一下 BrowserPlatformLocation 類的構(gòu)造函數(shù):
// 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 構(gòu)造函數(shù)中,我們調(diào)用 _init()
方法沸停,在方法體中纵寝,我們調(diào)用 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;
}
}
那什么時候會調(diào)用 setDOM()
或 setRootDomAdapter()
方法呢绰垂?通過查看 Angular 源碼,我們發(fā)現(xiàn)在瀏覽器平臺初始化時火焰,會調(diào)用 setRootDomAdapter()
方法劲装。具體如下:
export const INTERNAL_BROWSER_PLATFORM_PROVIDERS: Provider[] = [
{provide: PLATFORM_INITIALIZER, useValue: initDomAdapter, multi: true},
...
];
initDomAdapter() 方法
export function initDomAdapter() {
BrowserDomAdapter.makeCurrent();
BrowserGetTestability.init();
}
從上面代碼中,可以看出在 initDomAdapter() 方法中昌简,我們又調(diào)用了 BrowserDomAdapter 類提供的靜態(tài)方法 makeCurrent()
占业,該方法的實現(xiàn)如下:
export class BrowserDomAdapter extends GenericBrowserDomAdapter {
static makeCurrent() { setRootDomAdapter(new BrowserDomAdapter()); }
}
現(xiàn)在我們已經(jīng)知道調(diào)用 getDom()
方法后,我們獲得的是 BrowserDomAdapter 對象纯赎。該對象為我們提供 getLocation()
和 getHistory()
方法谦疾,用于獲取 Location 和 History 對象。以上兩個方法的具體實現(xiàn)如下:
getHistory(): History { return window.history; }
getLocation(): Location { return window.location; }
此外該對象中還包含一個 getBaseHref()
方法犬金,用于獲取基礎(chǔ)路徑:
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 類的構(gòu)造函數(shù)念恍,我們再來分析該類中幾個重要的方法:
getBaseHrefFromDOM()
// 用于獲取base元素的href屬性
getBaseHrefFromDOM(): string { return getDOM().getBaseHref(this._doc) !; }
onPopState()
// 設(shè)置popstate事件的監(jiān)聽函數(shù)
onPopState(fn: LocationChangeListener): void {
getDOM().getGlobalEventTarget(this._doc, 'window')
.addEventListener('popstate', fn, false);
}
interface LocationChangeListener { (e: LocationChangeEvent): any; }
interface LocationChangeEvent { type: string; }
onHashChange()
// 設(shè)置hashchange事件的監(jiān)聽函數(shù)
onHashChange(fn: LocationChangeListener): void {
getDOM().getGlobalEventTarget(this._doc, 'window')
.addEventListener('hashchange', fn, false);
}
pushState()
// 添加會話歷史狀態(tài)
pushState(state: any, title: string, url: string): void {
if (supportsState()) {
this._history.pushState(state, title, url);
} else {
this._location.hash = url;
}
}
// 判斷是否支持state相關(guān)API
export function supportsState(): boolean {
return !!window.history.pushState;
}
replaceState()
// 修改會話歷史狀態(tài)
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(); }
現(xiàn)在終于介紹完 PlatformLocation
對象六剥,讓我們回過頭來繼續(xù)分析我們的主角 - HashLocationStrategy 類。前面我們已經(jīng)分析了該類的構(gòu)造函數(shù)峰伙,我們再來看一下該類其它的方法:
// 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);
}
// 獲取基礎(chǔ)路徑
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;
}
// 添加會話歷史狀態(tài)
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);
}
// 更新會話歷史狀態(tài)
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(); }
}
到現(xiàn)在為止匣摘,我們已經(jīng)完整分析了 HashLocationStrategy 策略店诗。最后我們來分析 PathLocationStrategy 策略。
PathLocationStrategy
PathLocationStrategy 類也是繼承于 LocationStrategy 抽象類恋沃,如果使用該策略必搞,我們必須設(shè)置 APP_BASE_HREF
或在入口文件如 (index.html) 文件中設(shè)置 <base>
元素的 href 屬性。我們也先來分析該類的構(gòu)造函數(shù):
// 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) {
// 若未設(shè)置APP_BASE_HREF的值囊咏,則從base元素中
href = this._platformLocation.getBaseHrefFromDOM();
}
// 若發(fā)現(xiàn)未設(shè)置基礎(chǔ)路徑恕洲,則會拋出異常∶犯睿可能有一些初學者霜第,會遇到這個問題
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);
}
// 獲取基礎(chǔ)路徑
getBaseHref(): string { return this._baseHref; }
// 基于_baseHref及internal值,生成完整的URL地址
prepareExternalUrl(internal: string): string {
return Location.joinWithSlash(this._baseHref, internal);
}
// 根據(jù)傳遞的參數(shù)值户辞,返回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;
}
// 添加會話歷史狀態(tài)
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);
}
// 更新會話歷史狀態(tài)
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 策略刃榨,后續(xù)的文章,我們會基于該基礎(chǔ)双仍,深入分析 Angular 的路由模塊枢希。