likes
comments
collection
share

【vue-router源码】六、router.resolve源码解析

作者站长头像
站长
· 阅读数 291

前言

【vue-router源码】系列文章将带你从0开始了解vue-router的具体实现。该系列文章源码参考vue-router v4.0.15。源码地址:https://github.com/vuejs/router阅读该文章的前提是你最好了解vue-router的基本使用,如果你没有使用过的话,可通过vue-router官网学习下。

该篇文章将介绍router.resolve的实现。

使用

router.resolve方法返回路由地址的标准化版本。

router.resolve('admin')
router.resolve({ path: '/admin' })

resolve

resolve接收两个参数:rawLocationcurrentLocation(可选)。其中rawLocation是待转换的路由,rawLocation可以是个对象也可以是个字符串。currentLocation不传默认是currentRoute

resolve中有是两个分支:

  • 如果rawLocationstring类型调用parseURL解析rawLocation:

    const locationNormalized = parseURL(
    parseQuery,
    rawLocation,
    currentLocation.path
    )

    parseURL接收三个参数:parseQuery(一个query解析函数)、location(被解析的location)、currentLocation(当前的location)。

    export function parseURL(
    parseQuery: (search: string) => LocationQuery,
    location: string,
    currentLocation: string = '/'
    ): LocationNormalized {
    let path: string | undefined,
      query: LocationQuery = {},
      searchString = '',
      hash = ''
    
    // location中?的位置
    const searchPos = location.indexOf('?')
    // location中#的位置,如果location中有?,在?之后找#
    const hashPos = location.indexOf('#', searchPos > -1 ? searchPos : 0)
    
    // 如果
    if (searchPos > -1) {
      // 从location中截取[0, searchPos)位置的字符串作为path
      path = location.slice(0, searchPos)
      // 从location截取含search的字符串,不包含hash部分
      searchString = location.slice(
        searchPos + 1,
        hashPos > -1 ? hashPos : location.length
      )
      // 调用parseQuery生成query对象
      query = parseQuery(searchString)
    }
    // 如果location中有hash
    if (hashPos > -1) {
      path = path || location.slice(0, hashPos)
      // 从location中截取[hashPos, location.length)作为hash(包含#)
      hash = location.slice(hashPos, location.length)
    }
    
    // 解析以.开头的相对路径
    path = resolveRelativePath(path != null ? path : location, currentLocation)
    // empty path means a relative query or hash `?foo=f`, `#thing`
    
    return {
        // fullPath = path + searchString + hash
      fullPath: path + (searchString && '?') + searchString + hash,
      path,
      query,
      hash,
    }
    }

    来看下,相对路径的解析过程:

    export function resolveRelativePath(to: string, from: string): string {
    // 如果to以/开头,说明是个绝对路径,直接返回即可
    if (to.startsWith('/')) return to
    // 如果from不是以/开头,那么说明from不是绝对路径,也就无法推测出to的绝对路径,此时直接返回to
    if (__DEV__ && !from.startsWith('/')) {
      warn(
        `Cannot resolve a relative location without an absolute path. Trying to resolve "${to}" from "${from}". It should look like "/${from}".`
      )
      return to
    }
    
    if (!to) return from
    // 使用/分割from与to
    const fromSegments = from.split('/')
    const toSegments = to.split('/')
    
    // 初始化position默认为fromSegments的最后一个索引
    let position = fromSegments.length - 1
    let toPosition: number
    let segment: string
    
    for (toPosition = 0; toPosition < toSegments.length; toPosition++) {
      segment = toSegments[toPosition]
      // 保证position不会小于0
      if (position === 1 || segment === '.') continue
      if (segment === '..') position--
      else break
    }
    
    return (
      fromSegments.slice(0, position).join('/') +
      '/' +
      toSegments
        .slice(toPosition - (toPosition === toSegments.length ? 1 : 0))
        .join('/')
    )
    }

    to=ccfrom=/aa/bb,经过resolveRelativePath后:/aa/ccto=ccfrom=/aa/bb/,经过resolveRelativePath后:/aa/bb/ccto=./ccfrom=/aa/bb,经过resolveRelativePath后:/aa/ccto=./ccfrom=/aa/bb/,经过resolveRelativePath后:/aa/bb/ccto=../ccfrom=/aa/bb,经过resolveRelativePath后:/aato=../ccfrom=/aa/bb/,经过resolveRelativePath后:/aa/cc如果from/to=ccto=./ccto=../ccto=../../ccto=./../ccto=.././cc经过resolveRelativePath始终返回/cc

回到resolve中,解析完rawLocation后,调用matcher.resolve

const matchedRoute = matcher.resolve(
  { path: locationNormalized.path },
  currentLocation
)
// 使用routerHistory.createHref创建href
const href = routerHistory.createHref(locationNormalized.fullPath)

最后返回对象:

return assign(locationNormalized, matchedRoute, {
  // 对params中的value进行decodeURIComponent
  params:decodeParams(matchedRoute.params),
  // 对hash进行decodeURIComponent
  hash: decode(locationNormalized.hash),
  redirectedFrom: undefined,
  href,
})
  • rawLocation不是string类型
let matcherLocation: MatcherLocationRaw

// 如果rawLocation中有path属性
if ('path' in rawLocation) {
  // rawLocation中的params会被忽略
  if (
    __DEV__ &&
    'params' in rawLocation &&
    !('name' in rawLocation) &&
    Object.keys(rawLocation.params).length
  ) {
    warn(
      `Path "${
        rawLocation.path
      }" was passed with params but they will be ignored. Use a named route alongside params instead.`
    )
  }
  // 处理path为绝对路径
  matcherLocation = assign({}, rawLocation, {
    path: parseURL(parseQuery, rawLocation.path, currentLocation.path).path,
  })
} else {
  // 删除空的参数
  const targetParams = assign({}, rawLocation.params)
  for (const key in targetParams) {
    if (targetParams[key] == null) {
      delete targetParams[key]
    }
  }
  // 对params进行编码
  matcherLocation = assign({}, rawLocation, {
    params: encodeParams(rawLocation.params),
  })
  // 将当前位置的params编码 当前位置的参数被解码,我们需要对它们进行编码以防匹配器合并参数
  currentLocation.params = encodeParams(currentLocation.params)
}

// 调用matcher.resolve获取路由相关信息
const matchedRoute = matcher.resolve(matcherLocation, currentLocation)
const hash = rawLocation.hash || ''

if (__DEV__ && hash && !hash.startsWith('#')) {
  warn(
    `A \`hash\` should always start with the character "#". Replace "${hash}" with "#${hash}".`
  )
}

// 由于matcher已经合并了当前位置的参数,所以需要进行解码
matchedRoute.params = normalizeParams(decodeParams(matchedRoute.params))

// 生成完整path
const fullPath = stringifyURL(
  stringifyQuery,
  assign({}, rawLocation, {
    hash: encodeHash(hash),
    path: matchedRoute.path,
  })
)
// routerHistory.createHref会删除#之前的任意字符
const href = routerHistory.createHref(fullPath)
if (__DEV__) {
  if (href.startsWith('//')) {
    warn(
      `Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.`
    )
  } else if (!matchedRoute.matched.length) {
    warn(
      `No match found for location with path "${
        'path' in rawLocation ? rawLocation.path : rawLocation
      }"`
    )
  }
}

return assign(
  {
    fullPath,
    hash,
    query:
    // 如果query是个嵌套对象,normalizeQuery会将嵌套的对象toString,如果用户使用qs等库,我们需要保持query的状态
    // https://github.com/vuejs/router/issues/328#issuecomment-649481567
      stringifyQuery === originalStringifyQuery
        ? normalizeQuery(rawLocation.query)
        : ((rawLocation.query || {}) as LocationQuery),
  },
  matchedRoute,
  {
    redirectedFrom: undefined,
    href,
  }
)