关于vue-router:vuerouter

6次阅读

共计 16210 个字符,预计需要花费 41 分钟才能阅读完成。

vue@3.3.4 源码解析

前端路由次要有 2 局部组成:1、url 的解决;2、组件的加载

[toc]

install 函数

export function install (Vue) {
  // 防止反复加载
  if (install.installed && _Vue === Vue) return
  install.installed = true

  _Vue = Vue

  const isDef = v => v !== undefined

  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {i(vm, callVal)
    }
  }

  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)
    }
  })
  // $router 和 $route 绑定在 Vue 的原型对象上,不便全局拜访
  Object.defineProperty(Vue.prototype, '$router', {get () {return this._routerRoot._router}
  })

  Object.defineProperty(Vue.prototype, '$route', {get () {return this._routerRoot._route}
  })

  // 注册 router-view 和 router-link 组件
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

  const strats = Vue.config.optionMergeStrategies
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

vuex 一样,vue-router的注入机会也是在 beforeCreated. 不过在destroyed 的生命周期钩子中多了一步

VueRouter 类

History 类

export class History {
  // 定义公有变量
  router: Router
  base: string
  current: Route
  pending: ?Route
  cb: (r: Route) => void
  ready: boolean
  readyCbs: Array<Function>
  readyErrorCbs: Array<Function>
  errorCbs: Array<Function>
  listeners: Array<Function>
  cleanupListeners: Function

  // implemented by sub-classes
  +go: (n: number) => void
  +push: (loc: RawLocation, onComplete?: Function, onAbort?: Function) => void
  +replace: (
    loc: RawLocation,
    onComplete?: Function,
    onAbort?: Function
  ) => void
  +ensureURL: (push?: boolean) => void
  +getCurrentLocation: () => string
  +setupListeners: Function

  constructor (router: Router, base: ?string) {
    this.router = router
    this.base = normalizeBase(base)
    // start with a route object that stands for "nowhere"
    this.current = START
    this.pending = null
    this.ready = false
    this.readyCbs = []
    this.readyErrorCbs = []
    this.errorCbs = []
    this.listeners = []}

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

  onReady (cb: Function, errorCb: ?Function) {if (this.ready) {cb()
    } else {this.readyCbs.push(cb)
      if (errorCb) {this.readyErrorCbs.push(errorCb)
      }
    }
  }

  onError (errorCb: Function) {this.errorCbs.push(errorCb)
  }
  // 重点:transitionTo (
    location: RawLocation,
    onComplete?: Function,
    onAbort?: Function
  ) {
    let route
    // catch redirect option https://github.com/vuejs/vue-router/issues/3201
    try {
      // 获取匹配的路由对象
      route = this.router.match(location, this.current)
    } catch (e) {
      this.errorCbs.forEach(cb => {cb(e)
      })
      // Exception should still be thrown
      throw e
    }
    // 保留旧路由对象
    const prev = this.current
    this.confirmTransition(
      route,
      () => {
        // 更新路由对象
        this.updateRoute(route)
        onComplete && onComplete(route)
        this.ensureURL()
        this.router.afterHooks.forEach(hook => {hook && hook(route, prev)
        })

        // 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) {
          // Initial redirection should not mark the history as ready yet
          // because it's triggered by the redirection instead
          // https://github.com/vuejs/vue-router/issues/3225
          // https://github.com/vuejs/vue-router/issues/3331
          if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) {
            this.ready = true
            this.readyErrorCbs.forEach(cb => {cb(err)
            })
          }
        }
      }
    )
  }

  confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
    const current = this.current
    this.pending = route
    const abort = err => {
      // changed after adding errors with
      // https://github.com/vuejs/vue-router/pull/3047 before that change,
      // redirect and aborted navigation would produce an err == null
      if (!isNavigationFailure(err) && isError(err)) {if (this.errorCbs.length) {
          this.errorCbs.forEach(cb => {cb(err)
          })
        } else {if (process.env.NODE_ENV !== 'production') {warn(false, 'uncaught error during route navigation:')
          }
          console.error(err)
        }
      }
      onAbort && onAbort(err)
    }
    const lastRouteIndex = route.matched.length - 1
    const lastCurrentIndex = current.matched.length - 1
    if (isSameRoute(route, current) &&
      // in the case the route map has been dynamically appended to
      lastRouteIndex === lastCurrentIndex &&
      route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
    ) {this.ensureURL()
      if (route.hash) {handleScroll(this.router, current, route, false)
      }
      return abort(createNavigationDuplicatedError(current, route))
    }

    const {updated, deactivated, activated} = resolveQueue(
      this.current.matched,
      route.matched
    )

    const queue: Array<?NavigationGuard> = [].concat(
      // in-component leave guards
      extractLeaveGuards(deactivated),
      // global before hooks
      this.router.beforeHooks,
      // in-component update hooks
      extractUpdateHooks(updated),
      // in-config enter guards
      activated.map(m => m.beforeEnter),
      // async components
      resolveAsyncComponents(activated)
    )

    const iterator = (hook: NavigationGuard, next) => {if (this.pending !== route) {return abort(createNavigationCancelledError(current, route))
      }
      try {hook(route, current, (to: any) => {if (to === false) {// next(false) -> abort navigation, ensure current URL
            this.ensureURL(true)
            abort(createNavigationAbortedError(current, route))
          } else if (isError(to)) {this.ensureURL(true)
            abort(to)
          } else if (
            typeof to === 'string' ||
            (typeof to === 'object' &&
              (typeof to.path === 'string' || typeof to.name === 'string'))
          ) {// next('/') or next({path: '/'}) -> redirect
            abort(createNavigationRedirectedError(current, route))
            if (typeof to === 'object' && to.replace) {this.replace(to)
            } else {this.push(to)
            }
          } else {
            // confirm transition and pass on the value
            next(to)
          }
        })
      } catch (e) {abort(e)
      }
    }

    runQueue(queue, iterator, () => {
      // wait until async components are resolved before
      // extracting in-component enter guards
      const enterGuards = extractEnterGuards(activated)
      const queue = enterGuards.concat(this.router.resolveHooks)
      runQueue(queue, iterator, () => {if (this.pending !== route) {return abort(createNavigationCancelledError(current, route))
        }
        this.pending = null
        onComplete(route)
        if (this.router.app) {this.router.app.$nextTick(() => {handleRouteEntered(route)
          })
        }
      })
    })
  }

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

  setupListeners () {// Default implementation is empty}

  teardown () {
    // clean up event listeners
    // https://github.com/vuejs/vue-router/issues/2341
    this.listeners.forEach(cleanupListener => {cleanupListener()
    })
    this.listeners = []

    // reset current history route
    // https://github.com/vuejs/vue-router/issues/3294
    this.current = START
    this.pending = null
  }
}

HTML5History

export class HTML5History extends History {
  _startLocation: string

  constructor (router: Router, base: ?string) {super(router, base)
     // 
    this._startLocation = getLocation(this.base)
  }
  // 设置监听器
  setupListeners () {
    // 如果存在 listener,间接返回
    if (this.listeners.length > 0) {return}

    const router = this.router
    const expectScroll = router.options.scrollBehavior
    // 是否反对 history.pushState api
    const supportsScroll = supportsPushState && expectScroll
        
    // 如果浏览器反对 pushState 的 api,则设置 popstate 的监听事件
    if (supportsScroll) {this.listeners.push(setupScroll())
    }
    
    // 解决路由监听事件函数
    const handleRoutingEvent = () => {
      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 事件,导致路由状态更新不同步
      const location = getLocation(this.base)
      if (this.current === START && location === this._startLocation) {return}

      this.transitionTo(location, route => {if (supportsScroll) {
          // 保留页面滚动条地位
          handleScroll(router, route, current, true)
        }
      })
    }
    window.addEventListener('popstate', handleRoutingEvent)
    this.listeners.push(() => {window.removeEventListener('popstate', handleRoutingEvent)
    })
  }
  // 封装 history go 办法
  go (n: number) {window.history.go(n)
  }
  // 
  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)
  }

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

  getCurrentLocation (): string {return getLocation(this.base)
  }
}

export function getLocation (base: string): string {
  let path = window.location.pathname
  const pathLowerCase = path.toLowerCase()
  const baseLowerCase = base.toLowerCase()
  // base="/a" shouldn't turn path="/app"into"/a/pp"
  // https://github.com/vuejs/vue-router/issues/3555
  // so we ensure the trailing slash in the base
  if (base && ((pathLowerCase === baseLowerCase) ||
    (pathLowerCase.indexOf(cleanPath(baseLowerCase + '/')) === 0))) {path = path.slice(base.length)
  }
  return (path || '/') + window.location.search + window.location.hash
}

getLocation

获取 url 的 path,不蕴含 base

export function getLocation (base: string): string {
  let path = window.location.pathname
  const pathLowerCase = path.toLowerCase()
  const baseLowerCase = base.toLowerCase()
  // base="/a" shouldn't turn path="/app"into"/a/pp"
  // https://github.com/vuejs/vue-router/issues/3555
  // so we ensure the trailing slash in the base
  if (base && ((pathLowerCase === baseLowerCase) ||
    (pathLowerCase.indexOf(cleanPath(baseLowerCase + '/')) === 0))) {path = path.slice(base.length)
  }
  return (path || '/') + window.location.search + window.location.hash
}

setupScroll 函数

export function setupScroll () {
  // Prevent browser scroll behavior on History popstate
  if ('scrollRestoration' in window.history) {window.history.scrollRestoration = 'manual'}
  // Fix for #1585 for Firefox
  // Fix for #2195 Add optional third attribute to workaround a bug in safari https://bugs.webkit.org/show_bug.cgi?id=182678
  // Fix for #2774 Support for apps loaded from Windows file shares not mapped to network drives: replaced location.origin with
  // window.location.protocol + '//' + window.location.host
  // location.host contains the port and location.hostname doesn't
  const protocolAndPath = window.location.protocol + '//' + window.location.host
  const absolutePath = window.location.href.replace(protocolAndPath, '')
  // preserve existing history state as it could be overriden by the user
  const stateCopy = extend({}, window.history.state)
  stateCopy.key = getStateKey()
  window.history.replaceState(stateCopy, '', absolutePath)
  window.addEventListener('popstate', handlePopState)
  return () => {window.removeEventListener('popstate', handlePopState)
  }
}

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
  const history = window.history
  try {if (replace) {
      // preserve existing history state as it could be overriden by the user
      const stateCopy = extend({}, history.state)
      stateCopy.key = getStateKey()
      history.replaceState(stateCopy, '', url)
    } else {history.pushState({ key: setStateKey(genStateKey()) }, '', url)
    }
  } catch (e) {window.location[replace ? 'replace' : 'assign'](url)
  }
}

replaceState 函数

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

HashHistory

export class HashHistory extends History {constructor (router: Router, base: ?string, fallback: boolean) {super(router, base)
    // check history fallback deeplinking
    if (fallback && checkFallback(this.base)) {return}
    ensureSlash()}

  // this is delayed until the app mounts
  // to avoid the hashchange listener being fired too early
  setupListeners () {if (this.listeners.length > 0) {return}

    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll

    if (supportsScroll) {this.listeners.push(setupScroll())
    }

    const handleRoutingEvent = () => {
      const current = this.current
      if (!ensureSlash()) {return}
      this.transitionTo(getHash(), route => {if (supportsScroll) {handleScroll(this.router, route, current, true)
        }
        if (!supportsPushState) {replaceHash(route.fullPath)
        }
      })
    }
    // 如果反对 popstate 则,应用 popstate 监听,否则应用 hashchange 监听,留神 vue-router4 曾经不反对 hashchange 监听
    const eventType = supportsPushState ? 'popstate' : 'hashchange'
    window.addEventListener(
      eventType,
      handleRoutingEvent
    )
    this.listeners.push(() => {window.removeEventListener(eventType, handleRoutingEvent)
    })
  }
  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
    )
  }

  go (n: number) {window.history.go(n)
  }

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

  getCurrentLocation () {return getHash()
  }
}

pushHash

function pushHash (path) {if (supportsPushState) {pushState(getUrl(path))
  } else {window.location.hash = path}
}

replaceHash

function replaceHash (path) {if (supportsPushState) {replaceState(getUrl(path))
  } else {window.location.replace(getUrl(path))
  }
}

Hash 路由与 History 的区别

  1. hash 路由在地址栏 URL 上有 #,而 history 路由没有会难看一点
  2. 咱们进行回车刷新操作,hash 路由会加载到地址栏对应的页面,而 history 路由个别就 404 报错了,所以 history 在部署的时候,如 nginx,须要只渲染⾸⻚,让⾸⻚依据门路从新跳转。
  3. hash 路由反对低版本的浏览器,而 history 路由是 HTML5 新增的 API。(IE10 及以上)
  4. 默认扭转 hash 路由,浏览器端不会发出请求,次要用于锚点;history 路由 go/back/forward 以及浏览器中的后退,后退按钮,个别都会向服务器发动申请
  5. hash 模式,是不⽀持 SSR 的,然而 history 模式能够做 SSR

History 对象

  • pushState / replaceState 都不会触发 popState 事件
  • popState 什么时候触发

    • 点击浏览器的后退 / 后退按钮
    • back/forward/go

导航守卫的触发程序

  1. 【组件】- 前一个组件的 beforeRouteLeave
  2. 【全局】- router.beforeEach
  3. 【组件】- 如果是路由的参数变动,会触发 beforeRouteUpdate
  4. 【配置文件】- beforeEnter
  5. 【组件】外部申明的 beforeRouteEnter
  6. 【全局】beforeResolve
  7. 【全局】router.afterEach

手写 Hsitory 路由

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>History 路由 </title>
  </head>
  <body>
    <div id="container">
      <button onclick="window.location.hash ='#'"> 首页 </button>
      <button onclick="window.location.hash ='#about'"> 对于咱们 </button>
      <button onclick="window.location.hash ='#user'"> 用户列表 </button>
    </div>

    <div id="context"></div>
    <script>
      class HistoryRouter {constructor() {this.routes = {};
          this._bindPopstate();
          // this.init();}

        init(path) {window.history.replaceState({ path}, null, path);
          const cb = this.routes[path];
          if (cb) {cb();
          }
        }

        route(path, callback) {this.routes[path] = callback || function () {};
        }

        go(path) {window.history.pushState({ path}, null, path);
          const cb = this.routes[path];
          if (cb) {cb();
          }
        }

        _bindPopstate() {window.addEventListener("popstate", (e) => {
            const path = e.state && e.state.path;
            this.routes[path] && this.routes[path]();});
        }
      }

      const Route = new HistoryRouter();

      Route.route("./about", () => changeText("对于咱们页面"));
      Route.route("./user", () => changeText("用户列表页"));
      Route.route("./", () => changeText("首页"));

      function changeText(arg) {document.getElementById("context").innerHTML = arg;
      }

      container.addEventListener("click", (e) => {if (e.target.tagName === "A") {e.preventDefault();
          Route.go(e.target.getAttribute("href"));
        }
      });
    </script>
  </body>
</html>

手写 Hash 路由

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Hash 路由 </title>
  </head>
  <body>
    <div id="container">
      <button onclick="window.location.hash ='#'"> 首页 </button>
      <button onclick="window.location.hash ='#about'"> 对于咱们 </button>
      <button onclick="window.location.hash ='#user'"> 用户列表 </button>
    </div>

    <div id="context"></div>
    <script>
      class HashRouter {constructor() {this.routes = {};
          this.refresh = this.refresh.bind(this);
          window.addEventListener("load", this.refresh);
          window.addEventListener("hashchange", this.refresh);
        }

        route(path, callback) {this.routes[path] = callback || function () {};
        }

        refresh() {const path = `/${window.location.hash.slice(1) || ""}`;
          this.routes[path]();}
      }
      const Route = new HashRouter();

      Route.route("/about", () => changeText("对于咱们页面"));
      Route.route("/user", () => changeText("用户列表页"));
      Route.route("/", () => changeText("首页"));
      
      function changeText(arg) {document.getElementById("context").innerHTML = arg;
      }

    </script>
  </body>
</html>

webpack import 函数

vue 路由懒加载时通过 Vue 的异步组件和 Webpack 的代码宰割性能来实现的。

代码宰割是 webpack 最引人注目的个性之一。这个个性容许您将代码宰割成各种包,而后能够按需或并行加载这些包。它能够用来实现更小的 bundle 和管制资源加载优先级,如果应用正确,会对加载工夫产生重大影响。

这里次要剖析 import() 这种代码宰割形式,其余形式疏忽。

  1. 首先,webpack遇到 import 办法 时,会将其当成一个代码宰割点,也就是说碰到 import 办法 了,那么就去解析import 办法
  2. 而后,import援用的文件,webpack会将其编译成一个 jsonp,也就是一个自执行函数,而后函数外部是援用的文件的内容,因为到时候是通过jsonp 的办法去加载的
  3. 具体就是,import援用文件,会先调用 require.ensure 办法 (打包的后果来看叫require.e),这个办法次要是结构一个promise,会将resolverejectpromise放到一个数组中,将 promise 放到一个队列中。
  4. 而后,调用 require.load(打包后果来看叫require.l) 办法,这个办法次要是创立一个 jsonp,也就是创立一个script 标签,标签的 url 就是文件加载地址,而后塞到 document.head 中,一塞进去,就会加载该文件了。
  5. 加载完,就去执行这段 jsonp,次要就是把moduleIdmodule内容存到 modules 数组中,而后再去走 webpack 内置的require
  6. webpack 内置的 require,次要是先判断缓存,这个 moduleId 是否缓存过了,如果缓存过了,就间接返回。如果没有缓存,再持续往下走,也就是加载 module 内容,而后最终内容会挂在都 module,exports 上,返回 module.exports 就返回了援用文件的最终执行后果。

简略讲就是:promise.all +jsonp + 动态创建 script 标签

参考文章

  • window History
  • hash 路由和 history 路由的区别
  • webpack 动静加载原理 /webpack 异步加载原理 /webpack 动静解析 import()原理
正文完
 0