Files
reader/docs/frontend-react-migration.md
T
hectorqin 8584d7b4e3 fix(web): align React UI with legacy reader UX
Unify media secondary page chrome, loading states, playback controls, favorites filtering, and source/library screens with the legacy reader experience.
2026-10-05 13:23:27 +08:00

86 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 前端 React 架构说明
本文保留原“前端 React 迁移”文件名以避免已有链接断开,内容描述当前实现约定。生产 Web 入口已经使用 React 19;后续页面和组件应遵循本文及[架构与数据边界](architecture.md)中的边界,不再向旧的 Screen 调度链添加代码。
## 当前技术栈
| 能力 | 当前实现 | 责任 |
| --- | --- | --- |
| UI 运行时 | React 19 + React DOM | 页面、组件和生命周期 |
| 路由 | React Router 7 `createHashRouter` | Hash URL、嵌套路由、Outlet、参数、错误边界和导航 |
| 服务端状态 | `@tanstack/react-query` | 查询缓存、去重、mutation、失效和预取 |
| 客户端共享状态 | Zustand | 认证、设置、同步和播放状态 |
| 基础组件 | Mantine | 主题、表单、菜单、通知、加载和管理界面 |
| 图标 | `lucide-react` | React 图标组件 |
| 构建 | Vite + TypeScript | H5 与 Android WebView 共用的相对资源单 Bundle |
继续使用 Hash 路由是因为同一份产物同时由 H5 服务和 Android WebView 加载;两个宿主都不能为任意路径提供客户端回退。新的链接应使用 `#/...` 形式。
## 启动与运行时
`web/src/main.tsx` 创建 `AppRuntime`,然后调用 `createRoot()` 渲染 `AppProviders` 和 `RouterProvider`。`web/src/app/runtime.ts` 只负责组装共享资源,不选择页面:
- 根据环境选择 Web 或 Android 平台,创建 `ReaderApi`、`MediaApi`、`MediaPlayer`、`PlaybackService`、`SyncEngine`、`OfflineStore`、`SettingsStore` 和 Query Client;
- 恢复本地会话、服务地址、离线作用域和同步状态;
- 暴露 flush、登录状态、阅读器注册和设置更新等宿主能力。
`AppProviders` 提供 `RuntimeContext`、`QueryClientProvider`、`MantineProvider` 和全局通知。Provider 生命周期负责启动/停止播放服务、连接会话与同步监听、写入根主题属性,并在页面隐藏或离开时刷新进度、离线数据和播放器状态,重新联网时续期会话。
## 目录边界
当前 Web 代码按以下实际目录组织:
```text
web/src/
app/ # AppShell、Provider、运行时和 React Router
features/
auth/pages/ # 登录
library/pages/ # 书库浏览和文件管理
media/ # 影音 API、页面、组件、查询、变更、服务、状态和样式
reader/pages/ # 阅读路由页面
settings/pages/ # 用户与服务设置
shelf/pages/,queries/ # 书架
sources/hooks/,pages/ # 书源及扩展页面
shared/query/,stores/,ui/ # 跨领域查询、状态和 UI 边界
api/, core/, formats/, net/ # API、平台、格式和网络基础能力
render/ # 分页、PDF、文本、朗读等渲染服务
store/ # 离线、出版物、设备设置存储
ui/ # 阅读器命令式舞台及可复用交互组件
styles/ # 应用和阅读器全局样式
```
页面组件只组合页面展示、查询和交互。API 请求通过领域 API、query 或 mutation 访问;跨页面状态放在 Zustand store;播放器、主题和阅读器等长生命周期行为放在 service。旧的 `web/src/media`、旧 Screen 页面、旧手写生产路由和 `mountUI` 已从生产入口移除。`web/src/app.ts` 与 `app/legacy-compat.ts` 仅保留 Android 诊断和旧测试所需的兼容形状,不负责生产页面调度。
## 路由与兼容
路由定义集中在 `web/src/app/router/routes.tsx`,根级 `AppShell` 先经过 `AuthBoundary`,认证后渲染页面 Outlet。影音由 `MediaLayout` 提供频道上下文、主题生命周期和公共频道入口,具体页面继续按路由拆分。
保留的兼容入口是有意的:
- `/settings` 重定向到 `/shelf`,兼容旧书架设置深链接;
- `/sources/search` 仍打开书源中心;
- `/sources/:sourceId/:pageId` 与新的 `/sources/:sourceId/pages/:pageId` 都可打开扩展页;
- `/media`、`/media/music` 和 `/media/audiobook` 重定向到对应频道默认页,未知路径回到 `/media/video`。
兼容路由只保证旧链接仍能进入正确的 React 页面;新代码应直接使用当前路由树中的命名空间和嵌套路径。
## 阅读器托管边界
`features/reader/pages/ReaderPage.tsx` 是 React 与阅读器命令式实现之间的边界。页面取得书籍后,在 effect 中创建 `ReaderScreen`,把 `screen.element` 放入 route host,并通过 `runtime.registerReader()` 注册进度刷新回调。清理 effect 时先注销回调,再调用 `screen.dispose()` 并清空 host。
`ReaderScreen` 内部仍负责章节加载、Shadow DOM、分页量测、手势、朗读和阅读工具;React 只托管它的创建、更新和销毁,不介入书籍正文的布局量测。ReaderScreen 的 chrome 使用 React 组件,正文舞台保持命令式 DOM,以确保 `getBoundingClientRect()` 和章节分页的节点生命周期稳定。
## 播放器和媒体主题生命周期
播放器是应用级资源。`AppProviders` 把 `runtime.player.element` 挂到 `document.body`,调用 `runtime.playback.start()` 建立 `PlaybackService` 对 `MediaPlayer` 的监听;路由切换只卸载控制界面,不销毁播放会话。`AuthenticatedShell` 根据当前 URL 控制播放器可见性,并处理播放器发出的 `open-controls` 事件,将用户带到带有 `item` 和 `part` 查询参数的播放器路由。Provider 卸载时停止监听并移除播放器节点。
影音主题由 `MediaLayout` 创建 `MediaThemeController`,作用域来自 `runtime.mediaApi.preferenceScope()`。控制器只向 `body` 写入带 `--media-theme-` 前缀的变量,响应系统主题、同页主题事件和存储变化;离开影音路由时清理变量和监听器,因此不会污染书架或阅读器主题。
## 后续改动约定
- 新页面放入对应的 `features/<domain>/pages`,通过 `routes.tsx` 接入;
- 领域组件、查询、变更、服务和样式留在各自 feature 内,跨领域依赖经过 `shared` 或 `app` 边界;
- 需要长生命周期的资源必须由 Provider 或明确的 service 管理,并在清理阶段释放;
- 阅读器正文继续通过 `ReaderScreen` 托管,不把命令式舞台改写为普通 React 子树;
- 新链接使用 Hash 路由和当前嵌套路由,不新增旧 Screen 或手写 pathname 分支。