在介紹 LocationStrategy 策略之前,我們先來了解以下相關(guān)知識:
只讀的,其值為一個整數(shù),標(biāo)志包括當(dāng)前頁面在內(nèi)的會話歷史中的記錄數(shù)量,比如我們通常打開一個空白窗口,length 為 0,再訪問一個頁面,其 length 變?yōu)?1。
允許 Web 應(yīng)用在會話歷史導(dǎo)航時顯式地設(shè)置默認(rèn)滾動復(fù)原,其值為 auto 或 manual。
只讀,返回代表會話歷史堆棧頂部記錄的任意可序列化類型數(shù)據(jù)值,我們可以以此來區(qū)別不同會話歷史紀(jì)錄。
返回會話歷史記錄中的上一個頁面,等價于 window.history.go(-1) 和點(diǎn)擊瀏覽器的后退按鈕。
進(jìn)入會話歷史記錄中的下一個頁面,等價于 window.history.go(1) 和點(diǎn)擊瀏覽器的前進(jìn)按鈕。
加載會話歷史記錄中的某一個頁面,通過該頁面與當(dāng)前頁面在會話歷史中的相對位置定位,如,-1
代表當(dāng)前頁面的上一個記錄,1
代表當(dāng)前頁面的下一個頁面。若不傳參數(shù)或傳入0,則會重新加載當(dāng)前頁面;若參數(shù)超出當(dāng)前會話歷史紀(jì)錄數(shù),則不進(jìn)行操作。
在會話歷史堆棧頂部插入一條記錄,該方法接收三個參數(shù),一個 state 對象,一個頁面標(biāo)題,一個 URL:
更新會話歷史堆棧頂部記錄信息,支持的參數(shù)信息與 pushState()
一致。
pushState() 與 replaceState() 的區(qū)別:pushState()是在 history 棧中添加一個新的條目,replaceState() 是替換當(dāng)前的記錄值。此外這兩個方法改變的只是瀏覽器關(guān)于當(dāng)前頁面的標(biāo)題和 URL 的記錄情況,并不會刷新或改變頁面展示。
window.onpopstate 是 popstate
事件在 window 對象上的事件句柄。每當(dāng)處于激活狀態(tài)的歷史記錄條目發(fā)生變化時,popstate 事件就會在對應(yīng) window 對象上觸發(fā)。如果當(dāng)前處于激活狀態(tài)的歷史記錄條目是由 history.pushState() 方法創(chuàng)建,或者由 history.replaceState() 方法修改過的,則 popstate 事件對象的 state 屬性包含了這個歷史記錄條目的 state 對象的一個拷貝。
調(diào)用 history.pushState() 或者 history.replaceState() 不會觸發(fā) popstate 事件。popstate 事件只會在瀏覽器某些行為下觸發(fā),比如點(diǎn)擊后退、前進(jìn)按鈕 (或者在 JavaScript 中調(diào)用 history.back()、history.forward()、history.go() 方法)。
當(dāng)網(wǎng)頁加載時,各瀏覽器對 popstate 事件是否觸發(fā)有不同的表現(xiàn),Chrome 和 Safari 會觸發(fā) popstate 事件,而 Firefox 不會。
Hash 模式是基于錨點(diǎn)定位的內(nèi)部鏈接機(jī)制,在 URL 加上 #
,然后在 #
后面加上 hash 標(biāo)簽,根據(jù)不同的標(biāo)簽做定位。示例如下:
https://segmentfault.com/u/angular4#user
導(dǎo)入 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 模式則直接使用跟"真實(shí)"的 URL 一樣,如上面的路徑,在 HTML 5 模式地址如下:
https://segmentfault.com/u/angular4/user
HTML 5 模式下 URL 有兩種訪問方式:
在 HTML 5 模式下,Angular 使用了 HTML 5 的 pushState()
API 來動態(tài)改變?yōu)g覽器的 URL 而不用重新刷新頁面。
導(dǎo)入 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>
標(biāo)簽中設(shè)置基礎(chǔ)路徑。
<base>
標(biāo)簽為頁面上的所有鏈接規(guī)定默認(rèn)地址或默認(rèn)目標(biāo)。通常情況下,瀏覽器會從當(dāng)前文檔的 URL 中提取相應(yīng)的路徑來補(bǔ)全相對 URL 中缺失的部分。使用 <base>
標(biāo)簽可以改變這一點(diǎn)。瀏覽器隨后將不再使用當(dāng)前文檔的 URL,而使用指定的基本 URL 來解析所有的相對 URL。這其中包括<a>
、<img>
、<link>
、<form>
標(biāo)簽中的 URL。具體使用示例如下:
<base href="/">
LocationStrategy 用于從瀏覽器 URL 中讀取路由狀態(tài)。Angular 中提供兩種 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;
// 進(jìn)入會話歷史記錄中的下一個頁面
abstract forward(): void;
// 返回會話歷史記錄中的上一個頁面
abstract back(): void;
// 設(shè)置popstate監(jiān)聽
abstract onPopState(fn: LocationChangeListener): void;
// 獲取base地址信息
abstract getBaseHref(): string;
}
了解完 LocationStrategy 抽象類,接下來我們先來介紹 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 對象。
// 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 類的實(shí)例。我們也先來看一下 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() 方法返回的是什么對象呢?其實(shí)該方法返回的是 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},
...
];
export function initDomAdapter() {
BrowserDomAdapter.makeCurrent();
BrowserGetTestability.init();
}
從上面代碼中,可以看出在 initDomAdapter() 方法中,我們又調(diào)用了 BrowserDomAdapter 類提供的靜態(tài)方法 makeCurrent()
,該方法的實(shí)現(xiàn)如下:
export class BrowserDomAdapter extends GenericBrowserDomAdapter {
static makeCurrent() { setRootDomAdapter(new BrowserDomAdapter()); }
}
現(xiàn)在我們已經(jīng)知道調(diào)用 getDom()
方法后,我們獲得的是 BrowserDomAdapter 對象。該對象為我們提供 getLocation()
和 getHistory()
方法,用于獲取 Location 和 History 對象。以上兩個方法的具體實(shí)現(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ù),我們再來分析該類中幾個重要的方法:
// 用于獲取base元素的href屬性
getBaseHrefFromDOM(): string { return getDOM().getBaseHref(this._doc) !; }
// 設(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; }
// 設(shè)置hashchange事件的監(jiān)聽函數(shù)
onHashChange(fn: LocationChangeListener): void {
getDOM().getGlobalEventTarget(this._doc, 'window')
.addEventListener('hashchange', fn, false);
}
// 添加會話歷史狀態(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;
}
// 修改會話歷史狀態(tài)
replaceState(state: any, title: string, url: string): void {
if (supportsState()) {
this._history.replaceState(state, title, url);
} else {
this._location.hash = url;
}
}
// 進(jìn)入會話歷史記錄中的下一個頁面
forward(): void { this._history.forward(); }
// 進(jìn)入會話歷史記錄中的上一個頁面
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);
}
// 進(jìn)入會話歷史記錄中的下一個頁面
forward(): void { this._platformLocation.forward(); }
// 進(jìn)入會話歷史記錄中的上一個頁面
back(): void { this._platformLocation.back(); }
}
到現(xiàn)在為止,我們已經(jīng)完整分析了 HashLocationStrategy 策略。最后我們來分析 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ǔ)路徑,則會拋出異常。可能有一些初學(xué)者,會遇到這個問題
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);
}
// 進(jìn)入會話歷史記錄中的下一個頁面
forward(): void { this._platformLocation.forward(); }
// 進(jìn)入會話歷史記錄中的上一個頁面
back(): void { this._platformLocation.back(); }
}
終于介紹完 HashLocationStrategy 和 PathLocationStrategy 策略,后續(xù)的文章,我們會基于該基礎(chǔ),深入分析 Angular 的路由模塊。
更多建議: