Vue 应用壳与路由页面
本页说明主应用 src 下 Vue Router 如何选择页面、加载嵌套内容,以及在导航时触发权限判断、数据准备和空闲预热。
用途与范围
面向维护主应用导航的开发者:覆盖 src/router/router.js 中的路由表、守卫和页面加载策略;仅依据已核实的 Home.vue 与 MyMusic.vue 路由出口说明嵌套渲染。播放器、页面内部业务、Android 应用壳和应用启动装配属于其他主题,不在此推断其实现。主应用入口及根组件的装配细节未从已读取源码确认。
概述
路由使用 createWebHashHistory();页面组件通过 createRouteLoader 延迟导入。/mymusic 提供子路由(歌单、专辑、艺人、推荐、DJ 与本地音乐);此外有首页、云盘、登录、汽水、私人 FM、搜索和设置等独立页面。路由守卫依赖 Pinia store、isLogin 及 noticeOpen 进行状态检查,并在详情页进入前调用 store 更新函数。首轮导航结束后,在允许的模式下分批预热页面组件。参见路由表与加载器及导航钩子。
架构
Source: router.js
图中 Routes 表示源码中的 routes 数组而非独立服务:路由器持有它,MyMusic 作为父路由关联详情组件;store 由路由模块使用,不表示这些页面直接依赖 store。路由表中的 component 指向延迟导入函数,导航完成后的钩子才安排组件预热。
路由组织与控制流
路径和页面边界
| 路径 | 名称 | 组件/进入行为 |
|---|---|---|
/ | homepage | HomePage;homePage 关闭则转 mymusic |
/cloud | clouddisk | CloudDisk;先检查 cloudDiskPage,再检查登录;未登录转 login 并提示 |
/login、/login/account | login、account | LoginPage、LoginContent |
/siren、/siren/album/:id | siren、sirenAlbum | 共用 SirenPage |
/mymusic | mymusic | MyMusic;允许本地模式或登录用户;还有从首页/搜索进入子路径的例外 |
/mymusic/playlist/:id、/mymusic/album/:id、/mymusic/artist/:id | playlist、album、artist | LibraryDetail;按来源路由和 ID 判断是否重新获取详情 |
/mymusic/playlist/rec、/mymusic/dj/:id | rec、dj | RecommendSongs(须登录)、RadioDetail |
/mymusic/local/files、/mymusic/local/album/:id、/mymusic/local/artist/:id | localFiles、localAlbum、localArtist | LocalMusicDetail;从其他同名路由进入时准备本地详情 |
/personalfm | personalfm | PersonalFMPage;须登录 |
/library、/search、/settings | library、search、settings | LibraryDetail、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 安排组件预热。参见全局导航钩子。
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()。本地详情和搜索、搜索守卫。
用法示例(仓库中的实际写法)
下面的片段均直接取自路由实现,用于说明扩展时需要遵循的现有模式,而不是另外定义的调用接口。
可重试的懒加载包装
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(...)),预热数组复用这些函数,因此导航与空闲预热共享加载结果。
按参数变化准备详情
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
这个现有路由展示了共享详情页的加载条件与歌单专用参数;要修改数据刷新的条件,应同时审视专辑和艺人守卫。
全局模式约束与后台初始化
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 状态,并非已验证的环境变量配置:
| 选项/条件 | 类型 | 源码值/默认 | 作用 |
|---|---|---|---|
history | Router history | createWebHashHistory() | 使用 hash 历史模式 |
routeComponentPreloadBatchSize | number | 2 | 每批最多取两个加载器并行预热 |
runIdleTask 选项 | object | { timeout: 1500, fallbackDelay: 700 } | 预热批次的空闲调度参数;其内部语义需查该工具实现 |
localOnlyRouteNames | Set | settings、mymusic、localFiles、localAlbum、localArtist | 本地模式允许的目标路由名称 |
userStore.homePage、userStore.cloudDiskPage、userStore.localOnlyMode | store 状态 | 默认值未在已读取代码中确认 | 分别控制首页入口、云盘入口及本地模式限制 |
依据:历史模式及守卫、预热参数、页面开关。新增页面需要在 routes 中注册相应路径、名称和懒加载组件;若需本地模式可见,必须同时核对 localOnlyRouteNames;若希望导航后预热,还需把加载器加入 routeComponentPreloadLoaders。这些是源码中两个独立维护的清单,不能假定注册路由就自动预热。定义位置。
API 与运行细节
| 符号 | 签名/输入 | 返回与行为 | 失败或边界 |
|---|---|---|---|
createRouteLoader | createRouteLoader(loader);loader 为返回 Promise 的页面加载函数 | 返回无参加载函数,缓存首次 Promise | loader() 拒绝时清空缓存并重新抛错,使未来调用可再尝试;同步抛错不经过该 .catch |
preloadRouteComponentBatch | async preloadRouteComponentBatch(startIndex = 0) | window 不存在或索引达到数组长度时直接返回;其他情况下按批交给 runIdleTask,完成后递归调度下一批 | 使用 Promise.allSettled,单个组件导入失败不会令此批的等待直接拒绝;不保证预热成功 |
scheduleRouteComponentPreload | scheduleRouteComponentPreload() | 非浏览器、本地模式或已经启动时直接返回;其他情况将标记置为 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;本页没有比较两套实现,迁移时不应直接假设它们完全一致。