mirror of
https://github.com/hectorqin/reader.git
synced 2026-11-12 05:50:54 +08:00
Audit of README, docs/, android/README.md, android/contract.md and CLAUDE.md against the current server, web and Android code: V3 navigation and settings locations, Docker Hub release images, GitHub clone URL and CI, missing API endpoints and error codes, architecture of lazy chunks/themes/sources, media pages and routes, the Android bridge contract, and the new file-management and member-upload permissions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
106 lines
5.9 KiB
Markdown
106 lines
5.9 KiB
Markdown
# 外壳与 H5 的接口
|
||
|
||
这份文件是外壳与 H5 之间的全部约定。两边分别用 Kotlin 和 TypeScript 写,互相无法类型检查,
|
||
所以以 `web/src/android-bridge.d.ts` 为准,由 `android/scripts/check-shell.sh` 双向核对。
|
||
改动任何一侧都要同步另一侧和本文件。
|
||
|
||
## 1. 启动流程
|
||
|
||
```
|
||
MainActivity 启动
|
||
→ WebHost 配置 WebView,注册 JS 接口与媒体消息通道
|
||
→ 加载 https://appassets.androidplatform.net/assets/index.html(+ 快捷方式对应的 #/media/... 片段)
|
||
→ H5 检测 window.ReaderAndroid,选择 Android 平台
|
||
→ 服务地址:用户已保存的地址 > configuredServerUrl()(构建时 -PdefaultServerUrl)> 留空待填
|
||
→ H5 自己完成登录、会话保存(存储层与浏览器相同)和后续所有请求
|
||
```
|
||
|
||
外壳**不**读写令牌、**不**向 `localStorage` 注入会话,也不 eval 业务脚本。资源来源
|
||
`https://appassets.androidplatform.net` 被 H5 视为无效的服务地址,服务端 CORS 单独放行它。
|
||
|
||
## 2. JS 接口
|
||
|
||
外壳用 `addJavascriptInterface` 注册两个对象。只有标注 `@JavascriptInterface` 的方法对页面可见。
|
||
|
||
### `window.ReaderAndroid`(`bridge/ReaderBridge.kt`,当前 `SHELL_VERSION = 3`)
|
||
|
||
| 方法 | 返回 | 说明 |
|
||
| --- | --- | --- |
|
||
| `shellVersion()` | number | H5 要求 ≥ `MIN_SHELL_VERSION`(1),否则按普通浏览器运行 |
|
||
| `configuredServerUrl()` | string | 构建时预设的服务地址,可为空;d.ts 中为可选方法 |
|
||
| `deviceLabel()` | string | 写进进度记录的设备名 |
|
||
| `connectivity()` | string | `online` / `offline`,来自系统连接管理器 |
|
||
| `watchConnectivity(cb)` | void | 见下文「回调」 |
|
||
| `toast(message)` | void | 原生 Toast |
|
||
| `hasNetwork()` | boolean | 当前是否有网络 |
|
||
| `viewport()` | string | JSON:物理像素视口与密度 |
|
||
| `cacheUsage()` | string | JSON:`webViewBytes`、`dataBytes`、`availableBytes` |
|
||
| `diagnostics()` | string | JSON:外壳版本、设备、系统、应用版本,用于问题报告 |
|
||
| `renderPage(request)` | boolean | shell ≥ 2。见第 3 节 |
|
||
| `hidePage()` | void | 移除原生页面,露出 WebView |
|
||
| `canOpenDocument(mediaType)` | boolean | 目前总是 `false`,PDF 留在 WebView |
|
||
| `speech()` | object | Kotlin 侧访问器;页面看到的是下面的独立接口 |
|
||
|
||
### `window.ReaderAndroidSpeech`(`bridge/SpeechBridge.kt`,shell ≥ 3)
|
||
|
||
`addJavascriptInterface` 只能暴露对象的方法,不能暴露属性,所以朗读接口单独注册;
|
||
H5 的 `promoteSpeechInterface`(`web/src/core/android-platform.ts`)把它挂到
|
||
`ReaderAndroid.speech`,d.ts 中声明为 `speech?: SpeechBridge`。
|
||
|
||
方法:`available()`、`init()`、`onSpeechEvent(cb)`、`onVoices(cb)`、`speak(text, utteranceId)`、
|
||
`pause()`、`stop()`、`setRate(rate)`、`setPitch(pitch)`、`setVolume(volume)`、`setVoice(voiceId)`、`shutdown()`。
|
||
朗读队列与游标在 JS;`utteranceId` 由 H5 生成,事件原样带回,用来丢弃过期句子的 `done`。
|
||
`interruptForMedia()` 没有注解,只供 Kotlin 侧在开始播放影音时调用,不属于页面契约。
|
||
|
||
### 回调
|
||
|
||
JS 接口只能传原始类型,所以 Kotlin 端的 `watchConnectivity`、`onSpeechEvent`、`onVoices`
|
||
接收的是**全局函数名**(须是安全标识符),之后用 `evaluateJavascript` 调用它。H5 包装层把函数注册为
|
||
`window.__readerConnectivity`、`__readerSpeechEvent`、`__readerSpeechVoices` 再把名字传过去;
|
||
连接状态传字符串,朗读事件与声音列表传 JSON。页面代码按 d.ts 直接传函数即可。
|
||
|
||
## 3. 原生整页图片
|
||
|
||
H5 在 `reader-view.ts` 中通过 `pageHost` 询问外壳,仅在页面为 `image` 模式且读者选择「适应整页」时:
|
||
|
||
```
|
||
ReaderAndroid.renderPage(JSON.stringify({ sectionId, mode: 'image', mediaType, bytes: <base64>, fit: 'contain' }))
|
||
→ true:NativePageView 已覆盖在 WebView 上绘制
|
||
→ false:外壳拒绝(模式/适配方式不支持、无法解码、超时 2 秒),H5 在 WebView 中照常绘制
|
||
```
|
||
|
||
外壳只解码请求里的 `bytes`(JPEG/PNG/WebP),不按 `path` 自行取图。离开该页时 H5 调用 `hidePage()`。
|
||
外壳分支用到的模式名必须在 `web/src/formats/types.ts` 的 `RenderMode` 中,`check-shell.sh` 会检查。
|
||
|
||
## 4. 影音通道 `window.ReaderMedia`
|
||
|
||
`media/MediaBridge.kt` 用 `WebViewCompat.addWebMessageListener` 注册,只接受资源来源、只接受主框架,
|
||
需要 WebView 支持 `WEB_MESSAGE_LISTENER`;不支持时不注册,H5 使用网页播放器。H5 发送 JSON 字符串
|
||
`{ action, ... }`(`web/src/features/media/services/native-audio.ts`),外壳通过回复通道回传播放状态。
|
||
|
||
`action`:`play`、`showVideo`、`pause`、`resume`、`stop`、`previous`、`next`、`selectQueue`、`editQueue`、
|
||
`seek`、`speed`(0.5–3.0)、`audioTrack`、`sleep`、`reconnect`、`status`。播放由
|
||
`MediaPlaybackService`(Media3)执行;视频画面在 `MediaVideoActivity` 中显示。消息超过约 900 KB 被忽略。
|
||
|
||
## 5. 生命周期与系统键
|
||
|
||
- `onPause` 时外壳执行 `window.__readerFlush && window.__readerFlush()`,H5 借此保存阅读位置。
|
||
- 返回键:WebView 有历史则后退,否则退出。
|
||
- 外壳**目前不拦截音量键**,也没有「原生翻页器退出后通知 H5」的事件;旧版文档中的
|
||
`reader:native-page` / `reader:native-return` 事件并不存在。
|
||
|
||
## 6. 离线
|
||
|
||
离线下载与缓存由 H5 负责(与浏览器共用实现),外壳不另建书籍缓存。H5 使用的服务端接口:
|
||
|
||
```
|
||
GET /api/v1/books/:id/content
|
||
→ 整本书,带 ETag,可 Range 续传
|
||
→ 目录书(漫画目录)返回 400 DIRECTORY_BOOK;分章书返回 400 CHAPTER_BOOK
|
||
|
||
GET /api/v1/books/:id/file?path=<manifest files 中列出的 rel_path>
|
||
→ 单个文件;path 只能取 manifest 列出的值,其他值得不到别的文件
|
||
```
|
||
|
||
服务端不可达时客户端读本地缓存并静默降级,不能靠报错弹窗。
|