38舔腾、從vue-router看前端路由的兩種實(shí)現(xiàn)

本文由淺入深觀摩vue-router源碼是如何通過(guò)hash與History interface兩種方式實(shí)現(xiàn)前端路由岛杀,介紹了相關(guān)原理诱篷,并對(duì)比了兩種方式的優(yōu)缺點(diǎn)與注意事項(xiàng)壶唤。最后分析了如何實(shí)現(xiàn)可以直接從文件系統(tǒng)加載而不借助后端服務(wù)器的Vue單頁(yè)應(yīng)用。

隨著前端應(yīng)用的業(yè)務(wù)功能越來(lái)越復(fù)雜棕所、用戶對(duì)于使用體驗(yàn)的要求越來(lái)越高闸盔,單頁(yè)應(yīng)用(SPA)成為前端應(yīng)用的主流形式。大型單頁(yè)應(yīng)用最顯著特點(diǎn)之一就是采用前端路由系統(tǒng)琳省,通過(guò)改變URL迎吵,在不重新請(qǐng)求頁(yè)面的情況下,更新頁(yè)面視圖针贬。

“更新視圖但不重新請(qǐng)求頁(yè)面”是前端路由原理的核心之一击费,目前在瀏覽器環(huán)境中這一功能的實(shí)現(xiàn)主要有兩種方式:

  • 利用URL中的hash(“#”)

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

vue-router是Vue.js框架的路由插件,下面我們從它的源碼入手坚踩,邊看代碼邊看原理荡灾,由淺入深觀摩vue-router是如何通過(guò)這兩種方式實(shí)現(xiàn)前端路由的瓤狐。

模式參數(shù)

在vue-router中是通過(guò)mode這一參數(shù)控制路由的實(shí)現(xiàn)模式的:

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

創(chuàng)建VueRouter的實(shí)例對(duì)象時(shí)瞬铸,mode以構(gòu)造函數(shù)參數(shù)的形式傳入。帶著問(wèn)題閱讀源碼础锐,我們就可以從VueRouter類的定義入手嗓节。一般插件對(duì)外暴露的類都是定義在源碼src根目錄下的index.js文件中,打開(kāi)該文件皆警,可以看到VueRouter類的定義拦宣,摘錄與mode參數(shù)有關(guān)的部分如下:

export default class VueRouter {

  mode: string; // 傳入的字符串參數(shù),指示history類別
  history: HashHistory | HTML5History | AbstractHistory; // 實(shí)際起作用的對(duì)象屬性信姓,必須是以上三個(gè)類的枚舉
  fallback: boolean; // 如瀏覽器不支持鸵隧,'history'模式需回滾為'hash'模式

  constructor (options: RouterOptions = {}) {

    let mode = options.mode || 'hash' // 默認(rèn)為'hash'模式
    this.fallback = mode === 'history' && !supportsPushState // 通過(guò)supportsPushState判斷瀏覽器是否支持'history'模式
    if (this.fallback) {
      mode = 'hash'
    }
    if (!inBrowser) {
      mode = 'abstract' // 不在瀏覽器環(huán)境下運(yùn)行需強(qiáng)制為'abstract'模式
    }
    this.mode = mode

    // 根據(jù)mode確定history實(shí)際的類并實(shí)例化
    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

    // 根據(jù)history的類別執(zhí)行相應(yīng)的初始化操作和監(jiān)聽(tīng)
    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類暴露的以下方法實(shí)際是調(diào)用具體history對(duì)象的方法
  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. 作為參數(shù)傳入的字符串屬性mode只是一個(gè)標(biāo)記,用來(lái)指示實(shí)際起作用的對(duì)象屬性history的實(shí)現(xiàn)類意推,兩者對(duì)應(yīng)關(guān)系如下:

    modehistory'history'HTML5History'hash'HashHistory'abstract'AbstractHistory

  2. 在初始化對(duì)應(yīng)的history之前豆瘫,會(huì)對(duì)mode做一些校驗(yàn):若瀏覽器不支持HTML5History方式(通過(guò)supportsPushState變量判斷),則mode強(qiáng)制設(shè)為'hash'菊值;若不是在瀏覽器環(huán)境下運(yùn)行外驱,則mode強(qiáng)制設(shè)為'abstract'

  3. VueRouter類中的onReady(), push()等方法只是一個(gè)代理育灸,實(shí)際是調(diào)用的具體history對(duì)象的對(duì)應(yīng)方法,在init()方法中初始化時(shí)昵宇,也是根據(jù)history對(duì)象具體的類別執(zhí)行不同操作

在瀏覽器環(huán)境下的兩種方式磅崭,分別就是在HTML5History,HashHistory兩個(gè)類中實(shí)現(xiàn)的瓦哎。他們都定義在src/history文件夾下砸喻,繼承自同目錄下base.js文件中定義的History類。History中定義的是公用和基礎(chǔ)的方法杭煎,直接看會(huì)一頭霧水恩够,我們先從HTML5History,HashHistory兩個(gè)類中看著親切的push(), replace()方法的說(shuō)起羡铲。

HashHistory

看源碼前先回顧一下原理:

hash(“#”)符號(hào)的本來(lái)作用是加在URL中指示網(wǎng)頁(yè)中的位置:

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

符號(hào)本身以及它后面的字符稱之為hash蜂桶,可通過(guò)window.location.hash屬性讀取。它具有如下特點(diǎn):

  • hash雖然出現(xiàn)在URL中也切,但不會(huì)被包括在HTTP請(qǐng)求中扑媚。它是用來(lái)指導(dǎo)瀏覽器動(dòng)作的,對(duì)服務(wù)器端完全無(wú)用雷恃,因此疆股,改變hash不會(huì)重新加載頁(yè)面

  • 可以為hash的改變添加監(jiān)聽(tīng)事件:

    window.addEventListener("hashchange", funcRef, false)
    
  • 每一次改變hash(window.location.hash),都會(huì)在瀏覽器的訪問(wèn)歷史中增加一個(gè)記錄

利用hash的以上特點(diǎn)倒槐,就可以來(lái)實(shí)現(xiàn)前端路由“更新視圖但不重新請(qǐng)求頁(yè)面”的功能了旬痹。

HashHistory.push()

我們來(lái)看HashHistory中的push()方法:

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  this.transitionTo(location, route => {
    pushHash(route.fullPath)
    onComplete && onComplete(route)
  }, onAbort)
}

function pushHash (path) {
  window.location.hash = path
}

transitionTo()方法是父類中定義的是用來(lái)處理路由變化中的基礎(chǔ)邏輯的,push()方法最主要的是對(duì)window的hash進(jìn)行了直接賦值:

window.location.hash = route.fullPath

hash的改變會(huì)自動(dòng)添加到瀏覽器的訪問(wèn)歷史記錄中讨越。

那么視圖的更新是怎么實(shí)現(xiàn)的呢两残,我們來(lái)看父類History中transitionTo()方法的這么一段:

transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  const route = this.router.match(location, this.current)
  this.confirmTransition(route, () => {
    this.updateRoute(route)
    ...
  })
}

updateRoute (route: Route) {

  this.cb && this.cb(route)

}

listen (cb: Function) {
  this.cb = cb
}

可以看到,當(dāng)路由變化時(shí)把跨,調(diào)用了History中的this.cb方法人弓,而this.cb方法是通過(guò)History.listen(cb)進(jìn)行設(shè)置的∽胖穑回到VueRouter類定義中崔赌,找到了在init()方法中對(duì)其進(jìn)行了設(shè)置:

init (app: any /* Vue component instance */) {

  this.apps.push(app)

  history.listen(route => {
    this.apps.forEach((app) => {
      app._route = route
    })
  })
}

根據(jù)注釋,app為Vue組件實(shí)例耸别,但我們知道Vue作為漸進(jìn)式的前端框架健芭,本身的組件定義中應(yīng)該是沒(méi)有有關(guān)路由內(nèi)置屬性_route,如果組件中要有這個(gè)屬性秀姐,應(yīng)該是在插件加載的地方慈迈,即VueRouter的install()方法中混合入Vue對(duì)象的,查看install.js源碼囊扳,有如下一段:

export function install (Vue) {

  Vue.mixin({
    beforeCreate () {
      if (isDef(this.$options.router)) {
        this._router = this.$options.router
        this._router.init(this)
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      }
      registerInstance(this, this)
    },
  })
}

通過(guò)Vue.mixin()方法吩翻,全局注冊(cè)一個(gè)混合兜看,影響注冊(cè)之后所有創(chuàng)建的每個(gè) Vue 實(shí)例,該混合在beforeCreate鉤子中通過(guò)Vue.util.defineReactive()定義了響應(yīng)式的_route屬性狭瞎。所謂響應(yīng)式屬性细移,即當(dāng)_route值改變時(shí),會(huì)自動(dòng)調(diào)用Vue實(shí)例的render()方法熊锭,更新視圖弧轧。

總結(jié)一下,從設(shè)置路由改變到視圖更新的流程如下:

$router.push() --> HashHistory.push() --> History.transitionTo() --> History.updateRoute() --> {app._route = route} --> vm.render()

HashHistory.replace()

replace()方法與push()方法不同之處在于碗殷,它并不是將新路由添加到瀏覽器訪問(wèn)歷史的棧頂精绎,而是替換掉當(dāng)前的路由:

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
  )
}

可以看出,它與push()的實(shí)現(xiàn)結(jié)構(gòu)上基本相似锌妻,不同點(diǎn)在于它不是直接對(duì)window.location.hash進(jìn)行賦值代乃,而是調(diào)用window.location.replace方法將路由進(jìn)行替換。

監(jiān)聽(tīng)地址欄

以上討論的VueRouter.push()和VueRouter.replace()是可以在vue組件的邏輯代碼中直接調(diào)用的仿粹,除此之外在瀏覽器中搁吓,用戶還可以直接在瀏覽器地址欄中輸入改變路由,因此VueRouter還需要能監(jiān)聽(tīng)瀏覽器地址欄中路由的變化吭历,并具有與通過(guò)代碼調(diào)用相同的響應(yīng)行為堕仔。在HashHistory中這一功能通過(guò)setupListeners實(shí)現(xiàn):

setupListeners () {
  window.addEventListener('hashchange', () => {
    if (!ensureSlash()) {
      return
    }
    this.transitionTo(getHash(), route => {
      replaceHash(route.fullPath)
    })
  })
}

該方法設(shè)置監(jiān)聽(tīng)了瀏覽器事件hashchange,調(diào)用的函數(shù)為replaceHash晌区,即在瀏覽器地址欄中直接輸入路由相當(dāng)于代碼調(diào)用了replace()方法

HTML5History

History interface是瀏覽器歷史記錄棧提供的接口摩骨,通過(guò)back(), forward(), go()等方法,我們可以讀取瀏覽器歷史記錄棧的信息朗若,進(jìn)行各種跳轉(zhuǎn)操作恼五。

從HTML5開(kāi)始,History interface提供了兩個(gè)新的方法:pushState(), replaceState()使得我們可以對(duì)瀏覽器歷史記錄棧進(jìn)行修改:

window.history.pushState(stateObject, title, URL)
window.history.replaceState(stateObject, title, URL)
  • stateObject: 當(dāng)瀏覽器跳轉(zhuǎn)到新的狀態(tài)時(shí)捡偏,將觸發(fā)popState事件唤冈,該事件將攜帶這個(gè)stateObject參數(shù)的副本

  • title: 所添加記錄的標(biāo)題

  • URL: 所添加記錄的URL

這兩個(gè)方法有個(gè)共同的特點(diǎn):當(dāng)調(diào)用他們修改瀏覽器歷史記錄棧后峡迷,雖然當(dāng)前URL改變了银伟,但瀏覽器不會(huì)立即發(fā)送請(qǐng)求該URL(the browser won't attempt to load this URL after a call to pushState()),這就為單頁(yè)應(yīng)用前端路由“更新視圖但不重新請(qǐng)求頁(yè)面”提供了基礎(chǔ)绘搞。

我們來(lái)看vue-router中的源碼:

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  const { current: fromRoute } = this
  this.transitionTo(location, route => {
    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(cleanPath(this.base + route.fullPath))
    handleScroll(this.router, route, fromRoute, false)
    onComplete && onComplete(route)
  }, onAbort)
}

// src/util/push-state.js
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
  const history = window.history
  try {
    if (replace) {
      history.replaceState({ key: _key }, '', url)
    } else {
      _key = genKey()
      history.pushState({ key: _key }, '', url)
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url)
  }
}

export function replaceState (url?: string) {
  pushState(url, true)
}

代碼結(jié)構(gòu)以及更新視圖的邏輯與hash模式基本類似彤避,只不過(guò)將對(duì)window.location.hash直接進(jìn)行賦值window.location.replace()改為了調(diào)用history.pushState()和history.replaceState()方法。

在HTML5History中添加對(duì)修改瀏覽器地址欄URL的監(jiān)聽(tīng)是直接在構(gòu)造函數(shù)中執(zhí)行的:

constructor (router: Router, base: ?string) {

  window.addEventListener('popstate', e => {
    const current = this.current
    this.transitionTo(getLocation(this.base), route => {
      if (expectScroll) {
        handleScroll(router, route, current, true)
      }
    })
  })
}

當(dāng)然了HTML5History用到了HTML5的新特特性夯辖,是需要特定瀏覽器版本的支持的琉预,前文已經(jīng)知道,瀏覽器是否支持是通過(guò)變量supportsPushState來(lái)檢查的:

// src/util/push-state.js
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
})()

以上就是hash模式與history模式源碼的導(dǎo)讀蒿褂,這兩種模式都是通過(guò)瀏覽器接口實(shí)現(xiàn)的圆米,除此之外vue-router還為非瀏覽器環(huán)境準(zhǔn)備了一個(gè)abstract模式卒暂,其原理為用一個(gè)數(shù)組stack模擬出瀏覽器歷史記錄棧的功能。當(dāng)然娄帖,以上只是一些核心邏輯也祠,為保證系統(tǒng)的魯棒性源碼中還有大量的輔助邏輯,也很值得學(xué)習(xí)近速。此外在vue-router中還有路由匹配诈嘿、router-view視圖組件等重要部分,關(guān)于整體源碼的閱讀推薦滴滴前端的這篇文章

兩種模式比較

在一般的需求場(chǎng)景中削葱,hash模式與history模式是差不多的奖亚,但幾乎所有的文章都推薦使用history模式,理由竟然是:"#" 符號(hào)太丑...0_0 "

如果不想要很丑的 hash析砸,我們可以用路由的 history 模式 ——官方文檔

當(dāng)然昔字,嚴(yán)謹(jǐn)?shù)奈覀兛隙ú粦?yīng)該用顏值評(píng)價(jià)技術(shù)的好壞。根據(jù)MDN的介紹首繁,調(diào)用history.pushState()相比于直接修改hash主要有以下優(yōu)勢(shì):

  • pushState設(shè)置的新URL可以是與當(dāng)前URL同源的任意URL李滴;而hash只可修改#后面的部分,故只可設(shè)置與當(dāng)前同文檔的URL

  • pushState設(shè)置的新URL可以與當(dāng)前URL一模一樣蛮瞄,這樣也會(huì)把記錄添加到棧中所坯;而hash設(shè)置的新值必須與原來(lái)不一樣才會(huì)觸發(fā)記錄添加到棧中

  • pushState通過(guò)stateObject可以添加任意類型的數(shù)據(jù)到記錄中;而hash只可添加短字符串

  • pushState可額外設(shè)置title屬性供后續(xù)使用

history模式的一個(gè)問(wèn)題

我們知道對(duì)于單頁(yè)應(yīng)用來(lái)講挂捅,理想的使用場(chǎng)景是僅在進(jìn)入應(yīng)用時(shí)加載index.html芹助,后續(xù)在的網(wǎng)絡(luò)操作通過(guò)Ajax完成,不會(huì)根據(jù)URL重新請(qǐng)求頁(yè)面闲先,但是難免遇到特殊情況状土,比如用戶直接在地址欄中輸入并回車(chē),瀏覽器重啟重新加載應(yīng)用等伺糠。

hash模式僅改變hash部分的內(nèi)容蒙谓,而hash部分是不會(huì)包含在HTTP請(qǐng)求中的:

http://oursite.com/#/user/id   // 如重新請(qǐng)求只會(huì)發(fā)送http://oursite.com/

故在hash模式下遇到根據(jù)URL請(qǐng)求頁(yè)面的情況不會(huì)有問(wèn)題。

而history模式則會(huì)將URL修改得就和正常請(qǐng)求后端的URL一樣

http://oursite.com/user/id

在此情況下重新向后端發(fā)送請(qǐng)求训桶,如后端沒(méi)有配置對(duì)應(yīng)/user/id的路由處理累驮,則會(huì)返回404錯(cuò)誤。官方推薦的解決辦法是在服務(wù)端增加一個(gè)覆蓋所有情況的候選資源:如果 URL 匹配不到任何靜態(tài)資源舵揭,則應(yīng)該返回同一個(gè) index.html 頁(yè)面谤专,這個(gè)頁(yè)面就是你 app 依賴的頁(yè)面。同時(shí)這么做以后午绳,服務(wù)器就不再返回 404 錯(cuò)誤頁(yè)面置侍,因?yàn)閷?duì)于所有路徑都會(huì)返回 index.html 文件。為了避免這種情況,在 Vue 應(yīng)用里面覆蓋所有的路由情況蜡坊,然后在給出一個(gè) 404 頁(yè)面杠输。或者秕衙,如果是用 Node.js 作后臺(tái)抬伺,可以使用服務(wù)端的路由來(lái)匹配 URL,當(dāng)沒(méi)有匹配到路由的時(shí)候返回 404灾梦,從而實(shí)現(xiàn) fallback峡钓。

直接加載應(yīng)用文件

Tip: built files are meant to be served over an HTTP server.

Opening index.html over file:// won't work.

Vue項(xiàng)目通過(guò)vue-cli的webpack打包完成后,命令行會(huì)有這么一段提示若河。通常情況能岩,無(wú)論是開(kāi)發(fā)還是線上,前端項(xiàng)目都是通過(guò)服務(wù)器訪問(wèn)萧福,不存在 "Opening index.html over file://" 拉鹃,但程序員都知道,需求和場(chǎng)景永遠(yuǎn)是千奇百怪的鲫忍,只有你想不到的膏燕,沒(méi)有產(chǎn)品經(jīng)理想不到的。

本文寫(xiě)作的初衷就是遇到了這樣一個(gè)問(wèn)題:需要快速開(kāi)發(fā)一個(gè)移動(dòng)端的展示項(xiàng)目悟民,決定采用WebView加載Vue單頁(yè)應(yīng)用的形式坝辫,但沒(méi)有后端服務(wù)器提供,所以所有資源需從本地文件系統(tǒng)加載:

// AndroidAppWrapper
public class MainActivity extends AppCompatActivity {

    private WebView webView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        webView = new WebView(this);
        webView.getSettings().setJavaScriptEnabled(true);
        webView.loadUrl("file:///android_asset/index.html");
        setContentView(webView);
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if ((keyCode == KeyEvent.KEYCODE_BACK) && webView.canGoBack()) {
            webView.goBack();
            return true;
        }
        return false;
    }
}

此情此景看來(lái)是必須 "Opening index.html over file://" 了射亏,為此近忙,我首先要進(jìn)行了一些設(shè)置

  • 在項(xiàng)目config.js文件中將assetsPublicPath字段的值改為相對(duì)路徑 './'

  • 調(diào)整生成的static文件夾中圖片等靜態(tài)資源的位置與代碼中的引用地址一致

這是比較明顯的需要改動(dòng)之處,但改完后依舊無(wú)法順利加載智润,經(jīng)過(guò)反復(fù)排查發(fā)現(xiàn)及舍,項(xiàng)目在開(kāi)發(fā)時(shí),router設(shè)置為了history模式(為了美觀...0_0")窟绷,當(dāng)改為hash模式后就可正常加載了锯玛。

為什么會(huì)出現(xiàn)這種情況呢?我分析原因可能如下:

當(dāng)從文件系統(tǒng)中直接加載index.html時(shí)兼蜈,URL為:

file:///android_asset/index.html

而首頁(yè)視圖需匹配的路徑為path: '/' :

export default new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'index',
      component: IndexView
    }
  ]
})

我們先來(lái)看history模式攘残,在HTML5History中:

ensureURL (push?: boolean) {
  if (getLocation(this.base) !== this.current.fullPath) {
    const current = cleanPath(this.base + this.current.fullPath)
    push ? pushState(current) : replaceState(current)
  }
}

export function getLocation (base: string): string {
  let path = window.location.pathname
  if (base && path.indexOf(base) === 0) {
    path = path.slice(base.length)
  }
  return (path || '/') + window.location.search + window.location.hash
}

邏輯只會(huì)確保存在URL,path是通過(guò)剪切的方式直接從window.location.pathname獲取到的饭尝,它的結(jié)尾是index.html肯腕,因此匹配不到 '/' 献宫,故 "Opening index.html over file:// won't work" 钥平。

再看hash模式,在HashHistory中:

export class HashHistory extends History {
  constructor (router: Router, base: ?string, fallback: boolean) {
    ...
    ensureSlash()
  }

  // this is delayed until the app mounts
  // to avoid the hashchange listener being fired too early
  setupListeners () {
    window.addEventListener('hashchange', () => {
      if (!ensureSlash()) {
        return
      }
      ...
    })
  }

  getCurrentLocation () {
    return getHash()
  }
}

function ensureSlash (): boolean {
  const path = getHash()
  if (path.charAt(0) === '/') {
    return true
  }
  replaceHash('/' + path)
  return false
}

export function getHash (): string {
  const href = window.location.href
  const index = href.indexOf('#')
  return index === -1 ? '' : href.slice(index + 1)
}

我們看到在代碼邏輯中,多次出現(xiàn)一個(gè)函數(shù)ensureSlash()涉瘾,當(dāng)#符號(hào)后緊跟著的是'/'知态,則返回true,否則強(qiáng)行插入這個(gè)'/'立叛,故我們可以看到负敏,即使是從文件系統(tǒng)打開(kāi)index.html,URL依舊會(huì)變?yōu)橐韵滦问剑?/p>

file:///C:/Users/dist/index.html#/

getHash()方法返回的path為 '/' 秘蛇,可與首頁(yè)視圖的路由匹配其做。

故要想從文件系統(tǒng)直接加載Vue單頁(yè)應(yīng)用而不借助后端服務(wù)器,除了打包后的一些路徑設(shè)置外赁还,還需確保vue-router使用的是hash模式妖泄。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市艘策,隨后出現(xiàn)的幾起案子蹈胡,更是在濱河造成了極大的恐慌,老刑警劉巖朋蔫,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件罚渐,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡驯妄,警方通過(guò)查閱死者的電腦和手機(jī)荷并,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)青扔,“玉大人璧坟,你說(shuō)我怎么就攤上這事∈昱常” “怎么了雀鹃?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)励两。 經(jīng)常有香客問(wèn)我黎茎,道長(zhǎng),這世上最難降的妖魔是什么当悔? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任傅瞻,我火速辦了婚禮,結(jié)果婚禮上盲憎,老公的妹妹穿的比我還像新娘嗅骄。我一直安慰自己,他們只是感情好饼疙,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布溺森。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪屏积。 梳的紋絲不亂的頭發(fā)上医窿,一...
    開(kāi)封第一講書(shū)人閱讀 49,036評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音炊林,去河邊找鬼姥卢。 笑死,一個(gè)胖子當(dāng)著我的面吹牛渣聚,可吹牛的內(nèi)容都是我干的独榴。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼奕枝,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼括眠!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起倍权,我...
    開(kāi)封第一講書(shū)人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤掷豺,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后薄声,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體当船,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年默辨,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了德频。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡缩幸,死狀恐怖壹置,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情表谊,我是刑警寧澤钞护,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站爆办,受9級(jí)特大地震影響难咕,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜距辆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一余佃、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧跨算,春花似錦爆土、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)氧猬。三九已至,卻和暖如春立润,著一層夾襖步出監(jiān)牢的瞬間狂窑,已是汗流浹背媳板。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工桑腮, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蛉幸。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓破讨,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親奕纫。 傳聞我的和親對(duì)象是個(gè)殘疾皇子提陶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • PS:轉(zhuǎn)載請(qǐng)注明出處作者: TigerChain地址http://www.reibang.com/p/9a7d7...
    TigerChain閱讀 63,346評(píng)論 9 218
  • 隨著前端應(yīng)用的業(yè)務(wù)功能起來(lái)越復(fù)雜隙笆,用戶對(duì)于使用體驗(yàn)的要求越來(lái)越高,單面(SPA)成為前端應(yīng)用的主流形式升筏。大型單頁(yè)應(yīng)...
    指尖跳動(dòng)閱讀 1,398評(píng)論 0 0
  • 介紹 vue-router是一個(gè)vue插件撑柔。其實(shí)質(zhì)是在location.hash、location.replace...
    AmazRan閱讀 1,541評(píng)論 0 6
  • 隨著前端應(yīng)用的業(yè)務(wù)功能起來(lái)越復(fù)雜您访,用戶對(duì)于使用體驗(yàn)的要求越來(lái)越高铅忿,單面(SPA)成為前端應(yīng)用的主流形式。大型單頁(yè)應(yīng)...
    bayi_lzp閱讀 5,672評(píng)論 0 2
  • 一灵汪、前言 要學(xué)習(xí)vue-router就要先知道這里的路由是什么檀训?為什么我們不能像原來(lái)一樣直接用 標(biāo)簽編寫(xiě)鏈接哪?...
    浪里行舟閱讀 67,643評(píng)論 12 204