我有一碗酒亩歹,可以慰風(fēng)塵
前端路由定義
在SPA中炊汹,路由指的是URL與UI之間的映射,這種映射是單向的醋寝,即URL變化引起UI更新(無需刷新界面)
實(shí)現(xiàn)前端路由
實(shí)現(xiàn)前端路由口糕,需要解決兩個(gè)核心問題
- 如何改變URL卻不引起頁面刷新
- 如何檢測URL變化了
vue-router里面有hash和history兩種方式板祝,下面介紹一下這兩種方式
hash實(shí)現(xiàn)
hash指的是URL中hash(#)及后面的那一part,改變URL中的hash部分不會(huì)引起頁面刷新走净,并且可以通過hashchange事件監(jiān)聽URL的變化。改變URL的方式有如下幾種:
- 通過瀏覽器的前進(jìn)后退改變URL
- 通過<a>標(biāo)簽改變URL
- 通過window.location改變URL
history實(shí)現(xiàn)
HTML5中的history提供了pushState和replaceState兩個(gè)方法孤里,這兩個(gè)方法改變URL的path部分不會(huì)引起頁面刷新
history提供類似hashchange事件的popstate事件伏伯,但又有不同之處
- 通過瀏覽器前進(jìn)后退改變URL是會(huì)觸發(fā)popstate事件
- 通過pushState/replaceState或者<a>標(biāo)簽改變URL不會(huì)觸發(fā)popstate事件
- 我們可以攔截pushState/replaceState的調(diào)用和<a>標(biāo)簽的點(diǎn)擊事件來檢測URL的變化
- 通過js調(diào)用history的back,go,forward方法來觸發(fā)該事件
實(shí)操js實(shí)現(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ā)的時(shí)候说搅,改變r(jià)outerView中的內(nèi)容
- 監(jiān)聽了DOMContentLoaded事件,初次的時(shí)候需要渲染成對(duì)應(yīng)的內(nèi)容
基于HTML5 history實(shí)現(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ā)瀏覽器的前進(jìn)后退按鈕也可以弄唧,或者在控制臺(tái)輸入history.go,back,forward賦值來觸發(fā)popState事件)。這里需要注意的就是霍衫,當(dāng)改變path值時(shí)候引,默認(rèn)會(huì)觸發(fā)頁面的跳轉(zhuǎn),所以需要攔截 <a> 標(biāo)簽點(diǎn)擊事件默認(rèn)行為敦跌, 點(diǎn)擊時(shí)使用 pushState 修改 URL并更新手動(dòng) UI澄干,從而實(shí)現(xiàn)點(diǎn)擊鏈接更新 URL 和 UI 的效果。
- 我們監(jiān)聽popState事件柠傍。一旦事件觸發(fā)麸俘,就改變r(jià)outerView的內(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)使得每個(gè)組件都可以擁有router實(shí)例
- 通過new VueRouter({})獲得實(shí)例,也就是說VueRouter其實(shí)是一個(gè)類
class VueRouter {
}
- 使用Vue.use()患整,而Vue.use其實(shí)就是執(zhí)行對(duì)象的install這個(gè)方法
class VueRouter {
}
VueRouter.install = function () {
}
export default VueRouter
分析Vue.use
Vue.use(plugin)
1拜效,參數(shù)
{ object | Function } plugin
2喷众,用法
安裝Vue.js插件。如果插件是一個(gè)對(duì)象拂檩,必須提供install方法侮腹。如果插件是一個(gè)函數(shù),它會(huì)被作為install方法稻励。調(diào)用install方法時(shí)父阻,會(huì)將Vue作為參數(shù)傳入。install方法被同一個(gè)插件多次調(diào)用時(shí)望抽,插件也只會(huì)被安裝一次加矛。
3, 作用
注冊插件煤篙,此時(shí)只需要調(diào)用install方法并將Vue作為參數(shù)傳入即可斟览。但在細(xì)節(jié)上有兩部分邏輯要處理:
1、插件的類型辑奈,可以是install方法苛茂,也可以是一個(gè)包含install方法的對(duì)象。
2鸠窗、插件只能被安裝一次妓羊,保證插件列表中不能有重復(fù)的插件。
4稍计,實(shí)現(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方法,并接收一個(gè)參數(shù)plugin臣嚣。
2净刮、首先判斷插件是不是已經(jīng)別注冊過,如果被注冊過硅则,則直接終止方法執(zhí)行淹父,此時(shí)只需要使用indexOf方法即可。
3抢埋、toArray方法我們在就是將類數(shù)組轉(zhuǎn)成真正的數(shù)組弹灭。使用toArray方法得到arguments。除了第一個(gè)參數(shù)之外揪垄,剩余的所有參數(shù)將得到的列表賦值給args穷吮,然后將Vue添加到args列表的最前面。這樣做的目的是保證install方法被執(zhí)行時(shí)第一個(gè)參數(shù)是Vue饥努,其余參數(shù)是注冊插件時(shí)傳入的參數(shù)捡鱼。
4、由于plugin參數(shù)支持對(duì)象和函數(shù)類型酷愧,所以通過判斷plugin.install和plugin哪個(gè)是函數(shù)驾诈,即可知用戶使用哪種方式祖冊的插件缠诅,然后執(zhí)行用戶編寫的插件并將args作為參數(shù)傳入。
5乍迄、最后管引,將插件添加到installedPlugins中,保證相同的插件不會(huì)反復(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 是給每個(gè)vue實(shí)例添加?xùn)|西的,在router中給每個(gè)組件添加了$route和$router
這倆的區(qū)別是:
$router是VueRouter的實(shí)例對(duì)象漾狼,$route是當(dāng)前路由對(duì)象重慢,也就是說$route是$router的一個(gè)屬性
// 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實(shí)例逊躁,作為Vue 參數(shù)的一部分似踱。
但是這里就是有一個(gè)問題咯,這里的Vue 是根組件啊稽煤。也就是說目前只有根組件有這個(gè)router值核芽,而其他組件是還沒有的枷邪,所以我們需要讓其他組件也擁有這個(gè)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)前實(shí)例掛載到_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的時(shí)候绿店,再執(zhí)行install的時(shí)候,將Vue作為參數(shù)傳進(jìn)去庐橙。,
- mixin的作用是將mixin的內(nèi)容混合到Vue的初始參數(shù)options中假勿。相信使用vue的同學(xué)應(yīng)該使用過mixin了。
- 為什么是beforeCreate而不是created呢态鳖?因?yàn)槿绻窃赾reated操作的話转培,$options已經(jīng)初始化好了。
- 如果判斷當(dāng)前組件是根組件的話浆竭,就將我們傳入的router和_root掛在到根組件實(shí)例上浸须。
- 如果判斷當(dāng)前組件是子組件的話,就將我們_root根組件掛載到子組件邦泄。注意是引用的復(fù)制删窒,因此每個(gè)組件都擁有了同一個(gè)_root根組件掛載在它身上。
? 為啥判斷是子組件就直接取父組件的_root根組件呢
來顺囊,看一下父子組件執(zhí)行順序先
父beforeCreate-> 父created -> 父beforeMounte -> 子beforeCreate ->子create ->子beforeMount ->子 mounted -> 父mounted
在執(zhí)行子組件的beforeCreate的時(shí)候肌索,父組件已經(jīng)執(zhí)行完beforeCreate了,拿到_root那就沒問題了
在vueRouter文件中
const router = new VueRouter({
mode:"history",
routes
})
我們傳了兩個(gè)參數(shù),一個(gè)模式mode,一個(gè)是路由數(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)前實(shí)例掛載到_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)诚亚,為了方便管理晕换,用一個(gè)對(duì)象來表示。初始化的時(shí)候判斷是那種模式站宗,并將當(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)前實(shí)例掛載到_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
其實(shí)/$route就是獲取當(dāng)前的路徑
VueRouter.install = function (v) {
Vue = v
Vue.mixin({
beforeCreate() {
if (this.$options && this.$options.router) { // 如果是根組件
this._root = this // 把當(dāng)前實(shí)例掛載到_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指向的是一個(gè)Proxy代理對(duì)象,代理Vue組件梢灭,而我們前面講到每個(gè)組件都有一個(gè)_root屬性指向根組件夷家,根組件上有_router這個(gè)路由實(shí)例。
所以我們可以從router實(shí)例上獲得路由表或辖,也可以獲得當(dāng)前路徑瘾英。
然后再把獲得的組件放到h()里進(jìn)行渲染。
現(xiàn)在已經(jīng)實(shí)現(xiàn)了router-view組件的渲染颂暇,但是有一個(gè)問題缺谴,就是你改變路徑,視圖是沒有重新渲染的耳鸯,所以需要將_router.history進(jìn)行響應(yīng)式化湿蛔。
VueRouter.install = function (v) {
Vue = v
Vue.mixin({
beforeCreate() {
if (this.$options && this.$options.router) { // 如果是根組件
this._root = this // 把當(dāng)前實(shí)例掛載到_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)前實(shí)例掛載到_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