Harmony os—“阅读进度条 + 章节目录联动”完整模板(Web 计算进度,通过数据通道同步到 ArkTS)
Harmony os—“阅读进度条 + 章节目录联动”完整模板(Web 计算进度,通过数据通道同步到 ArkTS)
这套模板适合做 长文阅读 / 协议阅读 / 小说章节 这类场景:
- Web 负责计算滚动进度、判断当前章节
- ArkTS 负责显示原生进度条、章节目录(点击可跳转)
- 两边通过数据通道互通(message/postMessage)
核心点:
- ArkTS:
.onMessage()接收 Web 上报进度- ArkTS:
controller.postMessage()发送“跳转章节 / 请求同步”指令给 Web- Web:scroll 事件节流,上报
{progress, currentChapterId, title, scrollTop}
1)ArkTS 页面:WebReadingPage.ets(原生进度条 + 章节目录 + Web)
// entry/src/main/ets/pages/WebReadingPage.ets
import { webview } from '@kit.ArkWeb';
type ProgressMsg = {
type: 'SCROLL_PROGRESS',
payload: {
progress: number;
currentChapterId?: string;
currentChapterTitle?: string;
scrollTop?: number;
}
}
type ReadyMsg = { type: 'WEB_READY' }
type ToWebMsg =
| { type: 'SCROLL_TO_CHAPTER'; payload: { id: string } }
| { type: 'REQUEST_SYNC' };
@Entry
@Component
struct WebReadingPage {
private controller: webview.WebviewController = new webview.WebviewController();
@State progress: number = 0; // 0~100
@State currentChapterTitle: string = '(未开始)';
@State lastScrollTop: number = 0;
// 目录(示例:你也可以从接口/配置读取)
private chapters: Array<{ id: string; title: string }> = [
{ id: 'ch1', title: '第 1 章:开场' },
{ id: 'ch2', title: '第 2 章:核心概念' },
{ id: 'ch3', title: '第 3 章:实战拆解' },
{ id: 'ch4', title: '第 4 章:常见坑' },
{ id: 'ch5', title: '第 5 章:收尾总结' },
];
private sendToWeb(msg: ToWebMsg) {
try {
this.controller.postMessage(msg);
} catch (e) {
// 有时候 Web 还没 ready 会失败,项目里可以做队列缓存
console.info('postMessage failed: ' + JSON.stringify(e));
}
}
build() {
Column() {
// 顶部信息栏(原生)
Column({ space: 8 }) {
Text(`阅读进度:${this.progress}%`)
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text(`当前章节:${this.currentChapterTitle}`)
.fontSize(13)
.opacity(0.75)
// 简易原生进度条(不用额外组件,直接用两层矩形)
Stack() {
// 底条
Rect()
.height(8)
.width('100%')
.radius(999)
.opacity(0.15)
// 进度条
Rect()
.height(8)
.width(`${this.progress}%`)
.radius(999)
}
.width('100%')
}
.padding(12)
// 主体:左侧目录 + 右侧 Web
Row() {
// 目录区
Column() {
Text('目录')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.padding({ bottom: 8 })
Scroll() {
Column({ space: 8 }) {
ForEach(this.chapters, (item) => {
Button(item.title)
.width('100%')
.onClick(() => {
this.sendToWeb({ type: 'SCROLL_TO_CHAPTER', payload: { id: item.id } });
})
})
}
.padding({ right: 8 })
}
.height('100%')
}
.width('35%')
.padding(12)
// Web 区
Column() {
Web({
src: $rawfile('web/reading.html'),
controller: this.controller
})
.javaScriptAccess(true)
.onMessage((event) => {
const data = event.data as ProgressMsg | ReadyMsg;
if (!data || !('type' in data)) return;
if (data.type === 'WEB_READY') {
// Web 告诉 App:已就绪,可同步一次状态
this.sendToWeb({ type: 'REQUEST_SYNC' });
return;
}
if (data.type === 'SCROLL_PROGRESS') {
const p = Math.max(0, Math.min(100, Math.round(data.payload.progress)));
this.progress = p;
this.lastScrollTop = data.payload.scrollTop ?? this.lastScrollTop;
if (data.payload.currentChapterTitle) {
this.currentChapterTitle = data.payload.currentChapterTitle;
}
}
})
.height('100%')
.width('100%')
}
.width('65%')
.padding({ top: 12, right: 12, bottom: 12 })
}
.height('100%')
}
.width('100%')
.height('100%')
}
}
想让目录“高亮当前章节”?下一步我也能给你加:
只要 Web 上报currentChapterId,ArkTS 根据 id 给按钮换样式即可。
2)Web 页面:resources/rawfile/web/reading.html(计算进度 + 上报 + 接收跳转)
<!-- entry/src/main/resources/rawfile/web/reading.html -->
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Reading</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
padding: 16px;
line-height: 1.8;
}
h2 {
margin: 28px 0 10px;
padding-top: 10px;
border-top: 1px solid #eee;
}
.hint {
padding: 10px 12px;
background: #f6f6f6;
border-radius: 10px;
}
.spacer { height: 800px; } /* 模拟长内容 */
</style>
</head>
<body>
<div class="hint">
这是一个示例长文页面:滚动时会把“阅读进度 + 当前章节”通过数据通道发给 ArkTS。
</div>
<h2 id="ch1" data-title="第 1 章:开场">第 1 章:开场</h2>
<p>这里是正文……(你可以替换成真实文章内容)</p>
<div class="spacer"></div>
<h2 id="ch2" data-title="第 2 章:核心概念">第 2 章:核心概念</h2>
<p>这里是正文……</p>
<div class="spacer"></div>
<h2 id="ch3" data-title="第 3 章:实战拆解">第 3 章:实战拆解</h2>
<p>这里是正文……</p>
<div class="spacer"></div>
<h2 id="ch4" data-title="第 4 章:常见坑">第 4 章:常见坑</h2>
<p>这里是正文……</p>
<div class="spacer"></div>
<h2 id="ch5" data-title="第 5 章:收尾总结">第 5 章:收尾总结</h2>
<p>这里是正文……</p>
<div class="spacer"></div>
<script>
// ====== 工具:节流(滚动事件必须节制,不然消息太多)======
function throttle(fn, wait) {
let last = 0;
let timer = null;
return function (...args) {
const now = Date.now();
if (now - last >= wait) {
last = now;
fn.apply(this, args);
} else {
clearTimeout(timer);
timer = setTimeout(() => {
last = Date.now();
fn.apply(this, args);
}, wait - (now - last));
}
};
}
// ====== 计算阅读进度:scrollTop / (scrollHeight - clientHeight) ======
function calcProgress() {
const doc = document.documentElement;
const scrollTop = doc.scrollTop || document.body.scrollTop || 0;
const max = (doc.scrollHeight - doc.clientHeight) || 1;
const progress = (scrollTop / max) * 100;
return { progress, scrollTop };
}
// ====== 计算当前章节:找“顶部最近的 h2” ======
function calcCurrentChapter() {
const headings = Array.from(document.querySelectorAll('h2[id]'));
const y = 80; // 顶部偏移,留一点余量更贴近阅读感受
let current = headings[0];
for (const h of headings) {
const rect = h.getBoundingClientRect();
if (rect.top <= y) current = h;
else break;
}
return {
id: current?.id || '',
title: current?.dataset?.title || current?.textContent || ''
};
}
// ====== 向 App 上报(数据通道)======
function postProgress() {
const { progress, scrollTop } = calcProgress();
const ch = calcCurrentChapter();
window.postMessage({
type: 'SCROLL_PROGRESS',
payload: {
progress,
scrollTop,
currentChapterId: ch.id,
currentChapterTitle: ch.title
}
});
}
const postProgressThrottled = throttle(postProgress, 120);
window.addEventListener('scroll', () => {
postProgressThrottled();
}, { passive: true });
// ====== 接收 App 指令:跳转到章节 ======
window.addEventListener('message', (event) => {
const msg = event.data;
if (!msg || !msg.type) return;
if (msg.type === 'SCROLL_TO_CHAPTER') {
const id = msg.payload?.id;
const el = id && document.getElementById(id);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
// 跳完也上报一次,让原生进度立刻同步
setTimeout(postProgress, 50);
}
}
if (msg.type === 'REQUEST_SYNC') {
// App 请求同步一次当前状态
postProgress();
}
});
// Web ready 通知:让 App 知道可以 postMessage 了
window.postMessage({ type: 'WEB_READY' });
// 初次上报一次(避免原生进度条一直是 0)
setTimeout(postProgress, 50);
</script>
</body>
</html>
3)怎么接到你之前的“工程模板”里?
你之前的模板已经有 Web + controller + 数据通道思路了。现在只要做两件事:
- 把
reading.html放到:
entry/src/main/resources/rawfile/web/reading.html - 新增一个页面
WebReadingPage.ets,然后在 Index 里加一个入口按钮跳转即可。
4)我建议你下一步加的“更像真实产品”的两件事
如果你要把它做成“能上线的体验”,我建议加:
A. 目录高亮当前章节(很爽)
- Web 已经在上报
currentChapterId - ArkTS 用这个 id 给对应 Button 换背景/字体即可
B. 记忆阅读位置(回到上次阅读点)
- Web 上报
scrollTop - ArkTS 持久化(Preferences)
- 进入页面后 App 发消息给 Web:
SCROLL_TO_Y

浙公网安备 33010602011771号