Read history
XXHolic opened this issue · 0 comments
目录
引子
history 经常看到,想看看里面封装的方式。
history 版本 4.10.1。
简介
history 库是基于浏览器内置 History 和 Location API 轻量封装。它的目的不是提供这些 API 的完整实现,而是为了让使用者更加容易的使用不同的导航方法。
该库提供了 3 种不同方法创建一个 history
对象,根据环境的需要有下面的选择:
createBrowserHistory
方法适用于那些支持 HTML5 history API 的现代浏览器。createHashHistory
方法适用于当页面重新加载时,想把当前位置存储在当前 URL 的hash
上,以此避免向服务器发送请求的情景。createMemoryHistory
方法适用于参考实现,也可以用于非 DOM 环境,例如 React Native 或 tests 。
更多说明见 history Doc 。
实现
createBrowserHistory
主要逻辑代码
import { createLocation } from './LocationUtils.js';
import createTransitionManager from './createTransitionManager.js';
function createBrowserHistory(props = {}) {
const globalHistory = window.history;
const canUseHistory = supportsHistory();
const needsHashChangeListener = !supportsPopStateOnHashChange();
const {
forceRefresh = false,
getUserConfirmation = getConfirmation,// 浏览器自带确认弹窗
keyLength = 6 // 创建随机 key 后截取的位置
} = props;
const basename = props.basename
? stripTrailingSlash(addLeadingSlash(props.basename)) // 格式兼容处理
: '';
// 转变管理器,添加事件监听、执行,以及 url 转变时都会触发里面对应的方法。
const transitionManager = createTransitionManager();
// getDOMLocation 方法将 window.history.state 和 window.location 对象中属性统一放在一个对象上
function getDOMLocation(historyState) {
const { key, state } = historyState || {};
const { pathname, search, hash } = window.location;
let path = pathname + search + hash;
if (basename) path = stripBasename(path, basename);
return createLocation(path, state, key);
}
// getHistoryState 方法获取 window.history.state, 没有(IE 10/11)就返回 {}
const initialLocation = getDOMLocation(getHistoryState());
let allKeys = [initialLocation.key];
function setState(nextState) {
Object.assign(history, nextState);
history.length = globalHistory.length;
transitionManager.notifyListeners(history.location, history.action);
}
// Public interface
function createHref(location) {
return basename + createPath(location);
}
function push(path, state) {
const action = 'PUSH';
const location = createLocation(path, state, createKey(), history.location);
transitionManager.confirmTransitionTo(location,
action,
getUserConfirmation,
ok => {
/* ... */
})
}
function replace(path, state) {
const action = 'REPLACE';
const location = createLocation(path, state, createKey(), history.location);
transitionManager.confirmTransitionTo(location,
action,
getUserConfirmation,
ok => {
/* ... */
})
}
function go(n) { /* ... */ }
function goBack() { /* ... */ }
function goForward() { /* ... */ }
let listenerCount = 0; // 用于 checkDOMListeners 方法判断
function checkDOMListeners(delta) { /* ... */ } // popstate、hashchange 事件监听
let isBlocked = false; // 用于 block 方法判断
function block(prompt = false) { /* ... */ } // 页面离开前的提示信息或操作
function listen(listener) { /* ... */ }
const history = {
length: globalHistory.length,
action: 'POP',
location: initialLocation,
createHref,
push,
replace,
go,
goBack,
goForward,
block,
listen
};
return history;
代码中主要处理有:
- createLocation 方法对传入的参数信息进行统一组装处理,产生一个新的
location
对象。 - 有一个手动维护的数组,存放历史记录。
- 对
history.pushState
、history.replaceState
分别包装了一层判断和兼容处理,提供了改变前的提示功能。处理方法放在工具 createTransitionManager 中。 - 事件监听使用了发布订阅模式处理,放在工具 createTransitionManager 中。
- 关键的改变都会触发
setState
方法,同步数据到history
对象,并执行监听事件。
createHashHistory
createHashHistory 方法封装的方式跟 createBrowserHistory 差不多,主要区别是处理 URL 部分的不同。
主要逻辑代码
import { createLocation } from './LocationUtils.js';
import createTransitionManager from './createTransitionManager.js';
const HashPathCoders = {
hashbang: {
encodePath: path =>
path.charAt(0) === '!' ? path : '!/' + stripLeadingSlash(path),
decodePath: path => (path.charAt(0) === '!' ? path.substr(1) : path)
},
noslash: {
encodePath: stripLeadingSlash,
decodePath: addLeadingSlash
},
slash: {
encodePath: addLeadingSlash,
decodePath: addLeadingSlash
}
};
function createHashHistory(props = {}) {
const globalHistory = window.history;
const canGoWithoutReload = supportsGoWithoutReloadUsingHash();
const { getUserConfirmation = getConfirmation, hashType = 'slash' } = props;
const basename = props.basename
? stripTrailingSlash(addLeadingSlash(props.basename))
: '';
const { encodePath, decodePath } = HashPathCoders[hashType];
function getDOMLocation() {
let path = decodePath(getHashPath());
if (basename) path = stripBasename(path, basename);
return createLocation(path);
}
const transitionManager = createTransitionManager();
function setState(nextState) {
Object.assign(history, nextState);
history.length = globalHistory.length;
transitionManager.notifyListeners(history.location, history.action);
}
let forceNextPop = false;
let ignorePath = null;
// 在执行其他操作之前,确保哈希已正确编码。
const path = getHashPath();
const encodedPath = encodePath(path);
if (path !== encodedPath) replaceHashPath(encodedPath);
const initialLocation = getDOMLocation();
let allPaths = [createPath(initialLocation)];
// Public interface
function createHref(location) {
const baseTag = document.querySelector('base');
let href = '';
if (baseTag && baseTag.getAttribute('href')) {
href = stripHash(window.location.href);
}
return href + '#' + encodePath(basename + createPath(location));
}
function push(path, state) {
const action = 'PUSH';
const location = createLocation(
path,
undefined,
undefined,
history.location
);
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
/* ... */
})
}
function replace(path, state) {
const action = 'REPLACE';
const location = createLocation(
path,
undefined,
undefined,
history.location
);
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
/* ... */
})
}
function go(n) { /* ... */ }
function goBack() { /* ... */ }
function goForward() { /* ... */ }
let listenerCount = 0;
function checkDOMListeners(delta) { /* ... */ }
let isBlocked = false;
function block(prompt = false) { /* ... */ }
function listen(listener) { /* ... */ }
const history = {
length: globalHistory.length,
action: 'POP',
location: initialLocation,
createHref,
push,
replace,
go,
goBack,
goForward,
block,
listen
};
return history;
}
与 createBrowserHistory
不同的地方有:
hashType
处理方式有分类。- 哈希值额外的编码和解码处理。
- 改变使用的是
window.location
对象的方法。
createMemoryHistory
createMemoryHistory 方法没有借助浏览器 history
、location
API,是一个导航模拟实现。
import { createPath } from './PathUtils.js';
import { createLocation } from './LocationUtils.js';
import createTransitionManager from './createTransitionManager.js';
function clamp(n, lowerBound, upperBound) {
return Math.min(Math.max(n, lowerBound), upperBound);
}
function createMemoryHistory(props = {}) {
const {
getUserConfirmation,
initialEntries = ['/'],
initialIndex = 0,
keyLength = 6
} = props;
// 事件统一处理,状态转变过程处理
const transitionManager = createTransitionManager();
// 同步数据状态信息
function setState(nextState) {
Object.assign(history, nextState);
history.length = history.entries.length;
// 监听事件执行
transitionManager.notifyListeners(history.location, history.action);
}
function createKey() {
return Math.random()
.toString(36)
.substr(2, keyLength);
}
const index = clamp(initialIndex, 0, initialEntries.length - 1);
// 初始化,每一个路径都对应一个路径对象
const entries = initialEntries.map(entry =>
typeof entry === 'string'
? createLocation(entry, undefined, createKey())
: createLocation(entry, undefined, entry.key || createKey())
);
// Public interface
const createHref = createPath;
function push(path, state) {
const action = 'PUSH';
const location = createLocation(path, state, createKey(), history.location);
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (!ok) return;
const prevIndex = history.index;
const nextIndex = prevIndex + 1;
const nextEntries = history.entries.slice(0);
if (nextEntries.length > nextIndex) {
nextEntries.splice(
nextIndex,
nextEntries.length - nextIndex,
location
);
} else {
nextEntries.push(location);
}
setState({
action,
location,
index: nextIndex,
entries: nextEntries
});
}
);
}
function replace(path, state) {
const action = 'REPLACE';
const location = createLocation(path, state, createKey(), history.location);
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (!ok) return;
history.entries[history.index] = location;
setState({ action, location });
}
);
}
function go(n) {
const nextIndex = clamp(history.index + n, 0, history.entries.length - 1);
const action = 'POP';
const location = history.entries[nextIndex];
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (ok) {
setState({
action,
location,
index: nextIndex
});
} else {
// Mimic the behavior of DOM histories by
// causing a render after a cancelled POP.
setState();
}
}
);
}
function goBack() {
go(-1);
}
function goForward() {
go(1);
}
function canGo(n) {
const nextIndex = history.index + n;
return nextIndex >= 0 && nextIndex < history.entries.length;
}
function block(prompt = false) {
return transitionManager.setPrompt(prompt);
}
function listen(listener) {
return transitionManager.appendListener(listener);
}
const history = {
length: entries.length,
action: 'POP',
location: entries[index],
index,
entries,
createHref,
push,
replace,
go,
goBack,
goForward,
canGo,
block,
listen
};
return history;
}
这个方法中比较特别的是每一个路径都会显式的生成一个对应对象,另外两个方法都是利用原生 API 的功能实现。
跟前面对比,感觉这个就是 history
整体封装思路的表达:对相关的属性进行聚合,对事件监听进行统一处理,操作方法对外提供包装后一致的 API,状态变化内部进行记录。
先看 createMemoryHistory
方法的实现逻辑,再看另外两个方法的逻辑会更顺畅。
参考资料
🗑️
下面是一些无关紧要的东西。
看源码感觉总是会消耗我大量元气~~~:sweat::sweat::sweat:
最近看自己的笔记时,感觉少了点什么,回想自己看过印象比较深刻文章中,大概有下面几种特征:
- 开头有张很大的图,而且不是随便的图,看起来有逼格的图。
- 开头是桥段情景式,文中和文末都有情节关联。
- 行文中间适时的放一些表情包或有意思的图。
- 纯文字式的调侃。
- 行文由浅入深,循序渐进,一气呵成。
想了想后,自己要么风格不合适,要么还没达到那种层次,不过还是想要加点什么东西。
最后决定以后在结尾的最后面,随便加些其它不同类型的东西:
- 一方面算是“事后一根烟”的调节
- 另一方面希望以后看到最后时有种“哎呦,还有这个~~”的感觉
这个出自《怪化猫》,我很喜欢稀奇古怪的故事题材,浮世绘的风格显得更加诡异。
《怪化猫》讲的是一个卖药郎各地旅行斩妖的故事。里面有个很有意思的设定:
斩妖时想要拔出退魔之剑,需要找出三种要素:形、真、理
- 形:妖物的形,是由人的因缘形成
- 真:事情的真相
- 理:心中的隐情
拔退魔之剑的动画 GIF 太大了预览加载不出来:sweat:!有兴趣的自己下载看吧!源图片在这里。