
我有一碗酒,可以慰風(fēng)塵
前端路由定義
在SPA中,路由指的是URL與UI之間的映射,這種映射是單向的,即URL變化引起UI更新(無需刷新界面)
實現(xiàn)前端路由
實現(xiàn)前端路由,需要解決兩個核心問題
- 如何改變URL卻不引起頁面刷新
- 如何檢測URL變化了
vue-router里面有hash和history兩種方式,下面介紹一下這兩種方式
hash實現(xiàn)
hash指的是URL中hash(#)及后面的那一part,改變URL中的hash部分不會引起頁面刷新,并且可以通過hashchange事件監(jiān)聽URL的變化。改變URL的方式有如下幾種:
- 通過瀏覽器的前進后退改變URL
- 通過<a>標(biāo)簽改變URL
- 通過window.location改變URL
history實現(xiàn)
HTML5中的history提供了pushState和replaceState兩個方法,這兩個方法改變URL的path部分不會引起頁面刷新
history提供類似hashchange事件的popstate事件,但又有不同之處
- 通過瀏覽器前進后退改變URL是會觸發(fā)popstate事件
- 通過pushState/replaceState或者<a>標(biāo)簽改變URL不會觸發(fā)popstate事件
- 我們可以攔截pushState/replaceState的調(diào)用和<a>標(biāo)簽的點擊事件來檢測URL的變化
- 通過js調(diào)用history的back,go,forward方法來觸發(fā)該事件
實操js實現(xiàn)前端路由
基于hash
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
</head>
<body>
<ul>
<li><a href="#/home">home</a></li>
<li><a href="#/about">about</a></li>
</ul>
<!-- 路由渲染口子 -->
<div id="routerView"></div>
</body>
<script>
let routerView = document.getElementById('routerView')
window.addEventListener('hashchange', () => {
const hash = location.hash
routerView.innerHTML = hash
})
window.addEventListener('DOMContentLoaded', () => {
if (!location.hash) {
location.hash = "/"
} else {
const hash = location.hash
routerView.innerHTML = hash
}
})
</script>
</html>
效果圖

上面的代碼干了哪些活?
- 通過<a>標(biāo)簽的href屬性來改變URL中的hash值
- 監(jiān)聽了hashchange事件,當(dāng)事件觸發(fā)的時候,改變routerView中的內(nèi)容
- 監(jiān)聽了DOMContentLoaded事件,初次的時候需要渲染成對應(yīng)的內(nèi)容
基于HTML5 history實現(xiàn)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
</head>
<body>
<ul>
<li><a href="#/home">home</a></li>
<li><a href="#/about">about</a></li>
</ul>
<!-- 路由渲染口子 -->
<div id="routerView"></div>
<script>
const router = document.getElementById('routerView')
window.addEventListener('DOMContentLoaded', () => {
const linkList = document.querySelectorAll('a[href]')
linkList.forEach(el => el.addEventListener('click', function(e) {
e.preventDefault()
history.pushState(null, '', el.getAttribute('href'))
router.innerHTML = location.pathname
}))
})
window.addEventListener('popstate', () => {
router.innerHTML = location.pathname
})
</script>
</body>
</html>
- 我們通過a標(biāo)簽的href屬性來改變URL的path值(當(dāng)然,你觸發(fā)瀏覽器的前進后退按鈕也可以,或者在控制臺輸入history.go,back,forward賦值來觸發(fā)popState事件)。這里需要注意的就是,當(dāng)改變path值時,默認(rèn)會觸發(fā)頁面的跳轉(zhuǎn),所以需要攔截 <a> 標(biāo)簽點擊事件默認(rèn)行為, 點擊時使用 pushState 修改 URL并更新手動 UI,從而實現(xiàn)點擊鏈接更新 URL 和 UI 的效果。
- 我們監(jiān)聽popState事件。一旦事件觸發(fā),就改變routerView的內(nèi)容
vue 中vue-router
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import About from "../views/About.vue"
Vue.use(VueRouter)
const routes = [
{
path: '/home',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
]
const router = new VueRouter({
mode:"history",
routes
})
export default router
// App.vue
<template>
<div id="app">
<div id="nav">
<router-link to="/home">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view/>
</div>
</template>
截圖如下

改造vue-router文件
import Vue from 'vue'
// import VueRouter from 'vue-router'
import VueRouter from './myVueRouter'
import Home from '../views/Home.vue'
import About from "../views/About.vue"
Vue.use(VueRouter)
const routes = [
{
path: '/home',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
]
const router = new VueRouter({
mode:"history",
routes
})
export default router
分析vue-router文件干了啥
1, 通過import VueRouter from 'vue-router' 引入了VueRouter
2,const router = new VueRouter({})
3,Vue.use(VueRouter)使得每個組件都可以擁有router實例
- 通過new VueRouter({})獲得實例,也就是說VueRouter其實是一個類
class VueRouter {
}
- 使用Vue.use(),而Vue.use其實就是執(zhí)行對象的install這個方法
class VueRouter {
}
VueRouter.install = function () {
}
export default VueRouter
分析Vue.use
Vue.use(plugin)
1,參數(shù)
{ object | Function } plugin
2,用法
安裝Vue.js插件。如果插件是一個對象,必須提供install方法。如果插件是一個函數(shù),它會被作為install方法。調(diào)用install方法時,會將Vue作為參數(shù)傳入。install方法被同一個插件多次調(diào)用時,插件也只會被安裝一次。
3, 作用
注冊插件,此時只需要調(diào)用install方法并將Vue作為參數(shù)傳入即可。但在細節(jié)上有兩部分邏輯要處理:
1、插件的類型,可以是install方法,也可以是一個包含install方法的對象。
2、插件只能被安裝一次,保證插件列表中不能有重復(fù)的插件。
4,實現(xiàn)
Vue.use = function(plugin) {
const installPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installPlugins.indexOf(plugin) > -1) {
return
}
const args = toArray(arguments, 1)
args.unshift(this)
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, plugin, args)
}
installPlugins.push(plugin)
return this
}
1、在Vue.js上新增了use方法,并接收一個參數(shù)plugin。
2、首先判斷插件是不是已經(jīng)別注冊過,如果被注冊過,則直接終止方法執(zhí)行,此時只需要使用indexOf方法即可。
3、toArray方法我們在就是將類數(shù)組轉(zhuǎn)成真正的數(shù)組。使用toArray方法得到arguments。除了第一個參數(shù)之外,剩余的所有參數(shù)將得到的列表賦值給args,然后將Vue添加到args列表的最前面。這樣做的目的是保證install方法被執(zhí)行時第一個參數(shù)是Vue,其余參數(shù)是注冊插件時傳入的參數(shù)。
4、由于plugin參數(shù)支持對象和函數(shù)類型,所以通過判斷plugin.install和plugin哪個是函數(shù),即可知用戶使用哪種方式祖冊的插件,然后執(zhí)行用戶編寫的插件并將args作為參數(shù)傳入。
5、最后,將插件添加到installedPlugins中,保證相同的插件不會反復(fù)被注冊。
了解以上開始寫myVueRouter.js
let Vue = null
class VueRouter {
}
VueRouter.install = function (v) {
Vue = v
Vue.component('router-link',{
render(h) {
return h('a', {}, 'Home')
}
})
Vue.component('router-view', {
render(h) {
return h('h1', {}, '我是首頁')
}
})
}
export default VueRouter
簡易版果然跑起來了,截圖如下

完善install方法
install 是給每個vue實例添加?xùn)|西的,在router中給每個組件添加了$route和$router
這倆的區(qū)別是:
$router是VueRouter的實例對象,$route是當(dāng)前路由對象,也就是說$route是$router的一個屬性
// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
new Vue({
router,
render: h => h(App),
}).$mount('#app')
我們可以發(fā)現(xiàn)這里只是將router ,也就是./router導(dǎo)出的router實例,作為Vue 參數(shù)的一部分。
但是這里就是有一個問題咯,這里的Vue 是根組件啊。也就是說目前只有根組件有這個router值,而其他組件是還沒有的,所以我們需要讓其他組件也擁有這個router。
因此,install方法我們可以這樣完善
let Vue = null
class VueRouter {
}
VueRouter.install = function (v) {
Vue = v
Vue.mixin({
beforeCreate() {
if (this.$options && this.$options.router) { // 如果是根組件
this._root = this // 把當(dāng)前實例掛載到_root上
this._router = this.$options.router
} else {
this._root = this.$parent && this.$parent._root
}
Object.defineProperty(this, '$router', {
get () {
return this._root._router
}
})
}
})
Vue.component('router-link',{
render(h) {
return h('a', {}, 'Home')
}
})
Vue.component('router-view', {
render(h) {
return h('h1', {}, '我是首頁')
}
})
}
export default VueRouter
一通操作之下的解釋
- 參數(shù)Vue,我們在分析Vue.use的時候,再執(zhí)行install的時候,將Vue作為參數(shù)傳進去。,
- mixin的作用是將mixin的內(nèi)容混合到Vue的初始參數(shù)options中。相信使用vue的同學(xué)應(yīng)該使用過mixin了。
- 為什么是beforeCreate而不是created呢?因為如果是在created操作的話,$options已經(jīng)初始化好了。
- 如果判斷當(dāng)前組件是根組件的話,就將我們傳入的router和_root掛在到根組件實例上。
- 如果判斷當(dāng)前組件是子組件的話,就將我們_root根組件掛載到子組件。注意是引用的復(fù)制,因此每個組件都擁有了同一個_root根組件掛載在它身上。
? 為啥判斷是子組件就直接取父組件的_root根組件呢
來,看一下父子組件執(zhí)行順序先
父beforeCreate-> 父created -> 父beforeMounte -> 子beforeCreate ->子create ->子beforeMount ->子 mounted -> 父mounted
在執(zhí)行子組件的beforeCreate的時候,父組件已經(jīng)執(zhí)行完beforeCreate了,拿到_root那就沒問題了
在vueRouter文件中
const router = new VueRouter({
mode:"history",
routes
})
我們傳了兩個參數(shù),一個模式mode,一個是路由數(shù)組routes
let Vue = null
class VueRouter {
constructor (options) {
this.mode = options.mode || 'hash'
this.routes = options.routes || []
this.routesMap = this.createMap(this.routes)
}
createMap (routes) {
return routes.reduce((pre, current) => {
pre[current.path] = current.component
return pre
}, {})
}
}
VueRouter.install = function (v) {
Vue = v
Vue.mixin({
beforeCreate() {
if (this.$options && this.$options.router) { // 如果是根組件
this._root = this // 把當(dāng)前實例掛載到_root上
this._router = this.$options.router
} else {
this._root = this.$parent && this.$parent._root
}
Object.defineProperty(this, '$router', {
get () {
return this._root._router
}
})
}
})
Vue.component('router-link',{
render(h) {
return h('a', {}, 'Home')
}
})
Vue.component('router-view', {
render(h) {
return h('h1', {}, '我是首頁')
}
})
}
export default VueRouter
路由中需要存放當(dāng)前的路徑,來表示當(dāng)前的路徑狀態(tài),為了方便管理,用一個對象來表示。初始化的時候判斷是那種模式,并將當(dāng)前的路徑保存到current中
let Vue = null
class HistoryRoute {
constructor () {
this.current = null
}
}
class VueRouter {
constructor (options) {
this.mode = options.mode || 'hash'
this.routes = options.routes || []
this.routesMap = this.createMap(this.routes)
this.history = new HistoryRoute()
this.init()
}
init () {
if (this.mode === 'hash') {
// 先判斷用戶打開是是否有hash值,沒有跳轉(zhuǎn)到#/
location.hash ? '' : location.hash = '/'
window.addEventListener('load', () => {
this.history.current = location.hash.slice(1)
})
window.addEventListener('hashchange', () => {
this.history.current = location.hash.slice(1)
})
} else {
location.pathname ? '' : location.pathname = '/'
window.addEventListener('load', () => {
this.history.current = location.pathname
})
window.addEventListener('popstate', () => {
this.history.current = location.pathname
})
}
}
createMap (routes) {
return routes.reduce((pre, current) => {
pre[current.path] = current.component
return pre
}, {})
}
}
VueRouter.install = function (v) {
Vue = v
Vue.mixin({
beforeCreate() {
if (this.$options && this.$options.router) { // 如果是根組件
this._root = this // 把當(dāng)前實例掛載到_root上
this._router = this.$options.router
} else {
this._root = this.$parent && this.$parent._root
}
Object.defineProperty(this, '$router', {
get () {
return this._root._router
}
})
}
})
Vue.component('router-link',{
render(h) {
return h('a', {}, 'Home')
}
})
Vue.component('router-view', {
render(h) {
return h('h1', {}, '我是首頁')
}
})
}
export default VueRouter
完善$route
其實/$route就是獲取當(dāng)前的路徑
VueRouter.install = function (v) {
Vue = v
Vue.mixin({
beforeCreate() {
if (this.$options && this.$options.router) { // 如果是根組件
this._root = this // 把當(dāng)前實例掛載到_root上
this._router = this.$options.router
} else {
this._root = this.$parent && this.$parent._root
}
Object.defineProperty(this, '$router', {
get () {
return this._root._router
}
})
Object.defineProperty(this, '$route', {
get () {
return this._root._router.history.current
}
})
}
})
Vue.component('router-link',{
render(h) {
return h('a', {}, 'Home')
}
})
Vue.component('router-view', {
render(h) {
return h('h1', {}, '我是首頁')
}
})
}
完善router-view
Vue.component('router-view', {
render(h) {
let current = this._self._root._router.history.current
let routeMap = this._self._root._router.routesMap
return h(routeMap[current])
}
})
render函數(shù)里的this指向的是一個Proxy代理對象,代理Vue組件,而我們前面講到每個組件都有一個_root屬性指向根組件,根組件上有_router這個路由實例。
所以我們可以從router實例上獲得路由表,也可以獲得當(dāng)前路徑。
然后再把獲得的組件放到h()里進行渲染。
現(xiàn)在已經(jīng)實現(xiàn)了router-view組件的渲染,但是有一個問題,就是你改變路徑,視圖是沒有重新渲染的,所以需要將_router.history進行響應(yīng)式化。
VueRouter.install = function (v) {
Vue = v
Vue.mixin({
beforeCreate() {
if (this.$options && this.$options.router) { // 如果是根組件
this._root = this // 把當(dāng)前實例掛載到_root上
this._router = this.$options.router
// 新增
Vue.util.defineReactive(this, 'xxx', this._router.history)
} else {
this._root = this.$parent && this.$parent._root
}
Object.defineProperty(this, '$router', {
get () {
return this._root._router
}
})
Object.defineProperty(this, '$route', {
get () {
return this._root._router.history.current
}
})
}
})
Vue.component('router-link',{
render(h) {
return h('a', {}, 'Home')
}
})
Vue.component('router-view', {
render(h) {
let current = this._self._root._router.history.current
let routeMap = this._self._root._router.routesMap
return h(routeMap[current])
}
})
}
效果如下


完善router-link
Vue.component('router-link',{
props: {
to: String
},
render(h) {
let mode = this._self._root._router.mode
let to = mode === 'hash' ? '#'+this.to : this.to
return h('a', {attrs: {href: to}}, this.$slots.default)
}
})
截圖如下


myVueRouter.js完整代碼
let Vue = null
class HistoryRoute {
constructor () {
this.current = null
}
}
class VueRouter {
constructor (options) {
this.mode = options.mode || 'hash'
this.routes = options.routes || []
this.routesMap = this.createMap(this.routes)
this.history = new HistoryRoute()
this.init()
}
init () {
if (this.mode === 'hash') {
// 先判斷用戶打開是是否有hash值,沒有跳轉(zhuǎn)到#/
location.hash ? '' : location.hash = '/'
window.addEventListener('load', () => {
this.history.current = location.hash.slice(1)
})
window.addEventListener('hashchange', () => {
this.history.current = location.hash.slice(1)
})
} else {
location.pathname ? '' : location.pathname = '/'
window.addEventListener('load', () => {
this.history.current = location.pathname
})
window.addEventListener('popstate', () => {
this.history.current = location.pathname
})
}
}
createMap (routes) {
return routes.reduce((pre, current) => {
pre[current.path] = current.component
return pre
}, {})
}
}
VueRouter.install = function (v) {
Vue = v
Vue.mixin({
beforeCreate() {
if (this.$options && this.$options.router) { // 如果是根組件
this._root = this // 把當(dāng)前實例掛載到_root上
this._router = this.$options.router
// 新增
Vue.util.defineReactive(this, 'xxx', this._router.history)
} else {
this._root = this.$parent && this.$parent._root
}
Object.defineProperty(this, '$router', {
get () {
return this._root._router
}
})
Object.defineProperty(this, '$route', {
get () {
return this._root._router.history.current
}
})
}
})
Vue.component('router-link',{
props: {
to: String
},
render(h) {
let mode = this._self._root._router.mode
let to = mode === 'hash' ? '#'+this.to : this.to
return h('a', {attrs: {href: to}}, this.$slots.default)
}
})
Vue.component('router-view', {
render(h) {
let current = this._self._root._router.history.current
let routeMap = this._self._root._router.routesMap
return h(routeMap[current])
}
})
}
export default VueRouter