saven
Version:
220 lines (194 loc) • 5.48 kB
JavaScript
import EventEmitter from './eventEmitter'
import {
pushHash,
replaceHash,
getCurrentHash,
normalizeUrl,
createLocation
} from './utils'
import { navigateTo } from './apis'
const history = window.history
const eventEmitter = new EventEmitter()
let historyState = parseInt(history.state, 10) || 0
const initUrl = normalizeUrl(getCurrentHash())
const initLocation = createLocation(initUrl, historyState)
let ignoredUrl = null
const HISTORYKEY = 'taroRouterHistory'
const generateSerializer = (obj) => {
return () => {
localStorage.setItem(HISTORYKEY, JSON.stringify(obj))
}
}
class History {
constructor () {
history.replaceState(historyState, '', null)
window.addEventListener('hashchange', this.onHashchange)
window.addEventListener('popstate', this.onPopstate)
const stack = this.deserialize()
if (this.validateStack(stack)) {
this.locationStack = stack
this.serializeStack = generateSerializer(this.locationStack)
} else {
console.warn('Stack in storage invalid')
this.locationStack = [ initLocation ]
this.serializeStack = generateSerializer(this.locationStack)
this.serializeStack()
}
}
validateStack (stack) {
if (stack.length === 0) return false
const historyMap = {}
const hasDuplicateState = stack.some(v => {
const state = v.state
if (historyMap[state]) return true
historyMap[state] = true
})
if (hasDuplicateState) return false
const stackCurrent = stack[stack.length - 1]
const isThisone = stackCurrent.state === initLocation.state &&
stackCurrent.fullUrl === initLocation.fullUrl
if (isThisone) return true
else return false
}
deserialize () {
try {
const stackStr = localStorage.getItem(HISTORYKEY)
const stack = JSON.parse(stackStr)
if (!Array.isArray(stack)) return []
return stack
} catch (e) {
return []
}
}
onHashchange = (e) => {
history.replaceState(historyState, '', '')
}
onPopstate = (e) => {
const nextUrl = normalizeUrl(getCurrentHash())
if (typeof e.state !== 'number') {
if (nextUrl === ignoredUrl) return
ignoredUrl = null
navigateTo({
url: nextUrl
})
return
}
const isBackPage = e.state < historyState
if (isBackPage) {
this.goBack({
url: nextUrl,
state: e.state
})
} else {
navigateTo({
url: nextUrl,
state: e.state,
isForward: true
})
}
}
listen = (fn) => {
eventEmitter.on('change', fn)
}
unlisten = (fn) => {
eventEmitter.off('change', fn)
}
emit = (location, type, opts) => {
eventEmitter.emit('change', location, type, opts)
}
/**
* 返回当前的location
*
* @returns {location} location对象
*/
now () {
const current = this.locationStack.length - 1
return this.locationStack[current]
}
/**
* 返回当前的history栈长度
*
* @returns {number} history.stack的长度
*/
len () {
return this.locationStack.length
}
/**
* 在history.stack中push新的location
*
* @param {object} param0 push的对象
*/
push = ({ url, isForward, state }) => {
const nextUrl = normalizeUrl(url)
const currentUrl = this.now().url
if (!isForward && (nextUrl === currentUrl)) return
/**
* 带state 是forward 否则是navigate
*/
if (!state) {
historyState += 1
} else {
historyState = state
}
const location = createLocation(nextUrl, historyState)
this.locationStack.push(location)
this.serializeStack()
this.emit(location, 'PUSH')
ignoredUrl = nextUrl
if (!isForward) {
pushHash(nextUrl)
}
}
/**
* 在history.stack中replace新的location
*
* @param {object} param0 replace的对象
*/
replace = ({ url }) => {
const nextUrl = normalizeUrl(url)
const currentUrl = this.now().url
if (nextUrl === currentUrl) return
const location = createLocation(nextUrl, historyState)
this.locationStack.pop()
this.locationStack.push(location)
this.serializeStack()
this.emit(location, 'REPLACE')
ignoredUrl = nextUrl
replaceHash({
url: nextUrl,
state: historyState
})
history.replaceState
}
/**
* 从history.stack中回退
*
* @param {object} param0 回退的配置项
*/
goBack = ({ url, state }) => {
const len = this.len()
const foundStateIdx = this.locationStack.findIndex(v => {
return v.state === state
})
const delta = foundStateIdx > -1
? this.locationStack.length - foundStateIdx - 1
: 1
if (len > delta) {
this.locationStack.splice(-delta)
this.serializeStack()
const location = this.now()
historyState = location.state
this.emit(location, 'BACK', { delta })
} else if (delta <= 1 && url) {
const location = createLocation(normalizeUrl(url), state)
historyState = state
this.locationStack.length = 1
this.locationStack[0] = location
this.serializeStack()
this.emit(location, 'BACK', { delta })
} else {
return console.warn('goBack delta out of range')
}
}
}
export default new History()