The front-end routing
routing
Routing is the distribution of requests and the transmission of information from the original address to the destination address over the network
The front-end routing
define
In an HTML page to achieve interaction with the user without refreshing and skipping pages, at the same time, match a special URL for each view display in SPA, change this URL and do not let the browser like the server to send requests, and can listen to the CHANGE of URL.
SPA
Single Page Web application
Simply speaking, SPA is a WEB project with only one HTML page. Once the page is loaded, SPA will not reload or jump the page because of the user’s operation. Instead, JS is used to dynamically change the content of HTML to simulate jumping between views.
Traditional page —
Each HTML page is completed by an HTML file that contains the complete HTML structure
SPA page –
An application has only one HTML file, but the HTML file contains a placeholder, and the content of the placeholder is determined by the view. The page switch of SPA is the view switch
Hash mode, History mode, memory mode
Hash pattern
The sample
Usage scenarios
You can use front-end routing in any situation
disadvantages
SEO is not friendly and the server cannot receive hash.
For example, if you visit http://www.baidu.com/#river, the Request URL is http://www.baidu.com/
The history mode
The sample
Usage scenarios
When the back end renders all front-end routes to the same page (not the 404 page), you can use history mode
disadvantages
Internet Explorer 8 and below are not supported
The memory model
The sample
different
Unlike hash mode and History mode, which store the path by URL, memory mode stores the path in local storage, and mobile mode stores the path in mobile database.
disadvantages
Valid for single machine only
VueRouter source
The official documentation
To realize the Vue Router
Using Vue Router(Hash mode)
To use history mode, simply add
const router = new VueRouter({
mode: "history",
routes
})
Copy the code
Vue-router source code analysis
The source code
Let’s first look at the implementation path of VUE.
An instance object of VueRouter needs to be instantiated in the entry file and passed into the Options of the Vue instance.
export default class VueRouter {
static install: () = > void;
static version: string;
app: any;
apps: Array<any>;
ready: boolean;
readyCbs: Array<Function>;
options: RouterOptions;
mode: string;
history: HashHistory | HTML5History | AbstractHistory;
matcher: Matcher;
fallback: boolean;
beforeHooks: Array<? NavigationGuard>; resolveHooks:Array<? NavigationGuard>; afterHooks:Array<? AfterNavigationHook>;constructor (options: RouterOptions = {}) {
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
// Create a matcher matching function
this.matcher = createMatcher(options.routes || [], this)
// Instantiate the specific History according to mode, default to 'hash' mode
let mode = options.mode || 'hash'
SupportsPushState check whether the browser supports the 'history' mode
// If 'history' is set but the browser does not support it, 'history' mode will revert to 'hash' mode
PushState controls whether a route should revert to hash mode when the browser does not support history.pushState. The default value is true.
this.fallback = mode === 'history'&&! supportsPushState && options.fallback ! = =false
if (this.fallback) {
mode = 'hash'
}
// If it is not inside the browser, it becomes 'abstract' mode
if(! inBrowser) { mode ='abstract'
}
this.mode = mode
// Select the corresponding History class to instantiate according to the different schema
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if(process.env.NODE_ENV ! = ='production') {
assert(false.`invalid mode: ${mode}`) } } } match ( raw: RawLocation, current? : Route, redirectedFrom? : Location ): Route {return this.matcher.match(raw, current, redirectedFrom) } get currentRoute (): ? Route {return this.history && this.history.current
}
init (app: any /* Vue component instance */) { process.env.NODE_ENV ! = ='production' && assert(
install.installed,
`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
`before creating root instance.`
)
this.apps.push(app)
// main app already initialized.
if (this.app) {
return
}
this.app = app
const history = this.history
// Perform initialization and listening operations according to the category of history
if (history instanceof HTML5History) {
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
const setupHashListener = () = > {
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
history.listen(route= > {
this.apps.forEach((app) = > {
app._route = route
})
})
}
// Route before jump
beforeEach (fn: Function) :Function {
return registerHook(this.beforeHooks, fn)
}
// Route navigation is confirmed between
beforeResolve (fn: Function) :Function {
return registerHook(this.resolveHooks, fn)
}
// After a route jump
afterEach (fn: Function) :Function {
return registerHook(this.afterHooks, fn)
}
// The callback function called when the first hop is complete
onReady (cb: Function, errorCb? :Function) {
this.history.onReady(cb, errorCb)
}
// Routing error reported
onError (errorCb: Function) {
this.history.onError(errorCb)
}
// Route add, this method adds a record to the history stack, click back to return to the previous page.push (location: RawLocation, onComplete? :Function, onAbort? :Function) {
this.history.push(location, onComplete, onAbort)
}
// This method does not add a new record to history. Click Back to switch to the previous page. The last record does not exist.replace (location: RawLocation, onComplete? :Function, onAbort? :Function) {
this.history.replace(location, onComplete, onAbort)
}
// How many pages to jump forward or backward from the current page, like window.history.go(n). N can be positive or negative. A positive number returns the previous page
go (n: number) {
this.history.go(n)
}
// Go back to the previous page
back () {
this.go(-1)}// Proceed to the next page
forward () {
this.go(1) } getMatchedComponents (to? : RawLocation | Route):Array<any> {
const route: any = to
? to.matched
? to
: this.resolve(to).route
: this.currentRoute
if(! route) {return[]}return [].concat.apply([], route.matched.map(m= > {
return Object.keys(m.components).map(key= > {
return m.components[key]
})
}))
}
resolve (
to: RawLocation,
current?: Route,
append?: boolean
): {
location: Location,
route: Route,
href: string,
// for backwards compat
normalizedTo: Location,
resolved: Route
} {
const location = normalizeLocation(
to,
current || this.history.current,
append,
this
)
const route = this.match(location, current)
const fullPath = route.redirectedFrom || route.fullPath
const base = this.history.base
const href = createHref(base, fullPath, this.mode)
return {
location,
route,
href,
// for backwards compat
normalizedTo: location,
resolved: route
}
}
addRoutes (routes: Array<RouteConfig>) {
this.matcher.addRoutes(routes)
if (this.history.current ! == START) {this.history.transitionTo(this.history.getCurrentLocation())
}
}
}
Copy the code
HashHistory
• Hash appears in the URL, but is not included in the HTTP request. It is used to direct browser actions and has no impact on the server, so changing the hash does not reload the page.
• You can add listening events for hash changes:
window.addEventListener("hashchange",funcRef,false)
Copy the code
• Each change to hash(window.location.hash) adds a record to the browser’s access history.
export class HashHistory extends History {
constructor (router: Router, base: ? string, fallback: boolean) {
super(router, base)
// check history fallback deeplinking
// If you are demoted from history mode, you need to do a demotion check
if (fallback && checkFallback(this.base)) {
// Returns if retracted and retracted
return
}
ensureSlash()
}
.......
function checkFallback (base) {
const location = getLocation(base)
// Get the real location value excluding base
if (!/ # ^ / / /.test(location)) {
// If the address does not start with /#
// It needs to be degraded to the hash mode /#
window.location.replace(
cleanPath(base + '/ #' + location)
)
return true}}function ensureSlash () :boolean {
// Get the hash value
const path = getHash()
if (path.charAt(0) = = ='/') {
// If it starts with /, just return
return true
}
// If not, you need to manually replace the hash value once
replaceHash('/' + path)
return false
}
export function getHash () :string {
// We can't use window.location.hash here because it's not
// consistent across browsers - Firefox will pre-decode it!
// Because of compatibility issues, window.location.hash is not used directly here
// Because Firefox decode hash value
const href = window.location.href
const index = href.indexOf(The '#')
return index === -1 ? ' ' : decodeURI(href.slice(index + 1))}// Get the url before the hash
function getUrl (path) {
const href = window.location.href
const i = href.indexOf(The '#')
const base = i >= 0 ? href.slice(0, i) : href
return `${base}#${path}`
}
// Add a hash
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path))
} else {
window.location.hash = path
}
}
/ / replace the hash
function replaceHash (path) {
if (supportsPushState) {
replaceState(getUrl(path))
} else {
window.location.replace(getUrl(path))
}
}
Copy the code
Hash changes are automatically added to the browser’s access history. To see how this is done, look at the transitionTo() method:
transitionTo (location: RawLocation, onComplete? :Function, onAbort? :Function) {
const route = this.router.match(location, this.current) // Find a matching route
this.confirmTransition(route, () = > { // Verify whether to convert
this.updateRoute(route) / / update the route
onComplete && onComplete(route)
this.ensureURL()
// fire ready cbs once
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb= > { cb(route) })
}
}, err= > {
if (onAbort) {
onAbort(err)
}
if (err && !this.ready) {
this.ready = true
this.readyErrorCbs.forEach(cb= > { cb(err) })
}
})
}
// Update the route
updateRoute (route: Route) {
const prev = this.current // Pre-hop route
this.current = route // configure a jump route
this.cb && this.cb(route) This callback is registered in the index file and updates the hijacked _router
this.router.afterHooks.forEach(hook= > {
hook && hook(route, prev)
})
}
}
Copy the code
pushState
export function pushState (url? : string, replace? : boolean) {
saveScrollPosition()
// try... catch the pushState call to get around Safari
// DOM Exception 18 where it limits to 100 pushState calls
/ / added a try... Catch is because Safari has a limit of 100 calls to pushState
// DOM Exception 18 is thrown when reached
const history = window.history
try {
if (replace) {
// replace the key is the current key and no need to generate a new one
history.replaceState({ key: _key }, ' ', url)
} else {
// Regenerate key
_key = genKey()
// enter the new key value
history.pushState({ key: _key }, ' ', url)
}
} catch (e) {
// specify a new address when the limit is reached
window.location[replace ? 'replace' : 'assign'](url)
}
}
Copy the code
replaceState
// Call pushState directly and pass replace to true
export function replaceState (url? : string) {
pushState(url, true)}Copy the code
The common feature of pushState and replaceState is that when they are called to modify the browser history stack, the browser does not immediately send a request for the current URL even though it has changed. This provides a basis for single-page front-end routing, updating the view without rerequesting the page.
supportsPushState
export const supportsPushState = inBrowser && (function () {
const ua = window.navigator.userAgent
if (
(ua.indexOf('Android 2.')! = = -1 || ua.indexOf('the Android 4.0')! = = -1) &&
ua.indexOf('Mobile Safari')! = = -1 &&
ua.indexOf('Chrome') = = = -1 &&
ua.indexOf('Windows Phone') = = = -1
) {
return false
}
return window.history && 'pushState' in window.history
})()
Copy the code
When the _route value changes, the render() method of the Vue instance is automatically called to update the view. $router.push()–>HashHistory.push()–>History.transitionTo()–>History.updateRoute()–>{app._route=route}–>vm.render()
Listening address bar
In the browser, the user can type the change route directly into the browser address bar, so you also need to listen for the change of route in the browser address bar and have the same response behavior as calling it through code. In HashHistory this is done with setupListeners listening for Hashchange:
setupListeners () {
window.addEventListener('hashchange'.() = > {
if(! ensureSlash()) {return
}
this.transitionTo(getHash(), route= > {
replaceHash(route.fullPath)
})
})
}
Copy the code
HTML5History
History Interface is the interface provided by the browser History stack. By using methods such as back(),forward(), and Go (), we can read the browser History stack information and perform various jump operations.
export class HTML5History extends History {
constructor (router: Router, base: ? string) {
super(router, base)
const expectScroll = router.options.scrollBehavior // Indicates the rollback mode
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll) {
setupScroll()
}
const initLocation = getLocation(this.base)
// Monitor popState events
window.addEventListener('popstate'.e= > {
const current = this.current
// Avoiding first `popstate` event dispatched in some browsers but first
// history route not updated since async guard at the same time.
// Avoid issuing "popState" events for the first time in some browsers
The history route was not updated at the same time due to asynchronous listening at the same time.
const location = getLocation(this.base)
if (this.current === START && location === initLocation) {
return
}
this.transitionTo(location, route= > {
if (supportsScroll) {
handleScroll(router, route, current, true)}})})}Copy the code
Hash mode only changes the contents of the hash part, which is not included in the HTTP request (hash with #) :
oursite.com/#/user/id // If requested, only oursite.com/ will be sent
Therefore, page requests based on urls are not a problem in hash mode
The history mode changes the URL to the same as the normal request back end (history without #).
oursite.com/user/id
If the request is sent to the back end and the back end is not configured to handle the /user/ ID GET route, a 404 error will be returned.
The official recommended solution is to add a candidate resource on the server that covers all cases: if the URL doesn’t match any static resource, it should return the same index.html page that your app relies on. At the same time, the server no longer returns the 404 error page because the index.html file is returned for all paths. To avoid this, override all routing cases in the Vue application and then render a 404 page. Alternatively, if node.js is used as the background, you can use server-side routing to match urls and return 404 if no route is matched, thus implementing a Fallback.
Compare the two modes
In general, the hash mode is similar to the history mode. According to MDN, calling history.pushstate () has the following advantages over modifying the hash directly:
• pushState can be any url of the same origin as the current URL, and hash can change only the part after #, so only the url of the current document can be set
• pushState can set a new URL to be exactly the same as the current URL, which will also add the record to the stack, and the new hash value must be different to trigger the record to be added to the stack
• pushState can be added to any type of data record through the stateObject, while hash can only be added to a short string
AbstractHistory
The ‘abstract’ mode, which does not involve records associated with the browser address, is the same as ‘HashHistory’ in that it emulates the browser history stack with arrays
// Abstract. Js implementation, here through the stack data structure to simulate the routing path
export class AbstractHistory extends History {
index: number;
stack: Array<Route>;
constructor (router: Router, base: ? string) {
super(router, base)
this.stack = []
this.index = -1
}
// For go simulation
go (n: number) {
// New record location
const targetIndex = this.index + n
// Less than or more than exceeds returns
if (targetIndex < 0 || targetIndex >= this.stack.length) {
return
}
// Get the new route object
// Because it is browser-independent, this must be already accessed
const route = this.stack[targetIndex]
// This calls confirmTransition directly
// Instead of calling transitionTo and traversing the match logic
this.confirmTransition(route, () = > {
this.index = targetIndex
this.updateRoute(route)
})
}
// Confirm whether to convert the route
confirmTransition (route: Route, onComplete: Function, onAbort? :Function) {
const current = this.current
const abort = err= > {
if (isError(err)) {
if (this.errorCbs.length) {
this.errorCbs.forEach(cb= > { cb(err) })
} else {
warn(false.'uncaught error during route navigation:')
console.error(err)
}
}
onAbort && onAbort(err)
}
// If the two routes are the same, no operation is performed
if (
isSameRoute(route, current) &&
route.matched.length === current.matched.length
) {
this.ensureURL()
return abort()
}
// The following is the processing of various hook functions
/ / * * * * * * * * * * * * * * * * * * * * *})}Copy the code
reference
Front-end Advancements thoroughly understand front-end routing
Vue Introduction — Brief Analysis of VUE-Router