一、卡片网络加载优化指导
1. 卡片显示效果优化
1.1 优化说明
(1)首次加载:一般指用户手机上第一次使用卡片,手机本地还没有卡片的相关资源和用户数据,手机会下载卡片 rpk 包并进行安装,安装完成后进行展示。会执行卡片生命周期 onInit->onReady->onShow;
(2)卡片曝光:指卡片对用户可见,手机从息屏再到卡片展示页面,算是一次曝光;会执行卡片生命周期 onShow;
(3)重新加载:首次加载完成以后,手机系统的一些动作会导致负一屏卡片重新进行创建加载,如:用户切换手机字体、主题、重启手机或者手机的一些其他机制,会使卡片重新加载并显示。此时会执行卡片生命周期 onInit->onReady->onShow(卡片对用户可见时执行,不可见不会执行此生命周期);
(4)占位图提示页面:卡片首次加载或者重新加载时,有可能 fetch 请求返回数据较慢,卡片需要先显示文本宫格占位、图标占位或者沉浸式占位的 UI 文案提示页面作为过渡页,样式参见:《快应用卡片设计指南》1.10 空内容卡片样式
(5)无网络失败页面:卡片在首次加载时,如果手机无网络情况下,需要显示“数据加载失败,点击重试”文案提示页面;点击该文案页面时判断:1. 仍然无网络则跳转至卡片落地页处理无网络场景;2. 网络恢复时进行请求数据操作,恢复卡片正常显示。
(6)业务加载失败页面:卡片在首次加载时,如果网络存在,接口失败或者访问网络超时等失败情况时,请自行处理失败页面展示;
(7)缓存方案(可选):如果处在手机无网络或者 fetch 失败,需要获取上次存储的数据(每次 fetch 的数据都使用 storage 存储到本地)进行展示。开发者不必担心使用上次存储的数据显示时图片会无法加载,因为每次图片加载成功后,引擎框架会默认缓存卡片中的图片资源,只要 Image 组件的静态地址不变,图片在无网的情况下也可以正常显示;
注意:卡片的 fetch 不支持直接设置超时参数,所以开发者使用 Promise 自己封装超时处理函数,超时时间固定为 5s(不可以使用其他值),fetch 5s 后返回的数据需要废弃。
(8)卡片刷新场景(可选):为防止卡片影响客户端的流畅度和手机流量、功耗。
需要限制卡片访问网络或者刷新卡片刷新界面(onShow生命周期)的频率,在刷新管控机制下,卡片在一个时间周期内仅能获取到一次onShow生命周期的钩子函数回调,达到限制刷新的目的。onShow生命周期刷新时间周期建议大于等于3S。
1.2 卡片首次加载处理
1.3 卡片二次刷新
1.4 示例demo
(1)HTML部分
<template>
<!-- 卡片最外层容器 -->
<div class="card" @click="toH5">
<!-- 加载初始占位布局 -->
<div class="layer-box" if="{{showLoadView}}">
<div class="title-box">
<div class="title-logo"></div>
<div class="title-txt-box"></div>
</div>
<div class="content-box"></div>
<div class="footer-box">
<div class="footer-txt-box"></div>
<div class="footer-img-box"></div>
</div>
</div>
<!-- 数据加载异常布局 -->
<div class="network-box" if="{{showLoadFailedView}}" onclick="clickRetry">
<image class="network-img" src="./image/位图备份.png"></image>
<text class="network-txt">数据加载失败,点击重试</text>
</div>
<!-- stack标签用于有需要插入背景图的卡片需求,若无背景图需求无需用stack标签 -->
<stack class="stack" if="{{showCardContent}}">
<!-- 卡片stack标签第一个元素,设置百分百宽高,做成背景图效果 -->
<div class="bg-img">
<!-- 此处背景图标要用image标签 -->
<image
src="../Common/res/img_pic.png"
forcedark="false"
class="icon"
></image>
</div>
<!-- 卡片stack标签第二个元素,卡片布局容器,设置百分百宽高铺满父元素,若无背景图需求,直接来到此处布局容器 -->
<div class="layer">
<!-- 标题区域 -->
<div class="title">
<div class="title-logo">
<image
forcedark="false"
class="logo"
src="../Common/res/icon_icon.png"
></image>
</div>
<text class="title-text">{{ title }}</text>
</div>
<!-- 内容区域 -->
<div class="content">
<text class="content-title">内容区</text>
<text class="content-text">{{ cardInfo.content }}</text>
</div>
<!-- 底部区域 -->
<div class="footer">
<div class="footer-box">
<text class="footer-text">{{ cardInfo.fooderName }}</text>
</div>
</div>
</div>
</stack>
</div>
</template>
(2)CSS部分
<style lang="less">
/* 卡片最外层 最外层容器无需给宽高,由负一屏或桌面设定宽高 */
.card {
padding: 12dp;
/* 此处给宽高是为了让调试器devtools显示内容方便测试效果,实际开发需要去掉宽高 */
height: 152dp;
width: 100%;
/* 按照最新深色模式规范需要给卡片设置背景色,映衬规范需要#ffffff作为背景色;
若自定义背景色,要满足深色模式则需要通过媒体查询来实现,详见深色模式开发文档 */
background-color: #ffffff;
}
/* stack标签用于有需要插入背景图的卡片需求,若无背景图需求无需用stack标签*/
.stack {
width: 100%;
}
/*卡片布局容器,设置百分百宽高铺满父元素,若无背景图需求,不需要stack标签结构直接来到此处布局容器*/
.layer {
width: 100%;
height: 100%;
justify-content: space-between;
flex-direction: column;
}
/* 卡片标题区域 */
.title {
display: flex;
height: 16dp;
flex-direction: row;
.title-logo {
width: 16dp;
height: 16dp;
justify-content: center;
align-items: center;
flex-shrink: 0;
.logo {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 50%;
}
}
.title-text {
font-size: 12dp;
color: rgba(0, 0, 0, 0.9);
padding-left: 8dp;
}
}
/* 占位图 */
.layer-box {
width: 100%;
height: 100%;
flex-direction: column;
justify-content: space-between;
.title-box {
display: flex;
height: 16dp;
flex-direction: row;
align-items: center;
/* 占位图 */
.title-logo {
width: 16dp;
height: 16dp;
border-radius: 50%;
margin-right: 8dp;
background-color: #87ceeb;
}
.title-txt-box {
width: 60%;
height: 8dp;
background-color: #ccc;
border-radius: 8dp;
}
}
.content-box {
height: 60dp;
background-color: #ccc;
}
.footer-box {
height: 40dp;
justify-content: space-between;
align-items: flex-end;
.footer-txt-box {
height: 8dp;
width: 50%;
background-color: #ccc;
}
.footer-img-box {
height: 40dp;
width: 40dp;
border-radius: 50%;
background-color: #87ceeb;
}
}
}
/* 无网络布局 */
.network-box {
width: 100%;
height: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
.network-img {
width: 80dp;
height: 60dp;
object-fit: contain;
}
.network-txt {
font-size: 10dp;
color: rgba(0, 0, 0, 0.9);
padding-top: 4dp;
}
}
/*/卡片内容区域 */
.content {
height: 60dp;
flex-direction: column;
justify-content: center;
}
.content-title {
font-size: 16dp;
color: rgba(0, 0, 0, 0.9);
font-weight: 500;
}
.content-text {
font-size: 14dp;
color: rgba(0, 0, 0, 0.6);
font-weight: 400;
/* 注意:卡片内容区域给定高度,内容展示要注意字数限制内容过长采用省略方式,避免导致内容缺失问题 */
lines: 2;
text-overflow: ellipsis;
}
/* 卡片底部区域 */
.footer {
height: 40dp;
flex-direction: row;
justify-content: space-between;
align-items: flex-end;
.footer-box {
height: 16dp;
flex-direction: row;
.footer-text {
font-size: 10dp;
color: rgba(0, 0, 0, 0.6);
}
}
}
/* 背景图标 */
.bg-img {
width: 100%;
height: 100%;
flex-direction: column-reverse;
align-items: flex-end;
}
.icon {
width: 40dp;
height: 40dp;
object-fit: contain;
}
</style>
(3)JS部分
<script>
import fetch from '@system.fetch'
import network from '@system.network'
import router from '@system.router'
// 封装节流函数 用于限制频繁点击或频繁触发事件的处理
function throttle(func, delay = 500) {
let timer = null
let that = this
return function (...arg) {
if (timer) return
timer = setTimeout(() => {
func.apply(that, arg)
timer = null
}, delay)
}
}
//封装获取网络状态
function getNetwork() {
return new Promise((resolve, reject) => {
network.getType({
success: function (data) {
console.log(`handling success: ${data.type}`)
resolve(data.type)
},
fail: function (error) {
console.error("get network error:" + error);
reject(error);
}
})
})
}
export default {
private: {
title: '标题(2x4)',
showLoadView: false, //初始占位
showLoadFailedView: false, //数据加载失败
showCardContent: false, //成功渲染
hasInited: false,
timeout: null,
cardInfo: {
content: '',
fooderName: ''
}
},
onInit() {
this.initDate()
// 构建点击无网络失败页面节流函数 防止频繁点击无网络失败页面
this.filedFetchThottle = throttle(this.filedFetch.bind(this), 500)
//构建频繁触发updateData数据更新的节流函数 防止频繁触发onShow里数据的更新
this.updateDataThottle = throttle(this.updateData.bind(this), 3000)
},
onShow() {
if(!this.hasInited) return
//防止频繁触发onShow里数据的更新
this.updateDataThottle()
},
// 正常卡片状态下的跳转(以跳转百度为示例)
toH5() { router.push({ uri: 'hnquick://browser//parameter?url=http://www.baidu.com' })
},
/**
* 初始化卡片数据
*/
async initDate() {
this.showLoadingPage() //初始化显示占位图
const netWorkStatus = await getNetwork()
if (netWorkStatus == 'none') {
this.showLoadFailedPage()
return
}
const res = await this.fetchData()
const { code, data } = res
if (code == 200) {
this.cardInfo.content = '离开[深圳航站],下一站[广州处理中转站]'
this.cardInfo.fooderName = '来自顺丰速运'
this.showCard()
} else {
// 自行处理业务失败逻辑
}
this.hasInited = true
},
//数据更新
async updateData() {
const res = await this.fetchData()
const { code, data } = res
if (code == 200) {
this.cardInfo.content = '离开[深圳航站],下一站[广州处理中转站]'
this.cardInfo.fooderName = '来自顺丰速运'
this.showCard()
}
},
//接口调用失败或者无网络时跳转去无网络加载失败页面
async filedFetch() {
const netWorkStatus = await getNetwork()
console.log('netWorkStatus',netWorkStatus)
if (netWorkStatus == 'none') {
router.push({ uri: 'hnquick://browser//parameter?url=http://www.baidu.com' })
} else {
this.updateData()
}
},
showLoadingPage: function () {
this.showLoadFailedView = false
this.showCardContent = false
this.showLoadView = true
},
showLoadFailedPage: function () {
this.showCardContent = false
this.showLoadView = false
this.showLoadFailedView = true
},
showCard: function () {
this.showLoadFailedView = false
this.showLoadView = false
this.showCardContent = true
},
//网络超时处理
timeoutHandler(promiseInstance) {
const timeout = new Promise((_, reject) => {
setTimeout(() => {
reject('网络超时')
}, 5000)
})
return Promise.race([timeout, promiseInstance])
},
/**
* 发起fetch请求数据
*/
fetchData() {
return this.timeoutHandler(
new Promise((resolve, reject) => {
fetch
.fetch({
url: 'https://xxx', // 接口链接
method: 'GET',
})
.then((response) => {
resolve(response.data)
})
.catch((error, code) => {
console.log(`🐛 request fetchIconUrlData fail, error= ${error}, code = ${code}`)
resolve({ code, data: error })
})
})
)
},
// 数据加载失败
clickRetry(evt) {
// 阻止事件冒泡
evt.stopPropagation()
// 防止频繁点击无网络失败页面
this.filedFetchThottle()
},
}
</script>
|