feat: cache routes and deprecate hashbang support (#1521)

This commit is contained in:
Phan An 2022-10-09 10:55:58 +02:00 committed by GitHub
parent 5474655e90
commit 63a66bc511
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 68 additions and 51 deletions

View file

@ -14,8 +14,8 @@
<AlbumThumbnail :entity="album"/>
<footer>
<a :href="`#!/album/${album.id}`" class="name" data-testid="name">{{ album.name }}</a>
<a v-if="isStandardArtist" :href="`#!/artist/${album.artist_id}`" class="artist">{{ album.artist_name }}</a>
<a :href="`#/album/${album.id}`" class="name" data-testid="name">{{ album.name }}</a>
<a v-if="isStandardArtist" :href="`#/artist/${album.artist_id}`" class="artist">{{ album.artist_name }}</a>
<span v-else class="text-secondary">{{ album.artist_name }}</span>
<p class="meta">
<span class="left">

View file

@ -15,7 +15,7 @@
<footer>
<div class="info">
<a :href="`#!/artist/${artist.id}`" class="name" data-testid="name">{{ artist.name }}</a>
<a :href="`#/artist/${artist.id}`" class="name" data-testid="name">{{ artist.name }}</a>
</div>
<p class="meta">
<span class="left">

View file

@ -39,7 +39,7 @@
<icon :icon="faSliders"/>
</button>
<a v-else :class="{ active: viewingQueue }" class="queue control" href="#!/queue">
<a v-else :class="{ active: viewingQueue }" class="queue control" href="#/queue">
<icon :icon="faListOl"/>
</a>

View file

@ -4,8 +4,8 @@
<template v-if="song">
<h3 class="title">{{ song.title }}</h3>
<p class="meta">
<a :href="`/#!/artist/${song.artist_id}`" class="artist">{{ song.artist_name }}</a>
<a :href="`/#!/album/${song.album_id}`" class="album">{{ song.album_name }}</a>
<a :href="`/#/artist/${song.artist_id}`" class="artist">{{ song.artist_name }}</a>
<a :href="`/#/album/${song.album_id}`" class="album">{{ song.album_name }}</a>
</p>
</template>

View file

@ -3,7 +3,7 @@
exports[`renders 1`] = `
<div class="other-controls" data-testid="other-controls" data-v-add48cbe="">
<div class="wrapper" data-v-add48cbe="">
<!--v-if--><button class="control" data-testid="toggle-visualizer-btn" title="Show/hide the visualizer" type="button" data-v-add48cbe=""><br data-testid="icon" icon="[object Object]" data-v-add48cbe=""></button><button title="Like Fahrstuhl to Heaven by Led Zeppelin" class="text-secondary like" data-testid="like-btn" data-v-5d366bb1="" data-v-add48cbe=""><br data-testid="btn-like-unliked" icon="[object Object]" data-v-5d366bb1=""></button><button class="active control text-uppercase" data-testid="toggle-extra-panel-btn" title="View song information" type="button" data-v-add48cbe=""> Info </button><a class="queue control" href="#!/queue" data-v-add48cbe=""><br data-testid="icon" icon="[object Object]" data-v-add48cbe=""></a><br data-testid="RepeatModeSwitch" data-v-add48cbe=""><br data-testid="Volume" data-v-add48cbe="">
<!--v-if--><button class="control" data-testid="toggle-visualizer-btn" title="Show/hide the visualizer" type="button" data-v-add48cbe=""><br data-testid="icon" icon="[object Object]" data-v-add48cbe=""></button><button title="Like Fahrstuhl to Heaven by Led Zeppelin" class="text-secondary like" data-testid="like-btn" data-v-5d366bb1="" data-v-add48cbe=""><br data-testid="btn-like-unliked" icon="[object Object]" data-v-5d366bb1=""></button><button class="active control text-uppercase" data-testid="toggle-extra-panel-btn" title="View song information" type="button" data-v-add48cbe=""> Info </button><a class="queue control" href="#/queue" data-v-add48cbe=""><br data-testid="icon" icon="[object Object]" data-v-add48cbe=""></a><br data-testid="RepeatModeSwitch" data-v-add48cbe=""><br data-testid="Volume" data-v-add48cbe="">
</div>
</div>
`;

View file

@ -4,7 +4,7 @@ exports[`renders with a song 1`] = `
<div class="middle-pane" data-testid="footer-middle-pane" data-v-2ff4ca72="">
<div id="progressPane" class="progress" data-v-2ff4ca72="">
<h3 class="title" data-v-2ff4ca72="">Fahrstuhl to Heaven</h3>
<p class="meta" data-v-2ff4ca72=""><a href="/#!/artist/3" class="artist" data-v-2ff4ca72="">Led Zeppelin</a> <a href="/#!/album/4" class="album" data-v-2ff4ca72="">Led Zeppelin IV</a></p>
<p class="meta" data-v-2ff4ca72=""><a href="/#/artist/3" class="artist" data-v-2ff4ca72="">Led Zeppelin</a> <a href="/#/album/4" class="album" data-v-2ff4ca72="">Led Zeppelin IV</a></p>
<div class="plyr" data-v-2ff4ca72=""><audio controls="" crossorigin="anonymous" data-v-2ff4ca72=""></audio></div>
</div>
</div>

View file

@ -5,7 +5,7 @@
<ul class="menu">
<li>
<a :class="['home', activeScreen === 'Home' ? 'active' : '']" href="#!/home">
<a :class="['home', activeScreen === 'Home' ? 'active' : '']" href="#/home">
<icon :icon="faHome" fixed-width/>
Home
</a>
@ -16,31 +16,31 @@
@dragover="onQueueDragOver"
@drop="onQueueDrop"
>
<a :class="['queue', activeScreen === 'Queue' ? 'active' : '']" href="#!/queue">
<a :class="['queue', activeScreen === 'Queue' ? 'active' : '']" href="#/queue">
<icon :icon="faListOl" fixed-width/>
Current Queue
</a>
</li>
<li>
<a :class="['songs', activeScreen === 'Songs' ? 'active' : '']" href="#!/songs">
<a :class="['songs', activeScreen === 'Songs' ? 'active' : '']" href="#/songs">
<icon :icon="faMusic" fixed-width/>
All Songs
</a>
</li>
<li>
<a :class="['albums', activeScreen === 'Albums' ? 'active' : '']" href="#!/albums">
<a :class="['albums', activeScreen === 'Albums' ? 'active' : '']" href="#/albums">
<icon :icon="faCompactDisc" fixed-width/>
Albums
</a>
</li>
<li>
<a :class="['artists', activeScreen === 'Artists' ? 'active' : '']" href="#!/artists">
<a :class="['artists', activeScreen === 'Artists' ? 'active' : '']" href="#/artists">
<icon :icon="faMicrophone" fixed-width/>
Artists
</a>
</li>
<li v-if="useYouTube">
<a :class="['youtube', activeScreen === 'YouTube' ? 'active' : '']" href="#!/youtube">
<a :class="['youtube', activeScreen === 'YouTube' ? 'active' : '']" href="#/youtube">
<icon :icon="faYoutube" fixed-width/>
YouTube Video
</a>
@ -55,19 +55,19 @@
<ul class="menu">
<li>
<a :class="['settings', activeScreen === 'Settings' ? 'active' : '']" href="#!/settings">
<a :class="['settings', activeScreen === 'Settings' ? 'active' : '']" href="#/settings">
<icon :icon="faTools" fixed-width/>
Settings
</a>
</li>
<li>
<a :class="['upload', activeScreen === 'Upload' ? 'active' : '']" href="#!/upload">
<a :class="['upload', activeScreen === 'Upload' ? 'active' : '']" href="#/upload">
<icon :icon="faUpload" fixed-width/>
Upload
</a>
</li>
<li>
<a :class="['users', activeScreen === 'Users' ? 'active' : '']" href="#!/users">
<a :class="['users', activeScreen === 'Users' ? 'active' : '']" href="#/users">
<icon :icon="faUsers" fixed-width/>
Users
</a>

View file

@ -52,9 +52,9 @@ const isRecentlyPlayedList = (list: PlaylistLike): list is RecentlyPlayedList =>
const active = ref(false)
const url = computed(() => {
if (isPlaylist(list.value)) return `#!/playlist/${list.value.id}`
if (isFavoriteList(list.value)) return '#!/favorites'
if (isRecentlyPlayedList(list.value)) return '#!/recently-played'
if (isPlaylist(list.value)) return `#/playlist/${list.value.id}`
if (isFavoriteList(list.value)) return '#/favorites'
if (isRecentlyPlayedList(list.value)) return '#/recently-played'
throw new Error('Invalid playlist-like type.')
})

View file

@ -11,7 +11,7 @@
</template>
<template v-slot:meta>
<a v-if="isNormalArtist" :href="`#!/artist/${album.artist_id}`" class="artist">{{ album.artist_name }}</a>
<a v-if="isNormalArtist" :href="`#/artist/${album.artist_id}`" class="artist">{{ album.artist_name }}</a>
<span v-else class="nope">{{ album.artist_name }}</span>
<span>{{ pluralize(songs, 'song') }}</span>
<span>{{ duration }}</span>

View file

@ -17,7 +17,7 @@
<div class="details">
<h3>{{ song.title }}</h3>
<p class="by text-secondary">
<a :href="`#!/artist/${song.artist_id}`">{{ song.artist_name }}</a>
<a :href="`#/artist/${song.artist_id}`">{{ song.artist_name }}</a>
- {{ pluralize(song.play_count, 'play') }}
</p>
</div>

View file

@ -1,7 +1,7 @@
// Vitest Snapshot v1
exports[`renders 1`] = `
<div class="all-songs song-list-wrap main-scroll-wrap" data-testid="song-list" tabindex="0">
<div class="song-list-wrap main-scroll-wrap" data-testid="song-list" tabindex="0">
<div class="sortable song-list-header"><span class="track-number" data-testid="header-track-number" role="button" title="Sort by track number"> # <!--v-if--><!--v-if--></span><span class="title" data-testid="header-title" role="button" title="Sort by title"> Title <br data-testid="icon" icon="[object Object]" class="text-highlight"><!--v-if--></span><span class="artist" data-testid="header-artist" role="button" title="Sort by artist"> Artist <!--v-if--><!--v-if--></span><span class="album" data-testid="header-album" role="button" title="Sort by album"> Album <!--v-if--><!--v-if--></span><span class="time" data-testid="header-length" role="button" title="Sort by song duration"><!--v-if--><!--v-if--> &nbsp; <br data-testid="icon" icon="[object Object]" class="duration-header"></span><span class="favorite"></span><span class="play"></span></div><br data-testid="virtual-scroller" item-height="35" items="">
</div>
`;

View file

@ -1,3 +1,3 @@
// Vitest Snapshot v1
exports[`renders 1`] = `<transition-stub data-v-e7b6c7f6=""><button title="Scroll to top" type="button" data-v-e7b6c7f6="" style="display: none;"><br data-testid="icon" icon="[object Object]" data-v-e7b6c7f6="">&nbsp; Top </button></transition-stub>`;
exports[`renders 1`] = `<transition-stub name="fade" appear="false" persisted="true" css="true" data-v-e7b6c7f6=""><button title="Scroll to top" type="button" data-v-e7b6c7f6="" style="display: none;"><br data-testid="icon" icon="[object Object]" data-v-e7b6c7f6="">&nbsp; Top </button></transition-stub>`;

View file

@ -1,6 +1,6 @@
<template>
<span class="profile" id="userBadge" v-if="currentUser">
<a class="view-profile" data-testid="view-profile-link" href="/#!/profile" title="View/edit user profile">
<a class="view-profile" data-testid="view-profile-link" href="/#/profile" title="View/edit user profile">
<img :alt="`Avatar of ${currentUser.name}`" :src="currentUser.avatar" class="avatar"/>
<span class="name">{{ currentUser.name }}</span>
</a>

View file

@ -1,3 +1,3 @@
// Vitest Snapshot v1
exports[`renders 1`] = `<span class="profile" id="userBadge"><a class="view-profile" data-testid="view-profile-link" href="/#!/profile" title="View/edit user profile"><img alt="Avatar of John Doe" src="https://gravatar.com/foo" class="avatar"><span class="name">John Doe</span></a><a title="Log out" class="logout control" data-testid="btn-logout" href="" role="button"><br data-testid="icon" icon="[object Object]"></a></span>`;
exports[`renders 1`] = `<span class="profile" id="userBadge"><a class="view-profile" data-testid="view-profile-link" href="/#/profile" title="View/edit user profile"><img alt="Avatar of John Doe" src="https://gravatar.com/foo" class="avatar"><span class="name">John Doe</span></a><a title="Log out" class="logout control" data-testid="btn-logout" href="" role="button"><br data-testid="icon" icon="[object Object]"></a></span>`;

View file

@ -40,9 +40,7 @@ eventBus.on({
await userStore.logout()
authService.destroy()
forceReloadWindow()
},
KOEL_READY: () => router.resolve()
}
})
router.onRouteChanged(() => {

View file

@ -16,6 +16,7 @@ export type Route = {
type RouteChangedHandler = (newRoute: Route, oldRoute: Route | undefined) => any
// @TODO: Remove support for hashbang (#!) and only support hash (#)
export default class Router {
public $currentRoute: Ref<Route>
@ -23,12 +24,13 @@ export default class Router {
private readonly homeRoute: Route
private readonly notFoundRoute: Route
private routeChangedHandlers: RouteChangedHandler[] = []
private cache: Map<string, { route: Route, params: RouteParams }> = new Map()
constructor (routes: Route[]) {
this.routes = routes
this.homeRoute = routes.find(route => route.screen === 'Home')!
this.notFoundRoute = routes.find(route => route.screen === '404')!
this.$currentRoute = ref<Route>(this.homeRoute)
this.$currentRoute = ref(this.homeRoute)
watch(
this.$currentRoute,
@ -43,32 +45,49 @@ export default class Router {
}
public async resolve () {
if (!location.hash || location.hash === '#!/') {
if (!location.hash || location.hash === '#/' || location.hash === '#!/') {
return this.activateRoute(this.homeRoute)
}
for (let i = 0; i < this.routes.length; i++) {
const route = this.routes[i]
const matches = location.hash.match(new RegExp(`^#!${route.path}/?(?:\\?(.*))?$`))
const matched = this.tryMatchRoute(location.hash)
const [route, params] = matched ? [matched.route, matched.params] : [null, null]
if (matches) {
const searchParams = new URLSearchParams(new URL(location.href.replace('#!/', '')).search)
const routeParams = Object.assign(Object.fromEntries(searchParams.entries()), matches.groups || {})
if (!route) {
return this.triggerNotFound()
}
if (route.onBeforeEnter && route.onBeforeEnter(routeParams) === false) {
return this.triggerNotFound()
if (route.onBeforeEnter && route.onBeforeEnter(params) === false) {
return this.triggerNotFound()
}
if (route.redirect) {
const to = route.redirect(params)
return typeof to === 'string' ? this.go(to) : this.activateRoute(to, params)
}
return this.activateRoute(route, params)
}
private tryMatchRoute (hash: string) {
if (!this.cache.has(hash)) {
for (let i = 0; i < this.routes.length; i++) {
const route = this.routes[i]
const matches = hash.match(new RegExp(`^#!?${route.path}/?(?:\\?(.*))?$`))
if (matches) {
const searchParams = new URLSearchParams(new URL(location.href.replace(/#!?\?/, '')).search)
this.cache.set(hash, {
route,
params: Object.assign(Object.fromEntries(searchParams.entries()), matches.groups || {})
})
break
}
if (route.redirect) {
const to = route.redirect(routeParams)
return typeof to === 'string' ? this.go(to) : this.activateRoute(to, routeParams)
}
return this.activateRoute(route, routeParams)
}
}
await this.triggerNotFound()
return this.cache.get(hash)
}
public async triggerNotFound () {
@ -93,8 +112,8 @@ export default class Router {
path = `/${path}`
}
if (!path.startsWith('/#!')) {
path = `/#!${path}`
if (!path.startsWith('/#')) {
path = `/#${path}`
}
path = path.substring(1, path.length)

View file

@ -158,7 +158,7 @@ new class extends UnitTestCase {
it('gets shareable URL', () => {
const song = factory<Song>('song', { id: 'foo' })
expect(songStore.getShareableUrl(song)).toBe('http://localhost/#!/song/foo')
expect(songStore.getShareableUrl(song)).toBe('http://localhost/#/song/foo')
})
it('syncs with the vault', () => {

View file

@ -117,7 +117,7 @@ export const songStore = {
: `${commonStore.state.cdn_url}play/${song.id}?api_token=${authService.getToken()}`
},
getShareableUrl: (song: Song) => `${window.BASE_URL}#!/song/${song.id}`,
getShareableUrl: (song: Song) => `${window.BASE_URL}#/song/${song.id}`,
syncWithVault (songs: Song | Song[]) {
return arrayify(songs).map(song => {