近期被問到一個問題,在大家項目中使用的是Vue的SPA(單頁面)仍是Vue的多頁面設計?javascript
這篇文章主要圍繞Vue的SPA單頁面設計展開。 關於如何展開Vue多頁面設計請點擊查看。html
官網vue-router文檔前端
首先咱們須要知道vue-router是什麼,它是幹什麼的?vue
這裏指的路由並非指咱們平時所說的硬件路由器,這裏的路由就是SPA(單頁應用)的路徑管理器。 換句話說,vue-router就是WebApp的連接路徑管理系統。java
vue-router是Vue.js官方的路由插件,它和vue.js是深度集成的,適合用於構建單頁面應用。git
那與傳統的頁面跳轉有什麼區別呢?github
1.vue的單頁面應用是基於路由和組件的,路由用於設定訪問路徑,並將路徑和組件映射起來。vue-router
2.傳統的頁面應用,是用一些超連接來實現頁面切換和跳轉的。npm
在vue-router單頁面應用中,則是路徑之間的切換,也就是組件的切換。路由模塊的本質 就是創建起url和頁面之間的映射關係。後端
至於爲啥不能用a標籤,這是由於用Vue作的都是單頁應用,就至關於只有一個主的index.html頁面,因此你寫的標籤是不起做用的,必須使用vue-router來進行管理。
SPA(single page application):單一頁面應用程序,有且只有一個完整的頁面;當它在加載頁面的時候,不會加載整個頁面的內容,而只更新某個指定的容器中內容。
單頁面應用(SPA)的核心之一是:
1.更新視圖而不從新請求頁面;
2.vue-router在實現單頁面前端路由時,提供了三種方式:Hash模式、History模式、abstract模式,根據mode參數來決定採用哪種方式。
路由模式
vue-router 提供了三種運行模式:
● hash: 使用 URL hash 值來做路由。默認模式。
● history: 依賴 HTML5 History API 和服務器配置。查看 HTML5 History 模式。
● abstract: 支持全部 JavaScript 運行環境,如 Node.js 服務器端。
vue-router 默認模式是 hash 模式 —— 使用 URL 的 hash 來模擬一個完整的 URL,當 URL 改變時,頁面不會去從新加載。
hash(#)是URL 的錨點,表明的是網頁中的一個位置,單單改變#後的部分(/#/..),瀏覽器只會加載相應位置的內容,不會從新加載網頁,也就是說 #是用來指導瀏覽器動做的,對服務器端徹底無用,HTTP請求中不包括#;同時每一次改變#後的部分,都會在瀏覽器的訪問歷史中增長一個記錄,使用」後退」按鈕,就能夠回到上一個位置;因此說Hash模式經過錨點值的改變,根據不一樣的值,渲染指定DOM位置的不一樣數據。
HTML5 History API提供了一種功能,能讓開發人員在不刷新整個頁面的狀況下修改站點的URL,就是利用 history.pushState API 來完成 URL 跳轉而無須從新加載頁面;
因爲hash模式會在url中自帶#,若是不想要很醜的 hash,咱們能夠用路由的 history 模式,只須要在配置路由規則時,加入"mode: 'history'",這種模式充分利用 history.pushState API 來完成 URL 跳轉而無須從新加載頁面。
//main.js文件中 const router = new VueRouter({ mode: 'history', routes: [...] })
當使用 history 模式時,URL 就像正常的 url,例如 yoursite.com/user/id,比較好… 不過這種模式要玩好,還須要後臺配置支持。由於咱們的應用是個單頁客戶端應用,若是後臺沒有正確的配置,當用戶在瀏覽器直接訪問
因此呢,你要在服務端增長一個覆蓋全部狀況的候選資源:若是 URL 匹配不到任何靜態資源,則應該返回同一個 index.html 頁面,這個頁面就是你 app 依賴的頁面。
export const routes = [ {path: "/", name: "homeLink", component:Home} {path: "/register", name: "registerLink", component: Register}, {path: "/login", name: "loginLink", component: Login}, {path: "*", redirect: "/"}]
此處就設置若是URL輸入錯誤或者是URL 匹配不到任何靜態資源,就自動跳到到Home頁面。
abstract模式是使用一個不依賴於瀏覽器的瀏覽歷史虛擬管理後端。
根據平臺差別能夠看出,在 Weex 環境中只支持使用 abstract 模式。 不過,vue-router 自身會對環境作校驗,若是發現沒有瀏覽器的 API,vue-router 會自動強制進入 abstract 模式,因此 在使用 vue-router 時只要不寫 mode 配置便可,默認會在瀏覽器環境中使用 hash 模式,在移動端原生環境中使用 abstract 模式。 (固然,你也能夠明確指定在全部狀況下都使用 abstract 模式)
1:下載 npm i vue-router -S
**2:在main.js中引入 ** import VueRouter from 'vue-router';
3:安裝插件 Vue.use(VueRouter);
4:建立路由對象並配置路由規則
let router = new VueRouter({routes:[{path:'/home',component:Home}]});
5:將其路由對象傳遞給Vue的實例,options中加入 router:router
6:在app.vue中留坑
<router-view></router-view>
具體實現請看以下代碼:
//main.js文件中引入 import Vue from 'vue'; import VueRouter from 'vue-router'; //主體 import App from './components/app.vue'; import index from './components/index.vue' //安裝插件 Vue.use(VueRouter); //掛載屬性 //建立路由對象並配置路由規則 let router = new VueRouter({ routes: [ //一個個對象 { path: '/index', component: index } ] }); //new Vue 啓動 new Vue({ el: '#app', //讓vue知道咱們的路由規則 router: router, //能夠簡寫router render: c => c(App), }) 複製代碼
最後記得在在app.vue中「留坑」
//app.vue中 <template> <div> <!-- 留坑,很是重要 --> <router-view></router-view> </div> </template> <script> export default { data(){ return {} } } </script> 複製代碼
咱們先來看看vue的實現路徑。
在入口文件中須要實例化一個 VueRouter 的實例對象 ,而後將其傳入 Vue 實例的 options 中。
1 export default class VueRouter { 2 static install: () => void; 3 static version: string; 4 5 app: any; 6 apps: Array<any>; 7 ready: boolean; 8 readyCbs: Array<Function>; 9 options: RouterOptions; 10 mode: string; 11 history: HashHistory | HTML5History | AbstractHistory; 12 matcher: Matcher; 13 fallback: boolean; 14 beforeHooks: Array<?NavigationGuard>; 15 resolveHooks: Array<?NavigationGuard>; 16 afterHooks: Array<?AfterNavigationHook>; 17 18 constructor (options: RouterOptions = {}) { 19 this.app = null 20 this.apps = [] 21 this.options = options 22 this.beforeHooks = [] 23 this.resolveHooks = [] 24 this.afterHooks = [] 25 // 建立 matcher 匹配函數 26 this.matcher = createMatcher(options.routes || [], this) 27 // 根據 mode 實例化具體的 History,默認爲'hash'模式 28 let mode = options.mode || 'hash' 29 // 經過 supportsPushState 判斷瀏覽器是否支持'history'模式 30 // 若是設置的是'history'可是若是瀏覽器不支持的話,'history'模式會退回到'hash'模式 31 // fallback 是當瀏覽器不支持 history.pushState 控制路由是否應該回退到 hash 模式。默認值爲 true。 32 this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false 33 if (this.fallback) { 34 mode = 'hash' 35 } 36 // 不在瀏覽器內部的話,就會變成'abstract'模式 37 if (!inBrowser) { 38 mode = 'abstract' 39 } 40 this.mode = mode 41 // 根據不一樣模式選擇實例化對應的 History 類 42 switch (mode) { 43 case 'history': 44 this.history = new HTML5History(this, options.base) 45 break 46 case 'hash': 47 this.history = new HashHistory(this, options.base, this.fallback) 48 break 49 case 'abstract': 50 this.history = new AbstractHistory(this, options.base) 51 break 52 default: 53 if (process.env.NODE_ENV !== 'production') { 54 assert(false, `invalid mode: ${mode}`) 55 } 56 } 57 } 58 59 match ( 60 raw: RawLocation, 61 current?: Route, 62 redirectedFrom?: Location 63 ): Route { 64 return this.matcher.match(raw, current, redirectedFrom) 65 } 66 67 get currentRoute (): ?Route { 68 return this.history && this.history.current 69 } 70 71 init (app: any /* Vue component instance */) { 72 process.env.NODE_ENV !== 'production' && assert( 73 install.installed, 74 `not installed. Make sure to call \`Vue.use(VueRouter)\` ` + 75 `before creating root instance.` 76 ) 77 78 this.apps.push(app) 79 80 // main app already initialized. 81 if (this.app) { 82 return 83 } 84 85 this.app = app 86 87 const history = this.history 88 // 根據history的類別執行相應的初始化操做和監聽 89 if (history instanceof HTML5History) { 90 history.transitionTo(history.getCurrentLocation()) 91 } else if (history instanceof HashHistory) { 92 const setupHashListener = () => { 93 history.setupListeners() 94 } 95 history.transitionTo( 96 history.getCurrentLocation(), 97 setupHashListener, 98 setupHashListener 99 ) 100 } 101 102 history.listen(route => { 103 this.apps.forEach((app) => { 104 app._route = route 105 }) 106 }) 107 } 108 // 路由跳轉以前 109 beforeEach (fn: Function): Function { 110 return registerHook(this.beforeHooks, fn) 111 } 112 // 路由導航被確認之間前 113 beforeResolve (fn: Function): Function { 114 return registerHook(this.resolveHooks, fn) 115 } 116 // 路由跳轉以後 117 afterEach (fn: Function): Function { 118 return registerHook(this.afterHooks, fn) 119 } 120 // 第一次路由跳轉完成時被調用的回調函數 121 onReady (cb: Function, errorCb?: Function) { 122 this.history.onReady(cb, errorCb) 123 } 124 // 路由報錯 125 onError (errorCb: Function) { 126 this.history.onError(errorCb) 127 } 128 // 路由添加,這個方法會向history棧添加一個記錄,點擊後退會返回到上一個頁面。 129 push (location: RawLocation, onComplete?: Function, onAbort?: Function) { 130 this.history.push(location, onComplete, onAbort) 131 } 132 // 這個方法不會向history裏面添加新的記錄,點擊返回,會跳轉到上上一個頁面。上一個記錄是不存在的。 133 replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { 134 this.history.replace(location, onComplete, onAbort) 135 } 136 // 相對於當前頁面向前或向後跳轉多少個頁面,相似 window.history.go(n)。n可爲正數可爲負數。正數返回上一個頁面 137 go (n: number) { 138 this.history.go(n) 139 } 140 // 後退到上一個頁面 141 back () { 142 this.go(-1) 143 } 144 // 前進到下一個頁面 145 forward () { 146 this.go(1) 147 } 148 149 getMatchedComponents (to?: RawLocation | Route): Array<any> { 150 const route: any = to 151 ? to.matched 152 ? to 153 : this.resolve(to).route 154 : this.currentRoute 155 if (!route) { 156 return [] 157 } 158 return [].concat.apply([], route.matched.map(m => { 159 return Object.keys(m.components).map(key => { 160 return m.components[key] 161 }) 162 })) 163 } 164 165 resolve ( 166 to: RawLocation, 167 current?: Route, 168 append?: boolean 169 ): { 170 location: Location, 171 route: Route, 172 href: string, 173 // for backwards compat 174 normalizedTo: Location, 175 resolved: Route 176 } { 177 const location = normalizeLocation( 178 to, 179 current || this.history.current, 180 append, 181 this 182 ) 183 const route = this.match(location, current) 184 const fullPath = route.redirectedFrom || route.fullPath 185 const base = this.history.base 186 const href = createHref(base, fullPath, this.mode) 187 return { 188 location, 189 route, 190 href, 191 // for backwards compat 192 normalizedTo: location, 193 resolved: route 194 } 195 } 196 197 addRoutes (routes: Array<RouteConfig>) { 198 this.matcher.addRoutes(routes) 199 if (this.history.current !== START) { 200 this.history.transitionTo(this.history.getCurrentLocation()) 201 } 202 } 203 }
• hash雖然出如今url中,但不會被包括在http請求中,它是用來指導瀏覽器動做的,對服務器端沒影響,所以,改變hash不會從新加載頁面。
• 能夠爲hash的改變添加監聽事件:
• 每一次改變hash(window.location.hash),都會在瀏覽器訪問歷史中增長一個記錄。
export class HashHistory extends History { constructor (router: Router, base: ?string, fallback: boolean) { super(router, base) // check history fallback deeplinking // 若是是從history模式降級來的,須要作降級檢查 if (fallback && checkFallback(this.base)) { // 若是降級且作了降級處理,則返回 return } ensureSlash() } .......
function checkFallback (base) { const location = getLocation(base) // 獲得除去base的真正的 location 值 if (!/^\/#/.test(location)) { // 若是此時地址不是以 /# 開頭的 // 須要作一次降級處理,降爲 hash 模式下應有的 /# 開頭 window.location.replace( cleanPath(base + '/#' + location) ) return true } } function ensureSlash (): boolean { // 獲得 hash 值 const path = getHash() if (path.charAt(0) === '/') { // 若是是以 / 開頭的,直接返回便可 return true } // 不是的話,須要手動保證一次 替換 hash 值 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! // 由於兼容性的問題,這裏沒有直接使用 window.location.hash // 由於 Firefox decode hash 值 const href = window.location.href const index = href.indexOf('#') return index === -1 ? '' : decodeURI(href.slice(index + 1)) } // 獲得hash以前的url地址 function getUrl (path) { const href = window.location.href const i = href.indexOf('#') const base = i >= 0 ? href.slice(0, i) : href return `${base}#${path}` } // 添加一個hash function pushHash (path) { if (supportsPushState) { pushState(getUrl(path)) } else { window.location.hash = path } } // 替代hash function replaceHash (path) { if (supportsPushState) { replaceState(getUrl(path)) } else { window.location.replace(getUrl(path)) } }
hash的改變會自動添加到瀏覽器的訪問歷史記錄中。 那麼視圖的更新是怎麼實現的呢,看下 transitionTo()方法:
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) { const route = this.router.match(location, this.current) //找到匹配路由 this.confirmTransition(route, () => { //確認是否轉化 this.updateRoute(route) //更新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) }) } }) } //更新路由 updateRoute (route: Route) { const prev = this.current // 跳轉前路由 this.current = route // 裝備跳轉路由 this.cb && this.cb(route) // 回調函數,這一步很重要,這個回調函數在index文件中註冊,會更新被劫持的數據 _router this.router.afterHooks.forEach(hook => { hook && hook(route, prev) }) } }
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 // 加了 try...catch 是由於 Safari 有調用 pushState 100 次限制 // 一旦達到就會拋出 DOM Exception 18 錯誤 const history = window.history try { if (replace) { // replace 的話 key 仍是當前的 key 不必生成新的 history.replaceState({ key: _key }, '', url) } else { // 從新生成 key _key = genKey() // 帶入新的 key 值 history.pushState({ key: _key }, '', url) } } catch (e) { // 達到限制了 則從新指定新的地址 window.location[replace ? 'replace' : 'assign'](url) } }
replaceState
// 直接調用 pushState 傳入 replace 爲 true export function replaceState (url?: string) { pushState(url, true) } 複製代碼
pushState和replaceState兩種方法的共同特色:當調用他們修改瀏覽器歷史棧後,雖然當前url改變了,但瀏覽器不會當即發送請求該url,這就爲單頁應用前端路由,更新視圖但不從新請求頁面提供了基礎。
supportsPushState
export const supportsPushState = inBrowser && (function () { const ua = window.navigator.userAgent if ( (ua.indexOf('Android 2.') !== -1 || ua.indexOf('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 })()
其實所謂響應式屬性,即當_route值改變時,會自動調用Vue實例的render()方法,更新視圖。 $router.push()-->HashHistory.push()-->History.transitionTo()-->History.updateRoute()-->{app._route=route}-->vm.render()
監聽地址欄
在瀏覽器中,用戶能夠直接在瀏覽器地址欄中輸入改變路由,所以還須要監聽瀏覽器地址欄中路由的變化 ,並具備與經過代碼調用相同的響應行爲,在HashHistory中這一功能經過setupListeners監聽hashchange實現:
setupListeners () { window.addEventListener('hashchange', () => { if (!ensureSlash()) { return } this.transitionTo(getHash(), route => { replaceHash(route.fullPath) }) }) }
History interface是瀏覽器歷史記錄棧提供的接口,經過back(),forward(),go()等方法,咱們能夠讀取瀏覽器歷史記錄棧的信息,進行各類跳轉操做。
export class HTML5History extends History { constructor (router: Router, base: ?string) { super(router, base) const expectScroll = router.options.scrollBehavior //指回滾方式 const supportsScroll = supportsPushState && expectScroll if (supportsScroll) { setupScroll() } const initLocation = getLocation(this.base) //監控popstate事件 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. // 避免在某些瀏覽器中首次發出「popstate」事件 // 因爲同一時間異步監聽,history路由沒有同時更新。 const location = getLocation(this.base) if (this.current === START && location === initLocation) { return } this.transitionTo(location, route => { if (supportsScroll) { handleScroll(router, route, current, true) } }) }) }
hash模式僅改變hash部分的內容,而hash部分是不會包含在http請求中的(hash帶#):
oursite.com/#/user/id //如請求,只會發送http://oursite.com/
因此hash模式下遇到根據url請求頁面不會有問題
而history模式則將url修改的就和正常請求後端的url同樣(history不帶#)
若是這種向後端發送請求的話,後端沒有配置對應/user/id的get路由處理,會返回404錯誤。
官方推薦的解決辦法是在服務端增長一個覆蓋全部狀況的候選資源:若是 URL 匹配不到任何靜態資源,則應該返回同一個 index.html 頁面,這個頁面就是你 app 依賴的頁面。同時這麼作之後,服務器就再也不返回 404 錯誤頁面,由於對於全部路徑都會返回 index.html 文件。爲了不這種狀況,在 Vue 應用裏面覆蓋全部的路由狀況,而後在給出一個 404 頁面。或者,若是是用 Node.js 做後臺,可使用服務端的路由來匹配 URL,當沒有匹配到路由的時候返回 404,從而實現 fallback。
兩種模式比較
通常的需求場景中,hash模式與history模式是差很少的,根據MDN的介紹,調用history.pushState()相比於直接修改hash主要有如下優點:
• pushState設置的新url能夠是與當前url同源的任意url,而hash只可修改#後面的部分,故只可設置與當前同文檔的url
• pushState設置的新url能夠與當前url如出一轍,這樣也會把記錄添加到棧中,而hash設置的新值必須與原來不同纔會觸發記錄添加到棧中
• pushState經過stateObject能夠添加任意類型的數據記錄中,而hash只可添加短字符串 pushState可額外設置title屬性供後續使用
'abstract'模式,不涉及和瀏覽器地址的相關記錄,流程跟'HashHistory'是同樣的,其原理是經過數組模擬瀏覽器歷史記錄棧的功能
//abstract.js實現,這裏經過棧的數據結構來模擬路由路徑 export class AbstractHistory extends History { index: number; stack: Array<Route>; constructor (router: Router, base: ?string) { super(router, base) this.stack = [] this.index = -1 } // 對於 go 的模擬 go (n: number) { // 新的歷史記錄位置 const targetIndex = this.index + n // 小於或大於超出則返回 if (targetIndex < 0 || targetIndex >= this.stack.length) { return } // 取得新的 route 對象 // 由於是和瀏覽器無關的 這裏獲得的必定是已經訪問過的 const route = this.stack[targetIndex] // 因此這裏直接調用 confirmTransition 了 // 而不是調用 transitionTo 還要走一遍 match 邏輯 this.confirmTransition(route, () => { this.index = targetIndex this.updateRoute(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 ( isSameRoute(route, current) && route.matched.length === current.matched.length ) { this.ensureURL() return abort() } //下面是各種鉤子函數的處理 //********************* }) }
看到這裏你已經對vue-router的路由基本掌握的差很少了,要是喜歡看源碼能夠點擊查看
要是喜歡能夠給我一個star,github
做者:DIVI連接:https://juejin.im/post/5bc6eb875188255c9c755df2來源:掘金著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。