vue項目地址上的#是哪來的?(前端路由的hash模式和history模式)


 

效果:

 原因:這是因為vue是單頁面應用的原因,在前進或后退的時候使用這種方式將保持路徑的正確性,#是vue的hash模式,這是一種默認的方式。此時router/index.js文件是這樣的:

復制代碼
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'home',
    component: Home
  },
  {
    path: '/about',
    name: 'about',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

const router = new VueRouter({
  routes
})

export default router
復制代碼

如果想去掉這個#,可以將hash模式改成history模式(默認為hash模式),即在index.js中加上mode: “history”,如下圖:

復制代碼
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'home',
    component: Home
  },
  {
    path: '/about',
    name: 'about',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

const router = new VueRouter({
  routes,
  mode: 'history'
})

export default router
復制代碼

效果如下:

用Hash模式來實現簡易路由:

首先定義四個a鏈接,每個a鏈接對應一個hash值.再定義一個component標簽來展示對應的組件

當點擊每個a鏈接,改變地址欄中的url的hash值,當hash值變化,我們期望在主體區展示對應的組件。但是如何才能監聽到hash值的變化呢?就要用到一個事件,叫window.onhashchange事件,通過hash值的變化就自動觸發這個事件在事件中就能通過location.hash拿到最新的hash值,然后做switchcase判斷,從而決定在主體區域去渲染什么樣的組件。

 后端路由---->Ajax前端渲染---->SPA(前端路由)的演變過程:

在早期web開發中,絕大多數網站都采用后端路由的形式來渲染每一個網頁

后端路由指的是url請求地址與服務器資源之間的對應關系。后端路由的渲染方式是后端渲染,這樣渲染方式是有性能問題的。后端渲染存在性能問題,假設用戶與服務器之間經常要提交表單這樣的數據交互行為,后端路由就會造成網頁的頻繁刷新,體驗非常的差,因此就出現了Ajax技術,實現前端頁面的局部刷新,很大程度上提高用戶體驗,但是單純的Ajax技術並不支持瀏覽器的前進后退這些歷史操作,也就是說瀏覽器沒有辦法保存用戶在網頁上的瀏覽狀態的,因此前端又出現了SPA單頁面程序開發技術,所謂的SPA指的是整個網站只有一個頁面,內容的變化通過Ajax局部更新實現,同時SPA還支持瀏覽器地址欄的前進和后退操作。如何才能實現SPA呢?SPA最核心的技術是前端路由,前端路由的本質是用戶事件與事件處理函數之間的對應關系。通過前端路由,可以提高用戶的操作體驗,同時也能讓網頁打開速度更快。

SPA的優缺點:

SPA的優點:1、用戶操作體驗好,用戶不用刷新頁面,整個交互過程都是通過Ajax來操作,故是客戶端渲染。2、適合前后端分離開發;

SPA的缺點:1、首頁加載慢,SPA會將js、CSS打包成一個文件,在加載頁面顯示的時候加載打包文件,如果打包文件較大或者網速慢則用戶體驗不好,所以門戶不采用單頁面應用。

2、SEO不友好,故門戶、課程介紹不采用單頁面應用,而管理系統采用單頁面應用。

哈希路由(hash模式)和歷史路由(history模式)源碼解析:

隨着前端應用的業務功能越來越復雜、用戶對於使用體驗的要求越來越高,單頁應用(SPA)成為前端應用的主流形式。大型單頁應用最顯著特點之一就是采用前端路由系統,通過改變URL,在不重新請求頁面的情況下,更新頁面視圖。“更新視圖但不重新請求頁面”是前端路由原理的核心之一,目前在瀏覽器環境中這一功能的實現主要有兩種方式:

1、利用URL中的hash(“#”)

2、利用History interface在 HTML5中新增的方法

模式參數

在vue-router中是通過mode這一參數控制路由的實現模式的:

const router = new VueRouter({
  mode: 'history',
  routes: [...]
})

源碼:

復制代碼
export default class VueRouter {
  
  mode: string; // 傳入的字符串參數,指示history類別
  history: HashHistory | HTML5History | AbstractHistory; // 實際起作用的對象屬性,必須是以上三個類的枚舉
  fallback: boolean; // 如瀏覽器不支持,'history'模式需回滾為'hash'模式
  
  constructor (options: RouterOptions = {}) {
    
    let mode = options.mode || 'hash' // 默認為'hash'模式
    this.fallback = mode === 'history' && !supportsPushState // 通過supportsPushState判斷瀏覽器是否支持'history'模式
    if (this.fallback) {
      mode = 'hash'
    }
    if (!inBrowser) {
      mode = 'abstract' // 不在瀏覽器環境下運行需強制為'abstract'模式
    }
    this.mode = mode

    // 根據mode確定history實際的類並實例化
    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}`)
        }
    }
  }

  init (app: any /* Vue component instance */) {
    
    const history = this.history

    // 根據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
      })
    })
  }

  // VueRouter類暴露的以下方法實際是調用具體history對象的方法
  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    this.history.push(location, onComplete, onAbort)
  }

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    this.history.replace(location, onComplete, onAbort)
  }
}
復制代碼

1、作為參數傳入的字符串屬性mode只是一個標記,用來指示實際起作用的對象屬性history的實現類,兩者對應關系如下:

'history':HTML5History

'hash':HashHistory

'abstract':AbstractHistory

2、在初始化對應的history之前,會對mode做一些校驗:若瀏覽器不支持HTML5History方式(通過supportsPushState變量判斷),則mode強制設為'hash';若不是在瀏覽器環境下運行,則mode強制設為'abstract'

在瀏覽器環境下的兩種方式,分別就是在HTML5History,HashHistory兩個類中實現的。

hash(“#”)符號的本來作用是加在URL中指示網頁中的位置:

http://www.example.com/index.html#print

#符號后面的字符稱之為hash。

復制代碼
export function getHash (): string {
  // 因為兼容性問題 這里沒有直接使用 window.location.hash
  // 因為 Firefox decode hash 值
  const href = window.location.href
  const index = href.indexOf('#')
  // 如果此時沒有 # 則返回 ''
  // 否則 取得 # 后的所有內容
  return index === -1 ? '' : href.slice(index + 1)
}
復制代碼

hash值以斜杠(slash)開頭:

復制代碼
// 保證 hash 以 / 開頭
function ensureSlash (): boolean {
  // 得到 hash 值
  const path = getHash()
  // 如果說是以 / 開頭的 直接返回即可
  if (path.charAt(0) === '/') {
    return true
  }
  // 不是的話 需要手工保證一次 替換 hash 值
  replaceHash('/' + path)
  return false
}
復制代碼

獲取不帶base的location。

復制代碼
// 得到 不帶 base 值的 location
export function getLocation (base: string): string {
  let path = window.location.pathname
  if (base && path.indexOf(base) === 0) {
    path = path.slice(base.length)
  }
  // 是包含 search 和 hash 的
  return (path || '/') + window.location.search + window.location.hash
}
復制代碼

在不帶base的location前添加/#,

如果設置的是 history 但是如果瀏覽器不支持的話 ,強制退回到 hash。如果說此時的地址不是以 /# 開頭的,需要做一次降級處理 降級為 hash 模式下應有的 /# 開頭

復制代碼
 checkFallback () {
    // 得到除去 base 的真正的 location 值
    const location = getLocation(this.base)
    if (!/^\/#/.test(location)) {
      // 如果說此時的地址不是以 /# 開頭的
      // 需要做一次降級處理 降級為 hash 模式下應有的 /# 開頭
      window.location.replace(
        cleanPath(this.base + '/#' + location)
      )
      return true
    }
  }
復制代碼

HashHistory

繼承History基類:

復制代碼
// 繼承 History 基類
export class HashHistory extends History {
  constructor (router: VueRouter, base: ?string, fallback: boolean) {
    // 調用基類構造器
    super(router, base)

    // 如果說是從 history 模式降級來的
    // 需要做降級檢查
    if (fallback && this.checkFallback()) {
      // 如果降級 且 做了降級處理 則什么也不需要做
      return
    }
    // 保證 hash 是以 / 開頭
    ensureSlash()
  }
復制代碼

可以看到在實例化過程中主要做兩件事情:針對於不支持 history api 的降級處理,以及保證默認進入的時候對應的 hash 值是以 / 開頭的,如果不是則替換。

友善高級的 HTML5History

HTML5History 則是利用 history.pushState/repaceState API 來完成 URL 跳轉而無須重新加載頁面,頁面地址和正常地址無異;

復制代碼
// ...
import { cleanPath } from '../util/path'
import { History } from './base'
// 記錄滾動位置工具函數
import {
  saveScrollPosition,
  getScrollPosition,
  isValidPosition,
  normalizePosition,
  getElementPosition
} from '../util/scroll-position'

// 生成唯一 key 作為位置相關緩存 key
const genKey = () => String(Date.now())
let _key: string = genKey()

export class HTML5History extends History {
  constructor (router: VueRouter, base: ?string) {
    // 基類構造函數
    super(router, base)

    // 定義滾動行為 option
    const expectScroll = router.options.scrollBehavior
    // 監聽 popstate 事件 也就是
    // 瀏覽器歷史記錄發生改變的時候(點擊瀏覽器前進后退 或者調用 history api )
    window.addEventListener('popstate', e => {
// ...
    })

    if (expectScroll) {
      // 需要記錄滾動行為 監聽滾動事件 記錄位置
      window.addEventListener('scroll', () => {
        saveScrollPosition(_key)
      })
    }
  }
// ...
}
// ...
復制代碼

可以看到在這種模式下,初始化作的工作相比 hash 模式少了很多,只是調用基類構造函數以及初始化監聽事件,不需要再做額外的工作。

history 改變

history 改變可以有兩種,一種是用戶點擊鏈接元素,一種是更新瀏覽器本身的前進后退導航來更新。

第一種方式:更新瀏覽器本身的前進后退導航

先來說瀏覽器導航發生變化的時候會觸發對應的事件:對於 hash 模式而言觸發 window 的 hashchange 事件,對於 history 模式而言則觸發 window 的 popstate 事件。

hash模式

復制代碼
 onHashChange () {
    // 不是 / 開頭
    if (!ensureSlash()) {
      return
    }
    // 調用 transitionTo
    this.transitionTo(getHash(), route => {
      // 替換 hash
      replaceHash(route.fullPath)
    })
  }
復制代碼

replaceHash()方法直接調用 replace 強制替換 以避免產生“多余”的歷史記錄,其實就是更新瀏覽器的 hash 值,push 和 replace 的場景下都是一個效果。

復制代碼
function replaceHash (path) {
  const i = window.location.href.indexOf('#')
  // 直接調用 replace 強制替換 以避免產生“多余”的歷史記錄
  // 主要是用戶初次跳入 且hash值不是以 / 開頭的時候直接替換
  // 其余時候和push沒啥區別 瀏覽器總是記錄hash記錄
  window.location.replace(
    window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path
  )
}
復制代碼

transitionTo 方法的功能是路由跳轉,它接收三個參數:1. location:要轉向的路由地址。2. onComplete:完成后的回調。3. onAbort:取消時的回調

復制代碼
// 確認過渡
  confirmTransition (route: Route, cb: Function) {
    const current = this.current  // 當前路由
    // 如果是相同 直接返回
    if (isSameRoute(route, current)) {  // 如果目標路由route與當前路由相同,取消跳轉
      this.ensureURL()
      return
    }
    const {
      deactivated,
      activated
    } = resolveQueue(this.current.matched, route.matched)

    // 整個切換周期的隊列
    const queue: Array<?NavigationGuard> = [].concat(
      // leave 的鈎子
      extractLeaveGuards(deactivated),
      // 全局 router before hooks
      this.router.beforeHooks,
      // 將要更新的路由的 beforeEnter 鈎子
      activated.map(m => m.beforeEnter),
      // 異步組件
      resolveAsyncComponents(activated)
    )

    this.pending = route
    // 每一個隊列執行的 iterator 函數
    const iterator = (hook: NavigationGuard, next) => {
// ...
    }
    // 執行隊列 leave 和 beforeEnter 相關鈎子
    runQueue(queue, iterator, () => {
//...
    })
  }
復制代碼

history模式

復制代碼
window.addEventListener('popstate', e => {
  // 取得 state 中保存的 key
  _key = e.state && e.state.key
  // 保存當前的先
  const current = this.current
  // 調用 transitionTo
  this.transitionTo(getLocation(this.base), next => {
    if (expectScroll) {
      // 處理滾動
      this.handleScroll(next, current, true)
    }
  })
})
復制代碼

第二種方式:點擊鏈接交互

即點擊了 <router-link>,回顧下這個組件在渲染的時候做的事情:

復制代碼
// ...
  render (h: Function) {
// ...

    // 事件綁定
    const on = {
      click: (e) => {
        // 忽略帶有功能鍵的點擊
        if (e.metaKey || e.ctrlKey || e.shiftKey) return
        // 已阻止的返回
        if (e.defaultPrevented) return
        // 右擊
        if (e.button !== 0) return
        // `target="_blank"` 忽略
        const target = e.target.getAttribute('target')
        if (/\b_blank\b/i.test(target)) return
        // 阻止默認行為 防止跳轉
        e.preventDefault()
        if (this.replace) {
          // replace 邏輯
          router.replace(to)
        } else {
          // push 邏輯
          router.push(to)
        }
      }
    }
    // 創建元素需要附加的數據們
    const data: any = {
      class: classes
    }

    if (this.tag === 'a') {
      data.on = on
      data.attrs = { href }
    } else {
      // 找到第一個 <a> 給予這個元素事件綁定和href屬性
      const a = findAnchor(this.$slots.default)
      if (a) {
        // in case the <a> is a static node
        a.isStatic = false
        const extend = _Vue.util.extend
        const aData = a.data = extend({}, a.data)
        aData.on = on
        const aAttrs = a.data.attrs = extend({}, a.data.attrs)
        aAttrs.href = href
      } else {
        // 沒有 <a> 的話就給當前元素自身綁定時間
        data.on = on
      }
    }
    // 創建元素
    return h(this.tag, data, this.$slots.default)
  }
// ...
復制代碼

這里一個關鍵就是綁定了元素的 click 事件,當用戶觸發后,會調用 router 的 push 或 replace 方法來更新路由。下邊就來看看這兩個方法定義,

復制代碼
 push (location: RawLocation) {
    this.history.push(location)
  }

  replace (location: RawLocation) {
    this.history.replace(location)
  }
復制代碼

HashHistory

復制代碼
// ...
  push (location: RawLocation) {
    // 調用 transitionTo
    this.transitionTo(location, route => {
// ...
    })
  }

  replace (location: RawLocation) {
    // 調用 transitionTo
    this.transitionTo(location, route => {
// ...
    })
  }
// ...
復制代碼

操作是類似的,主要就是調用基類的 transitionTo 方法來過渡這次歷史的變化,在完成后更新當前瀏覽器的 hash 值。

HashHistory的push方法:

將新路由添加到瀏覽器訪問歷史的棧頂

復制代碼
 push (location: RawLocation) {
    // 調用 transitionTo
    this.transitionTo(location, route => {
      // 完成后 pushHash
      pushHash(route.fullPath)
    })
  }
function pushHash (path) {
  window.location.hash = path
}
復制代碼

 HashHistory的replace方法:

replace()方法與push()方法不同之處在於,它並不是將新路由添加到瀏覽器訪問歷史的棧頂,而是替換掉當前的路由

復制代碼
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  this.transitionTo(location, route => {
    replaceHash(route.fullPath)
    onComplete && onComplete(route)
  }, onAbort)
}
  
function replaceHash (path) {
  const i = window.location.href.indexOf('#')
  window.location.replace(
    window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path
  )
}
復制代碼

 location的replace()方法可用一個新頁面取代當前頁面。

其實就是更新瀏覽器的 hash 值,push 和 replace 的場景下都是一個效果。

復制代碼
transitionTo (location: RawLocation, cb?: Function) {
    // 調用 match 得到匹配的 route 對象
    const route = this.router.match(location, this.current)
    // 確認過渡
    this.confirmTransition(route, () => {
      // 更新當前 route 對象
      this.updateRoute(route)
      cb && cb(route)
      // 子類實現的更新url地址
      // 對於 hash 模式的話 就是更新 hash 的值
      // 對於 history 模式的話 就是利用 pushstate / replacestate 來更新
      // 瀏覽器地址
      this.ensureURL()
    })
  }
  // 確認過渡
  confirmTransition (route: Route, cb: Function) {
    const current = this.current
    // 如果是相同 直接返回
    if (isSameRoute(route, current)) {
      this.ensureURL()
      return
    }
    const {
      deactivated,
      activated
    } = resolveQueue(this.current.matched, route.matched)

    // 整個切換周期的隊列
    const queue: Array<?NavigationGuard> = [].concat(
      // leave 的鈎子
      extractLeaveGuards(deactivated),
      // 全局 router before hooks
      this.router.beforeHooks,
      // 將要更新的路由的 beforeEnter 鈎子
      activated.map(m => m.beforeEnter),
      // 異步組件
      resolveAsyncComponents(activated)
    )

    this.pending = route
    // 每一個隊列執行的 iterator 函數
    const iterator = (hook: NavigationGuard, next) => {
// ...
    }
    // 執行隊列 leave 和 beforeEnter 相關鈎子
    runQueue(queue, iterator, () => {
//...
    })
  }
復制代碼

回到 confirmTransition 的回調,最后還做了一件事情 ensureURL:

ensureURL (push?: boolean) {
  const current = this.current.fullPath
  if (getHash() !== current) {
    push ? pushHash(current) : replaceHash(current)
  }
}

此時 push 為 undefined,所以調用 replaceHash 更新瀏覽器 hash 值。



 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM