Repository Wiki
ldx123000/Hydrogen-Music

Vue 应用壳与路由页面

本页说明主应用 src 下 Vue Router 如何选择页面、加载嵌套内容,以及在导航时触发权限判断、数据准备和空闲预热。

用途与范围

面向维护主应用导航的开发者:覆盖 src/router/router.js 中的路由表、守卫和页面加载策略;仅依据已核实的 Home.vue 与 MyMusic.vue 路由出口说明嵌套渲染。播放器、页面内部业务、Android 应用壳和应用启动装配属于其他主题,不在此推断其实现。主应用入口及根组件的装配细节未从已读取源码确认。

概述

路由使用 createWebHashHistory();页面组件通过 createRouteLoader 延迟导入。/mymusic 提供子路由(歌单、专辑、艺人、推荐、DJ 与本地音乐);此外有首页、云盘、登录、汽水、私人 FM、搜索和设置等独立页面。路由守卫依赖 Pinia store、isLogin 及 noticeOpen 进行状态检查,并在详情页进入前调用 store 更新函数。首轮导航结束后,在允许的模式下分批预热页面组件。参见路由表与加载器及导航钩子。

架构

Loading diagram...

Source: router.js

图中 Routes 表示源码中的 routes 数组而非独立服务:路由器持有它,MyMusic 作为父路由关联详情组件;store 由路由模块使用,不表示这些页面直接依赖 store。路由表中的 component 指向延迟导入函数,导航完成后的钩子才安排组件预热。

路由组织与控制流

路径和页面边界

路径名称组件/进入行为
/homepageHomePage;homePage 关闭则转 mymusic
/cloudclouddiskCloudDisk;先检查 cloudDiskPage,再检查登录;未登录转 login 并提示
/login、/login/accountlogin、accountLoginPage、LoginContent
/siren、/siren/album/:idsiren、sirenAlbum共用 SirenPage
/mymusicmymusicMyMusic;允许本地模式或登录用户;还有从首页/搜索进入子路径的例外
/mymusic/playlist/:id、/mymusic/album/:id、/mymusic/artist/:idplaylist、album、artistLibraryDetail;按来源路由和 ID 判断是否重新获取详情
/mymusic/playlist/rec、/mymusic/dj/:idrec、djRecommendSongs(须登录)、RadioDetail
/mymusic/local/files、/mymusic/local/album/:id、/mymusic/local/artist/:idlocalFiles、localAlbum、localArtistLocalMusicDetail;从其他同名路由进入时准备本地详情
/personalfmpersonalfmPersonalFMPage;须登录
/library、/search、/settingslibrary、search、settingsLibraryDetail、SearchResult、Settings;搜索进入前调用 getSearchInfo(to.query.keywords)

以上均由路由表定义。/mymusic 的 children 使用完整路径,进入详情时仍作为 MyMusic 的子路由;已定位的 MyMusic.vue 路由出口通过具名 router-view 插槽与限定组件名称的 keep-alive 容纳详情,完整缓存行为需查看页面实现。另有 Home.vue 路由出口包裹 keep-alive;这里不推断根组件与这两个出口的挂载关系。

导航钩子次序与副作用

全局 beforeEach 首先限制本地模式:只有 settings、mymusic、localFiles、localAlbum、localArtist 名称在白名单内,其他目标被重定向到 mymusic。随后对以 /mymusic、/cloud、/personalfm 或 /siren 开头的 fullPath 异步触发 ensureDeferredAppInit(),不等待其完成便执行 next()。路由自身的 beforeEnter 再进行页面开关、登录或详情数据检查;afterEach 安排组件预热。参见全局导航钩子。

Loading diagram...

Source: router.js

该图展示正常导航的关键调用阶段;重定向会发起另一轮导航,不能将其理解为总会进入原目标。页面级判断详见首页与云盘、音乐库与私人 FM。

详情数据准备

playlist、album、artist 共用 LibraryDetail,但各自的 beforeEnter 比较 libraryInfo.value、来源名称与新旧 params.id:当详情为空、从其他名称进入、或 ID 改变时调用 updateLibraryDetail。歌单额外传 { deferRemaining: true }。三个钩子都在 finally 调用 next(),因此即使更新失败也会继续导航;错误传播与 UI 后续处理不在已读取的 store 实现证据内。来源同名且 ID 未变化时不重复更新。参见详情守卫。本地音乐守卫按来源名称决定是否调用 updateLocalMusicDetail,没有在此处 await;搜索守卫同样直接调用 getSearchInfo 后 next()。本地详情和搜索、搜索守卫。

用法示例(仓库中的实际写法)

下面的片段均直接取自路由实现,用于说明扩展时需要遵循的现有模式,而不是另外定义的调用接口。

可重试的懒加载包装

javascript
1function createRouteLoader(loader) { 2 let promise = null 3 return () => { 4 if (!promise) { 5 promise = loader().catch(error => { 6 promise = null 7 throw error 8 }) 9 } 10 return promise 11 } 12}

Source: router.js

同一加载器在成功加载后复用 Promise;失败时清空缓存,使下次调用可以重试。页面导入采用 createRouteLoader(() => import(...)),预热数组复用这些函数,因此导航与空闲预热共享加载结果。

按参数变化准备详情

javascript
1{ 2 path: '/mymusic/playlist/:id', 3 name: 'playlist', 4 component: LibraryDetail, 5 beforeEnter: async (to, from, next) => { 6 const needReload = !libraryInfo.value || from.name != 'playlist' || hasDifferentLibraryId(to, from) 7 try { 8 if (needReload) await updateLibraryDetail(to.params.id, to.name, { deferRemaining: true }) 9 } finally { 10 next() 11 } 12 } 13}

Source: router.js

这个现有路由展示了共享详情页的加载条件与歌单专用参数;要修改数据刷新的条件,应同时审视专辑和艺人守卫。

全局模式约束与后台初始化

javascript
1router.beforeEach((to, from, next) => { 2 if (userStore.localOnlyMode && !localOnlyRouteNames.has(String(to.name || ''))) { 3 next({ name: 'mymusic' }) 4 return 5 } 6 7 const fullPath = typeof to?.fullPath === 'string' ? to.fullPath : '' 8 const shouldWarmDeferredInit = fullPath.startsWith('/mymusic') 9 || fullPath.startsWith('/cloud') 10 || fullPath.startsWith('/personalfm') 11 || fullPath.startsWith('/siren') 12 13 if (shouldWarmDeferredInit) { 14 void ensureDeferredAppInit() 15 } 16 17 next() 18})

Source: router.js

本地模式的重定向会提前返回,避免同一次守卫再次调用 next();void 表明此处不把初始化当作导航完成的前置条件。

配置与扩展点

路由相关设置是源码常量和 store 状态,并非已验证的环境变量配置:

选项/条件类型源码值/默认作用
historyRouter historycreateWebHashHistory()使用 hash 历史模式
routeComponentPreloadBatchSizenumber2每批最多取两个加载器并行预热
runIdleTask 选项object{ timeout: 1500, fallbackDelay: 700 }预热批次的空闲调度参数;其内部语义需查该工具实现
localOnlyRouteNamesSetsettings、mymusic、localFiles、localAlbum、localArtist本地模式允许的目标路由名称
userStore.homePage、userStore.cloudDiskPage、userStore.localOnlyModestore 状态默认值未在已读取代码中确认分别控制首页入口、云盘入口及本地模式限制

依据:历史模式及守卫、预热参数、页面开关。新增页面需要在 routes 中注册相应路径、名称和懒加载组件;若需本地模式可见,必须同时核对 localOnlyRouteNames;若希望导航后预热,还需把加载器加入 routeComponentPreloadLoaders。这些是源码中两个独立维护的清单,不能假定注册路由就自动预热。定义位置。

API 与运行细节

符号签名/输入返回与行为失败或边界
createRouteLoadercreateRouteLoader(loader);loader 为返回 Promise 的页面加载函数返回无参加载函数,缓存首次 Promiseloader() 拒绝时清空缓存并重新抛错,使未来调用可再尝试;同步抛错不经过该 .catch
preloadRouteComponentBatchasync preloadRouteComponentBatch(startIndex = 0)window 不存在或索引达到数组长度时直接返回;其他情况下按批交给 runIdleTask,完成后递归调度下一批使用 Promise.allSettled,单个组件导入失败不会令此批的等待直接拒绝;不保证预热成功
scheduleRouteComponentPreloadscheduleRouteComponentPreload()非浏览器、本地模式或已经启动时直接返回;其他情况将标记置为 true,启动第一批启动标记不会在此函数中恢复;失败后也没有整体重新启动机制
hasDifferentLibraryId`(to, from) => String(to?.params?.id'') != String(from?.params?.id

以上接口是模块内部函数及常量,不是对外导出的公共 API。模块末尾仅 export default router,使用者取得的是配置完成的 Router 实例;路由注册到 Vue 应用的代码不在已读取证据中。模块导出。

失败模式、并发与性能

  • 加载失败与并发调用:同一加载器的并发调用共享缓存的 Promise;拒绝时置空以便未来重试。预热对一批加载器用 Promise.allSettled,避免一个导入失败中断整批等待。加载器及预热、批量调度。
  • 空闲预热:首个成功走到 afterEach 的导航会尝试安排预热;仅浏览器且非本地模式会执行。调度器将数组每两个一组串行调度、组内并行加载。routeComponentPreloadStarted 防止重复启动,但也意味着若首次调度之后状态改变,此处没有重新排队逻辑。实际空闲任务的实现及浏览器支持边界未从其源码核实。预热实现与钩子、afterEach。
  • 守卫继续导航:详情路由通过 finally 调用 next(),即 updateLibraryDetail 拒绝也不会由这些守卫阻止继续;不要把进入页面等同于数据必然更新成功。部分本地详情和搜索更新调用没有 await,其完成时机与错误如何呈现在页面上需要另查 store。远程详情、本地详情及搜索。
  • 重定向路径:云盘先检查开关,再检查登录;推荐歌单和私人 FM 检查登录并调用提示;全局本地模式白名单检查在路由表入口检查之前。由此维护权限时应同时考虑全局与路由级守卫,不能只修改某个页面。云盘及入口、推荐及私人 FM、全局守卫。

相关链接

Android 端也有单独的 apps/android/src/router/router.js;本页没有比较两套实现,迁移时不应直接假设它们完全一致。

Sources

(1 files)