The reason was that our team encountered a fundamental routing problem while migrating the distribution system internally. I vaguely knew why at that time, but we were so familiar with routing that we forgot many of its basic features and did not quickly troubleshoot the problems in the first time. I feel ashamed of it.
So I find a time to fill up the basic knowledge of the relevant route, and look at the vue-Router source code, in general, first talk about vue-Router is how to combine with VUE to do routing management. More detailed source code analysis will follow
The main content of this article is as follows
- What is routing
- The back-end routing
- The front-end routing
- vue-router
- How to inject vueRouter into vUE (plug-in installation)
- init VueRouter
- The constructor VueRouter
- How to change a URL
- How to render after URL change
- How to deal with go, forward and back
- setupListeners
- Hash routing
- The history of routing
routing
Since we’re going to talk about front-end routing, we should know what routing is first.
The routing concept was originally brought up by the back end. In the early days, it was all server-side rendering, before the front and back ends were separated, and the server would return the entire page, and the response would look something like this:
- The browser makes a request
- The server’s interfaces 80 or 443 listen for requests from the browser and parse the URL path
- According to the URL content, the server queries the corresponding resources, which may be HTML resources or image resources…. The corresponding resource is then processed and returned to the browser
- The browser receives the data and then decides based on the Content-Type how to parse the resource
So what is routing? We can simply understand as a way to interact with the server, through different routes we request different resources (HTML resources is just one of the ways).
The back-end routing
What we described above is actually back-end routing.
Back-end routing is also called server-side routing, because when a server receives an HTTP request from a client, it finds the corresponding mapping function based on the REQUESTED URL, executes the function, and sends the return value of the function to the client.
For the simplest static resource server, you can think of the mapping function of all urls as a file read operation. For dynamic resources, the mapping function may be a database read operation, some data processing, and so on.
Then according to these read data, the server side will use the corresponding template to render the page, and then return the rendered page.
The front-end routing
Front-end routing was born out of the rise of Ajax, which we all know is a browser for asynchronous loading technology, as I just explained, in the case of the front and back end is not separated, the server will directly return the entire HTML, Every small operation of the user will cause the entire page refresh (plus the previous Internet speed is still slow, so the user experience can be imagined).
In the late 90’s, Microsoft first implemented Ajax (Asynchronous JavaScript And XML) technology, which greatly improves the user experience without having to refresh the entire page for each operation.
With the development of technology, the three frames dominate the front end circle and become the main force of the front end development. The front end can also do more things, and gradually there are modular and componentized concepts.
Of course, there are single-page applications, MVVM has also appeared in front of the VISION of ER.
At this point, front-end developers are able to develop larger applications and become more powerful, so what does this have to do with front-end routing?
A more advanced version of the asynchronous interaction experience is SPA, a single page application. Single-page applications are not only refresh-free for page interactions, but also for page jumps. Since the jump to the page is refresh-free, that is, the backend request is no longer returned to THE HTML.
So, a large application will usually have dozens of pages (URL address) jump to each other, how does the front end know what url corresponding display content?
The answer is front-end routing
It can be understood that front-end routing is the front-end to do the task of the server to return different pages according to the different URL.
Advantages: good user experience, do not need to capture all from the server, and every time a quick show users faults: use the browser’s forward and back button will send the request again, there is no reasonable use of caching, single page not remember before the position of the scroll, not in advance, to remember the location of the rolling back.
What problem does front-end routing solve
- Front-end routing allows the front-end to maintain the routing and display logic of the page itself. Every page change does not need to notify the server.
- Better interactive experience: No need to pull resources from the server every time and quickly show them to the user
What are the disadvantages of front-end routing?
- The most notorious thing is that it’s bad for SEO
- Using the browser’s forward and back keys will re-send requests to retrieve data without making proper use of caching.
What is the implementation principle of front-end routing
After understanding what front-end routing is and what problems front-end routing solves, let’s take a closer look at how front-end routing works
The implementation principle of front-end routing is actually very simple. In essence, it detects URL changes, intercepts URLS and then resolves matching routing rules.
Hash routing
In the old days, people used hash to implement routing, and hash routing is just like that<a>
The anchor point of the link is the same, added after the address#
For example, my personal blog https://cherryblog.site/#/
#
And what follows, we call the hash of location
Then we click on the other TAB pages and notice that although the URL in the browser’s address bar has changed, the page has not been refreshed. When we open the console, we can see that switching the TAB simply sends the server the interface that requested the interface data, and does not re-request the HTML resource.This is because a hash change does not cause the browser to send a request to the server, so the page is not refreshed. But every time the hash changes, it is triggeredhaschange
Events. So we can use a wiretaphaschange
In response to changes.
In our current (2021) front-end development, there is usually a root node<div id="root"></div>
, and then insert what you want to show into the root node. The inserted content component is then replaced according to the route.
The history of routing
One problem with hash routing is that it’s not “pretty” because it has #
Fourteen years later, the HTML5 standard was released. Two new apis, pushState and replaceState, allow you to change urls without sending requests. There’s also an onPopState event. This allows you to implement front-end routing in a different way, but in the same way that you implement hash.
With HTML5 implementations, single-page routing urls don’t have an extra #, making them more aesthetically pleasing. But because there are no # numbers, the browser still sends a request to the server when the user does something like refresh the page. To avoid this, the implementation requires server support to redirect all routes to the root page. [HTML5 Histroy mode](HTML5 History mode)
Notice the direct callhistory.popState()
和 history.poshState()
It doesn’t triggerpopState
. Called only if browser behavior has been performedpopState
Such as clicking the browser’s forward and back buttons or making JS callshistory.back()
orhistory.forward()
vue-router
Let’s take a look at how vue-Router implements front-end routing with VUE.
The general idea is to use vue.util.definereActive to set the _route of the instance to a reactive object. The push, replace methods actively update the _route attribute. Go, back, or clicking the forward and back buttons will update _route in the onHashchange or onPopState callback. The _route update triggers a rerendering of the RoterView.
And then we’re going to look at how does that work
How to inject vueRouter into vUE (plug-in installation)
Vue provides a plug-in registration mechanism. Each plug-in needs to implement a static install method. When a plug-in is registered with vue. use, install is executed, and the first argument to this method is a Vue object.
In vue-Router, install is as follows.
import View from './components/view'
import Link from './components/link'
// Export the vue instance
export let _Vue
Use (vueRouter) is equivalent to vue.use (vueRouter. Install ()).
export function install (Vue) {
// If already registered and already has vUE instance, then return directly
if (install.installed && _Vue === Vue) return
install.installed = true
// Save the Vue instance for other plug-in files
_Vue = Vue
const isDef = v= >v ! = =undefined
// A recursive method to register instances
const registerInstance = (vm, callVal) = > {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
/** * Recursively mix all vUE components with two life cycles beforeCreate and destroyed * to initialize vuE-router in beforeCreated and _route response */
Vue.mixin({
beforeCreate () {
// Initialize vue-router
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
Vue.util.defineReactive(this.'_route'.this._router.history.current)
} else {
// make _route a responsive object
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this.this)
},
destroyed () {
registerInstance(this)}})$router = router instance * $route = current route */
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
/** * Inject two global components *
*
*/
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
/** * Vue. Config is an object that contains the global configuration of Vue
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
Copy the code
To ensure that the VueRouter is executed only once, an identity installed is added when the install logic is executed. Use a global variable to save the Vue to facilitate plug-in use of the Vue.
The core of the VueRouter installation is to mix beforeCreate and destroyed hook functions into all components of the Vue app via mixin.
It also adds instance objects to Vue
- _routerRoot: Points to the Vue instance
- _router: points to instance vueRouter
Initialize some getters on Vue’s Prototype
- $router, the instance of the current router
- $route, information about the current Router
Vue.util. DefineReactive, which is the way in Vue that the observer hijacks data, hijacking _route, notifying dependent components when _route fires setter methods.
The Vue.component method defines two global
and
components.
is similar to the A tag.
is the route exit. In
, the route is switched to render different Vue components. Finally, the merging strategy of route guard is defined and the merging strategy of Vue is adopted.
init VueRouter
We just mentioned that VueRouter’s init method (this._router.init(this)) is executed during install, so let’s look at what the init method does. Simply mount the Vue instance to the current router instance.
VueRouter’s init method (this._router.init(this)) is then executed during install. Init uses history.transitionto to transition routes. Matcher route matcher is the core function of route switching, route and component matching.
init (app: any /* Vue component instance */) {
this.apps.push(app)
// main app previously initialized
// return as we don't need to set up new history listener
if (this.app) {
return
}
// Mount the Vue instance on VueRouter
this.app = app
const history = this.history
// Hashchange events are monitored in setupListeners
// transitionTo is the function used to perform route navigation
if (history instanceof HTML5History || history instanceof HashHistory) {
const setupListeners = routeOrError= > {
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupListeners,
setupListeners
)
}
// Route global listener to maintain the current route
// Since _route is defined as a responsive attribute when install executes,
// When route changes _route updates, subsequent view updates render depending on _route
history.listen(route= > {
this.apps.forEach(app= > {
app._route = route
})
})
}
Copy the code
The constructor VueRouter
VueRouter’s constructor is relatively simple
- Attributes and methods are defined.
- Create a matcher matching function that looks for a route
- Set defaults and do a downgrade that does not support H5 history
- Instantiate different History objects based on different modes
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)
// Hash routes are used by default
let mode = options.mode || 'hash'
// H5 history is compatible
this.fallback =
mode === 'history'&&! supportsPushState && options.fallback ! = =false
if (this.fallback) {
mode = 'hash'
}
if(! inBrowser) { mode ='abstract'
}
this.mode = mode
// Distribution processing
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}`)}}}Copy the code
When instantiating the vueRouter, it defines apis like History: Push, replace, back, Go, forward, routing matchers, adding router dynamic update methods, and so on.
How to change a URL
So how does the VueRouter do this? $router. Push (‘/foo’, increment) how do we render the view to display the foo component when using _this_.$router.
const router = new VueRouter({
mode: 'history'.base: __dirname,
routes: [{path: '/'.component: Home },
{ path: '/foo'.component: Foo },
{ path: '/bar'.component: Bar },
{ path: encodeURI('/ e'), component: Unicode },
{ path: '/query/:q'.component: Query }
]
})
Copy the code
Remember what we just did in vue-Router constructor? Let me refresh your memory. From constructor we select different types of history to instantiate according to different modes (h5 history or hash history or abstract). Then call history.transitionto during init to initialize the route and complete the first route navigation.
The transitionTo method can be found in the history/base.js file. TransitionTo can accept location, onComplete, and onAbort, which are callbacks to the target path, successful path switchover, and failed path switchover.
The incoming location is found in the Router, the current route is updated, and the path switch succeeds callback is executed (in which the history is implemented differently for different modes).
Either the replaceHash or pushHash methods are called in the callback. They update the hash value of the location. If compatible with historyAPI, history.replaceState or history.pushState will be used. If not, the historyAPI will use window.location.replace or window.Location. hash.
The handleScroll method updates the position of our scrollbar.
transitionTo ( location: RawLocation, onComplete? :Function, onAbort? :Function
) {
// Call match to get a matching route object
const route = this.router.match(location, this.current)
// Transition processing
this.confirmTransition(
route,
() = > {
// Update the current route object
this.updateRoute(route)
// Update hash mode of URL address Update hash value History mode is updated using pushState/replaceState
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)
})
}
}
)
}
Copy the code
How to render a component after a URL change
At this point, you can make history objects in different modes have the same push replace functionality (see the implementation below for details)
So how to render correctly after the route change.
Remember the reactive principle of VUE we talked about earlier? We already set _router to be responsive during install. Any change to the _router triggers a rendering of the RouterView. (We updated _route in the transitionTo callback)
go, forward, back
The go, forward, and back methods defined on VueRouter are all go methods that call the history property.
How does the go method on the hash call history.go update the RouteView? The answer is that the Hash object has a popState or Hashchange listener added to the setupListeners method. An update to the RoterView is triggered in the event’s callback
setupListeners
When we click the back and forward buttons or call the back, forward and go methods. We did not actively update _app.route and current. How do we trigger updates to the RouterView? By listening for popState or hashchange events on the window. In the event callback, the _route and current updates are completed by calling the transitionTo method.
Alternatively, when using the push, replace method, the hash update comes after the _route update. With go, back, the hash update precedes the _route update.
setupListeners () { const router = this.router const expectScroll = router.options.scrollBehavior const supportsScroll = supportsPushState && expectScroll if (supportsScroll) { setupScroll() } window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => { const current = this.current if (! ensureSlash()) { return } this.transitionTo(getHash(), route => { if (supportsScroll) { handleScroll(this.router, route, current, true) } if (! supportsPushState) { replaceHash(route.fullPath) } }) }) }Copy the code
Hash routing
export class HashHistory extends History {
constructor (router: Router, base: ? string, fallback: boolean) {
super(router, base)
// check history fallback deeplinking
if (fallback && checkFallback(this.base)) {
return
}
ensureSlash()
}
// this is delayed until the app mounts
// to avoid the hashchange listener being fired too early
setupListeners () {
if (this.listeners.length > 0) {
return
}
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll) {
this.listeners.push(setupScroll())
}
// Add a hashchange event listener
window.addEventListener(
hashchange,
() = > {
const current = this.current
// Get the hash content and render the new page to the node of the UI-View via route configuration
this.transitionTo(getHash(), route= > {
if (supportsScroll) {
handleScroll(this.router, route, current, true)}if(! supportsPushState) { replaceHash(route.fullPath) } }) } )this.listeners.push(() = > {
window.removeEventListener(eventType, handleRoutingEvent) }) } push (location: RawLocation, onComplete? :Function, onAbort? :Function) {
const { current: fromRoute } = this
this.transitionTo(
location,
route= > {
pushHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort ) } replace (location: RawLocation, onComplete? :Function, onAbort? :Function) {
const { current: fromRoute } = this
this.transitionTo(
location,
route= > {
replaceHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
},
onAbort
)
}
go (n: number) {
window.history.go(n)
}
}
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path))
} else {
window.location.hash = path
}
}
function replaceHash (path) {
if (supportsPushState) {
replaceState(getUrl(path))
} else {
window.location.replace(getUrl(path))
}
}
Copy the code
H5 history routing
In fact, the implementation of hash is basically similar, the main difference is that
- Listen for different events
- The push and replace methods are implemented differently
export class HTML5History extends History {
_startLocation: string
constructor (router: Router, base: ? string) {
super(router, base)
this._startLocation = getLocation(this.base)
}
setupListeners () {
if (this.listeners.length > 0) {
return
}
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll) {
this.listeners.push(setupScroll())
}
// By listening for popState events
window.addEventListener('popstate'.() = > {
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.
const location = getLocation(this.base)
if (this.current === START && location === this._startLocation) {
return
}
this.transitionTo(location, route= > {
if (supportsScroll) {
handleScroll(router, route, current, true)}})})this.listeners.push(() = > {
window.removeEventListener('popstate', handleRoutingEvent)
})
}
go (n: number) {
window.history.go(n) } push (location: RawLocation, onComplete? :Function, onAbort? :Function) {
const { current: fromRoute } = this
this.transitionTo(location, route= > {
// Using pushState to update the URL does not cause the browser to send a request and thus not refresh the page
pushState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) } replace (location: RawLocation, onComplete? :Function, onAbort? :Function) {
const { current: fromRoute } = this
this.transitionTo(location, route= > {
// replaceState differs from pushState in that it is not recorded in the history stack
replaceState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) } ensureURL (push? : boolean) {if (getLocation(this.base) ! = =this.current.fullPath) {
const current = cleanPath(this.base + this.current.fullPath)
push ? pushState(current) : replaceState(current)
}
}
getCurrentLocation (): string {
return getLocation(this.base)
}
}
Copy the code
Can read here students really thank you ~~ this is my first time to write source code related content, has not studied very thoroughly, which will inevitably have some mistakes, I hope you correct ~~~