]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
perf: parseURL minor improvements
authorEduardo San Martin Morote <posva13@gmail.com>
Wed, 26 Jun 2024 08:49:57 +0000 (10:49 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Wed, 4 Dec 2024 15:10:34 +0000 (16:10 +0100)
packages/router/__tests__/location.spec.ts
packages/router/__tests__/router.spec.ts
packages/router/src/location.ts
packages/router/src/query.ts

index 8ccd8a4255755c0583c645581a76956662f4f4fe..98dadf1e8042afffbc17b09eefd2b7921712efc3 100644 (file)
@@ -134,7 +134,7 @@ describe('parseURL', () => {
     })
   })
 
-  it('parses ? after the hash', () => {
+  it('avoids ? after the hash', () => {
     expect(parseURL('/foo#?a=one')).toEqual({
       fullPath: '/foo#?a=one',
       path: '/foo',
@@ -149,11 +149,75 @@ describe('parseURL', () => {
     })
   })
 
+  it('works with empty query', () => {
+    expect(parseURL('/foo?#hash')).toEqual({
+      fullPath: '/foo?#hash',
+      path: '/foo',
+      hash: '#hash',
+      query: {},
+    })
+    expect(parseURL('/foo?')).toEqual({
+      fullPath: '/foo?',
+      path: '/foo',
+      hash: '',
+      query: {},
+    })
+  })
+
+  it('works with empty hash', () => {
+    expect(parseURL('/foo#')).toEqual({
+      fullPath: '/foo#',
+      path: '/foo',
+      hash: '#',
+      query: {},
+    })
+    expect(parseURL('/foo?#')).toEqual({
+      fullPath: '/foo?#',
+      path: '/foo',
+      hash: '#',
+      query: {},
+    })
+  })
+
+  it('works with a relative paths', () => {
+    expect(parseURL('foo', '/parent/bar')).toEqual({
+      fullPath: '/parent/foo',
+      path: '/parent/foo',
+      hash: '',
+      query: {},
+    })
+    expect(parseURL('./foo', '/parent/bar')).toEqual({
+      fullPath: '/parent/foo',
+      path: '/parent/foo',
+      hash: '',
+      query: {},
+    })
+    expect(parseURL('../foo', '/parent/bar')).toEqual({
+      fullPath: '/foo',
+      path: '/foo',
+      hash: '',
+      query: {},
+    })
+
+    expect(parseURL('#foo', '/parent/bar')).toEqual({
+      fullPath: '/parent/bar#foo',
+      path: '/parent/bar',
+      hash: '#foo',
+      query: {},
+    })
+    expect(parseURL('?o=o', '/parent/bar')).toEqual({
+      fullPath: '/parent/bar?o=o',
+      path: '/parent/bar',
+      hash: '',
+      query: { o: 'o' },
+    })
+  })
+
   it('calls parseQuery', () => {
     const parseQuery = vi.fn()
     originalParseURL(parseQuery, '/?é=é&é=a')
     expect(parseQuery).toHaveBeenCalledTimes(1)
-    expect(parseQuery).toHaveBeenCalledWith('é=é&é=a')
+    expect(parseQuery).toHaveBeenCalledWith('?é=é&é=a')
   })
 })
 
index bf11f31bae4e03900f18d9740f73e85001764600..f835f41e395c5380d09467b8b60d17e25bf99961 100644 (file)
@@ -14,8 +14,6 @@ import { START_LOCATION_NORMALIZED } from '../src/location'
 import { vi, describe, expect, it, beforeAll } from 'vitest'
 import { mockWarn } from './vitest-mock-warn'
 
-declare var __DEV__: boolean
-
 const routes: RouteRecordRaw[] = [
   { path: '/', component: components.Home, name: 'home' },
   { path: '/home', redirect: '/' },
@@ -173,7 +171,7 @@ describe('Router', () => {
     const parseQuery = vi.fn(_ => ({}))
     const { router } = await newRouter({ parseQuery })
     const to = router.resolve('/foo?bar=baz')
-    expect(parseQuery).toHaveBeenCalledWith('bar=baz')
+    expect(parseQuery).toHaveBeenCalledWith('?bar=baz')
     expect(to.query).toEqual({})
   })
 
index 08c2b744b4a1582b523afdffab188f7c2bc321a3..e0aa540526e5bd65c52da44ae6d5bdac2e87ccd8 100644 (file)
@@ -50,37 +50,43 @@ export function parseURL(
     searchString = '',
     hash = ''
 
-  // Could use URL and URLSearchParams but IE 11 doesn't support it
-  // TODO: move to new URL()
+  // NOTE: we could use URL and URLSearchParams but they are 2 to 5 times slower than this method
   const hashPos = location.indexOf('#')
-  let searchPos = location.indexOf('?')
-  // the hash appears before the search, so it's not part of the search string
-  if (hashPos < searchPos && hashPos >= 0) {
-    searchPos = -1
-  }
+  // let searchPos = location.indexOf('?')
+  let searchPos =
+    hashPos >= 0
+      ? // find the query string before the hash to avoid including a ? in the hash
+        // e.g. /foo#hash?query -> has no query
+        location.lastIndexOf('?', hashPos)
+      : location.indexOf('?')
 
-  if (searchPos > -1) {
+  if (searchPos >= 0) {
     path = location.slice(0, searchPos)
-    searchString = location.slice(
-      searchPos + 1,
-      hashPos > -1 ? hashPos : location.length
-    )
+    searchString =
+      '?' +
+      location.slice(searchPos + 1, hashPos > 0 ? hashPos : location.length)
 
     query = parseQuery(searchString)
   }
 
-  if (hashPos > -1) {
+  if (hashPos >= 0) {
+    // TODO(major): path ||=
     path = path || location.slice(0, hashPos)
     // keep the # character
     hash = location.slice(hashPos, location.length)
   }
 
-  // no search and no query
-  path = resolveRelativePath(path != null ? path : location, currentLocation)
-  // empty path means a relative query or hash `?foo=f`, `#thing`
+  // TODO(major): path ?? location
+  path = resolveRelativePath(
+    path != null
+      ? path
+      : // empty path means a relative query or hash `?foo=f`, `#thing`
+        location,
+    currentLocation
+  )
 
   return {
-    fullPath: path + (searchString && '?') + searchString + hash,
+    fullPath: path + searchString + hash,
     path,
     query,
     hash: decode(hash),
@@ -207,11 +213,12 @@ export function resolveRelativePath(to: string, from: string): string {
     return to
   }
 
+  // resolve '' with '/anything' -> '/anything'
   if (!to) return from
 
   const fromSegments = from.split('/')
   const toSegments = to.split('/')
-  const lastToSegment = toSegments[toSegments.length - 1]
+  const lastToSegment: string | undefined = toSegments[toSegments.length - 1]
 
   // make . and ./ the same (../ === .., ../../ === ../..)
   // this is the same behavior as new URL()
index 94d9146182eff0049c0738c0f893c2d9e09588c5..75a8cc70bf5ef2e8479f90de3b12a3914df08392 100644 (file)
@@ -56,8 +56,7 @@ export function parseQuery(search: string): LocationQuery {
   // avoid creating an object with an empty key and empty value
   // because of split('&')
   if (search === '' || search === '?') return query
-  const hasLeadingIM = search[0] === '?'
-  const searchParams = (hasLeadingIM ? search.slice(1) : search).split('&')
+  const searchParams = (search[0] === '?' ? search.slice(1) : search).split('&')
   for (let i = 0; i < searchParams.length; ++i) {
     // pre decode the + into space
     const searchParam = searchParams[i].replace(PLUS_RE, ' ')