在介紹 LocationStrategy 策略之前,我們先來了解以下相關(guān)知識:
只讀的,其值為一個整數(shù),標志包括當前頁面在內(nèi)的會話歷史中的記錄數(shù)量,比如我們通常打開一個空白窗口,length 為 0,再訪問一個頁面,其 length 變?yōu)?1。
允許 Web 應(yīng)用在會話歷史導航時顯式地設(shè)置默認滾動復原,其值為 auto 或 manual。
只讀,返回代表會話歷史堆棧頂部記錄的任意可序列化類型數(shù)據(jù)值,我們可以以此來區(qū)別不同會話歷史紀錄。
返回會話歷史記錄中的上一個頁面,等價于 window.history.go(-1) 和點擊瀏覽器的后退按鈕。
進入會話歷史記錄中的下一個頁面,等價于 window.history.go(1) 和點擊瀏覽器的前進按鈕。
加載會話歷史記錄中的某一個頁面,通過該頁面與當前頁面在會話歷史中的相對位置定位,如,-1
代表當前頁面的上一個記錄,1
代表當前頁面的下一個頁面。若不傳參數(shù)或傳入0,則會重新加載當前頁面;若參數(shù)超出當前會話歷史紀錄數(shù),則不進行操作。
在會話歷史堆棧頂部插入一條記錄,該方法接收三個參數(shù),一個 state 對象,一個頁面標題,一個 URL:
更新會話歷史堆棧頂部記錄信息,支持的參數(shù)信息與 pushState()
一致。
pushState() 與 replaceState() 的區(qū)別:pushState()是在 history 棧中添加一個新的條目,replaceState() 是替換當前的記錄值。此外這兩個方法改變的只是瀏覽器關(guān)于當前頁面的標題和 URL 的記錄情況,并不會刷新或改變頁面展示。
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 模式是基于錨點定位的內(nèi)部鏈接機制,在 URL 加上 #
,然后在 #
后面加上 hash 標簽,根據(jù)不同的標簽做定位。示例如下:
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 信息是不會提交到服務(wù)端,所以若要使用 SSR (Server-Side Rendered) ,就不能使用 Hash 模式即不能使用 HashLocationStrategy 策略。
HTML 5 模式則直接使用跟"真實"的 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 而不用重新刷新頁面。
導入 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 用于從瀏覽器 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;
// 進入會話歷史記錄中的下一個頁面
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 類的實例。我們也先來看一下 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
對象。
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()
,該方法的實現(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ù),我們再來分析該類中幾個重要的方法:
// 用于獲取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;
}
}
// 進入會話歷史記錄中的下一個頁面
forward(): void { this._history.forward(); }
// 進入會話歷史記錄中的上一個頁面
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 類也是繼承于 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ǔ)路徑,則會拋出異常??赡苡幸恍┏鯇W者,會遇到這個問題
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 的路由模塊。
更多建議: