原稿:语雀 · 【原理四】前端路由 & Vue-router · 原目录:Vue › 【原理四】前端路由 & Vue-router

Ajax

参考链接:前端面试题——自己实现Ajax - Smiley的文章 - 知乎

没有 Ajax 之前,浏览器发起请求的场景 2. 地址栏:用户在地址栏输入 http://baidu.com ,按回车,就向 http://baidu.com 发起了一个请求(同时页面刷新) 4. a 标签:用户点击页面中的 a 链接,也会发起一个请求(同时页面刷新) 6. img 标签:页面中如果有 img 标签,那么就会发起一个对此图片的请求(页面没有刷新,但是只能请求图片)类似的还有 link 标签、script 标签,都可以对一类文件的请求

除了最后的资源可以不刷新直接请求,其余的请求都必须要刷新页面,假设填写了一个表单,点击确定后进行后端验证,但验证的同时页面刷新,即便有了验证结果,表单内容也已经消失。这种炸裂的体验催生了 Ajax,其主要实现了不刷新页面的情况下进行请求

AJAX = 异步 JavaScript 和 XML(Asynchronous Javascript And XML)

AJAX 是一种用于创建快速动态网页的技术。通过在后台与服务器进行少量数据交换,AJAX 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。

以上是百科的结果,个人理解:异步 JS 是用于请求和响应的手段,而 XML 则是数据交换的格式——使用 js 调用异步通讯组件并使用格式化的数据来更新 web 页面上的内容或操作过程称为 Ajax

Ajax 的工作流程可分为四步: 2. 页面当中的事件或者函数触发 Ajax 请求的时候,建立 XMLHttpRequest 对象(XHR),以进行接下来的请求 4. 调用 XHR 的 open 函数 6. XHR 向服务器发送请求 8. 服务器处理请求之后,会把响应返回到网页,此时,XHR 会监听到这个响应,并执行响应的代码

Ajax 使得异步数据请求在不刷新页面的情况下可以进行,而 js 可以根据响应结果操作 DOM,实现渲染内容的更新,达到局部刷新的目的。这种异步请求和局部刷新的机制日渐流行,其中的数据交换格式也渐渐转向与 js 更搭的 JSON


前端路由

异步交互体验的更好扩展就是 SPA——单页面应用,不仅仅是无刷新的内容请求,页面跳转本身也是无刷新的,前端路由也随之出现——匹配不同的 url,更新视图但不重新请求页面

Ajax 不改变页面 url 实现了无刷新,但路由跳转无法避免 url 变化,势必造成页面的刷新,前端路由通常采取两种模式解决这个问题

hash 模式

该模式下的 url 通常会带有"#",类似

http://www.xxx.com/#/login
  • “#“是 DOM 中 Location 对象(当前 URL 信息)的 hash 属性,通常利用这个属性实现页内链接,点击后滚动到对应位置。”#” 只能指导浏览器动作,http 请求中"#“后内容会被忽略,改变 hash 值不会导致页面刷新
  • 真正处理 url 改变的地方:当 hash 改变时,会触发 hashchange 事件,该事件上的回调则会进行路由匹配、请求、响应、更新等等
function matchAndUpdate () {
   // todo 匹配 hash 做 dom 更新操作
}
window.addEventListener('hashchange', matchAndUpdate)

history 模式

html5 中出现了两个新 API:

window.history.pushState(stateObject, title, URL)
window.history.replaceState(stateObject, title, URL)
  • stateObject:浏览器跳转到新状态时,将触发 popState 事件,该事件将携带这个 stateObject 参数的副本
  • title:所添加记录的标题
  • URL:所添加记录的URL

这两个方法有个共同的特点:当调用它们修改浏览器历史记录栈后,虽然当前URL改变了,但浏览器不会立即发送请求该URL(the browser won’t attempt to load this URL after a call to pushState()),这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础

function matchAndUpdate () {
   // todo 匹配路径 做 dom 更新操作
}
window.addEventListener('popstate', matchAndUpdate)

Vue Router

参考链接:【源码拾遗】从vue-router看前端路由的两种实现 - Entronad的文章 - 知乎

注入

vue-router 和 vuex 一样,都是用 Vue.mixin() 进行了全局混入: 2. vue-router 在 beforeCreate() 钩子中创建this._router,表示 VueRouter 的实例 4. 之后执行 this._router.init() 方法初始化 router 6. 通过 defineReactive 把 this._route 变成每个组件上的响应式属性

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

vue-router 通过 mode 参数控制路由的模式,默认为 hash 模式,可以在创建 router 实例时修改

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

在 VueRouter 的构造函数中,根据传入 mode 的不同可以创建3类 history 对象:前两种即 history 和 hash 模式,history 对象本身承载了各类路由操作

HashHistory

image.png

hash.js 中 push() 和 repace() 即对应了 Vue Router 的 router.push()router.replace(),当中首先调用 transitionTo(),在传入的回调中调用 pushHash() 与 replaceHash()

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

怎么更新渲染?

transitionTo() 方法定义在 base.js: 2. 首先调用 router.match(),它会根据传入的 location 和当前路径匹配一个新路径 4. 执行 confirmTransition() 开始路径切换,在切换完成的回调中 6. 执行 updateRoute():在其中调用回调函数 this.cb,而 cb 在 listen() 被赋值,它的执行效果是修改 this.apps 数组中每个组件实例上的 _route 属性,一般的场景数组只会保存根 Vue 实例;而这一属性在已经在混入的 beforeCreate() 中成为了组件上的响应式属性 8. <router-view> 组件会在渲染时通过 getter 获取 _route,因此当该属性变化,会通知 <router-view> 的渲染 watcher 更新,重新渲染组件

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

updateRoute (route: Route) {
  this.cb && this.cb(route)
}

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

回到 VueRouter 的构造函数:当中调用 listen(),并传入了箭头函数作为 cb,函数中为 app 的 _route 属性赋值。app 中本身没有有关路由内置属性 _route,是在 VueRouter 的 install() 方法中混合入的,成为了 app 的响应式属性,因此当 _route 改变时,会触发重新渲染

怎么改变 url ?

在 transitionTo() 完成路径切换后的回调中进行,这两个方法实际上都是对 window.location 进行操作: