Vue-Router入门(四) :命名路由

前言

在上一篇文章Vue-Router入门(三) :嵌套路由和编程式导航介绍了编程式导航,了解了ts类型及底层的一部分源码,明白了路由导航是通过path或name进行相关导航,path是路径,name是命名路由,本片文章我们来学习命名路由的相关知识点。

命名路由

name表示路由的命名,任何一个路由都能够设置name属性,当我们想要跳转到某个路由时可以用name属性表示。

js 复制代码
   // 使用router-link跳转
   <router-link :to="{ name: 'home'}">home</router-link>
   // 使用router跳转
   router.push({ name: 'home' })

相比于 pathname有以下优点:

  • 没有硬编码的 URL
  • params 的自动编码/解码。
  • 防止你在 url 中出现打字错误。
  • 绕过路径排序(如显示一个)

name就是路由的名称,ts类型为RouteRecordName**:**

js 复制代码
   type RouteRecordName = string | symbol

源码解析

本部分所涉及的所有源码都在这里源码位置,作者也在文章末尾附加了用到了的函数源码。

name导航解析

上节讲解编程式导航时看了一下实现源码,导航方法是使用history API导航到具体的location,不管是path还是name都要找到对应的location,path本身就是url路径一部分比较好理解,name路由名称跟location关联性就不大了,我们看下源码是怎么处理的。

首先在pushWithRedirect函数中通过这段代码

js 复制代码
const targetLocation: RouteLocation = (pendingLocation = resolve(to))

使用resolve处理传过来的to路由,得到location,

接着name会使用matcher中的resolve

js 复制代码
const matchedRoute = matcher.resolve(matcherLocation, currentLocation);

我们看一下matcher的定义,通过matcher文件夹中的index.ts中的createRouterMatcher函数创建的。具体源码在末尾

js 复制代码
const matcher = createRouterMatcher(options.routes, options)

接着在createRouterMatcher函数中的resolve函数

js 复制代码
matcher = matcherMap.get(location.name)

通过matcherMap获取了matcher,matcherMap是一个字典,我们来看下它的定义:

js 复制代码
const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>()

小结

根据上面的源码我们可以梳理下用name导航时的过程:

  1. 在添加routes时,通过matcherMap将路由name与路由route映射起来。
  2. 在调用编程式导航方法时,会判断to参数中是否包含name字段。
  3. 通过matcher.resolve方法去获取name对应的route信息。
  4. 根据route信息获取跳转的url进行跳转。

paht导航解析

path处理就很简单,只有path时,把path拼接一下调用parseURL;path与name一起时会从params参数入手。

js 复制代码
    if (rawLocation.path != null) {
      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.`
        )
      }
      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]
        }
      }
      matcherLocation = assign({}, rawLocation, {
        params: encodeParams(targetParams),
      })
      currentLocation.params = encodeParams(currentLocation.params)
    }

总结

path与name两者都能用于路由导航,name命名路由的出现感觉更多是为了方便开发,具体的区别可以简单总结一下:

  • path会经过百分号编码。
  • path用query传参,name用params传参。
  • 两者源码处理逻辑不同。

附加源码

router.ts中的resolve函数

js 复制代码
function resolve(
    rawLocation: Readonly<RouteLocationRaw>,
    currentLocation?: RouteLocationNormalizedLoaded
  ): RouteLocation & { href: string } {
    // const objectLocation = routerLocationAsObject(rawLocation)
    // we create a copy to modify it later
    currentLocation = assign({}, currentLocation || currentRoute.value)
    if (typeof rawLocation === 'string') {
      const locationNormalized = parseURL(
        parseQuery,
        rawLocation,
        currentLocation.path
      )
      const matchedRoute = matcher.resolve(
        { path: locationNormalized.path },
        currentLocation
      )

      const href = routerHistory.createHref(locationNormalized.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 "${rawLocation}"`)
        }
      }

      // locationNormalized is always a new object
      return assign(locationNormalized, matchedRoute, {
        params: decodeParams(matchedRoute.params),
        hash: decode(locationNormalized.hash),
        redirectedFrom: undefined,
        href,
      })
    }

    if (__DEV__ && !isRouteLocation(rawLocation)) {
      warn(
        `router.resolve() was passed an invalid location. This will fail in production.\n- Location:`,
        rawLocation
      )
      rawLocation = {}
    }

    let matcherLocation: MatcherLocationRaw

    // path could be relative in object as well
    if (rawLocation.path != null) {
      if (
        __DEV__ &&
        'params' in rawLocation &&
        !('name' in rawLocation) &&
        // @ts-expect-error: the type is never
        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.`
        )
      }
      matcherLocation = assign({}, rawLocation, {
        path: parseURL(parseQuery, rawLocation.path, currentLocation.path).path,
      })
    } else {
      // remove any nullish param
      const targetParams = assign({}, rawLocation.params)
      for (const key in targetParams) {
        if (targetParams[key] == null) {
          delete targetParams[key]
        }
      }
      // pass encoded values to the matcher, so it can produce encoded path and fullPath
      matcherLocation = assign({}, rawLocation, {
        params: encodeParams(targetParams),
      })
      // current location params are decoded, we need to encode them in case the
      // matcher merges the params
      currentLocation.params = encodeParams(currentLocation.params)
    }

    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}".`
      )
    }

    // the matcher might have merged current location params, so
    // we need to run the decoding again
    matchedRoute.params = normalizeParams(decodeParams(matchedRoute.params))

    const fullPath = stringifyURL(
      stringifyQuery,
      assign({}, rawLocation, {
        hash: encodeHash(hash),
        path: matchedRoute.path,
      })
    )

    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 "${
            rawLocation.path != null ? rawLocation.path : rawLocation
          }"`
        )
      }
    }

    return assign(
      {
        fullPath,
        // keep the hash encoded so fullPath is effectively path + encodedQuery +
        // hash
        hash,
        query:
          // if the user is using a custom query lib like qs, we might have
          // nested objects, so we keep the query as is, meaning it can contain
          // numbers at `$route.query`, but at the point, the user will have to
          // use their own type anyway.
          // https://github.com/vuejs/router/issues/328#issuecomment-649481567
          stringifyQuery === originalStringifyQuery
            ? normalizeQuery(rawLocation.query)
            : ((rawLocation.query || {}) as LocationQuery),
      },
      matchedRoute,
      {
        redirectedFrom: undefined,
        href,
      }
    )
  }

matcher中的createRouterMatcher函数源码

js 复制代码
export function createRouterMatcher(
  routes: Readonly<RouteRecordRaw[]>,
  globalOptions: PathParserOptions
): RouterMatcher {
  // normalized ordered array of matchers
  const matchers: RouteRecordMatcher[] = []
  const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>()
  globalOptions = mergeOptions(
    { strict: false, end: true, sensitive: false } as PathParserOptions,
    globalOptions
  )

  function getRecordMatcher(name: RouteRecordName) {
    return matcherMap.get(name)
  }

  function addRoute(
  }

  function removeRoute(matcherRef: RouteRecordName | RouteRecordMatcher) {
  }

  function getRoutes() {
    return matchers
  }

  function insertMatcher(matcher: RouteRecordMatcher) {
  }

  function resolve(
    location: Readonly<MatcherLocationRaw>,
    currentLocation: Readonly<MatcherLocation>
  ): MatcherLocation {
    let matcher: RouteRecordMatcher | undefined
    let params: PathParams = {}
    let path: MatcherLocation['path']
    let name: MatcherLocation['name']

    if ('name' in location && location.name) {
      matcher = matcherMap.get(location.name)

      if (!matcher)
        throw createRouterError<MatcherError>(ErrorTypes.MATCHER_NOT_FOUND, {
          location,
        })

      // warn if the user is passing invalid params so they can debug it better when they get removed
      if (__DEV__) {
        const invalidParams: string[] = Object.keys(
          location.params || {}
        ).filter(paramName => !matcher!.keys.find(k => k.name === paramName))

        if (invalidParams.length) {
          warn(
            `Discarded invalid param(s) "${invalidParams.join(
              '", "'
            )}" when navigating. See https://github.com/vuejs/router/blob/main/packages/router/CHANGELOG.md#414-2022-08-22 for more details.`
          )
        }
      }

      name = matcher.record.name
      params = assign(
        // paramsFromLocation is a new object
        paramsFromLocation(
          currentLocation.params,
          // only keep params that exist in the resolved location
          // only keep optional params coming from a parent record
          matcher.keys
            .filter(k => !k.optional)
            .concat(
              matcher.parent ? matcher.parent.keys.filter(k => k.optional) : []
            )
            .map(k => k.name)
        ),
        // discard any existing params in the current location that do not exist here
        // #1497 this ensures better active/exact matching
        location.params &&
          paramsFromLocation(
            location.params,
            matcher.keys.map(k => k.name)
          )
      )
      // throws if cannot be stringified
      path = matcher.stringify(params)
    } else if (location.path != null) {
      // no need to resolve the path with the matcher as it was provided
      // this also allows the user to control the encoding
      path = location.path

      if (__DEV__ && !path.startsWith('/')) {
        warn(
          `The Matcher cannot resolve relative paths but received "${path}". Unless you directly called \`matcher.resolve("${path}")\`, this is probably a bug in vue-router. Please open an issue at https://github.com/vuejs/router/issues/new/choose.`
        )
      }

      matcher = matchers.find(m => m.re.test(path))
      // matcher should have a value after the loop

      if (matcher) {
        // we know the matcher works because we tested the regexp
        params = matcher.parse(path)!
        name = matcher.record.name
      }
      // location is a relative path
    } else {
      // match by name or path of current route
      matcher = currentLocation.name
        ? matcherMap.get(currentLocation.name)
        : matchers.find(m => m.re.test(currentLocation.path))
      if (!matcher)
        throw createRouterError<MatcherError>(ErrorTypes.MATCHER_NOT_FOUND, {
          location,
          currentLocation,
        })
      name = matcher.record.name
      // since we are navigating to the same location, we don't need to pick the
      // params like when `name` is provided
      params = assign({}, currentLocation.params, location.params)
      path = matcher.stringify(params)
    }

    const matched: MatcherLocation['matched'] = []
    let parentMatcher: RouteRecordMatcher | undefined = matcher
    while (parentMatcher) {
      // reversed order so parents are at the beginning

      matched.unshift(parentMatcher.record)
      parentMatcher = parentMatcher.parent
    }

    return {
      name,
      path,
      params,
      matched,
      meta: mergeMetaFields(matched),
    }
  }

  // add initial routes
  routes.forEach(route => addRoute(route))

  return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }
}
相关推荐
小小小小宇4 分钟前
Claude Code 自动运行方法大全
前端
道友可好6 分钟前
AI 测试全绿,代码却是错的
前端·人工智能·后端
国科安芯25 分钟前
商业航天通信载荷数字处理单元供电架构研究——基于ASP7A84AS的高精度低压差线性稳压器技术分析
前端·单片机·嵌入式硬件·fpga开发·架构·安全性测试
TangentDomain1 小时前
AI 写代码时代,游戏 UI 架构为什么停在 MVP?
前端·游戏·架构
英勇无比的消炎药1 小时前
前端提效神器全新AI组件库TinyRobot改写日常开发模式
前端·vue.js
GuWenyue1 小时前
10分钟搞定TodoList实战!从0搭建Bun+TS的RESTful接口服务
前端·typescript·bun
IMPYLH1 小时前
HTML 的 <a>元素
前端·javascript·html
PedroQue991 小时前
uni-router:uni-app路由管理新选择
前端·uni-app
Cerrda1 小时前
一行指令搞定复制:Vue 3 vCopy 实现解析
前端·代码规范
英勇无比的消炎药1 小时前
前端提效神器TinyRobot
前端·vue.js