React-router 实现最需要关注的就以下几方面:
-
路由值到视图的映射规则
-
对于路由变更的监听
-
路由操作方法
-
路由记录存储与操作
1. BrowserRouter
浏览器路由实现,最主要的两个概念是变更路由与监听路由变更。
History 是整个浏览器路由历史记录大对象,其中包含长度等属性 Location 是单一页面所对应的资源定位对象,我们可以理解为当页面跳转时,先生成 Location,然后将 Location push 到 History。
首先我们要明确几个概念:
- 历史操作。注意:此操作不会触发 popState
-
history.pushState
-
history.replaceState
- 监听变更
- window.addEventListener("popstate", () => {})
- 操作。注意:以下操作会触发 popState
-
history.back()
-
history.forward()
-
history.go()
1.1. 创建Router
javascript
export function createBrowserRouter(
routes: RouteObject[],
opts?: DOMRouterOpts
): RemixRouter {
return createRouter({
basename: opts?.basename,
future: {
...opts?.future,
v7_prependBasename: true,
},
history: createBrowserHistory({ window: opts?.window }),
hydrationData: opts?.hydrationData || parseHydrationData(),
routes,
mapRouteProperties,
}).initialize();
}
1.2. router下承载history
javascript
export function createBrowserHistory(
options: BrowserHistoryOptions = {}
): BrowserHistory {
function createBrowserLocation(
window: Window,
globalHistory: Window["history"]
) {
let { pathname, search, hash } = window.location;
return createLocation(
"",
{ pathname, search, hash },
(globalHistory.state && globalHistory.state.usr) || null,
(globalHistory.state && globalHistory.state.key) || "default"
);
}
function createBrowserHref(window: Window, to: To) {
return typeof to === "string" ? to : createPath(to);
}
return getUriBasedHistory(
createBrowserLocation,
createBrowserHref,
null,
options
);
}
1.3. history 确定 location
javascript
function getUrlBasedHistory(
getLocation: (window: Window, globalHistory: Window["history"]) => Location,
createHref: (window: Window, to: To) => string,
validateLocation: ((location: Location, to: To) => void) | null,
options: UrlHistoryOptions = {}
): UrlHistory {
let { window = document.defaultView!, v5Compat = false } = options;
let globalHistory = window.history;
let action = Action.Pop;
let listener: Listener | null = null;
let index = getIndex()!
if (index == null) {
index = 0;
globalHistory.replaceState({ ...globalHistory.state, idx: index }, "");
}
function getIndex(): number {
let state = globalHistory.state || { idx: null };
return state.idx;
}
function handlePop() {
action = Action.Pop;
let nextIndex = getIndex();
let delta = nextIndex == null ? null : nextIndex - index;
index = nextIndex;
if (listener) {
listener({ action, location: history.location, delta });
}
}
function push(to: To, state?: any) {
action = Action.Push;
let location = createLocation(history.location, to, state);
if (validateLocation) validateLocation(location, to);
index = getIndex() + 1;
let historyState = getHistoryState(location, index);
let url = history.createHref(location);
try {
globalHistory.pushState(historyState, "", url);
} catch (error) {
if (error instanceof DOMException && error.name === "DataCloneError") {
throw error;
}
window.location.assign(url);
}
if (v5Compat && listener) {
listener({ action, location: history.location, delta: 1 });
}
}
function replace(to: To, state?: any) {
action = Action.Replace;
let location = createLocation(history.location, to, state);
if (validateLocation) validateLocation(location, to);
index = getIndex();
let historyState = getHistoryState(location, index);
let url = history.createHref(location);
globalHistory.replaceState(historyState, "", url);
if (v5Compat && listener) {
listener({ action, location: history.location, delta: 0 });
}
}
function createURL(to: To): URL {
let base =
window.location.origin !== "null"
? window.location.origin
: window.location.href;
let href = typeof to === "string" ? to : createPath(to);
invariant(
base,
`No window.location.(origin|href) available to create URL for href: ${href}`
),
return new URL(href, base);
}
let history: History = {
get action() {
return action;
},
get location() {
return getLocation(window, globalHistory);
},
listen(fn: Listener) {
if (listener) {
throw new Error("A history only accepts one active listener");
}
window.addEventListener(PopStateEventType, handlePop);
listener = fn;
return () => {
window.removeEventListener(PopStateEventType, handlePop);
listener = null;
};
},
createHref(to) {
return createHref(window, to);
},
createURL,
encodeLocation(to) {
let url = createURL(to);
return {
pathname: url.pathname,
search: url.search,
hash: url.hash,
};
},
push,
replace,
go(n) {
return globalHistory.go(n);
},
};
return history;
}
1.4. 历史记录栈变更监听
事件处理解决后,接下来就是解决监听,我们上面提到监听 popState 以此来处理路由变更。
javascript
listen(fn: Listener) {
if (listener) {
throw new Error("A history only accepts one active listener");
}
window.addEventListener(PopStateEventType, handlePop);
listener = fn;
return () => {
window.removeEventListener(PopStateEventType, handlePop);
listener = null;
};
},
1.5. popState 逻辑处理
javascript
function handlePop() {
action = Action.Pop;
let nextIndex = getIndex()
let delta = nextIndex == null ? null : nextIndex - index
index = nextIndex
if (listener) {
listener({ action, location: history.location, delta })
}
}
2. MemoryRouter
内存型路由的上层实现与 BrowserRouter 类似,或者说作者在设计之初就考虑了规范的对外统一接口协议,你会发现在使用 React-router 时,不管你用什么类型记录,API 都是一样的,这就是抽象封装的魅力。
2.1. 创建 router
javascript
export function createMemoryRouter(
routes: RouteObject[],
opts?: {
basename?: string;
future?: Partial<Omit<RouterFutureConfig, "v7_prependBasename">>;
hydrationData?: HydrationState;
initialEntries?: InitialEntry[];
initialIndex?: number;
unstable_dataStrategy?: unstable_DataStrategyFunction;
}
): RemixRouter {
return createRouter({
basename: opts?.basename,
future: {
...opts?.future,
v7_prependBasename: true,
},
history: createMemoryHistory({
initialEntries: opts?.initialEntries,
initialIndex: opts?.initialIndex,
}),
hydrationData: opts?.hydrationData,
routes,
mapRouteProperties,
unstable_dataStrategy: opts?.unstable_dataStrategy,
}).initialize();
}
2.2. Router 下承载 history
javascript
export function createMemoryHistory(
options: MemoryHistoryOptions = {}
): MemoryHistory {
let { initialEntries = ["/"], initialIndex, v5Compat = false } = options;
let entries: Location[]; // Declare so we can access from createMemoryLocation
entries = initialEntries.map((entry, index) =>
createMemoryLocation(
entry,
typeof entry === "string" ? null : entry.state,
index === 0 ? "default" : undefined
)
);
let index = clampIndex(
initialIndex == null ? entries.length - 1 : initialIndex
);
let action = Action.Pop;
let listener: Listener | null = null;
function clampIndex(n: number): number {
return Math.min(Math.max(n, 0), entries.length - 1);
}
function getCurrentLocation(): Location {
return entries[index];
}
function createMemoryLocation(
to: To,
state: any = null,
key?: string
): Location {
let location = createLocation(
entries ? getCurrentLocation().pathname : "/",
to,
state,
key
);
warning(
location.pathname.charAt(0) === "/",
`relative pathnames are not supported in memory history: ${JSON.stringify(
to
)}`
);
return location;
}
function createHref(to: To) {
return typeof to === "string" ? to : createPath(to);
}
let history: MemoryHistory = {
get index() {
return index;
},
get action() {
return action;
},
get location() {
return getCurrentLocation();
},
createHref,
createURL(to) {
return new URL(createHref(to), "http://localhost");
},
encodeLocation(to: To) {
let path = typeof to === "string" ? parsePath(to) : to;
return {
pathname: path.pathname || "",
search: path.search || "",
hash: path.hash || "",
};
},
push(to, state) {
action = Action.Push;
let nextLocation = createMemoryLocation(to, state);
index += 1;
entries.splice(index, entries.length, nextLocation);
if (v5Compat && listener) {
listener({ action, location: nextLocation, delta: 1 });
}
},
replace(to, state) {
action = Action.Replace;
let nextLocation = createMemoryLocation(to, state);
entries[index] = nextLocation;
if (v5Compat && listener) {
listener({ action, location: nextLocation, delta: 0 });
}
},
go(delta) {
action = Action.Pop;
let nextIndex = clampIndex(index + delta);
let nextLocation = entries[nextIndex];
index = nextIndex;
if (listener) {
listener({ action, location: nextLocation, delta });
}
},
listen(fn: Listener) {
listener = fn;
return () => {
listener = null;
};
},
};
return history;
}
因为内存型路由跟浏览器历史记录没有关联,因此相较于 BrowserRouter,没有关于浏览器历史记录栈变更的监听,只有单纯的记录压入和跳转。
重点关注:
javascript
push(to, state) {
action = Action.Push;
let nextLocation = createMemoryLocation(to, state);
index += 1;
entries.splice(index, entries.length, nextLocation);
if (v5Compat && listener) {
listener({ action, location: nextLocation, delta: 1 });
}
}