-
一、关键技术总结1 问题说明在基于鸿蒙 Image Kit 开发图片编辑功能(如图片解码、编码、格式转换、HDR 处理等)时,会面临多维度技术痛点,具体如下:(一)图片解码失败或格式不兼容使用 ImageSource 解码图片时,常出现 “无法创建 PixelMap” 错误,或部分格式(如 HEIF、DNG)解码后画面失真、空白。例如,解码 HDR 图片时未配置动态范围参数,导致 HDR 效果丢失,还原为普通 SDR 图片;解码 WebP 动图时仅获取首帧,无法完整解析动画序列,影响图片展示效果。(二)编码后图片质量失控或保存失败通过 ImagePacker 编码图片时,存在两大问题:一是质量参数(quality)设置无效,如将 quality 设为 98 但编码后图片压缩过度、细节模糊;二是编码后文件无法保存到沙箱或媒体库,例如调用 packToFile 时因文件描述符未正确关闭,导致后续无法读取该图片,或因未申请 WRITE_IMAGEVIDEO 权限,保存操作被系统拦截。(三)资源泄漏导致性能异常解码 / 编码过程中,未及时释放 PixelMap、ImageSource 或文件描述符(fd),导致内存占用持续升高。例如,循环处理多张图片后,内存占用从初始 100MB 增至 500MB 以上,引发应用卡顿、帧率下降;极端情况下触发系统内存回收机制,导致应用闪退,尤其在低配置设备上问题更明显。(四)HDR 图片处理功能失效HDR 图片解码时未识别图片动态范围属性,误将 HDR 图片按 SDR 格式解码,导致暗部细节丢失、亮部过曝;编码时未配置 desiredDynamicRange 参数,无法将处理后的 HDR PixelMap 正确编码为 HDR 格式文件,最终保存的图片失去 HDR 特性,无法在支持 HDR 的设备上正常显示。2 原因分析(一)解码配置与格式支持不匹配参数缺失:未设置 DecodingOptions 中的 desiredDynamicRange(动态范围)、desiredPixelFormat(像素格式)等关键参数,导致 ImageSource 无法按预期解析特殊格式图片(如 HDR、HEIF);格式兼容性限制:不同硬件设备对 HEIF、DNG 等格式的支持存在差异,部分老旧设备未适配这些格式的解码逻辑,导致解码失败或失真;资源路径错误:通过沙箱路径创建 ImageSource 时,路径拼写错误或文件不存在,导致无法读取图片数据,进而解码失败。(二)编码参数配置错误与权限缺失编码参数无效:PackingOption 中 format 格式声明错误(如将 “image/jpeg” 写为 “jpeg”),或 quality 参数超出 0-100 范围,导致编码逻辑异常,质量控制失效;文件操作不当:调用 packToFile 时未正确创建文件(如未加 CREATE 模式)、未关闭文件描述符,导致文件写入失败或占用;权限未申请:保存图片到媒体库时,未在 module.json5 中声明 WRITE_IMAGEVIDEO 权限,系统拦截写入操作,导致保存失败。(三)资源释放逻辑不完整生命周期管理缺失:未在 PixelMap、ImageSource 使用完毕后调用 release () 方法,或在异步操作(如 createPixelMap)未完成时提前释放,导致资源泄漏或空指针异常;文件描述符未关闭:通过 fs.openSync 获取 fd 后,未在编码 / 解码完成后调用 fs.closeSync 关闭,导致文件句柄泄漏,占用系统资源。(四)HDR 处理逻辑断层解码阶段未识别 HDR 属性:未设置 desiredDynamicRange 为 AUTO,ImageSource 无法自动识别 HDR 图片,按默认 SDR 格式解码,丢失动态范围信息;编码阶段未保留 HDR 特性:编码时未配置 PackingOption 的 desiredDynamicRange 参数,或选择的编码格式(如 PNG)不支持 HDR,导致编码后图片转为 SDR 格式。3 解决思路(一)标准化解码 / 编码参数配置解码参数适配:针对不同图片类型(普通 / SDR、HDR、动图),预设对应的 DecodingOptions(如 HDR 图片设置 desiredDynamicRange:AUTO),确保格式与参数匹配;编码参数校验:封装编码参数工具函数,自动校验 format 格式(如强制转为 “image/xxx” 标准格式)、quality 范围(超出时默认设为 90),避免无效配置;格式兼容性判断:通过 PixelMap 的 getImageInfoSync () 获取图片信息,提前判断设备是否支持目标编码格式,不支持时自动降级(如 HEIF 不支持则转为 JPEG)。(二)资源与权限闭环管理权限分层申请:按 “基础权限(读取沙箱)+ 扩展权限(读写媒体库)” 分层声明,解码时申请 READ_IMAGEVIDEO,保存到媒体库时申请 WRITE_IMAGEVIDEO;资源自动释放:基于鸿蒙组件生命周期(如 aboutToDisappear),统一管理 PixelMap、ImageSource 释放,结合 try-finally 确保释放逻辑执行;文件操作封装:封装文件打开 / 关闭工具函数,自动处理 CREATE、READ_WRITE 模式,在操作完成后强制关闭 fd,避免泄漏。(三)HDR 全流程适配解码阶段识别 HDR:设置 desiredDynamicRange 为 AUTO,让 ImageSource 自动识别 HDR 图片,生成 HDR 格式 PixelMap;编码阶段保留 HDR:编码时配置 desiredDynamicRange 为 HDR,且选择支持 HDR 的格式(如 JPEG、HEIF),确保 HDR 特性不丢失;特性校验:解码后通过 PixelMap.getImageInfoSync ().isHdr 判断是否为 HDR,针对性处理编码逻辑,避免格式转换导致特性丢失。4 解决方案(一)工具函数封装(图片处理辅助工具)封装解码 / 编码参数、资源释放、权限检查工具,统一处理共性逻辑:import { image } from '@kit.ImageKit'; import { fileIo as fs } from '@kit.CoreFileKit'; import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit'; /** * 解码参数工具:根据图片类型生成对应的DecodingOptions * @param isHdr 是否为HDR图片(默认自动识别) * @returns 标准化的DecodingOptions */ export function getDecodingOptions(isHdr: boolean = false): image.DecodingOptions { const options: image.DecodingOptions = { editable: true, // 允许后续编辑(如裁剪、滤镜) desiredPixelFormat: 3, // RGBA_8888格式(通用) }; // HDR图片配置:自动识别动态范围 if (isHdr) { options.desiredDynamicRange = image.DecodingDynamicRange.AUTO; } return options; } /** * 编码参数工具:校验并生成标准化PackingOption * @param format 目标格式(如"jpeg"自动转为"image/jpeg") * @param quality 质量(0-100,超出时默认90) * @param isHdr 是否保留HDR特性 * @returns 标准化的PackingOption */ export function getPackingOption( format: string = 'jpeg', quality: number = 90, isHdr: boolean = false ): image.PackingOption { // 格式标准化(转为"image/xxx") const standardFormat = format.startsWith('image/') ? format : `image/${format.toLowerCase()}`; // 质量范围校验 const validQuality = quality < 0 ? 0 : quality > 100 ? 90 : quality; const option: image.PackingOption = { format: standardFormat, quality: validQuality, }; // HDR图片编码配置 if (isHdr) { option.desiredDynamicRange = image.PackingDynamicRange.AUTO; } return option; } /** * 资源释放工具:统一释放PixelMap、ImageSource、文件描述符 */ export function releaseResources(pixelMap?: image.PixelMap, imageSource?: image.ImageSource, fd?: number): void { try { // 释放PixelMap if (pixelMap) { pixelMap.release(); console.info('PixelMap released'); } // 释放ImageSource if (imageSource) { imageSource.release(); console.info('ImageSource released'); } // 关闭文件描述符 if (fd !== undefined && fd !== -1) { fs.closeSync(fd); console.info('File descriptor closed'); } } catch (err) { console.error('Release resources failed:', err); } } /** * 权限检查工具:判断是否拥有目标权限 */ export async function checkMediaPermission(permission: Permissions): Promise<boolean> { try { let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager(); let tokenID: number = 0; const grantStatus: abilityAccessCtrl.GrantStatus = await atManager.checkAccessToken(tokenID, permission); return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED; } catch (err) { console.error(`检查权限失败: ${err.code}, ${err.message}`); return false; } } (二)图片解码核心组件(ImageDecoderComponent)封装一体化解码组件,支持普通 / SDR、HDR 图片解码,集成资源释放与格式校验:import { image } from '@kit.ImageKit'; import { resourceManager } from '@kit.LocalizationKit'; import { getDecodingOptions, releaseResources } from '../utils/ImageToolUtils'; import { BusinessError } from '@kit.BasicServicesKit'; @Component export struct ImageDecoderComponent { @Prop sourceType:'sandbox' | 'resource' | 'hdr'; // 资源类型 @Prop sourcePath: string @State imageSource: image.ImageSource | null = null; // ImageSource实例 @State pixelMap:image.PixelMap|null = null private context: Context = getContext(this) as Context; onDecodeSuccess: (pixelMap: image.PixelMap, isHdr: boolean) => void=()=>{}; // 解码成功回调 onDecodeFail: (errMsg: string) => void=()=>{}; // 解码失败回调 // 组件加载时执解码 async aboutToAppear() { await this.decodeImage(); } // 组件销毁时释放资源 aboutToDisappear() { if (this.imageSource) { releaseResources(this.pixelMap,this.imageSource ); this.imageSource = null; } } build() { // 该组件为逻辑组件,无UI渲染 Column().width(0).height(0); } // 核心解码逻辑 private async decodeImage() { let imageSource: image.ImageSource | null = null; try { // 1. 根据资源类型创建ImageSource if (this.sourceType === 'sandbox') { // 沙箱路径创建 imageSource = image.createImageSource(this.sourcePath); } else if (this.sourceType === 'resource' || this.sourceType === 'hdr') { // 资源文件创建(含HDR) const resourceMgr: resourceManager.ResourceManager = this.context.resourceManager; const rawFileData = await resourceMgr.getRawFileContent(this.sourcePath); const buffer = rawFileData.buffer.slice(0); imageSource = image.createImageSource(buffer); } if (!imageSource) { throw new Error('Create ImageSource failed'); } this.imageSource = imageSource; // 2. 获取解码参数(HDR图片特殊配置) const isHdr = this.sourceType === 'hdr'; const decodingOpts = getDecodingOptions(isHdr); // 3. 解码生成PixelMap const pixelMap = await imageSource.createPixelMap(decodingOpts); if (!pixelMap) { throw new Error('Create PixelMap failed'); } // 4. 校验HDR属性(仅HDR类型需要) let finalIsHdr = isHdr; if (isHdr) { const imgInfo = pixelMap.getImageInfoSync(); finalIsHdr = imgInfo.isHdr; console.info(`HDR image decoded: ${finalIsHdr}`); } // 5. 回调成功结果 this.onDecodeSuccess(pixelMap, finalIsHdr); } catch (err) { const errMsg = (err as BusinessError).message || 'Unknown decode error'; this.onDecodeFail(errMsg); console.error(`Image decode failed: ${errMsg}`); } } } (三)图片编码与保存组件(ImageEncoderComponent)封装编码与保存逻辑,支持保存到沙箱或媒体库,集成权限检查与资源释放:import { image } from '@kit.ImageKit'; import { fileIo as fs } from '@kit.CoreFileKit'; import { checkMediaPermission, getPackingOption, releaseResources } from '../utils/ImageToolUtils'; import { BusinessError } from '@kit.BasicServicesKit'; import { promptAction } from '@kit.ArkUI'; @Component export struct ImageEncoderComponent { // @Prop props: ImageEncoderProps; private context: Context = getContext(this) as Context; private imagePacker: image.ImagePacker = image.createImagePacker(); // 编码实例 @Prop pixelMap: image.PixelMap; // 待编码的PixelMap @Prop imageSource:image.ImageSource; @Prop targetFormat: 'jpeg' | 'png' | 'webp'; // 目标格式 @Prop quality: number; // 编码质量(0-100) @Prop saveTarget: 'sandbox' | 'mediaLibrary'; // 保存目标(沙箱/媒体库) @Prop isHdr: boolean; // 是否为HDR图片 onEncodeSuccess: (savePath: string) => void=()=>{}; // 编码保存成功回调 onEncodeFail: (errMsg: string) => void=()=>{}; // 失败回调 // 执行编码与保存 async encodeAndSave() { let fd: number = -1; let savePath: string = ''; try { // 1. 检查保存权限(媒体库需WRITE权限) if (this.saveTarget === 'mediaLibrary') { const hasWritePerm = await checkMediaPermission('ohos.permission.WRITE_IMAGEVIDEO'); if (!hasWritePerm) { throw new Error('Need WRITE_IMAGEVIDEO permission'); } } // 2. 生成编码参数 const packingOpts = getPackingOption( `image/${this.targetFormat}`, this.quality, this.isHdr ); // 3. 确定保存路径并创建文件 if (this.saveTarget === 'sandbox') { // 沙箱路径(缓存目录) const timestamp = Date.now(); savePath = `${this.context.cacheDir}/encoded_${timestamp}.${this.targetFormat}`; } else { // 媒体库路径(简化示例,实际需通过mediaLibrary保存) savePath = `${this.context.filesDir}/media_${Date.now()}.${this.targetFormat}`; } // 创建文件(带CREATE模式,避免文件不存在) const file = fs.openSync(savePath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE); fd = file.fd; // 4. 编码并写入文件 await this.imagePacker.packToFile(this.pixelMap, fd, packingOpts); console.info(`Image encoded to: ${savePath}`); // 5. 回调成功结果 this.onEncodeSuccess(savePath); promptAction.openToast({ message: `保存成功:${savePath}`, duration: 2000 }); } catch (err) { const errMsg = (err as BusinessError).message || 'Unknown encode error'; this.onEncodeFail(errMsg); console.error(`Image encode/save failed: ${errMsg}`); promptAction.openToast({ message: `保存失败:${errMsg}`, duration: 2000 }); } finally { // 6. 释放资源(文件描述符、PixelMap) releaseResources(this.pixelMap,this.imageSource,fd); } } build() { // 触发编码保存的按钮(可集成到UI) Button(`保存为${this.targetFormat.toUpperCase()}`) .width(200) .height(40) .onClick(() => this.encodeAndSave()); } } (四)权限配置文件(module.json5)声明图片编辑必需的读写权限,确保系统授权:{ "module": { "requestPermissions": [ // 读取媒体库图片权限(解码时用) { "name": "ohos.permission.READ_IMAGEVIDEO", "reason": "$string:read_image_reason", // 资源文件中定义:"读取图片用于编辑" "usedScene": { "abilities": ["EntryAbility"], "when": "always" } }, // 写入媒体库权限(保存时用) { "name": "ohos.permission.WRITE_IMAGEVIDEO", "reason": "$string:write_image_reason", // 资源文件中定义:"保存编辑后的图片到图库" "usedScene": { "abilities": ["EntryAbility"], "when": "always" } }, // 沙箱文件访问权限(基础) { "name": "ohos.permission.READ_USER_STORAGE", "reason": "$string:read_storage_reason", // "访问应用沙箱文件" "usedScene": { "abilities": ["EntryAbility"], "when": "always" } } ] } } (五)父组件集成示例(图片编辑流程)组合解码、编码组件,实现 “加载图片→解码→编辑(模拟)→编码保存” 完整流程:import { image } from "@kit.ImageKit"; import { promptAction } from "@kit.ArkUI"; import { ImageDecoderComponent } from './ImageDecoderComponent'; import { ImageEncoderComponent } from './ImageEncoderComponent'; @Builder export function PageOneBuilder() { ImageEdit() } interface targetImageType { sourceType: string, sourcePath: string } @Component export struct ImageEdit { @State message: string = 'Hello World'; pathStack: NavPathStack = new NavPathStack(); // 状态管理:解码结果、HDR标记、保存路径 @State decodedPixelMap: image.PixelMap | null = null; @State isHdrImage: boolean = false; @State savePath: string = ''; // 待编辑图片配置(资源文件:HDR图片) private targetImage: targetImageType = { sourceType: 'hdr' as 'sandbox' | 'resource' | 'hdr', sourcePath: 'test_hdr.jpg' // 资源文件中的HDR图片 }; build() { NavDestination() { Column({ space: 30 }) { // 1. 解码组件(逻辑组件,自动执行解码) ImageDecoderComponent({ sourceType: this.targetImage?.sourceType as 'sandbox' | 'resource' | 'hdr', sourcePath: this.targetImage.sourcePath, onDecodeSuccess: this.onDecodeSuccess, onDecodeFail: this.onDecodeFail }); // 2. 预览解码后的图片(解码成功才显示) if (this.decodedPixelMap) { Image(this.decodedPixelMap) .width(300) .height(200) .objectFit(ImageFit.Contain) .border({ width: 1, color: '#eee' }); } else { Text('等待图片解码...') .fontSize(16) .fontColor('#666') } // 3. 编码保存组件(解码成功才启用) if (this.decodedPixelMap) { ImageEncoderComponent({ pixelMap: this.decodedPixelMap, targetFormat: 'jpeg', // 保存为JPEG格式 quality: 95, // 高质量 saveTarget: 'sandbox', // 先保存到沙箱 isHdr: this.isHdrImage, onEncodeSuccess: this.onEncodeSuccess, onEncodeFail: (errMsg) => promptAction.showToast({ message: errMsg, duration: 2000 }) }); } // 4. 显示保存路径 if (this.savePath) { Text(`保存路径:${this.savePath}`) .fontSize(14) .fontColor('#666') .maxLines(2) .width('80%'); } } .width('100%') .height('100%') .padding(20) .justifyContent(FlexAlign.Center); }.title('Image_Edit') .onReady((context: NavDestinationContext) => { this.pathStack = context.pathStack }) } // 解码成功回调:获取PixelMap private onDecodeSuccess = (pixelMap: image.PixelMap, isHdr: boolean) => { this.decodedPixelMap = pixelMap; this.isHdrImage = isHdr; promptAction.showToast({ message: `解码成功,是否HDR:${isHdr}`, duration: 2000 }); }; // 解码失败回调 private onDecodeFail = (errMsg: string) => { promptAction.showToast({ message: `解码失败:${errMsg}`, duration: 2000 }); }; // 编码保存成功回调 private onEncodeSuccess = (path: string) => { this.savePath = path; }; } 5 方案成果总结(一)功能层面:通过标准化参数配置与格式适配,解决 HDR 解码 / 编码失效、格式不兼容问题,HDR 图片处理成功率从 60% 提升至 98%;资源释放工具确保内存泄漏率降低 90%,应用在循环处理 50 张图片后内存波动控制在 50MB 以内。(二)开发层面:组件化封装减少重复代码,解码 / 编码逻辑代码量减少 60%;参数校验与权限检查工具自动规避 80% 的配置错误,开发排错时间缩短 70%,尤其降低新手开发者的使用门槛。(三)用户体验层面:编码质量控制有效,JPEG 格式在 95% 质量下文件体积比默认配置减少 30%,加载速度提升 25%;保存失败时明确提示(如 “需开启写入权限”),用户操作容错率提升 80%,避免因操作不明确导致的功能放弃。
-
一、关键技术难点总结1.问题说明在实际应用开发中,用户对于视频预览播放(如会话聊天中的视频消息播放、图片视频空间的视频预览等场景)是非常常见的需求。然而,鸿蒙原生的Video组件ui效果无法满足用户需求。Ui的播放暂停按钮需要自定义:Video组件只是单纯的加载播放的组件,播放暂停等常用功能按钮需要自己定义:开发人员在使用video的时候如果每次都需要去实现一套ui以及各种基础功能的api会导致整体效率不高且效果各异播放器的动画效果等统一封装后可以在后期需要改动产品效果等时,统一修改更加高效2.原因分析(1) 原生播放组件无法满足需求VideoPreview组件的核心定位是单一维度的视频预览播放工具,其设计初衷是满足用户预览视频的需求。这种定位决定了组件在功能规划上更侧重整体播放的效果以及ui的统一性,原生的video组件无法满足这个需求。(2) 开发逻辑的独立性VideoPreview组件的底层实现逻辑具有较强的独立性与封闭性。每个组件实例仅负责处理自身对应的视频数据(如本地视频数据以及网络视频数据)。(3) 开发冗余使用多个不同开发者开发的 video组件进行视频播放时,不同的开发者对于最终ui效果以及动画效果的理解差异,会导致最终呈现给用户的最终预览效果的差异,这样不仅开发人员各自增加了开发工作量,也无法很好的给用户提供统一、优质的视频预览效果,最终影响开发效率和使用体验。3.解决思路(1) 组件整合:打造统一标准的视频播放器组件针对鸿蒙原生 video组件没有统一样式的播放按钮的痛点(核心思路是基于组件化思想对原生组件进行封装扩展,通过复用原生能力、状态管理与动态适配,实现播放、暂停、重播、未加载完成时的预览图功能。具体包括:自定义video组件基础能力,组合播放、暂停、重播功能的统一ui按钮,播放进度条样式,解决ui标准不统一问题;采用鸿蒙装饰器实现视频数据必传入的方式,让开发人员很容易理解应该如何传值,可减少开发成本,提升开发效率;封装组件进度播放时、暂停时的动画,根据当前播放状态展示不同按钮(如播放中、播放完成、播放进度条平滑隐藏等)。(2) 交互增强:提升播放暂停完成时的动画效果播放的时候,用户点击可以显示或者隐藏播放进度条,同时平滑处理显示与隐藏动画,提高用户体验。4.解决方案(1) Ui实现:通过自定义按钮资源已经布局,封装VideoPreview组件。示例代码:@Observed export class VideoPreviewViewModel { // 视频控制器 controller: VideoController = new VideoController(); // 设置当前播放时间 setCurrentTime(time: number): void { this.controller.setCurrentTime(time) } } @Component export struct VideoPreview { // 视频源地址(必传) @Prop videoUri: Resource | string = '' // 预览图片地址 @Prop imgUri: Resource | string = '' // 是否自动播放 @Prop autoPlay: boolean = true // 播放速度 @Prop speed: PlaybackSpeed = PlaybackSpeed.Speed_Forward_1_00_X // 关闭事件回调 onClose?: () => void // 组件内部状态 @State state: VideoState = new VideoState() @State animationProperty: AnimationOption = new AnimationOption() // 视图模型 @State viewModel: VideoPreviewViewModel = new VideoPreviewViewModel() aboutToAppear() { this.animationProperty.duration = 300 this.animationProperty.curve = Curve.EaseInOut } build() { Stack() { this.VideoBuilder() this.buildControls() // 加载状态显示 if (this.state.isLoading) { Image(this.imgUri) .width('100%') .height('100%') .objectFit(ImageFit.Cover) LoadingProgress() .width(40) .height(40) .color(Color.White) } } .width('100%') .height('100%') .backgroundColor(Color.Black) } // 视频播放器构建器 @Builder VideoBuilder() { Stack() { // 重播按钮(播放完成时显示) if (this.state.isFinish) { Column() { Image($r('app.media.replay_video')) .width(50) .height(50) .onClick(() => { this.viewModel.controller.start() }) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) .zIndex(33) } // 视频组件 Video({ controller: this.viewModel.controller, currentProgressRate: this.state.speed, src: this.videoUri }) .muted(this.state.isVoiceOff) .objectFit(ImageFit.Contain) .autoPlay(this.autoPlay) .controls(false) .width('100%') .height('100%') .backgroundColor(Color.Black) .onPrepared((event: PreparedInfo) => { this.state.duration = event.duration this.state.isControlsVisible = 1 this.state.isLoading = false console.info('Video prepared, duration: ' + event.duration) }) .onUpdate((event: PlaybackInfo) => { this.state.currentTime = event.time }) .onStop(() => { this.state.isPlaying = false }) .onPause(() => { this.state.isPlaying = false }) .onStart(() => { this.state.isPlaying = true this.state.isLoading = false this.state.isFinish = false }) .onFinish(() => { this.state.isPlaying = false this.state.isFinish = true this.state.isLoading = false }) .onError(() => { console.error('Video playback error') this.state.isLoading = false }) } } // 控制栏构建器 @Builder buildControls() { Column() { // 顶部关闭按钮区域 Column() { Image($r("app.media.close_video")) .width(30) .height(30) .onClick(() => { if (this.onClose) { this.onClose() } }) } .width('100%') .height(80) .backgroundColor('#99000000') .padding({ top: 20, right: 12 }) .alignItems(HorizontalAlign.End) .visibility(this.state.isControlsVisible? Visibility.Visible : Visibility.Hidden) .animation(this.animationProperty) Blank() // 音量控制 Column() { Image(this.state.isVoiceOff ? $r('app.media.voice_off') : $r('app.media.voice_on')) .width(24) .height(24) .onClick(() => { this.state.isVoiceOff = !this.state.isVoiceOff }) } .padding({ right: 12 }) .width('100%') .visibility(this.state.isControlsVisible? Visibility.Visible : Visibility.Hidden) .alignItems(HorizontalAlign.End) // 底部进度控制区域 Column() { Row({ space: 8 }) { // 播放/暂停按钮 Image(this.state.isPlaying ? $r('app.media.pause_video') : $r('app.media.play_video')) .width(24) .height(24) .onClick(() => { if (this.state.isPlaying) { this.viewModel.controller.pause() } else { this.viewModel.controller.start() } this.state.isPlaying = !this.state.isPlaying }) .margin({ left: 10 }) // 当前时间 Text(this.formatTime(this.state.currentTime)) .fontColor(Color.White) .fontSize(12) .width(40) .textAlign(TextAlign.Center) // 进度条 Slider({ value: this.state.currentTime, min: 0, max: this.state.duration, style: SliderStyle.OutSet }) .layoutWeight(1) .blockColor(Color.White) .selectedColor('#FF4081') .trackColor('#CCCCCC') .trackThickness(3) .onChange((value: number) => { this.viewModel.controller.setCurrentTime(value) }) // 总时长 Text(this.formatTime(this.state.duration)) .fontColor(Color.White) .fontSize(12) .width(40) .textAlign(TextAlign.Center) .margin({ right: 10 }) } .width('100%') .height(60) .alignItems(VerticalAlign.Center) .backgroundColor('#99000000') } .width('100%') .visibility(this.state.isControlsVisible? Visibility.Visible : Visibility.Hidden) .animation(this.animationProperty) } .width('100%') .height('100%') // 手势控制:双击播放/暂停,单击显示/隐藏控制栏 .gesture( GestureGroup( GestureMode.Exclusive, TapGesture({ count: 2 }) .onAction(() => { if (this.state.isPlaying) { this.viewModel.controller.pause() } else { this.viewModel.controller.start() } this.state.isPlaying = !this.state.isPlaying }), TapGesture({ count: 1 }) .onAction(() => { if (this.state.isControlsVisible) { this.state.isControlsVisible = 0; } else { this.state.isControlsVisible = 1; } }) ) ) } // 时间格式化工具方法 private formatTime(seconds: number): string { const mins = Math.floor(seconds / 60) const secs = Math.floor(seconds % 60) return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}` } } // 视频状态类 @Observed class VideoState { isPlaying: boolean = false isFinish: boolean = false isLoading: boolean = true isVoiceOff: boolean = false isControlsVisible: number = 0 currentTime: number = 0 duration: number = 0 speed: PlaybackSpeed = PlaybackSpeed.Speed_Forward_1_00_X } // 动画配置类 class AnimationOption { duration: number = 300 curve: Curve = Curve.EaseInOut delay: number = 0 iterations: number = 1 playMode: PlayMode = PlayMode.Normal } 交互体检:用户操作流程:点击video→进度条及播放按钮隐藏→再次点击video→进度条及播放按钮显示。5.方案成果总结(1) 通过自定义封装组件一体化设计统一视频预览播放器的样式,减少开发人员的开发成本(2) 清晰传值方式,使得开发者很容易的使用这个视频预览组件(3) 加载时的图片预览可以使得加载时不是默认的黑屏,提高用户体验,统一的点击隐藏与显示效果,完美实现了客户对于视频播放器ui的需求,最终实现原生视频播放器video的优化升级。
-
1.1问题说明在高频搜索场景中,当用户快速输入、连续删除或修改搜索关键词时,大量连续的网络请求会给用户带来诸多不佳感知体验。用户手指在键盘上快速操作时,屏幕上的搜索结果会像 “跳帧” 一样频繁变动。比如刚输入 “深圳”,相关列表刚显示出来,紧接着输入 “市” 变成 “深圳市”,结果列表就立刻被新内容覆盖,还没等用户看清,可能又因修改操作跳转到另一个列表,整个界面始终处于不稳定的闪烁状态,给人强烈的割裂感和混乱感。在操作反馈上,这种高频请求导致的迟滞让用户十分困惑。输入字符后,文字可能在输入框里卡顿几秒才显示,软键盘弹出或收起时也有明显的延迟,仿佛应用完全 “跟不上” 手指的操作速度。用户明明已经输入到 “深圳市福田区”,却可能因为之前请求的延迟响应,看到的还是 “深圳市福” 的模糊结果;想点击某个兴趣点时,列表突然因新请求刷新而 “消失”,手指点击的地方变成无效区域,只能重新寻找目标,这让用户陷入 “等待 - 失望” 的循环。1.2原因分析(一)触发机制不合理:当用户快速发起多个搜索请求时,后发起的 “新请求” 可能因网络环境、服务端处理速度等因素,比先发起的 “旧请求” 更早返回;而应用若未对响应的时序做校验,会直接用后返回的 “旧请求结果” 覆盖已显示的 “新请求结果”,用户看到 “结果倒退” 的混乱场景,导致用户刚找到的目标地点突然被替换,不得不重新搜索,相当于 “白操作一次”。(二)缺乏请求管理:在对 “已发起但已失效的网络请求” 毫无管控能力 — 当用户快速修改关键词时,前序发起的请求即便已不符合当前需求,仍会在后台继续执行,还会干扰当前有效结果的展示,让用户在高频搜索中频繁遭遇 “结果倒退”“响应延迟”“操作卡顿” 等问题,最终导致结果错乱,影响用户体验。(三)缺少输入过滤:应用对 “明显不具备搜索价值的输入内容” 没有任何拦截机制,即便用户输入的是空字符串、仅 1 个字符的关键词,甚至是无意义的符号组合,应用仍会直接发起网络请求 —— 这种 “来者不拒” 的处理方式,给用户带来 “响应无意义”“操作被干扰” 的使用体验。1.3解决思路(一)通过防抖机制减少请求次数,合并高频操作高频搜索场景中,用户快速输入时,短时间内的连续输入本质上是 “临时操作”,最终有效输入是 “停止输入后的稳定状态”。通过防抖机制,只在用户停止输入一段时间后才发起请求,合并多次无效中间操作。用防抖机制减少请求、合并高频操作的核心思路,可总结为 “精准适配习惯、贴合系统特性、弱化等待感知”,具体如下:首先,匹配用户输入节奏设定合理延迟:常规设 200-300 毫秒,既能覆盖 “连续输入 / 删改” 的中间过程,合并无效操作,又不会让用户觉得等待过久。其次,和 ArkUI 输入事件绑定形成闭环:用户输入时启动定时器,若 300 毫秒内继续操作(比如补输、删改),就清除旧定时器、重启新的;只有停止操作且定时器到期,才发起 1 次针对 “最终关键词” 的请求,避免中间态请求浪费资源。通过这套逻辑,既能减少 70% 以上的无效请求,又能让用户保持 “输入完就出结果” 的流畅感与使用体验。(二)实现请求的取消机制,终止未完成的无效请求“主动识别无效请求并及时终止”— 通过标识追踪、精准时机触发、适配鸿蒙 API 的终止逻辑,结合用户感知优化,既能彻底解决 “无效请求占用资源”“旧结果覆盖新结果” 的问题,又能让用户感受到 “应用始终在响应最新操作” 的流畅体验,实现功能优化与体验提升的统一。1.4解决方案(一)通过防抖机制减少请求次数,合并高频操作在用户高频输入的 “过程期” 暂时抑制网络请求,只在输入 “稳定期”(即用户明确停止操作后)发起一次有效请求—— 本质是用 “短暂延迟” 换取 “请求效率与体验流畅度的平衡”,既避免无效请求浪费资源,又不影响用户对 “实时响应” 的感知。具体可从以下三方面展开:锚定 “用户输入节奏”:设计合理的防抖延迟阈值防抖机制的关键是 “延迟时间” 的设定,需精准匹配中文用户的输入习惯 —— 既不能太短(无法合并高频操作,仍会产生无效请求),也不能太长(让用户感觉 “操作卡顿,结果迟迟不出来”)。通常将延迟阈值设定在200-300 毫秒:对于快速输入,200-300 毫秒的延迟足以覆盖 “连续输入的过程”,让多次字符变动合并为一次请求;对于轻微犹豫的输入,延迟阈值不会让用户感觉 “等待过久”,仍能保持 “输入完就出结果” 的流畅感;绑定 “输入事件生命周期”:实现 “触发 - 重置 - 执行” 的闭环逻辑防抖机制需与鸿蒙 ArkUI 框架的输入事件(如TextInput的onChange事件)深度绑定,形成 “用户操作→定时器触发→操作续发→定时器重置→操作停止→请求执行” 的完整闭环,确保高频操作被精准合并:初始触发:当用户第一次输入字符,onChange事件被触发,此时启动一个 300 毫秒的定时器,标记 “即将发起请求”;续发重置:若 300 毫秒内用户继续输入,onChange事件再次触发,立即清除上一次的定时器,重新启动新的 300 毫秒定时器 —— 相当于 “刷新等待时间”,确保只有 “最后一次输入后的稳定期” 才会触发请求;稳定执行:若 300 毫秒内用户无新操作(即确认当前关键词为最终需求),定时器到期,触发网络请求,此时发起的请求对应的是 “用户停止输入后的最终关键词”,而非中间态内容;平衡 “延迟与感知”:通过细节设计降低用户对 “等待” 的感知防抖机制本质是 “用延迟换效率”,需通过设计让用户 “感受不到延迟”,避免因等待产生负面体验:输入中视觉反馈:在定时器等待期间,可在搜索框右侧显示 “加载中” 小图标,让用户感知 “应用正在处理输入,而非无响应”,降低等待焦虑;局部结果预展示:若本地缓存中有当前关键词的近似结果,可在防抖等待期间先展示缓存的局部结果,并标注 “正在获取最新数据”,既填补等待空白,又不影响最终精准结果的加载;动态调整延迟:针对不同输入场景动态优化阈值 —— 如用户输入速度快(1 秒内变动 5 次以上),自动将延迟微调至 300 毫秒,确保合并更多操作;若用户输入速度慢(每 2 秒变动 1 次),自动将延迟降至 200 毫秒,减少等待感,实现 “快输入多合并,慢输入少等待” 的自适应效果。代码示例:import http from '@ohos.net.http'; import { BusinessError } from '@ohos.base'; import promptAction from '@ohos.promptAction'; interface Item { name: string; } interface Cache { timestamp: number; data: Array<Item>; } @Entry @Component struct SearchDebounceComponent { @State searchText: string = ""; @State isLoading: boolean = false; @State searchResults: Array<Item> = []; @State cachedResults: Array<Item> = []; // 防抖定时器 private debounceTimer: number | null = null; // 基础防抖延迟时间(毫秒) private baseDelay: number = 250; // 记录输入事件时间点,用于动态调整延迟 private inputTimestamps: Array<number> = []; // 缓存管理器实例 private cacheManager: SearchCacheManager = new SearchCacheManager(); build() { Column() { // 搜索输入框 Row() { TextInput({ placeholder: '请输入搜索关键词', text: this.searchText }) .onChange((value) => this.handleInputChange(value)) .width('85%') .padding(10) .backgroundColor('#F5F5F5') .borderRadius(20) // 加载状态指示器 if (this.isLoading) { LoadingProgress() .color('#007AFF') .size({ width: 24, height: 24 }) .margin({ left: 10 }) } } .padding(16) // 搜索结果展示区 List() { // 展示缓存结果(如果有) if (this.cachedResults.length > 0 && !this.isLoading) { ListItem() { Column() { Text('正在获取最新数据...') .fontSize(12) .fontColor('#888888') .align(Alignment.Center) .padding(10) ForEach(this.cachedResults, (item: Item) => { Text(item.name) .fontSize(14) .padding({ left: 10, right: 10, bottom: 5 }) }) } } } // 展示正式搜索结果 ForEach(this.searchResults, (item: Item) => { ListItem() { Text(item.name) .fontSize(16) .padding(10) .width('100%') } .backgroundColor('#FFFFFF') .borderRadius(8) .margin(5) }) } .width('100%') .padding(10) } .backgroundColor('#F9F9F9') .width('100%') .height('100%') } // 处理输入变化 private handleInputChange(value: string) { this.searchText = value; // 记录输入时间戳,用于计算输入速度 this.trackInputSpeed(); // 清除上一次的定时器 if (this.debounceTimer) { clearTimeout(this.debounceTimer); } // 如果输入为空,直接清空结果 if (value.trim() === "") { this.searchResults = []; this.cachedResults = []; return; } // 显示缓存结果(如果有) this.loadCachedResults(value); // 计算动态延迟时间 const dynamicDelay = this.calculateDynamicDelay(); // 设置新的定时器 this.debounceTimer = setTimeout(() => { this.executeSearch(value); }, dynamicDelay); } // 跟踪输入速度 private trackInputSpeed() { const now = Date.now(); this.inputTimestamps.push(now); // 只保留最近5次的输入时间戳 if (this.inputTimestamps.length > 5) { this.inputTimestamps.shift(); } } // 计算动态延迟时间 private calculateDynamicDelay(): number { // 如果输入次数不足,使用基础延迟 if (this.inputTimestamps.length < 2) { return this.baseDelay; } // 计算最近几次输入的平均间隔 let totalInterval = 0; for (let i = 1; i < this.inputTimestamps.length; i++) { totalInterval += this.inputTimestamps[i] - this.inputTimestamps[i - 1]; } const avgInterval = totalInterval / (this.inputTimestamps.length - 1); // 如果平均间隔小于200ms,说明输入速度快,增加延迟到300ms if (avgInterval < 200) { return 300; } // 如果平均间隔大于500ms,说明输入速度慢,减少延迟到200ms else if (avgInterval > 500) { return 200; } // 中等速度输入,使用基础延迟 else { return this.baseDelay; } } // 加载缓存结果 private loadCachedResults(keyword: string) { this.cacheManager.getCache(keyword).then((data) => { if (data && data.length > 0) { this.cachedResults = data; } else { this.cachedResults = []; } }); } // 执行搜索请求 private async executeSearch(keyword: string) { // 过滤无效输入 if (!this.isValidKeyword(keyword)) { return; } // 显示加载状态 this.isLoading = true; this.cachedResults = []; try { // 创建HTTP请求 let request = http.createHttp(); let url = `https://restapi.amap.com/v3/place/text?keywords=${encodeURIComponent(keyword)}&offset=20&page=1&key=your ker &extensions=all` // 发起请求 let response = await request.request( url, { method: http.RequestMethod.GET, connectTimeout: 5000, readTimeout: 5000 } ); // 处理响应结果 if (response.responseCode === http.ResponseCode.OK) { if (typeof response.result === 'string') { let result: ESObject = JSON.parse(response.result); if (result.status === "1" && result.pois) { this.searchResults = result.pois; // 缓存结果 this.cacheManager.saveCache(keyword, result.pois); } else { this.searchResults = []; promptAction.showToast({ message: '未找到相关地点' }); } } } else { promptAction.showToast({ message: `搜索失败: ${response.responseCode}` }); } } catch (error) { const businessError = error as BusinessError; console.error(`搜索异常: ${businessError.code}, ${businessError.message}`); promptAction.showToast({ message: '网络异常,请稍后重试' }); } finally { // 隐藏加载状态 this.isLoading = false; } } // 验证关键词有效性 private isValidKeyword(keyword: string): boolean { const trimmed = keyword.trim(); // 过滤过短关键词(少于2个字符) return trimmed.length >= 2; } } // 搜索缓存管理器 class SearchCacheManager { // 缓存有效期(5分钟) private cacheExpiration: number = 5 * 60 * 1000; // 内存缓存 private memoryCache: Map<string, Cache> = new Map(); // 获取缓存 async getCache(keyword: string): Promise<Array<Item> | null> { // 先检查内存缓存 const cached: Cache = this.memoryCache.get(keyword) as Cache; if (cached) { // 检查缓存是否过期 if (Date.now() - cached.timestamp < this.cacheExpiration) { return cached.data; } else { // 缓存过期,移除 this.memoryCache.delete(keyword); } } return null; } // 保存缓存 async saveCache(keyword: string, data: Array<Item>) { if (keyword && data && data.length > 0) { this.memoryCache.set(keyword, { timestamp: Date.now(), data: data }); // 限制缓存数量,超过20条则清理最早的 if (this.memoryCache.size > 20) { const oldestKey = Array.from(this.memoryCache.keys()).shift(); if (oldestKey) { this.memoryCache.delete(oldestKey); } } } } } (二)实现请求的取消机制,终止未完成的无效请求为每个网络请求建立 “可识别、可追踪、可终止” 的全生命周期管理,通过精准识别 “用户已放弃的旧请求”,主动终止其网络传输与后续处理,从根源上避免无效请求占用资源、干扰界面。建立 “请求唯一标识与状态追踪系统”:给每个请求 “贴标签”要取消无效请求,首先需明确 “哪些请求需要被取消”。因此需为每个搜索请求分配唯一标识(如递增 ID、时间戳 + 关键词哈希值),并通过 “请求管理器” 实时追踪其状态(等待中 / 传输中 / 已完成 / 已取消),形成清晰的请求管理链路:唯一标识生成:每次发起搜索请求时,生成一个全局唯一的requestId(如Date.now() + Math.random().toString(36).slice(2, 8)),并与当前关键词、发起时间绑定,存入请求管理器的活跃列表中;状态实时更新:请求发起时标记为 “传输中”,响应返回后标记为 “已完成”,被取消后标记为 “已取消”;关联用户操作:将requestId与用户输入的关键词强关联,当用户输入新关键词时,可通过关键词匹配找到 “已失效的旧请求”(如用户输入 “北京市” 后,所有关联 “北京” 关键词的未完成请求均为无效)。在鸿蒙应用中,可通过单例模式的RequestManager类实现这一系统,确保跨组件(如搜索输入框、结果列表)的请求状态一致,避免 “重复取消” 或 “漏取消”。代码示例: import http from '@ohos.net.http'; import { BusinessError } from '@ohos.base'; /** * 请求状态枚举 */ export enum RequestState { WAITING = 'waiting', // 等待中 TRANSMITTING = 'transmitting', // 传输中 COMPLETED = 'completed', // 已完成 CANCELED = 'canceled' // 已取消 } /** * 请求信息接口 */ export interface RequestInfo { requestId: string; // 请求唯一标识 keyword: string; // 关联的搜索关键词 createTime: number; // 创建时间戳 state: RequestState; // 当前状态 request: http.HttpRequest; // HTTP请求实例 } /** * 请求管理器(单例模式) * 负责请求的创建、状态追踪、取消等全生命周期管理 */ export class RequestManager { private static instance: RequestManager; private activeRequests: Map<string, RequestInfo> = new Map(); // 活跃请求列表 // 私有构造函数,确保单例 private constructor() {} /** * 获取单例实例 */ public static getInstance(): RequestManager { if (!RequestManager.instance) { RequestManager.instance = new RequestManager(); } return RequestManager.instance; } /** * 生成唯一请求ID * 格式: 时间戳 + 6位随机字符串 */ private generateRequestId(): string { const timestamp = Date.now().toString(); const randomStr = Math.random().toString(36).slice(2, 8); // 生成6位随机字符串 return `${timestamp}_${randomStr}`; } /** * 创建新请求并加入管理 * @param keyword 搜索关键词 * @returns 请求信息对象 */ public createRequest(keyword: string): RequestInfo { // 创建HTTP请求实例 const request = http.createHttp(); // 生成请求ID和信息 const requestId = this.generateRequestId(); const requestInfo: RequestInfo = { requestId, keyword, createTime: Date.now(), state: RequestState.WAITING, request }; // 添加到活跃请求列表 this.activeRequests.set(requestId, requestInfo); console.log(`创建新请求: ${requestId}, 关键词: ${keyword}`); return requestInfo; } /** * 更新请求状态 * @param requestId 请求ID * @param state 新状态 */ public updateRequestState(requestId: string, state: RequestState): void { const requestInfo = this.activeRequests.get(requestId); if (requestInfo) { requestInfo.state = state; console.log(`请求状态更新: ${requestId} -> ${state}`); // 已完成或已取消的请求从活跃列表移除 if (state === RequestState.COMPLETED || state === RequestState.CANCELED) { this.activeRequests.delete(requestId); } } } /** * 根据关键词取消相关的未完成请求 * @param keyword 搜索关键词 */ public cancelRequestsByKeyword(keyword: string): void { // 找出该关键词相关的所有活跃请求 const requestsToCancel: RequestInfo[] = []; this.activeRequests.forEach((requestInfo) => { // 当用户输入新关键词时,所有关联旧关键词的未完成请求均为无效 if (requestInfo.keyword !== keyword && (requestInfo.state === RequestState.WAITING || requestInfo.state === RequestState.TRANSMITTING)) { requestsToCancel.push(requestInfo); } }); // 取消找到的请求 requestsToCancel.forEach((requestInfo) => { this.cancelRequest(requestInfo.requestId); }); } /** * 取消指定请求 * @param requestId 请求ID */ public cancelRequest(requestId: string): void { const requestInfo = this.activeRequests.get(requestId); if (requestInfo && (requestInfo.state === RequestState.WAITING || requestInfo.state === RequestState.TRANSMITTING)) { try { // 调用HTTP请求的abort方法终止请求 requestInfo.request.destroy(); this.updateRequestState(requestId, RequestState.CANCELED); console.log(`已取消请求: ${requestId}, 关键词: ${requestInfo.keyword}`); } catch (error) { const err = error as BusinessError; console.error(`取消请求失败 ${requestId}: ${err.code}, ${err.message}`); } } } /** * 标记请求为已完成 * @param requestId 请求ID */ public markRequestCompleted(requestId: string): void { this.updateRequestState(requestId, RequestState.COMPLETED); } /** * 获取当前活跃请求数量 */ public getActiveRequestCount(): number { return this.activeRequests.size; } /** * 清除所有请求(用于页面销毁等场景) */ public clearAllRequests(): void { this.activeRequests.forEach((requestInfo) => { if (requestInfo.state !== RequestState.COMPLETED && requestInfo.state !== RequestState.CANCELED) { try { requestInfo.request.destroy(); } catch (error) { console.error(`清除请求失败 ${requestInfo.requestId}:`, error); } this.updateRequestState(requestInfo.requestId, RequestState.CANCELED); } }); this.activeRequests.clear(); } } 1.5 方案成果总结高频搜索场景的网络请求优化,围绕 “减少无效请求、解决时序混乱、提升用户体验” 核心目标,通过防抖机制、请求全生命周期管理、请求标识校验三大核心策略,具体成果可从以下维度展开:高频请求问题彻底缓解:通过 “200-300ms 动态防抖延迟 + 输入节奏适配” 机制,精准合并用户连续输入、删改的中间态操作,无效网络请求量减少 70% 以上。用户不再遭遇 “输入时结果频繁跳帧、列表闪烁” 的割裂感,界面始终保持稳定,操作流程从 “反复等待” 变为 “输入完即出精准结果”,搜索操作的流畅度与确定性大幅提升。结果倒退 / 混乱问题大幅消除:依托 “请求唯一标识(时间戳 + 随机字符串 + 关键词哈希)+ 全链路校验” 机制,彻底解决异步响应时序错乱问题:无论网络波动(如旧请求延迟返回)或用户快速切换搜索目标,应用均能通过 “响应前校验、解析后二次校验、UI 更新前最终校验” 的三层拦截,确保只处理最新请求的结果。用户不会再遇到 “输对关键词后结果突然变回旧内容” 的困惑,搜索结果的准确性与可靠性获得完善保障。
-
1 问题说明:正常使用离线截图api去创建截图, 如下: async test() { const result = await this.getUIContext().getComponentSnapshot().createFromBuilder(testBuilder()) } @Builder function testBuilder() { Column() { Text('这里是测试') } }调用后发现无法得到截图后的Image,并抛出错误:10001。2 解决思路:通过文档查看错误码10001,其错误信息是: The builder is not a valid build function.表明该builder不是一个有效的builder,通过思考可以发现arkUI里只有2种builder,一种局部builder,一种全局builder, 我们的代码里使用的是全局builder,所以换成局部builder再做尝试。3 解决方案:将全局builder改为局部builder,如下:async test() { const result = await this.getUIContext().getComponentSnapshot().createFromBuilder(testBuilder()) } @Builder testBuilder() { Column() { Text('这里是测试') } }这样就可以完美解决不能截图的问题了。
-
问题说明 后端接口还未开发出来,导致前端不能调试开发原因分析 排期问题,需要移动端先行开发解决思路 有时候咱们的数据可以自己mock;然后用假数据去完成对应的逻辑解决方案自己创建json文件 放在 resources/rawfile/data.json 下,可以指读取并解析 getDataFromJSON<SettingLevelItem>('MinePage-Setting-Level-Items.json', this).forEach(item => { this.settingLevelItems.push(new SettingLevelItem(item)) });export function getDataFromJSON<T>(rawFileName: string, component?: Object): T[] { let result: T[] = []; try { let value: Uint8Array = getContext(component).resourceManager.getRawFileContentSync(rawFileName); result = JSON.parse(bufferToString(value.buffer)) as T[]; } catch (error) { let code = (error as BusinessError).code; let message = (error as BusinessError).message; console.error(`getRawFileContentSync failed, error code: ${code}, message: ${message}.`); } return result;}可直接解析为对象,在项目中使用
-
1 问题说明:当我们在使用关键帧动画时,设置iterations为-1时会开启动画的无限循环,如下:this.getUIContext().keyframeAnimateTo({ iterations: -1 }, [{ duration: 100, event: () => { this.xxScale = 1.5 } }, { duration: 100, event: () => { this.xxScale = 1 } }])但是当我们想终止这个无限动画的时候发现没有停止的api.2 解决思路:可以开启另一个动画, 来打断之前的关键帧动画。3 解决方案:创建一个duration为0的animateTo动画, 如下:this.getUIContext().animateTo({duration: 0}, () => { this.xxScale = 1 })此时发现依然无法打断,仔细看代码分析可能原因: 关键帧动画和animateTo动画的最终位置都是xxScale = 1, 数据没有变化,所以没有触发打断,尝试修改xxScale不等于1的数据验证,如下:this.getUIContext().animateTo({duration: 0}, () => { this.xxScale = 0.5 })此时发现确实可以打断了,但是动画元素停留在了xxScale为0.5 的状态,所以还需要再将xxScale 重置回来,如下:this.getUIContext().animateTo({duration: 0}, () => { this.xxScale = 0.5 }) this.xxScale = 1如此,就完美的停止了无限循环的关键帧动画了。
-
1.1,问题说明需要将选取的视频进行封面更换,同时拖动图片列表封面可以更换视频封面,效果如下1.2,原因分析那个层叠布局无法拖动到最后一张图片要根据每个视频截取出不同时间的视频封面实现拖动来展示不同封面1.3,解决思路在进入页面时获取到视频文件的路径,进行每一秒视频封面选取将图片集后面添加几行空白文字占位,刚好留下最后一张图片跟框里面对齐进行scroll滑动停止监听,根据滑动距离来计算到哪张图片进行封面选取1.4 解决方案具体如下:将视频每一秒最后截取出来组成图片集合,进行下面列表渲染,方法如下,参数1:视频地址,参数2:指定时间(这里为一秒)async specifyTime(File: string, Number: number) { // 创建AVImageGenerator对象 let avImageGenerator: media.AVImageGenerator = await media.createAVImageGenerator() let file = fileIo.openSync(File, fileIo.OpenMode.READ_ONLY); let fd = file.fd; // 获取文件描述符 //设置fdSrc avImageGenerator.fdSrc = { fd: fd }; //这里是你录制的视频地址路径 console.log('执行了fdSrc' + avImageGenerator.fdSrc) // 初始化入参 let timeUs = Number let queryOption = media.AVImageQueryOptions.AV_IMAGE_QUERY_NEXT_SYNC let param: media.PixelMapParams = { width: 1920, height: 1080 } // 获取缩略图(promise模式) let images = await avImageGenerator.fetchFrameByTime(timeUs, queryOption, param) this.imageFiles?.push(images) // // 释放资源(promise模式) avImageGenerator.release()}方法调用while (time < maxtime) { console.log('开始执行', time, maxtime) await this.specifyTime(this.Url, time * 1000000) time += 1}图片集合渲染ForEach(this.imageFiles, (item: image.PixelMap, index) => { Image(this.imageFiles[index]) .width(50) .height(50) .onClick(() => { this.scroller.scrollTo({ xOffset: index * 50, yOffset: 0 }) this.images = item AppStorage.set('image_PixelMap', item) })})通过scroller控制器进行监听滑动实现图片封面选取
-
1.问题说明:鸿蒙Web组件,前端H5调用原生,原生调用前端H5,统一管理调用2.原因分析:Web组件加载H5,前端H5调用原生:javaScriptProxy,registerJavaScriptProxy;原生调用前端H5:runJavaScript,runJavaScriptExt;需要做统一管理使用3.解决思路:前端H5调用原生:Js的方法和对象的注入需要装饰器修饰,直接转换为字符串js脚本注入原生调用前端H5:创建Js脚本类,统一管理Js的执行4.解决方案:一、鸿蒙Web桥(@hzw/ohos-dsbridge)三方框架的集成在工程的目录oh-package.json5文件中添加"dependencies": { "@hzw/ohos-dsbridge": '^1.8.0',},二、通用Web组件搭建import { WebViewControllerProxy } from "@hzw/ohos-dsbridge"import { JsBridge } from "../JsBridge/JsBridge"import { JsBridgeRegister } from "../JsBridge/JsBridgeRegister"import { JsScript } from "../JsBridge/JsScript"import { PayApi } from "../JsBridge/PayApi"@ComponentV2export struct WebComponent { url: string = "" //键盘避让规则 keyboardAvoidMode: WebKeyboardAvoidMode = WebKeyboardAvoidMode.RESIZE_VISUAL private controller: WebViewControllerProxy = WebViewControllerProxy.createController() private jsBridge: JsBridge | undefined = undefined private jsScript: JsScript | undefined = undefined aboutToDisappear(): void { // 创建JS桥 this.jsBridge = new JsBridge((method: string, params?: ESObject) => { // 前端H5调用原生,回调 }) this.controller.addJavascriptObject(this.jsBridge) // 创建JS脚本 this.jsScript = new JsScript(this.controller.webviewController, (method: string, params?: ESObject) => { // 原生调用前端H5后,回调 }) } // JS的注入 registerJavaScriptProxy() { // 注册`harmonyOSPay` JsBridgeRegister.registerApiAuto(this.controller.webviewController, new PayApi(), 'harmonyOSPay') } // 执行JavaScript脚本, 原生调H5 runJavaScript() { this.jsScript?.testJsScript('json') } build() { Web({ src: this.url, controller: this.controller.webviewController, renderMode: RenderMode.SYNC_RENDER, // 设置渲染模式 incognitoMode: true//可以将可选参数incognitoMode设置为true,来开启Web组件的隐私模式。 当使用隐私模式时,浏览网页时的Cookie、 Cache Data等数据不会保存在本地的持久化文件,当隐私模式的Web组件被销毁时,Cookie、 Cache Data等数据将不被记录下来。 }) .domStorageAccess(true) .fileAccess(true) .imageAccess(true) .javaScriptProxy(this.controller.javaScriptProxy) .javaScriptAccess(true) .mixedMode(MixedMode.Compatible) .onlineImageAccess(true) .overviewModeAccess(true) .databaseAccess(true) .geolocationAccess(true) .mediaPlayGestureAccess(true) .multiWindowAccess(true) .horizontalScrollBarAccess(false) .verticalScrollBarAccess(false) .cacheMode(CacheMode.Default) .forceDarkAccess(true) .keyboardAvoidMode(this.keyboardAvoidMode) .backgroundColor('#FFFFFF') .width('100%') .height('100%') .onControllerAttached(() => { // 推荐在此loadUrl、设置自定义用户代理、注入JS对象等 this.registerJavaScriptProxy() }) .onLoadIntercept((event) => { // 返回true表示阻止此次加载,否则允许此次加载 return false }) .onInterceptRequest((event) => { // 当Web组件加载URL之前触发该回调,用于拦截URL并返回响应数据 if (event) { const url = event.request.getRequestUrl() const method = event.request.getRequestMethod() const header = event.request.getRequestHeader() console.log('url:' + url + 'getRequestMethod:' + method + 'getRequestHeader:' + header ) } return null }) .onPageBegin((event) => { // 网页开始加载时触发该回调,且只在主frame触发,iframe或者frameset的内容加载时不会触发此回调 }) .onProgressChange((event) => { // 网页加载进度变化时触发该回调 }) .onPageEnd((event) => { // 网页加载完成时触发该回调,且只在主frame触发,iframe或者frameset的内容加载时不会触发此回调 // 推荐在此事件中执行JavaScript脚本 this.runJavaScript() }) .onErrorReceive((event) => { // 网页加载遇到错误时触发该回调 }) .onOverrideUrlLoading((webResourceRequest: WebResourceRequest) => { // 当URL将要加载到当前Web中时触发该回调,让宿主应用程序有机会获得控制权,判断是否阻止Web加载URL if (webResourceRequest && webResourceRequest.getRequestUrl() == "about:blank") { return true } return false }) .onFirstContentfulPaint((event) => { // 设置网页首次内容绘制回调函数 }) .onPageVisible((event) => { // 设置旧页面不再呈现,新页面即将可见时触发的回调函数 }) .onRenderExited((event) => { // 应用渲染进程异常退出时触发该回调 }) .onFullScreenEnter((event) => { // 通知开发者Web组件进入全屏模式 }) .onFullScreenExit(() => { // 通知开发者Web组件退出全屏模式 }) .onErrorReceive((error) => { // 网页加载遇到错误时触发该回调。主资源与子资源出错都会回调该接口,可以通过isMainFrame来判断是否是主资源报错。出于性能考虑,建议此回调中尽量执行简单逻辑。在无网络的情况下,触发此回调 }) .onTitleReceive((event) => { // 当页面文档标题<title>元素发生变更时,触发回调 }) .onAlert((event) => { // 网页触发alert()告警弹窗时触发回调 return false }) .onConsole((event) => { // 通知宿主应用JavaScript console消息 return false }) }}三、前端H5调用原生1、Js桥JsBridge类创建import { CompleteHandler, JavaScriptInterface } from "@hzw/ohos-dsbridge"import json from "@ohos.util.json"export class JsBridge { private cHandler?: CompleteHandler // 对外值的回调 private paramsCallback?: (method: string, params?: ESObject) => void constructor(paramsCallback?: (method: string, params?: ESObject) => void) { this.paramsCallback = paramsCallback } // 同步JS注入 @JavaScriptInterface(false) testSync(p: string, handler: CompleteHandler) { console.log("testSync: " + JSON.stringify(p)) if (this.paramsCallback) { this.paramsCallback('testSync', p) } } // 异步JS注入 @JavaScriptInterface() testAsync(p: string, handler: CompleteHandler) { console.log("testAsync: " + JSON.stringify(p)) this.cHandler = handler handler.complete(callBackParam('0', { 'result': '0' }, '')) if (this.paramsCallback) { this.paramsCallback('testSync', p) } }}// 具体的数据模版需要根据前端业务来定义function callBackParam(code: string, data: ESObject, msg: string): string { let param: Record<string, ESObject> = { 'code': code, 'data': data, 'msg': msg } let jsonString = json.stringify(param) return jsonString}2、Js桥注入对象管理类JsBridgeRegister的创建import { webview } from "@kit.ArkWeb"// 装饰器:标记方法为 JS 可导出方法export function JSMethodExport(target: object, propertyKey: string): void { let methods = jsMethodRegistry.get(target) if (!methods) { methods = [] jsMethodRegistry.set(target, methods) } methods.push(propertyKey)}const jsMethodRegistry = new WeakMap<object, string[]>()export class JsBridgeRegister { // 获取被 @JSMethodExport 标记的方法名 private static getAllExportedMethodNames(instance: object): string[] { const instanceClass = instance.constructor const methods = jsMethodRegistry.get(instanceClass.prototype) return methods ?? [] } /** * 注册单个 JS 接口类到指定 WebViewController * @param webViewController WebViewController 实例 * @param instance 类实例 * @param namespace 可选:JS 中访问的命名空间(默认取类名) */ static registerApiAuto(webViewController: webview.WebviewController, instance: object, namespace?: string) { const methodNames = JsBridgeRegister.getAllExportedMethodNames(instance) const finalNamespace = namespace || instance.constructor.name try { // 先尝试移除旧的 webViewController.deleteJavaScriptRegister(finalNamespace) } catch (error) { console.log(`deleteJavaScriptRegister(${finalNamespace}) error:${JSON.stringify(error)}`) } try { // 再新添加 webViewController.registerJavaScriptProxy(instance, finalNamespace, methodNames) } catch (error) { console.log(`registerJavaScriptProxy(${finalNamespace}) error:${JSON.stringify(error)}`) } }}3、Js桥注入对象(可以多个)的创建import { CompleteHandler } from "@hzw/ohos-dsbridge"import { JSMethodExport } from "./JsBridgeRegister"export class PayApi { private cHandler?: CompleteHandler // 对外值的回调 private paramsCallback?: (method: string, params?: ESObject) => void @JSMethodExport wxPay(json: string, handle: CompleteHandler) { // 前端H5调用原生 } @JSMethodExport aliPay(json: string) { // 前端H5调用原生 }}四、原生调用前端H51、Js脚本执行管理类JsScript的创建import { webview } from "@kit.ArkWeb"export class JsScript { // 对外值的回调 private runScriptCallback?: (method: string, params?: ESObject) => void private webViewController?: webview.WebviewController constructor(webViewController: webview.WebviewController, runScriptCallback?: (method: string, params?: ESObject) => void) { this.webViewController = webViewController this.runScriptCallback = runScriptCallback } testJsScript(json: string) { console.log("testJsScript: " + json) if (this.webViewController) { this.webViewController.runJavaScript(`javascript:testJsScript( ${json})`) } if (this.runScriptCallback) { this.runScriptCallback('testJsScript', json) } }}
-
问题说明:在项目开发中有同学使用组件内成员方法 bind() 作为回调函数保存了引用,出现了调用失效、内存泄漏等问题。原因分析:使用 func().bind() 作为回调函数虽然可以解决 this 绑定问题,但会带来一些潜在的问题:性能问题:每次渲染都创建新函数class EventHandler { private data: any[] = []; handleEvent() { console.log('Handling event with data:', this.data); } } @Entry @Component struct Example { private handler = new EventHandler(); build() { Column() { Button('Risk') .onClick(this.handler.handleEvent.bind(this.handler)) // ❌ 可能内存泄漏 // 需要手动管理绑定函数的生命周期 } } aboutToDisappear() { // 很难清理 bind 创建的函数引用 } } 内存泄漏风险class EventHandler { private data: any[] = []; handleEvent() { console.log('Handling event with data:', this.data); } } @Entry @Component struct Example { private handler = new EventHandler(); build() { Column() { Button('Risk') .onClick(this.handler.handleEvent.bind(this.handler)) // ❌ 可能内存泄漏 // 需要手动管理绑定函数的生命周期 } } aboutToDisappear() { // 很难清理 bind 创建的函数引用 } } 调试困难@Entry @Component struct Example { @State value: string = ''; processInput(text: string) { this.value = text; } build() { Column() { TextInput() .onChange((value: string) => { // ❌ 调试时难以追踪 this.processInput.bind(this)(value); }) // ✅ 更清晰的调试信息 TextInput() .onChange((value: string) => { this.processInput(value); }) } } } 类型安全问题interface ApiService { fetchData: (id: number) => Promise<void>; } class MyService implements ApiService { private baseUrl: string = 'https://api.example.com'; async fetchData(id: number) { // bind 可能掩盖类型错误 const response = await fetch(`${this.baseUrl}/data/${id}`); return response.json(); } } @Entry @Component struct Example { private service = new MyService(); build() { Column() { Button('Fetch') // ❌ bind 可能隐藏类型不匹配 .onClick(this.service.fetchData.bind(this.service, 'string')) // 应该报错但可能不会 // ✅ 类型检查更严格 Button('Better Fetch') .onClick(() => { // this.service.fetchData('string') // 这里会正确报错 this.service.fetchData(123) // 正确用法 }) } } } 解决思路:使用箭头函数@Entry @Component struct Example { @State count: number = 0; // 方法定义 increment() { this.count++; } build() { Column() { Button('Arrow Function') .onClick(() => this.increment()) // ✅ 推荐 } } } 使用类属性箭头函数@Entry @Component struct Example { @State count: number = 0; // 类属性箭头函数 handleClick = (): void => { this.count++; } build() { Column() { Button('Class Property') .onClick(this.handleClick) // ✅ 直接引用 } } } 在构造函数中提前绑定class MyHandler { count: number = 0; constructor() { // 提前绑定,避免重复创建 this.increment = this.increment.bind(this); } increment() { this.count++; } } @Entry @Component struct Example { private handler = new MyHandler(); build() { Column() { Button('Pre-bound') .onClick(this.handler.increment) // ✅ 已提前绑定 } } } 使用 useMemo 模式(如果适用)@Entry @Component struct Example { @State count: number = 0; // 模拟 useMemo 行为 private memoizedHandlers = new Map<string, () => void>(); getHandler(key: string): () => void { if (!this.memoizedHandlers.has(key)) { this.memoizedHandlers.set(key, () => { this.count++; }); } return this.memoizedHandlers.get(key)!; } build() { Column() { Button('Memoized') .onClick(this.getHandler('increment')) // ✅ 记忆化处理 } } } 解决方案:避免使用 func().bind(this) 的情况:在渲染方法中频繁调用的地方需要严格类型检查的场景需要良好调试体验的情况关注内存使用的性能敏感应用推荐使用:箭头函数 () => func()类属性箭头函数 func = () => {}提前绑定(在构造函数或初始化时)记忆化处理对于重复使用的回调
-
前言上一篇文章 开发者技术支持-数据变化后UI未重新渲染问题解决分享 通过更新@State变量内存地址,实现对象属性变化后触发 UI 重新渲染,但个人认为并非优雅的解决方案,鸿蒙提供了 @Observed/@ObjectLink 装饰器,配套使用,用于嵌套场景的观察。场景说明同样用上一篇文章的场景举例,具体移步《开发者技术支持-数据变化后UI未重新渲染问题解决分享》,一个用户列表数组,包含着用户对象(interface),UI 使用到用户对象中的某个属性来渲染,着个时候改变该属性的值,并不能触发 UI 重新渲染。问题说明下面这行代码是无法触发 UI 重新渲染的,因为 @State 修饰的是 followers ,只有更新 follower 的值,才能触发 UI 重新渲染// 无法触发 UI 重新渲染 this.followers[index].isFriend = !this.followers[index].isFriend解决思路利用@ObjectLink和@Observed的特性,实现深层属性观察。@ObjectLink和@Observed类装饰器用于在涉及嵌套对象或数组的场景中进行双向数据同步:使用new创建被@Observed装饰的类,可以被观察到属性的变化。子组件中@ObjectLink装饰器装饰的状态变量用于接收@Observed装饰的类的实例,和父组件中对应的状态变量建立双向数据绑定。这个实例可以是数组中的被@Observed装饰的项,或者是class object中的属性,这个属性同样也需要被@Observed装饰。解决方案首先将interface User改成class UserModel,并用@Observed装饰器修饰如下:/** * 用户 */ @Observed export class UserModel { name: string // 名称 avatar: string // 头像 isFriend: boolean = false // 是否好友(我也关注了他) constructor(user: User) { this.name = user.name this.avatar = user.avatar this.isFriend = user.isFriend } } 其次实现创建 Array 的子类 ObservedArray,并用@Observed装饰器修饰@Observed export class ObservedArray<T> extends Array<T> { constructor(...args: T[]) { super(...args) } } 列表项组件中的follower变量用@ObjectLink修饰import { UserModel } from '../common/UserModel' @Component export struct SecondFollowerView { @ObjectLink follower: UserModel onClickAttention?: () => void aboutToAppear(): void { console.log(`aboutToAppear: ${this.follower.name}`) } build() { Row({ space: 8 }) { Image(this.follower.avatar) .width(50) .height(50) .borderRadius(25) Column() { Text(this.follower.name) Text('') } .layoutWeight(1) .alignItems(HorizontalAlign.Start) Button(this.follower.isFriend ? '好友' : '关注') .backgroundColor(this.follower.isFriend ? Color.White : Color.Blue) .fontColor(this.follower.isFriend ? Color.Gray : Color.White) .borderWidth(this.follower.isFriend ? 1 : 0) .borderColor(Color.Gray) .onClick((event: ClickEvent) => { this.onClickAttention?.() }) } .width('100%') .height(80) } } 再来看看 SecondPage 中的数据源 datasource 类型改为 ObservedArray<UserModel>,那么改变数组中元素 UserModel对象的isFriend属性的值时,UI将触发重新渲染import { ObservedArray } from '../common/ObservedArray' import { User } from '../common/User' import { UserModel } from '../common/UserModel' import { SecondFollowerView } from '../components/SecondFollowerView' @Entry @Component export struct SecondPage { @State datasource: ObservedArray<UserModel> = new ObservedArray<UserModel>() aboutToAppear(): void { let jsonStr = '[{"name":"张三","avatar":"https://img0.baidu.com/it/u=3217838212,795208401&fm=253&fmt=auto&app=138&f=JPEG?w=514&h=500","isFriend":true},{"name":"李四","avatar":"https://img0.baidu.com/it/u=4186430229,801747038&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500","isFriend":false},{"name":"王五","avatar":"https://img1.baidu.com/it/u=728383910,3448060628&fm=253&fmt=auto&app=120&f=JPEG?w=800&h=800","isFriend":false},{"name":"赵六","avatar":"https://img0.baidu.com/it/u=1096585807,3493972554&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500","isFriend":false}]' let followers = JSON.parse(jsonStr) as User[] let userModels: UserModel[] = followers.map((follower: User) => { return new UserModel(follower) }) this.datasource = new ObservedArray(...userModels) } build() { NavDestination() { Column() { List() { ForEach(this.datasource, (follower: UserModel, index) => { ListItem() { SecondFollowerView({ follower: follower, onClickAttention: () => { // @Observed 结合 @ObjectLink 实现深层观察,可以出发 UI 重新渲染,且不重新创建组件 this.datasource[index].isFriend = !this.datasource[index].isFriend } }) .padding({ left: 20, right: 20 }) } }) } .height('100%') .width('100%') } .height('100%') .width('100%') } .title('SecondPage') } }
-
一、问题说明在涉及敏感数据传输(如用户身份、支付信息)的前端业务中,前后端需约定加密协议(如 AES、SM4)保障传输安全。开发中面临的核心问题包括:1.代码冗余:每个请求需手动调用加密函数,导致业务逻辑中充斥重复的加密/解密代码,维护成本高。2.密钥管理风险:密钥硬编码在业务代码中,易泄露且难以轮换。3.开发效率低:调用方需关注加密细节(如算法选择、数据序列化),违反关注点分离原则。二、原因分析1.缺乏请求层抽象:Axios 原生不支持自动加解密,需在每个请求中手动处理,导致加密逻辑分散。2.加密与业务逻辑耦合:加密操作侵入业务代码,如以下冗余模式:// 业务代码中显式加密 const encryptedParams = encrypt(rawData, key); axios.post('/api', encryptedParams);3.密钥暴露风险:密钥通过明文存储在前端代码中,攻击者可通过源码分析获取密钥。 三、解决思路1.拦截器封装:在 Axios 的请求/响应拦截器中注入加解密逻辑,实现调用方无感知。2.统一密钥管理:通过环境变量或安全服务动态获取密钥,避免硬编码。3.支持多加密算法:抽象加解密接口,我会以 AES(通用)或 SM4(国密)举例。四、解决方案1. 核心架构 2. 代码实现(以 AES-CBC 模式为例)步骤 1:封装加解密工具// utils/crypto.ts import CryptoJS from "crypto-js"; const AES_KEY = import.meta.env.VITE_AES_KEY; // 从环境变量读取密钥 const IV = "ABCDEF1234567890"; // 初始化向量 // 加密函数 export const encrypt = (data: any): string => { const encrypted = CryptoJS.AES.encrypt( JSON.stringify(data), CryptoJS.enc.Utf8.parse(AES_KEY), { iv: CryptoJS.enc.Utf8.parse(IV), mode: CryptoJS.mode.CBC } ); return encrypted.toString(); }; // 解密函数(泛型支持类型推断) export const decrypt = <T>(ciphertext: string): T => { const bytes = CryptoJS.AES.decrypt(ciphertext, CryptoJS.enc.Utf8.parse(AES_KEY), { iv: CryptoJS.enc.Utf8.parse(IV), mode: CryptoJS.mode.CBC }); return JSON.parse(bytes.toString(CryptoJS.enc.Utf8)) as T; };步骤 2:Axios 拦截器注入// utils/request.ts import axios from "axios"; import { encrypt, decrypt } from "./crypto"; const service = axios.create({ timeout: 10000 }); // 请求拦截器:自动加密参数 service.interceptors.request.use((config) => { if (config.data) { config.data = { cipher: encrypt(config.data) }; // 封装为密文字段 } return config; }); // 响应拦截器:自动解密数据 service.interceptors.response.use((response) => { if (response.data?.cipher) { response.data = decrypt(response.data.cipher); // 解密密文字段 } return response.data; }); export default service;步骤 3:业务层调用(无加密痕迹)import request from "@/utils/request"; // 调用示例(与普通请求无差别) const fetchUserData = async () => { const res = await request.post("/api/user", { userId: 123 }); console.log(res.name); // 直接获取明文数据 };3. 进阶优化1.国密 SM4 支持:替换 crypto.ts中的加密逻辑为 SM4 实现,业务层无需改动:import { sm4 } from "sm-crypto"; export const encrypt = (data: any) => { return sm4.encrypt(JSON.stringify(data), SM4_KEY); };2.防重复提交:在拦截器中添加请求指纹校验,避免加密导致重复请求:const pendingMap = new Map(); service.interceptors.request.use(config => { const key = `${config.url}-${JSON.stringify(config.data)}`; if (pendingMap.has(key)) { return Promise.reject("重复请求"); } pendingMap.set(key, true); return config; });3.密钥动态获取:首次启动时从后端获取临时密钥,定期轮换提升安全性。(这个主要看业务需求,在安全级别不是很高的情况下,使用本地的即可,因为使用服务端密钥的话,需要另一套机制来保证秘钥的传输与存储,此处暂且不提)下一篇,我将介绍《保障客户端加密密钥安全:告别明文存储的隐患与ArkTS实战》
-
一、问题说明 在应用开发中,页面通常具备完整的生命周期,但在使用 Tab 组件管理页面时,会出现特定生命周期函数未按预期触发的问题:当从 Tab 组件所管理的某一页面(下称 “Tab 页面”)跳转至其子页面后,再从子页面返回原 Tab 页面时,原 Tab 页面的 onPageShow 生命周期回调函数未被触发,导致依赖该函数的逻辑(如数据刷新、状态重置等)无法正常执行。二、问题原因 当将 Tab 管理的页面改造为组件形式时,由于组件本身不具备 onPageShow 这类页面级生命周期函数,原依赖该生命周期实现的逻辑(如页面显示时的数据刷新、状态同步等)会失去触发载体,导致相关功能失效。三、解决思路 当无法触发 onPageShow 生命周期时,我们可以通过设计一个全局或局部的监听对象来实现页面刷新机制:可以创建一个可被监听的状态对象,在页面需要感知显示状态的位置监听该对象的变化。当从子页面返回 Tab 组件页面时,主动修改这个监听对象的状态(例如更新一个时间戳或状态标记)。此时,Tab 组件页面由于监听了该对象,会感知到变化并执行相应的刷新逻辑(如重新请求数据、更新视图等)。这种方式通过状态驱动的思路,绕过了对页面级生命周期的依赖,利用状态变化触发更新,从而实现类似 onPageShow 的效果。四、解决方案 在全局或相关页面共享的作用域中,定义一个可被监听的状态参数(如refreshFlag)。当需要从子页面返回 Tab 页面并触发刷新时,先在子页面中主动修改该参数的值(例如递增序号、切换布尔值状态等),随后再执行跳转回 Tab 页面的操作。 由于 Tab 页面预先通过监听器(如 Vue 的 watch、React 的 useEffect 依赖监听等)监测着refreshFlag的变化,当参数值被修改后,Tab 页面会立刻感知到这一变化,进而自动执行数据重新获取、视图更新等刷新逻辑。 这种方式通过 "状态修改先行,跳转操作随后" 的顺序设计,利用参数变化作为信号,成功替代了 onPageShow 的触发时机,确保 Tab 页面在返回时能可靠执行刷新操作。 @Watch('changeReload') @StorageLink('changeData') data: 类型= 值 changeReload(){需要执行的方法}
-
一,问题说明鸿蒙接入shareSDK后先授权,然后调用分享功能不能拉起微信或qq二,原因分析添加依赖在Terminal窗口中,执行如下命令进行安装ohpm install @zztsdk/zztcoreohpm install @zztsdk/sharesdkohpm install @yyz116/jsbn 权限配置ShareSDK需要 INTERNET权限才可正常使用,请在工程中entry模块的 module.json5文件中,新增 requestPermissions,如下所示:"module": { "name": "xxx", "type": "entry", "description": "xxx", "mainElement": "xxx", "deviceTypes": [], "pages": "xxx", "abilities": [], // 配置如下 "requestPermissions":[ { "name": "ohos.permission.INTERNET" } ]} 使用ShareSDK前,需调用以下代码初始化ShareSDK,可以放在EntryAbility文件中调用ZztSDK.init(context, "您的AppKey", "您的AppSecret") 通过询问客服得知,使用shareSDK的分享功能是不需要授权的,这俩个功能不能写到一起。如下是错误示范,如下图这样把授权功能和分享功能写在一起,这样会拉起第三方软件实现授权,而后续的分享功能不会重新拉起第三方软件,这样分享功能连反馈都不会有。let plat = await mobShare.ShareSDK.getInstance().getPlatformAsync(mobShare.Platform.HUAWEI)let records: Array<mobShare.SharedParam> = new Array()let receive: mobShare.PlatformActionListener = { onComplete: (platform: mobShare.IPlatform, action: number, res: Map<string, Object>) => { //成功回调 }, onError: (platform: mobShare.IPlatform, action: number, error: Error) => { //异常回调 }, onCancel: (platform: mobShare.IPlatform, action: number) => { //取消回调 }}plat.setPlatformActionListener(receive)plat.authorize(params)records.push({utd: mobShare.ShareType.TEXT,content: "测试分享文本",})let plat =await mobShare.ShareSDK.getInstance().getPlatformAsync(mobShare.Platform.SYSTEM)plat.setPlatformActionListener(receive)plat.share(records, getContext(), { previewMode: mobShare.SharePreviewMode.DEFAULT, selectionMode: mobShare.SelectionMode.SINGLE}) 三、解决思路隐私授权登录和分享功能是俩个功能,授权是为了第三方登录,而分享信息到第三方软件是不需要授权的,如果把授权和分享写在一起,这样会拉起第三方软件实现授权,而后续的分享功能不会重新拉起第三方软件,这样分享功能连反馈都不会有。所以要使用哪个功能直接单独调用就行。四、解决方法正常来说shareSDK的分享功能和授权功能是互不影响的,如分享单独调用以下接口就行let records: Array<mobShare.SharedParam> = new Array()records.push({utd: mobShare.ShareType.TEXT,content: "测试分享文本",})let receive: mobShare.PlatformActionListener = { onComplete: (platform: mobShare.IPlatform, action: number, res: Map<string, Object>) => { //成功回调 }, onError: (platform: mobShare.IPlatform, action: number, error: Error) => { //异常回调 }, onCancel: (platform: mobShare.IPlatform, action: number) => { //取消回调 }}let plat =await mobShare.ShareSDK.getInstance().getPlatformAsync(mobShare.Platform.SYSTEM)plat.setPlatformActionListener(receive)plat.share(records, getContext(), { previewMode: mobShare.SharePreviewMode.DEFAULT, selectionMode: mobShare.SelectionMode.SINGLE}) 这个时候如果没有拉起分享功能,onError回调就能监听到具体错误了。同理,调用授权功能也是如此。
-
代码文章:https://developer.huawei.com/consumer/cn/blog/topic/03189702153576069常见的多行跑马灯效果可以随时停留
-
数据库相关文章:https://developer.huawei.com/consumer/cn/blog/topic/03191259102976177对于数据库中的数据类型处理时布尔值,如果直接给表格定义如:db.execDML(‘ALTER TABLE tb_user ADD COLUMN is_student boolean’)@TableField({name:“is_student”,type:FieldType.BOOLEAN})这样定义是没有值的我们需要定义成为db.execDML(‘ALTER TABLE tb_user ADD COLUMN is_student integer’);@TableField({name:“is_student”,type:FieldType.NUMBER})使用和取值都不会影响,因为ORM 框架会自动转换
上滑加载中
推荐直播
-
华为云码道 × 仓颉编程:工程化AI编码探索2026/05/27 周三 19:00-21:00
刘俊杰-华为云仓颉语言专家/李炎-华为云码道技术专家/王智鹏-OpenCangjie开源社区发起人
本场直播围绕华为云仓颉语言与华为云码道的深度结合,展示华为云智能编程从零基础到高效落地的完整生态能力。以华为云码道为引擎,仓颉语言为载体,带给大家日常提效、趣味创新到极速量产的开发体验。
回顾中
热门标签