摘录自:https://segmentfault.com/a/1190000012656017
history简介
通常有2种history,分别是hashHistory和browserHistory,本文带领大家从零开始实现一个hashHistory。
hashHistory:'#/home'
browserHistory: '/home'
下面的实现方案是根据官方history源码来分析的,以hashHistory源码结合本文学习。
实现方案
1、创建createHashHistory函数
const createHashHistory = () => {
const history = {};
return history;
};
export default createHashHistory;
2、先要了解history对象长什么样,接着,我们一个个去实现它
history = {
length: 1, // Number
action: "POP", // String
loaction: {}, // Object
createHref, // 函数
push, // 函数
replace, // 函数
go, // 函数
goBack, // 函数
goForward, // 函数
listen // 函数
}
3、实现length
在window下面有一个history对象,可以用来获取length。
const globalHistory = window.history;
history = {
length: globalHistory.length
};
4、action默认为POP,它还可能是PUSH或者REPLACE。我们不在这一步实现它,等下面实现push和replace的时候再来实现。
5、实现location
location对象包含下面几个key,这里能用到的是pathname。history.location和window.location是不一样的,history.location是window.location的精简版。你可以在浏览器控制台打印window.location看一下完整的location对象。
location = {
hash: "",
pathname: "/",
search: "",
state: undefined
}
定义一个getDOMLocation函数,用来获取封装后的location。
const decodePath = path =>
path.charAt(0) === "/" ? path : "/" + path
const getHashPath = () => {
// 如果url存在#,则去掉#,返回路径
// 比如:"http://localhost:8080/#/",返回'/'
const href = window.location.href;
const hashIndex = href.indexOf("#");
return hashIndex === -1 ? "" : href.substring(hashIndex + 1);
}
const getDOMLocation = () => {
// getHashPath获取url的路由,如果存在#,则去掉#
let path = decodePath(getHashPath());
// 创建location
return createLocation(path);
}
这一步的核心就是createLocation()的实现。但是,它不复杂,只是代码有点长,如果要了解,请看源码如下:
import resolvePathname from "resolve-pathname"
import valueEqual from "value-equal"
import { parsePath } from "./PathUtils"
export const createLocation = (path, state, key, currentLocation) => {
let location
if (typeof path === "string") {
// Two-arg form: push(path, state)
location = parsePath(path)
location.state = state
} else {
// One-arg form: push(location)
location = { ...path }
if (location.pathname === undefined) location.pathname = ""
if (location.search) {
if (location.search.charAt(0) !== "?")
location.search = "?" + location.search
} else {
location.search = ""
}
if (location.hash) {
if (location.hash.charAt(0) !== "#") location.hash = "#" + location.hash
} else {
location.hash = ""
}
if (state !== undefined && location.state === undefined)
location.state = state
}
try {
location.pathname = decodeURI(location.pathname)
} catch (e) {
if (e instanceof URIError) {
throw new URIError(
'Pathname "' +
location.pathname +
'" could not be decoded. ' +
"This is likely caused by an invalid percent-encoding."
)
} else {
throw e
}
}
if (key) location.key = key
if (currentLocation) {
// Resolve incomplete/relative pathname relative to current location.
if (!location.pathname) {
location.pathname = currentLocation.pathname
} else if (location.pathname.charAt(0) !== "/") {
location.pathname = resolvePathname(
location.pathname,
currentLocation.pathname
)
}
} else {
// When there is no prior location and pathname is empty, set it to /
if (!location.pathname) {
location.pathname = "/"
}
}
return location
}
export const locationsAreEqual = (a, b) =>
a.pathname === b.pathname &&
a.search === b.search &&
a.hash === b.hash &&
a.key === b.key &&
valueEqual(a.state, b.state)
6、实现createHref
你可能没有用过history.createHref(),它用来创建一个hash路由,也就是'#/'或者'#/home'这类的。
const createPath = location => {
const { pathname, search, hash } = location;
let path = pathname || "/";
if (search && search !== "?")
path += search.charAt(0) === "?" ? search : `?${search}`;
if (hash && hash !== "#") path += hash.charAt(0) === "#" ? hash : `#${hash}`;
return path;
}
const createHref = location =>
"#" + encodePath(createPath(location));
7、实现push方法
我们在使用push的时候,通常是history.push('/home')这种形式,不需要自己加#。
push实现的原理:判断push传入的路由和当前url的路由是否一样,如果一样,则不更新路由,否则就更新路由。
// 更新history对象的值,length、location和action
const setState = nextState => {
Object.assign(history, nextState);
history.length = globalHistory.length;
transitionManager.notifyListeners(history.loation, history.action);
}
// notifyListeners函数用来通知history的更新
const notifyListeners = (...args) => {
listeners.forEach(listener => listener(...args));
}
// 更新路由
const pushHashPath = path => (windows.location.hash = path);
// push核心代码
const push = (path, state) => {
// 更新action为'PUSH'
const action = "PUSH";
// 更新location对象
const location = createLocation(
path,
undefined,
undefined,
history.location
);
// 更新路由前的确认操作,confirmTransitionTo函数内部会处理好路由切换的状态判断,如果ok,则执行最后一个参数,它是回调函数。
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
// 如果不服路由切换的条件,就不更新路由
if (!ok) return;
// 获取location中的路径pathname,比如'/home'
const path = createPath(location);
const encodePath = encodePath(path);
// 比较当前的url中的路由和push函数传入的路由是否相同,
// 不相同则hashChanged为true。
const hashChanged = getHashPath() !== encodePath;
if (hashChanged) {
// 路由允许更新
ignorePath = path;
// 更新路由
pushHashPath(encodePath);
const prevIndex = allPaths.lastIndexOf(createPath(history.location));
const nextPaths = allPaths.slice(0, prevIndex === -1 ? 0 : prevIndex + 1);
nextPaths.push(path);
allPaths = nextPaths;
// setState更新history对象。
setState({ action, location });
} else {
// push的路由和当前路由一样,会发出一个警告
// "Hash history cannot PUSH the same path; a new entry will not be added to the history stack"
setState();
}
}
)
}
8、实现replace
replace和push都能更新路由,但是replace是更新当前路由,而push是增加一个历史记录。
// 更新路由
const replaceHashPath = path => {
const hashIndex = window.location.href.indexOf("#");
window.location.replace(
window.location.href.slice(0, hashIndex >= 0 ? hashIndex : 0) + "#" + path;
);
}
// replace核心代码
const replace = (path, state) => {
const action = "REPLACE";
const location = createLocation(
path,
undefined,
undefined,
history.location
);
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (!ok) return;
const path = createPath(location);
const encodePath = encodePath(path);
const hashChanged = getHashPath() !== encodePath;
// 到这里为止,前面的代码和push函数的实现是一样的
if (hashChanged) {
ignorePath = path;
// 更新路由
replaceHashPath(encodePath);
}
const prevIndex = allPaths.indexof(createPath(history.location));
if (prevIndex !== -1) allPaths[prevIndex] = path;
setState({ action, location });
}
)
}
9、实现go
go方法的使用时history.go(-1)这种形式:
// globalHistory是window.history
const go = n => globalHistory.go(n);
10、实现goBack
这个应该能一眼看懂了
const goBack = () => go(-1);
11、实现goForward
这个应该也能一眼看懂了
const goForward = () => go(1);
12、实现listen
const listen = listener => {
const unlisten = transitionManager.appendListener(listener);
checkDOMListeners(1);
return () => {
checkDOMListeners(-1);
unlisten();
}
}
// 监听hashchange的改变,handleHashChange函数用来判断是哪种类型的路由更新,replace、push等各种hash改变都实现了一个函数,具体看源码。
const checkDOMListeners = delta => {
listenerCount += delta;
if (listenerCount === 1) {
// 注册监听函数
window.addEventListener('hashchange', handleHashChange);
} else if (listenerCount === 0) {
// 转移监听函数
window.removeEvenListener('hashchange', handleHashChange);
}
}
// appendListener函数实现
ley listener = [];
const appendListener = fn => {
let isActive = true;
const listener = (...args) => {
if (isActive) fn(...args);
}
listeners.push(listener);
return () => {
isActive = false;
listeners = listeners.filter(item => item !== listener);
}
};
总结
history对象的所有属性和方法都实现了一遍,在react-router中,将history对象封装进了Router、Route等组件中,使得你可以在react组件中通过this.props.history读取。
看完源码,你会发现history的实现真的不复杂,找准思路,一个个函数去实现,再考虑兼容性,就非常完美了,以后你在其他博客上看到有人宣传自己搞了个自己的路由插件,不要觉得很牛逼,重构history换个新姿势就是一个插件了。