HarmonyOS Web 加载骨架屏 + Web 淡入动画模板(可直接用)
HarmonyOS Web 加载骨架屏 + Web 淡入动画模板(可直接用)
在真实项目里,Web 页面加载体验好不好,不取决于页面最终长什么样,而取决于:
👉 用户点进去后的前 1~3 秒你给了什么反馈。
如果这段时间是白屏、卡住、没反应,哪怕页面最后加载得再漂亮,体验分也已经掉了。
这篇文章我给你一套可以直接复制进工程使用的方案,实现这几个目标:
- 页面一进来 立刻显示原生骨架屏
- Web 在后台加载,不抢视觉
- Web 可展示时 淡入
- 骨架屏 淡出
- 全程无白屏、无闪烁、无突兀跳变
一、整体思路(先讲清楚,不然容易写歪)
一句话概括这套方案:
Web 始终存在,但一开始是透明的;
骨架屏覆盖在上面;
Web 准备好后淡入,骨架屏淡出。
关键点有 3 个:
- Web 不要等加载完才创建(否则必闪)
- 骨架屏是原生组件,不是 Web 里的 loading
- 动画只控制 opacity,不做 layout 变更
二、页面级完整模板(直接可用)
这是一个完整的 ArkTS 页面,你可以直接新建
WebSkeletonFadePage.ets使用。
import { webview } from '@kit.ArkWeb';
@Entry
@Component
struct WebSkeletonFadePage {
private controller: webview.WebviewController = new webview.WebviewController();
@State isSkeletonVisible: boolean = true;
@State skeletonOpacity: number = 1;
@State webOpacity: number = 0;
@State progress: number = 0;
@State showError: boolean = false;
private fadeInWebAndHideSkeleton() {
// 避免重复触发
if (!this.isSkeletonVisible && this.webOpacity >= 1) return;
// Web 淡入
animateTo({ duration: 240, curve: Curve.EaseOut }, () => {
this.webOpacity = 1;
});
// 骨架屏淡出
animateTo({ duration: 220, curve: Curve.EaseIn }, () => {
this.skeletonOpacity = 0;
});
// 动画结束后移除骨架屏
setTimeout(() => {
this.isSkeletonVisible = false;
}, 240);
}
build() {
Stack() {
// Web 组件(始终存在,只是透明度变化)
Web({
src: 'https://example.com', // 换成你的地址或 rawfile
controller: this.controller
})
.javaScriptAccess(true)
.onPageBegin(() => {
this.showError = false;
this.progress = 0;
this.isSkeletonVisible = true;
this.skeletonOpacity = 1;
this.webOpacity = 0;
})
.onProgressChange((p: number) => {
this.progress = p;
// 经验值:80% 基本可见首屏
if (p >= 80) {
this.fadeInWebAndHideSkeleton();
}
})
.onPageEnd(() => {
// 兜底,确保一定淡入
this.fadeInWebAndHideSkeleton();
})
.onRenderExited(() => {
this.showError = true;
this.isSkeletonVisible = false;
this.webOpacity = 1;
})
.opacity(this.webOpacity)
.width('100%')
.height('100%')
// 骨架屏
if (this.isSkeletonVisible) {
SkeletonLayer({
progress: this.progress,
opacity: this.skeletonOpacity
})
}
// 错误态(可选)
if (this.showError) {
Column({ space: 12 }) {
Text('加载失败')
.fontSize(18)
.fontWeight(FontWeight.Bold)
Text('请检查网络后重试')
.opacity(0.7)
Button('重试')
.onClick(() => {
this.showError = false;
this.isSkeletonVisible = true;
this.skeletonOpacity = 1;
this.webOpacity = 0;
this.controller.refresh();
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
.width('100%')
.height('100%')
}
}
三、骨架屏组件(轻量但“像样”)
1️⃣ 骨架屏整体结构
@Component
struct SkeletonLayer {
progress: number;
opacity: number;
build() {
Column({ space: 14 }) {
SkeletonBlock({ w: '70%', h: 18, r: 8 })
SkeletonBlock({ w: '40%', h: 14, r: 8 })
Column({ space: 12 }) {
ForEach([1, 2, 3, 4, 5], () => {
Column({ space: 10 }) {
SkeletonBlock({ w: '100%', h: 120, r: 12 })
Row({ space: 10 }) {
SkeletonBlock({ w: '22%', h: 14, r: 8 })
SkeletonBlock({ w: '30%', h: 14, r: 8 })
SkeletonBlock({ w: '18%', h: 14, r: 8 })
}
}
})
}
Text(`加载中… ${this.progress}%`)
.fontSize(12)
.opacity(0.6)
}
.padding(16)
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF')
.opacity(this.opacity)
}
}
2️⃣ 单个骨架块(带轻微流光)
@Component
struct SkeletonBlock {
w: string;
h: number;
r: number;
@State shimmerOffset: number = -60;
aboutToAppear() {
setInterval(() => {
animateTo({ duration: 900, curve: Curve.Linear }, () => {
this.shimmerOffset =
this.shimmerOffset >= 260 ? -60 : this.shimmerOffset + 80;
});
}, 950);
}
build() {
Stack() {
Rect()
.width(this.w)
.height(this.h)
.radius(this.r)
.opacity(0.12)
Rect()
.width('30%')
.height(this.h)
.radius(this.r)
.translate({ x: this.shimmerOffset })
.opacity(0.08)
}
}
}
四、为什么这套方案“看起来就高级”
这是我在项目里反复打磨出来的结论:
- 骨架屏是“页面结构的预告”,不是转圈圈
- 淡入动画是心理缓冲,让内容出现得“理所当然”
- Web 不重排、不重建,性能稳定
- 动画只改 opacity,最安全、最不容易出问题
五、我踩过的坑,你可以直接避开
❌ 骨架屏用 Web 自己的 loading
→ Web 没加载前你根本看不到它,白屏依旧。
❌ onPageEnd 才显示 Web
→ 页面其实早就能看了,被你硬生生挡住。
❌ 动画期间改布局
→ 闪、抖、性能下降,一堆莫名其妙的问题。
六、推荐的实际使用策略
- 首屏 Web 页面:用这套方案
- 页面内跳转 URL:只用淡入,不用骨架
- 协议页 / 轻页面:直接 loading 即可
七、一句话总结
Web 页面加载不是“等它加载完”,
而是“在它加载的这段时间,你给用户什么感觉”。

浙公网安备 33010602011771号