【uni-app UTS 插件】三端电子书阅读器:Android / iOS / 鸿蒙 Next 一套 API + 百搭阅读壳
【uni-app UTS 插件】三端电子书阅读器:Android / iOS / 鸿蒙 Next 一套 API + 百搭阅读壳
插件 ID:
ebook-reader
当前版本:1.9.2
支持:Vue2 / Vue3 · app-vue · app-nvue · app-uvue · Android · iOS · 鸿蒙 Next · H5 轻量降级
错误码段:8040xxx
配套:ebook-select-files(选书)·ebook-reader-handwrite(手写)·ebook-reader-ai(AI)
一、为什么跨端电子书这么难做?
在 uni-app 里做「像样的阅读器」,很多人会先想到 WebView + epub.js。上线一两个版本后,常见坑会成批出现:
渲染与性能
- 大 txt / epub 整本塞进 WebView,低端机内存暴涨甚至闪退
- 分页、字号、行距一改就要整页重排,滚动位置容易错乱
- 仿真翻页、听书断句、选区划线,Web 方案体验参差不齐
三端原生差异
- Android TTS 要音频焦点 + 媒体通知,后台才稳
- iOS 后台朗读要开音频会话,字体要用 CTFont
- 鸿蒙权限写在
module.json5,TTS 走 CoreSpeechKit,和 Android/iOS API 完全两套
产品边界
- 小说 App 要书架进度同步;教育 App 要笔记导出;B 端要水印和到期策略
- 手写 OCR、AI 划词、PDF 渲染体积大,不该全塞进一个主包
自己从零写三端原生阅读内核,周期以月计。有没有一套 UTS 插件,既能开箱嵌入页面,又能用纯 API 自绘 UI?
二、ebook-reader:内核 + 百搭阅读壳
ebook-reader 是一个 UTS 原生插件,用一套 JavaScript API 覆盖 Android、iOS、鸿蒙 Next 的本地电子书阅读场景,并附带可直接嵌入的 Vue 组件壳。
典型用途:
- 小说 / 网文阅读页
- 教材 / 教辅 / 学习 App 内阅读
- 企业内部分发文档(水印 + 访问策略)
- 需要自研书架 UI、只接阅读内核的项目
两种接入姿势
| 姿势 | 适合 | 怎么做 |
|---|---|---|
| 百搭阅读壳 | 快速上线阅读页 | <ebook-reader path="..." /> |
| 内核 API | 完全自定义 UI | createReader → openBook → getPageContent |
主包当前定位(1.9):
- ✅ txt / md / epub 文本层(含封面、简介、图片抽出)
- ✅ 分页 · 主题 · 字号 · 书签笔记 · TTS · 选区 · 手势 · 水印
- ✅ 阅读壳内 Canvas 贝塞尔仿真卷曲(
pageMode=simulation) - ❌ 完整 CSS 图文混排引擎、字体二进制打进主包、PDF / 古籍竖排 / 加密(增值或后续大版本)
三、5 分钟开箱接入(阅读壳)
1. 安装
将插件放入项目 uni_modules/ebook-reader,制作自定义调试基座后运行(改 utssdk/ 必须重做基座)。选书推荐同时装 ebook-select-files;鸿蒙再装 ebook-select-files-harmony。
2. 最小页面
easycom 会自动注册组件。宿主页面:
<template>
<ebook-reader
:path="bookPath"
:title="bookTitle"
:show-back="true"
watermark="仅供内部阅读"
@progress="onProgress"
@back="onBack"
@error="onError"
/>
</template>
<script>
export default {
data() {
return {
bookPath: '', // pickBookFile({ copyToCache: true }) 后的稳定 path
bookTitle: '我的书'
}
},
methods: {
onProgress(p) {
// 同步自有书架:p.page / p.percent / p.position
console.log(p.page, p.percent)
},
onBack() {
uni.navigateBack()
},
onError(err) {
console.error(err.code, err.message)
}
}
}
</script>
3. 书源三选一
| Prop | 说明 |
|---|---|
path | 本地沙盒路径(选书务必 copyToCache: true) |
url | 网络下书后打开 |
content | 内存正文;H5 / 任意端快速试读 |
命令式也可以:
this.$refs.reader.open({ path: '/path/to/a.epub', bookId: 'b1' })
this.$refs.reader.open({ content: '第一章\n\n正文…', format: 'txt' })
this.$refs.reader.pickAndOpen()
4. 壳内交互(用户无需再写)
- 点中央:显隐顶/底栏
- 点左右约 28% 或滑动:翻页
simulation:Canvas 贝塞尔卷曲,跟手拖动;可开纸张音效- 双指捏合调字号;上下滑调亮度
- 长按:复制 / 划线 / 笔记 / 朗读
- 底栏:目录 · 主题(冷纸/夜墨/青苔/旧笺)· Aa · 书签 · 听书
插件自带的 index.vue 就是这个壳的演示页(含试读样例),可直接对照。
四、完全自定义:内核 API
不需要内置 UI 时,只接内核:
import {
createReader,
openBook,
getPageContent,
nextPage,
onReaderEvent,
getReaderCapabilities,
pickBookFile
} from '@/uni_modules/ebook-reader'
const caps = getReaderCapabilities()
console.log(caps.platform, caps.supportsEpub, caps.supportsTts)
const { readerId } = await createReader({
theme: 'day',
viewportWidth: 360,
viewportHeight: 640,
statsIntervalMs: 30000
})
onReaderEvent(readerId, 'progress', (p) => {
console.log(p.page, p.pageCount, p.percent)
})
onReaderEvent(readerId, 'behavior', (e) => {
// 只抛不传:由业务层自己上报
console.log(e)
})
const pick = await pickBookFile({
count: 1,
extensions: ['txt', 'md', 'epub'],
copyToCache: true
})
if (!pick.ok) {
// 用户取消:8040001
return
}
await openBook(readerId, {
path: pick.files[0].path,
bookId: 'my_book',
restoreProgress: true
})
const page = await getPageContent(readerId)
console.log(page.text, page.images)
await nextPage(readerId)
重要:UTS 桥接不回传带方法的 class,所有 API 以
readerId为第一参数。不要指望拿到一个「Reader 实例对象」再调方法。
打开方式汇总
// 本地路径
await openBook(readerId, { path: '/sandbox/a.epub', bookId: 'b1' })
// 网络 URL(内部 downloadBook)
await openBook(readerId, { url: 'https://cdn.example.com/a.epub' })
// 内存正文(含 H5)
await openBook(readerId, { path: 'content:第一章\n\n正文', format: 'txt' })
// Base64 字节流
await openBook(readerId, { bytesBase64: '...', format: 'txt' })
五、三端分别做了什么?
Android
- 文本分页 + epub 文本层 / 图片抽出
- 原生页:TextView overlay;uni-app x 可 embed 到 ViewGroup
- TTS:音频焦点 + MediaSession 媒体通知(后台听书)
- 环境光、口袋模式(距离传感器)
- 翻页音效:ToneGenerator
- 自定义字体:Typeface
iOS
- 同构内核 API;原生页 UITextView
- embed:按
accessibilityIdentifier找容器 - TTS:后台音频会话
- 翻页音效:SystemSound
- 自定义字体:CTFont
setTheme('system')跟随深色模式
鸿蒙 Next
- 权限等声明走
module.json5(只写config.json不会进包) - TTS:
@kit.CoreSpeechKit - 环境光、口袋模式可用
- 原生页目前以系统对话框 overlay 为主
- 翻页音效等能力以事件抛给宿主 UI
H5
- 轻量降级:
content/content:试读 - 无本地选书、无完整 epub、无原生 TTS/传感器
- 阅读壳仍可跑通交互原型
六、几个实用特性详解
1. epub 文本层(不是完整 CSS 引擎)
当前主包解析路径:
META-INF/container.xml → OPF manifest/spine → 章节 HTML 抽纯文本
- 封面 / 简介:
BookMeta.coverPath/description - 图片:正文占位
[图 src="..." alt="..."],PageContent.images[].localPath可给宿主展示 - 目录:优先 NCX / nav,否则文内首行
适合「小说正文阅读」;复杂图文杂志排版需等完整 CSS 引擎大版本,或自研宿主渲染。
2. 仿真翻页:两套实现别混用
| 场景 | 实现 |
|---|---|
阅读壳 pageMode=simulation | Canvas 贝塞尔卷曲(er-page-curl),跟手拖动 |
原生页 showNativeReader | Android/iOS 轻量位移动画(非物理卷曲) |
壳内仿真已可用于 App-Vue 验证;原生层物理卷曲仍属后续规划。
3. TTS 听书
await ttsPlay(readerId, { fromSelection: false })
await ttsPause(readerId)
await ttsNext(readerId) // 翻页并继续读
await ttsPrev(readerId)
await setPauseMarksVisible(readerId, null) // null = 跟随 TTS 自动开断句标记
Android 可出媒体通知;iOS 开后台会话;鸿蒙走系统 Speech Kit。
4. B 端:水印 + 访问策略
await setWatermark(readerId, { text: '内部资料', opacity: 0.12, enabled: true })
await setBookAccessPolicy({
bookId: 'lease_001',
expireAt: Date.now() + 7 * 86400000,
allowOpen: true
})
// 到期 openBook → 8040106;禁止打开 → 8040107
行为埋点事件(bookOpen / pageTurn / dwell / selection 等)只抛不传,上报由业务层自己做。
5. 摘抄导出
const md = await exportNotes(readerId, 'md')
await copyExportToClipboard(readerId, 'json')
适合学习类 App 一键导出划线 + 笔记 + 页码。
七、增值模块怎么拆?
主包刻意做「薄」:同名 API 在主包是 stub,未安装时统一 8040004。
// 手写:请从增值包 import
import { enableHandwrite, ocrHandwrite } from '@/uni_modules/ebook-reader-handwrite'
// AI:不内置大模型,只调你的 endpoint
import { configureAi, aiLookup } from '@/uni_modules/ebook-reader-ai'
await configureAi({
apiKey: 'sk-xxx',
endpoint: 'https://your.api/v1'
})
const r = await aiLookup('ephemeral', 'dict')
| 模块 | 能力摘要 |
|---|---|
ebook-reader-handwrite | 笔迹叠加、持久化、端侧 OCR、批注朗读 |
ebook-reader-ai | 划词释义、章节摘要、抽词、生词卡(业务 HTTP) |
规划中:ebook-reader-pdf、ebook-reader-ancient、ebook-reader-secure。
八、能力矩阵速览(1.9)
| 能力 | Android | iOS | 鸿蒙 | H5 |
|---|---|---|---|---|
| 阅读壳 | ✅ | ✅ | ✅ | ✅(content) |
| 壳内贝塞尔卷曲 | ✅ | ✅ | ✅ | ✅ |
| txt / md | ✅ | ✅ | ✅ | ✅ |
| epub 文本层 | ✅ | ✅ | ✅ | ❌ |
| pickBookFile | ✅ | ✅ | ✅ | ❌ |
| TTS | ✅ | ✅ | ✅ | ❌ |
| 后台 TTS | ✅ 通知 | ✅ | ❌ | ❌ |
| 原生页 overlay | ✅ | ✅ | ✅ 对话框 | ❌ |
| 环境光 / 口袋模式 | ✅ / ✅ | ❌ / ❌ | ✅ / ✅ | ❌ |
| 水印 / 访问策略 | ✅ | ✅ | ✅ | ✅ |
更细的 Props / 事件 / API 表见插件 readme.md。
九、错误码与踩坑
常见错误码
| code | 含义 |
|---|---|
| 8040001 | 用户取消选书 |
| 8040002 | 参数非法 |
| 8040003 | 平台不支持 |
| 8040004 | 增值模块未安装 |
| 8040100 | 文件不存在 |
| 8040105 | 书籍过大(约 20MB 上限) |
| 8040106 / 8040107 | 过期 / 禁止打开 |
| 8040200 | 阅读器未创建或原生页容器失败 |
| 8040201 | 未打开书籍 |
| 8040300 | 字体失败 |
| 8040400 | TTS 不可用 |
必记踩坑
- 改原生后重做自定义基座
- 目录名 =
package.json的id=ebook-reader - 禁止导出名为
init的函数(iOS/Swift 保留字,云打包会挂) - 选书务必
copyToCache: true,再用稳定 path 打开 - 鸿蒙权限必须进
module.json5+$string:reason - UTS 返回值禁止
Promise<{...}>,要用命名类型(如FontInstallResult)
十、隐私与市场声明(可直接改写)
- 权限:主包无强制危险权限;选书随系统文件选择;按需网络下书、TTS 后台音频/通知、环境光/距离传感器
- 数据:默认仅本地存储进度 / 书签 / 笔记;行为事件只抛给宿主;AI 由业务自配服务端
- 广告:无
十一、版本演进(摘要)
| 版本 | 亮点 |
|---|---|
| 1.0 | txt/md 分页、书签笔记、三端 + Web |
| 1.1 | epub 文本层、前台 TTS |
| 1.2–1.3 | 下书、简繁、音效、水印、口袋模式、导出 |
| 1.4–1.6 | 选区手势、原生页、后台 TTS 通知 |
| 1.7–1.8 | epub 图片、鸿蒙 TTS、选书、封面简介 |
| 1.9.0 | 百搭阅读壳开箱即用 |
| 1.9.1 | 手写 / AI stub + 独立增值包 |
| 1.9.2 | FontInstallResult 修复;壳内 Canvas 贝塞尔卷曲 |
完整条目见插件 changelog.md。
十二、总结
如果你在做 uni-app 的小说、教育或企业阅读场景,又不想维护三套原生阅读器:
- 要快:挂
<ebook-reader>,进度事件回写书架 - 要自由:只用
createReader/openBook/getPageContent自绘 - 要增值:按需装 handwrite / AI,主包保持体积可控
一套 readerId API,Android / iOS / 鸿蒙共用;错误码统一 8040xxx,便于日志和客服排查。
欢迎在评论区交流接入问题;插件市场与更新日志以插件包内 readme.md / changelog.md 为准。
更多推荐





所有评论(0)