持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第23天,点击查看活动详情
前言
在前面的篇章中《手写history里的createBrowserHistory》我们介绍了react-router的核心库history,包括history的主要属性方法以及基本实现原理。
在上一篇章中我们并没有完善history的监听器内容,在本篇章我们主要是实现history里的listen和block方法。
listen
在history库里,我们先看看listen的作用:
import React from "react";
import { createBrowserHistory } from "history"
const history = createBrowserHistory()
history.listen(({ action, location }) => {
console.log(
`The current URL is ${location.pathname}${location.search}${location.hash}`
);
console.log(`The last navigation action was ${action}`);
});
window.h = history
export default function App() {
console.log(history)
return <>
APP
</>
}
在代码里我们给全局变量h赋予了history的引用,我们在控制台操作push方法,往地址栏push一个新的地址:
基于监听器的原理,我们可以通过队列任务实现如下监听器代码:
class ListenerManager {
// 维护监听器队列
listeners = [];
// 添加一个监听器,返回一个用于取消监听的方法
addListener(listener) {
this.listeners.push(listener);
const unListen = () => {
const index = this.listeners.indexOf(listener)
this.listeners.splice(index, 1);
}
return unListen;
}
// 依次执行队列里的监听器任务
triggerListener(location, action){
for (const listener of this.listeners) {
listener(location, action);
}
}
}
通过上述的代码我们实现了简单的监听器。
block
history提供了block的方法,通过调用history.block(blocker: Blocker),我们可以实现阻止导航离开当前页面。如提示用户知道如果他们离开当前页面将丢失一些未保存的更改等。
测试代码如下:
import React from "react";
import { createBrowserHistory } from "history"
const history = createBrowserHistory()
let unblock = history.block((tx) => {
let url = tx.location.pathname;
if (window.confirm(`Are you sure you want to go to ${url}?`)) {
// Unblock the navigation.
unblock();
// Retry the transition.
tx.retry();
}
});
window.h = history
export default function App() {
console.log(history)
return <>
APP
</>
}
同样的,我们在控制台调用push方法往地址栏push一个新地址:
基于上述功能效果,我们可以实现如下的阻塞器:
class BlockManager {
// 该属性是否有值,决定了是否有阻塞
prompt = null;
constructor(getUserConfirmation) {
this.getUserConfirmation = getUserConfirmation;
}
block(prompt) {
if (typeof prompt !== "string" && typeof prompt !== "function") {
throw new TypeError("block must be string or function");
}
this.prompt = prompt;
return () => {
this.prompt = null;
}
}
// 阻塞之后要执行的回调
triggerBlock(location, action, callback) {
if (!this.prompt) {
callback();
return;
}
//阻塞消息
let message;
if (typeof this.prompt === "string") {
message = this.prompt;
}
else if (typeof this.prompt === "function") {
message = this.prompt(location, action);
}
//调用getUserConfirmation
this.getUserConfirmation(message, result => {
if (result === true) {
callback();
}
})
}
}
通过上述的代码我们实现了简单的阻塞器。
完善history
基于之前的history代码,我们可以在其内部引入监听器和阻塞器,实现对listen和block的封装。
import ListenerManager from "./ListenerManager";
import BlockManager from "./BlockManager";
/**
* 创建一个history api的history对象
* @param {*} options
*/
export default function createBrowserHistory(options = {}) {
const {
basename = "",
forceRefresh = false,
keyLength = 6,
getUserConfirmation = (message, callback) => callback(window.confirm(message))
} = options;
// 创建监听器实例
const listenerManager = new ListenerManager();
// 创建阻塞器实例
const blockManager = new BlockManager(getUserConfirmation);
function go(step) {
window.history.go(step);
}
function goBack() {
window.history.back();
}
function goForward() {
window.history.forward();
}
function push(path, state) {
changePage(path, state, true);
}
function replace(path, state) {
changePage(path, state, false);
}
function changePage(path, state, isPush) {
let action = "PUSH";
if (!isPush) {
action = "REPLACE"
}
const pathInfo = handlePathAndState(path, state, basename);
const location = createLoactionFromPath(pathInfo);
blockManager.triggerBlock(location, action, () => {
if (isPush) {
window.history.pushState({
key: createKey(keyLength),
state: pathInfo.state
}, null, pathInfo.path);
}
else {
window.history.replaceState({
key: createKey(keyLength),
state: pathInfo.state
}, null, pathInfo.path);
}
listenerManager.triggerListener(location, action);
history.action = action;
history.location = location;
if (forceRefresh) {
window.location.href = pathInfo.path;
}
})
function createKey(keyLength) {
return Math.random().toString(36).substr(2, keyLength);
}
function handlePathAndState(path, state, basename) {
if (typeof path === "string") {
return {
path,
state
}
}else if (typeof path === "object") {
let pathResult = basename + path.pathname;
let { search = "", hash = "" } = path;
if (search.charAt(0) !== "?") {
search = "?" + search;
}
if (hash.charAt(0) !== "#") {
hash = "#" + hash;
}
pathResult += search;
pathResult += hash;
return {
path: pathResult,
state: path.state
}
}else {
throw new TypeError("path must be string or object");
}
}
function createLoactionFromPath(pathInfo, basename) {
let pathname = pathInfo.path.replace(/[#?].*$/, "");
let reg = new RegExp(`^${basename}`);
pathname = pathname.replace(reg, "");
var questionIndex = pathInfo.path.indexOf("?");
var sharpIndex = pathInfo.path.indexOf("#");
let search;
if (questionIndex === -1 || questionIndex > sharpIndex) {
search = "";
} else {
search = pathInfo.path.substring(questionIndex, sharpIndex);
}
let hash;
if (sharpIndex === -1) {
hash = "";
} else {
hash = pathInfo.path.substr(sharpIndex);
}
return {
hash,
pathname,
search,
state: pathInfo.state
}
}
}
function addDomListener() {
// opstate事件,仅能监听前进、后退、用户对地址hash的改变
// 法监听到pushState、replaceState
window.addEventListener("popstate", () => {
const location = createLocation(basename);
const action = "POP";
blockManager.triggerBlock(location, action, () => {
listenerManager.triggerListener(location, "POP");
history.location = location;
})
})
}
addDomListener();
function listen(listener) {
return listenerManager.addListener(listener);
}
function block(prompt) {
return blockManager.block(prompt);
}
function createHref(location) {
return basename + location.pathname + location.search + location.hash;
}
function createLocation(basename = "") {
let pathname = window.location.pathname;
const reg = new RegExp(`^${basename}`);
pathname = pathname.replace(reg, "");
const location = {
hash: window.location.hash,
search: window.location.search,
pathname
};
let state, historyState = window.history.state;
if (historyState === null) {
state = undefined;
}else if (typeof historyState !== "object") {
state = historyState;
}else {
if ("key" in historyState) {
location.key = historyState.key;
state = historyState.state;
}else {
state = historyState;
}
}
location.state = state;
return location;
}
const history = {
action: "POP",
createHref,
block,
length: window.history.length,
go,
goBack,
goForward,
push,
replace,
listen,
location: createLocation(basename)
};
return history;
}
小结
history作为react-router的核心库,我们完成的是其大致的实现,该库更多的是在此基础上对细节进行完善处理,我们只需要理解其基本实现原理即可。