preface
Front-end routing modes include Hash mode and History mode.
When the vue-router is initialized, it uses different routing modes according to mode, and thus generates different object instances. For example, HTML5History is used for history mode and HashHistory is used for hash mode.
init (app: any /* Vue component instance */) {
this.app = app
const { mode, options, fallback } = this
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, fallback)
break
case 'abstract':
this.history = new AbstractHistory(this)
break
default:
assert(false.`invalid mode: ${mode}`)}this.history.listen(route= > {
this.app._route = route
})
}
Copy the code
This time we will focus on the implementation of HTML5History and HashHistory.
HashHistory
Vue-router implements Hash routing by creating a HashHistory.
this.history = new HashHistory(this, options.base, fallback)
Copy the code
The three parameters represent:
- This: Router instance
- Base: indicates the base path of the application
- Fallback: History mode, but does not support History and is converted to Hash mode
HashHistory inherits from the History class, with some attributes and methods coming from the History class. Take a look at the HashHistory constructor.
constructor
Constructors do four main things.
- Call the superclass constructor through super, but let’s put that aside.
- Handles the case where History mode is converted to Hash mode because History is not supported.
- Make sure # is followed by a slash, if not.
- Implementation jumps to the hash page and listens for hash change events.
constructor(router: VueRouter, base: ? string, fallback: boolean) {super(router, base)
// check history fallback deeplinking
if (fallback && this.checkFallback()) {
return
}
ensureSlash()
this.transitionTo(getHash(), () => {
window.addEventListener('hashchange', () = > {this.onHashChange()
})
})
}
Copy the code
Let’s talk about these in detail.
checkFallback
The second thing the constructor does first, when fallback is true, is that older browsers (IE9) do not support History mode, so it will be relegated to Hash mode.
The URL is also checked using the checkFallback method.
checkFallback () {
// Remove the base prefix
const location = getLocation(this.base)
// If it does not start with /#
if (!/ # ^ / / /.test(location)) {
window.location.replace(
cleanPath(this.base + '/ #' + location)
)
return true}}Copy the code
The getLocation method is used to remove the base prefix, followed by the re to determine if the URL begins with /#. If not, replace the URL with one that begins with /#. Constructor () since switching routes using Hash URLS under IE9 causes the entire page to refresh, the listener hashchange does not work, so return directly.
Take a look at the implementation of the getLocation and cleanPath methods called in checkFallback.
The getLocation method basically removes the base prefix. If you search for base in the vue-Router official document, you can find that it is the base path of the application.
export function getLocation (base: string) :string {
let path = window.location.pathname
if (base && path.indexOf(base) === 0) {
path = path.slice(base.length)
}
return (path || '/') + window.location.search + window.location.hash
}
Copy the code
The cleanPath method replaces the double slash with a single slash to ensure that the URL path is correct.
export function cleanPath (path: string) :string {
return path.replace(/\/\//g.'/')}Copy the code
ensureSlash
Let’s look at the third thing the constructor does.
All the ensureSlash method does is make sure the URL root path has a slash, or if it doesn’t.
function ensureSlash () :boolean {
const path = getHash()
if (path.charAt(0) = = ='/') {
return true
}
replaceHash('/' + path)
return false
}
Copy the code
EnsureSlash uses getHash to retrieve the path after the # sign of the URL and replaceHash to replace the route.
function getHash () :string {
// We can't use window.location.hash here because it's not
// consistent across browsers - Firefox will pre-decode it!
const href = window.location.href
const index = href.indexOf(The '#')
return index === - 1 ? ' ' : href.slice(index + 1)}Copy the code
Hash is not available via window.location.href due to Firefox (as stated in the source code comment), but window.location.href.
function replaceHash (path) {
const i = window.location.href.indexOf(The '#')
window.location.replace(
window.location.href.slice(0, i >= 0 ? i : 0) + The '#' + path
)
}
Copy the code
What the replaceHash method does is replace the hash route after the # symbol.
onHashChange
Finally, look at the fourth thing the constructor does.
this.transitionTo(getHash(), () => {
window.addEventListener('hashchange', () = > {this.onHashChange()
})
})
Copy the code
TransitionTo is a method of the parent class History, which is more complex and mainly realizes the function of guard navigation. I’ll leave it here for now, and I’ll go into it later.
The next step is to listen for the Hashchange event and call the onHashChange method when the hash route changes.
onHashChange () {
if(! ensureSlash()) {return
}
this.transitionTo(getHash(), route => {
replaceHash(route.fullPath)
})
}
Copy the code
When the hash route is changed, that is, the page is redirected, the route starts with a slash, the guard navigation is triggered, and a new hash route is replaced.
HashHistory also implements push, replace, go and other programmatic navigation. If you are interested, you can directly look at the source code. Here is not a description, mainly using the above method to achieve.
HTML5History
Vue-router implements History mode routing by new an HTML5History.
this.history = new HTML5History(this, options.base)
Copy the code
HTML5History is also an inheritance and History class.
constructor
The HTML5History constructor does a few things:
- Call the parent class
transitionTo
Method to trigger guard navigation, more on that later. - Listening to the
popstate
Events. - Listens for scroll bar scrolling if there is scrolling behavior.
constructor(router: VueRouter, base: ? string) {super(router, base)
this.transitionTo(getLocation(this.base))
const expectScroll = router.options.scrollBehavior
window.addEventListener('popstate', e => {
_key = e.state && e.state.key
const current = this.current
this.transitionTo(getLocation(this.base), next => {
if (expectScroll) {
this.handleScroll(next, current, true)}})})if (expectScroll) {
window.addEventListener('scroll', () => {
saveScrollPosition(_key)
})
}
}
Copy the code
Let’s talk about these in detail.
scroll
Let’s start by listening for scrollbar events.
window.addEventListener('scroll', () => {
saveScrollPosition(_key)
})
Copy the code
After the scroll bar is rolled, vue-Router saves the scroll bar location. There are two things to know here, the saveScrollPosition method and the _key.
const genKey = (a)= > String(Date.now())
let _key: string = genKey()
Copy the code
_key is a current timestamp that is passed as a parameter every time the browser moves forward or backward, so that the page to which it jumps can be retrieved. So what does _key do.
Take a look at the implementation of saveScrollPosition:
export function saveScrollPosition (key: string) {
if(! key)return
window.sessionStorage.setItem(key, JSON.stringify({
x: window.pageXOffset,
y: window.pageYOffset
}))
}
Copy the code
Vue-router stores the scrollbar location in sessionStorage, where the key is _key.
So every time the browser scrolls, the location of the scroll bar will be saved in sessionStorage for later use.
popstate
The popState event is triggered when the browser moves forward or backward. Guard navigation is also triggered by the transitionTo call, or the handleScroll method if there is scrolling behavior.
The handleScroll method has a lot of code, so let’s take a look at how the scrolling behavior is used.
scrollBehavior (to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { x: 0.y: 0}}}Copy the code
If you want to simulate the behavior of “rolling to anchor” :
scrollBehavior (to, from, savedPosition) {
if (to.hash) {
return {
selector: to.hash
}
}
}
Copy the code
So there’s at least three things to determine, one is the save position, one is the selector, and one is the XY coordinate.
HandleScroll (delete some judgments) :
handleScroll (to: Route, from: Route, isPop: boolean) {
const router = this.router
const behavior = router.options.scrollBehavior
// wait until re-render finishes before scrolling
router.app.$nextTick((a)= > {
let position = getScrollPosition(_key)
const shouldScroll = behavior(to, from, isPop ? position : null)
if(! shouldScroll) {return
}
const isObject = typeof shouldScroll === 'object'
if (isObject && typeof shouldScroll.selector === 'string') {
const el = document.querySelector(shouldScroll.selector)
if (el) {
position = getElementPosition(el)
} else if (isValidPosition(shouldScroll)) {
position = normalizePosition(shouldScroll)
}
} else if (isObject && isValidPosition(shouldScroll)) {
position = normalizePosition(shouldScroll)
}
if (position) {
window.scrollTo(position.x, position.y)
}
})
}
Copy the code
We start with an if judgment, and if there’s a selector, we get the coordinates of the corresponding element.
Otherwise, the value returned by scrollBehavior is used as the coordinate, which may be the coordinate of savedPosition or a custom XY coordinate.
After a series of checks, the window.scrollTo method is finally called to set the scrollbar position.
There are three methods used to process coordinates, respectively:
- GetElementPosition: Gets element coordinates
- IsValidPosition: Verifies that the coordinates are valid
- NormalizePosition: Formats coordinates
The amount of code is not large, the specific code details interested can take a look.
HTML5History also implemented push, replace, go and other programmatic navigation.
The last
At this point, the implementation of HashHistory and HTML5History is generally understood. As we read, we kept coming across the parent class History and its transitionTo method, which we’ll learn more about in the next article.