HarmonyOS 6 上做个极简浏览器:Web 组件 + 前进后退 + 加载进度
前言
有一次我想在应用里塞一个“关于页”,不想用原生 UI 画,直接上 Web 页面最快。结果发现 HarmonyOS 6 的 Web 组件比我想象的完整:加载页面、前进后退、加载进度,全是现成能力,一个 WebviewController 全包了。
既然基础这么好用,我干脆把它做成一个“极简浏览器”:地址栏能输网址,工具栏能后退前进刷新,顶部一条实时加载进度条,默认打开一个打包在应用里的内置页面——全程不需要任何配置和权限。这篇就把这个 demo 从零写到完整,代码在文末,模拟器上直接跑。
写之前先定个调:这篇不打算做成一个“功能大全”式的教程,那太像说明书了。我想还原的是我实际搭这个浏览器时的思考顺序——先让页面能显示,再让它能跳转,然后处理历史导航,最后补上进度反馈。每一步都对应 Web 能力链上的一块拼图,走完一圈,你对 Web 组件在 HarmonyOS 6 里能干什么、不能干什么,心里就有数了。
还有一点提前说明:Web 组件本质上就是一套完整的浏览器内核。这意味着它既强大又“重”,一旦加载远程页面,页面上发生什么、能访问什么,规则都归内核管。这篇只做基础导航,不涉及 JS 注入、网页调试这些进阶玩法,先把地基打牢。

第一站:先让网页显示出来
Web 组件用起来和普通组件没什么两样,在 build 里声明,指定 src 和 controller 就行。src 是页面来源,controller 是我们操作浏览器的“方向盘”——跳转、前进后退、刷新全走它。
页面从哪来?最简单也最稳的方式:把 HTML 文件放进工程的 rawfile 目录,用 $rawfile() 引用。文件跟着安装包走,应用一装就有,不依赖网络。我准备了一个简单的内置页,一个渐变背景配一张说明卡片,放在 entry/src/main/resources/rawfile/index.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>HarmonyOS 内置页</title>
<style>
body { margin: 0; padding: 24px; background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d); min-height: 100vh; }
.card { background: rgba(255,255,255,0.92); border-radius: 16px; padding: 24px; max-width: 420px; margin: 8vh auto; }
</style>
</head>
<body>
<div class="card">
<h1>极简浏览器 · 内置页</h1>
<p>这是打包在应用里的本地页面,Web 组件默认加载它,离线可跑。</p>
</div>
</body>
</html>
然后一个 Web 组件把它加载进来:
import { webview } from '@kit.ArkWeb';
private controller: webview.WebviewController = new webview.WebviewController();
Web({ src: $rawfile('index.html'), controller: this.controller })
.width('100%')
.layoutWeight(1)
到这里,页面已经能显示出来了。但这只是个“静态显示器”,还谈不上浏览器。接下来给它装上地址栏、工具栏和进度条。

第二站:地址栏——想去哪就去哪
浏览器的灵魂是地址栏。一个 TextInput 接收网址,一个“打开”按钮触发跳转:
go() {
const u = this.url.trim();
if (u.length > 0) {
this.controller.loadUrl(u.startsWith('http') ? u : 'https://' + u);
}
}
loadUrl 是 WebviewController 的核心方法,传完整 URL 就跳转。我做了个小体贴:用户只输域名(比如 example.com)时自动补上 https:// 前缀,省得每次手打协议头。
地址栏的输入体验有两个小细节:一是给 TextInput 挂 onSubmit,让用户在键盘上按回车也能触发跳转,而不只是点按钮;二是 onChange 实时把输入同步到 @State url,这样 go() 里读到的永远是最新输入。这两个都是 ArkUI 文本输入组件的常规用法,组合起来就是浏览器地址栏的完整体验。至于输入的是不是合法 URL,Web 内核自己会兜底——填了个乱码它也只是加载失败,不会让应用崩溃。
第三站:前进与后退
真正的浏览器必须有前进后退。WebviewController 提供四个相关能力:accessBackward() / accessForward() 用来查询“能不能退/能不能进”,backward() / forward() 执行动作。
为什么要有查询方法?因为按钮的可用状态得跟着浏览历史走:刚打开第一个页面时,后退按钮应该是灰的;跳了几次之后,后退才亮起来。界面上的 enabled 状态就绑定这两个查询结果:
back() {
if (this.controller.accessBackward()) {
this.controller.backward();
this.refreshNav();
}
}
forward() {
if (this.controller.accessForward()) {
this.controller.forward();
this.refreshNav();
}
}
refreshNav() {
this.canBack = this.controller.accessBackward();
this.canForward = this.controller.accessForward();
}
这里有个版本小坑值得记:早期文档里的方法名是 canGoBack / canGoForward,但在 HarmonyOS 6.1.1(API 24)的 SDK 里,WebviewController 上的是 accessBackward / accessForward。我按老文档写,编译器直接报“方法不存在”。版本在迭代,写的时候以当前 SDK 声明为准。
顺手讲一下浏览历史的工作方式,免得你踩“前进后退为什么失灵”的坑:浏览器维护的是一棵“历史栈”,你从 A 跳到 B、再从 B 跳到 C,栈里是 A→B→C;此时后退到 B,再跳去 D,C 就从栈里被剪掉了——所以前进按钮会变灰,因为 C 之后已经没有“未来”了。你在 demo 里多跳几次,观察前进按钮的亮灭,就能直观感受到这个栈的形态。这也是为什么 accessBackward 的返回值会随着页面变化实时改变,界面必须每次操作后都重新查询一次。
第四站:加载进度条
页面加载是异步的,用户需要反馈。Web 组件提供了 onProgressChange 回调,加载过程中不停上报进度(0 到 100),我把它喂给顶部的 Progress 组件:
@State progress: number = 0;
.onProgressChange((event) => {
this.progress = event.newProgress;
if (event.newProgress === 100) {
this.refreshNav();
}
})
进度到 100 的时候顺便刷新一次前进后退状态——因为页面加载完成,浏览历史才稳定下来,此时查 accessBackward 才准确。
除了进度,Web 组件还提供了几个常用页面事件:onPageBegin 表示页面开始加载,onPageEnd 表示加载完成,onTitleReceive 能拿到网页标题(很多浏览器把标题当 Tab 名,就是靠它)。这篇用了 onPageEnd 兜底刷新导航状态——有些页面加载太快,进度回调可能直接跳过中间值,但 onPageEnd 一定会触发。
另外一个细节:onProgressChange 的回调参数里取进度用 newProgress 字段,单位是 0 到 100 的整数。把它直接塞给 Progress 组件的 value,进度条就活了。注意 ArkTS 里事件回调参数建议显式声明类型,让编译器帮你把关。
顺带一提,ArkUI 的 Progress 组件本身也分几种形态:Linear 线性条适合这种顶栏进度,Circular 圆形进度适合加载中图标,Ring 圆环适合仪表类。这次用 Linear,因为浏览器地址栏下方一条横着的进度线最符合直觉——你大概也在桌面浏览器里看惯了这种设计。样式上我把它压成 4 高、蓝色,加载时几乎不占空间,也不干扰页面阅读。
第五站:完整代码——直接运行
把下面这份 Index.ets 放进 Empty Ability 工程(HTML 文件按第一站放进 rawfile),编译运行即可。地址栏输入网址可在线浏览,不输入就停留在内置页面,全程离线也能演示前进后退和进度条:

import { webview } from '@kit.ArkWeb';
@Entry
@Component
struct Index {
@State url: string = '';
@State progress: number = 0;
@State canBack: boolean = false;
@State canForward: boolean = false;
private controller: webview.WebviewController = new webview.WebviewController();
aboutToAppear() {
// 初始就加载内置本地页面(rawfile),离线可跑
}
go() {
const u = this.url.trim();
if (u.length > 0) {
this.controller.loadUrl(u.startsWith('http') ? u : 'https://' + u);
}
}
back() {
if (this.controller.accessBackward()) {
this.controller.backward();
this.refreshNav();
}
}
forward() {
if (this.controller.accessForward()) {
this.controller.forward();
this.refreshNav();
}
}
refresh() {
this.controller.refresh();
}
refreshNav() {
this.canBack = this.controller.accessBackward();
this.canForward = this.controller.accessForward();
}
build() {
Column() {
// 顶栏
Row() {
Text('极简浏览器')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#1A1A1A')
Blank()
Text('Web 组件')
.fontSize(13)
.fontColor(Color.White)
.backgroundColor('#2B5CE6')
.borderRadius(10)
.padding({ left: 10, right: 10, top: 3, bottom: 3 })
}
.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 10 })
// 地址栏
Row({ space: 8 }) {
TextInput({ text: this.url, placeholder: '输入网址,或使用内置页面' })
.layoutWeight(1)
.height(38)
.fontSize(14)
.onChange((v: string) => {
this.url = v;
})
.onSubmit(() => {
this.go();
})
Button('打开')
.height(38)
.fontSize(14)
.backgroundColor('#2B5CE6')
.onClick(() => {
this.go();
})
}
.width('100%')
.padding({ left: 16, right: 16, bottom: 8 })
// 工具条:后退 / 前进 / 刷新
Row({ space: 12 }) {
Button('◀ 后退')
.height(34)
.fontSize(13)
.backgroundColor(this.canBack ? '#666666' : '#CCCCCC')
.enabled(this.canBack)
.onClick(() => {
this.back();
})
Button('前进 ▶')
.height(34)
.fontSize(13)
.backgroundColor(this.canForward ? '#666666' : '#CCCCCC')
.enabled(this.canForward)
.onClick(() => {
this.forward();
})
Button('刷新')
.height(34)
.fontSize(13)
.backgroundColor('#2B5CE6')
.onClick(() => {
this.refresh();
})
Blank()
Text(`${this.progress}%`)
.fontSize(12)
.fontColor('#888888')
}
.width('100%')
.padding({ left: 16, right: 16, bottom: 6 })
// 加载进度条
Progress({ value: this.progress, total: 100, type: ProgressType.Linear })
.width('100%')
.height(4)
.color('#2B5CE6')
.backgroundColor('#EEF0F4')
// Web 主体:默认加载内置页面
Web({ src: $rawfile('index.html'), controller: this.controller })
.layoutWeight(1)
.width('100%')
.onProgressChange((event) => {
this.progress = event.newProgress;
if (event.newProgress === 100) {
this.refreshNav();
}
})
.onPageEnd(() => {
this.refreshNav();
})
}
.width('100%')
.height('100%')
.backgroundColor('#F5F7FA')
}
}
第六站:关于 Web 组件的边界
做这个 demo 的过程里,有几个问题反复被问到,我在这里一次说清楚。
1. 一个 controller 只能绑一个 Web 组件。WebviewController 和 Web 组件是一一对应的。如果你在页面里放了两个 Web(比如主窗口加一个小窗),需要各自 new 一个 controller,不能共用,否则行为会错乱。
2. $rawfile 引用的是 rawfile 目录下的文件。写法是 $rawfile('文件名'),文件必须放在 entry/src/main/resources/rawfile 下。它和 $r('app.xxx') 资源引用不是一回事——前者是原样打包的原始文件,后者是走资源编译系统的。放错了目录,运行时会找不到文件。
3. 页面销毁时要释放 Web 资源。Web 组件占用的内核资源比较重,页面退出时应该调用 controller 的清理逻辑,避免内存泄漏。工程模板里页面有生命周期方法,把清理动作挂在 aboutToDisappear 里即可。
4. 加载失败要有反馈。离线打开一个不存在的网址,页面会显示内核自带的错误页,但也可能看起来像“白屏”。更稳妥的做法是监听 onErrorReceive 之类的错误回调,把错误信息打到日志里,方便排查是网络问题还是地址问题。
5. 权限的边界要分清。加载远程页面需要 INTERNET 权限;加载 rawfile 不需要任何权限。如果你只在模拟器上调试远程页面,宿主机网络可用,模拟器直接通;真机上则必须配权限,否则页面加载会失败。
这些边界不是坑,是 Web 组件作为“完整浏览器”的必然属性。知道了边界,用起来反而更放心——你知道它什么时候该出手,什么时候该收手。
最后交代一个环境事实:这篇所有代码都在 DevEco Studio 6.1.1 Release(Build #6.1.1.300)+ HarmonyOS 6.1.1 Release SDK(API Version 24)下编译运行通过,模拟器型号是 MateBook Pro 2in1。Web 组件的能力在不同版本之间有收敛也有增强,如果你用的 SDK 更老或更新,个别方法名可能不同——这正是我反复强调“以当前 SDK 声明为准”的原因。
最后看一眼效果
在模拟器上跑起来(我这套是 MateBook Pro 2in1,HarmonyOS 6.1.1),打开应用:
- 默认加载内置页,一张渐变卡片居中,无需网络。
- 地址栏输入一个真实网址点“打开”,页面切换,顶部进度条从 0 快速冲到 100。
- 跳转几次之后,◀ 后退按钮亮起来,点一下回到上一页;再点 前进 ▶,又能回到刚才那页——跟浏览器里的历史导航一模一样。
- 进度到 100 之前,右上角百分比数字一直在变,加载慢的页面能清楚看到进度在走。
一个能跳转、能进退、能看进度的“浏览器”,就这么成型了。

总结
这篇从一个“关于页”的需求出发,把 Web 组件拆成了三件套:src 决定显示什么,WebviewController 负责怎么操作,事件回调负责状态反馈。合在一起,就是一个五脏俱全的极简浏览器。
回看整个过程,最有价值的一课其实是“查 SDK”:canGoBack 到 accessBackward 的改名,如果不是编译器报错,我根本不会发现自己的知识已经过期。鸿蒙的 API 迭代很快,教程和博客写得再好,也不如动手编译一次来得准。以后凡是 Web 相关的方法,我都会先看当前 SDK 的声明再写。
留给你的课后作业:给地址栏加一个“访问历史”下拉,或者监听 onPageEnd 把每次访问记录到本地;再进一步,把内置页换成你自己的 HTML 资源页,一个带 Web 壳的混合应用就成了。
好了,极简浏览器就到这里。打开模拟器,跑起来,试着输几个网址、点几下后退前进,你会比看任何教程都更理解 Web 组件。
参考资料
更多推荐


所有评论(0)