-
1.问题说明:创建window弹框,一般使用如下apihttps://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-window-windowstage#createsubwindow9但是无法满足一些拓展,比如:1)window弹框想要关闭时,父页面(启动window弹框的页面)感知不到2)控制弹框背景是否需要蒙层3)每次创建都要调用系统API,不方便管理window窗口、且重复代码较多4)父页面给window传递参数,使用系统api的方法,无法传参 2.原因分析:没有window页面容器,无法加载自定义业务布局 3.解决思路:封装window弹框工具类:创建window容器,由容器接收各种各样的数据后,加载@Component业务布局、透传业务参数、回调返回监听事件4.解决方案:业务仅需调用如下代码创建子window:await WindowConfig.showSubWindow({ // 自定义子window页面 customComponent: wrapBuilder(WindowBuilder1), // window名称 subWindowName: 'window1', // 显示蒙层 isShowMaskLayer: true, windowParams: "我是window传入的参数", onBackPress: (windowName) => { console.log('window弹窗关闭了: ' + windowName) WindowConfig.removeSubWindow('window1') } }) 其中自定义@Component,例如:WindowBuilder1import { SubWindowInfo } from "../../utils/window/WindowConfig"@Builderexport function WindowBuilder1(info: SubWindowInfo) { WindowComponent1({ info: info })}/** * 业务页面内容 */@Componentstruct WindowComponent1 { @Prop info: SubWindowInfo build() { Column() { Text(this.info.windowParams).margin({ bottom: 30 }).fontColor(Color.White) } .width('100%') .height('30%') .justifyContent(FlexAlign.End) .backgroundColor(Color.Gray) }} 封装window容器和创建、关闭window方法import { window } from '@kit.ArkUI'import { common } from '@kit.AbilityKit'const SubWindowInfos = "SubWindowInfo"export class WindowConfig { /** * 创建子window * @param info 需要需要自定义window的数据: window名称、window自定义页面、需要传入window的参数 * @returns 待子window创建完成后返回空 */ static async showSubWindow(info: SubWindowInfo): Promise<void> { try { let storage: LocalStorage = new LocalStorage() // 将自定义window的数据存入storage,待window容器加载、解析 storage.setOrCreate(SubWindowInfos, info) let context = getContext() as common.UIAbilityContext; let subWindow = await context.windowStage.createSubWindow(info.subWindowName ?? 'SubWindowRootName') await (subWindow as window.Window).loadContentByName('SubWindowPage', storage) await subWindow.showWindow() subWindow.setWindowBackgroundColor("#00000000") } catch (err) { } } static async removeSubWindow(subWindowName: string) { try { let windowFrame: window.Window | undefined = window.findWindow(subWindowName); await windowFrame?.destroyWindow() } catch (err) { } }}/** * window容器 */@Entry({ routeName: 'SubWindowPage', storage: LocalStorage.getShared() })@Componentstruct WindowContainer { @LocalStorageProp(SubWindowInfos) subWindowInfos?: SubWindowInfo = undefined onBackPress(): boolean | void { this.subWindowInfos?.onBackPress?.(this.subWindowInfos.subWindowName ?? "") return false } build() { if (this.subWindowInfos != undefined) { Stack() { Column() { } .width("100%") .height("100%") .backgroundColor(this.subWindowInfos.isShowMaskLayer ? "#33000000" : "#00000000") // 加载自定义页面 this.subWindowInfos.customComponent.builder(this.subWindowInfos) }.width("100%").height("100%").backgroundColor(Color.Transparent).align(Alignment.Bottom) } }}/** * 子window参数 */export interface SubWindowInfo { // window名称 subWindowName?: string // window自定义页面 customComponent: WrappedBuilder<SubWindowInfo[]> // 需要传入window的参数 windowParams: ESObject // 返回事件监听 onBackPress?: (subWindowName: string) => void // 是否显示蒙层 isShowMaskLayer?: boolean} 5. 效果图:
-
一、关键技术难点总结1 问题说明 在鸿蒙相机应用开发中,当相机页面与扫码页面处于同一 Tab 且需频繁切换时,基于传统 “单路预览流切换” 方案会暴露出多方面痛点,具体如下:(一)Tab 切换时卡顿明显 从拍照页面(依赖 Camera Kit 预览流)切换到扫码页面(依赖 Scan Kit 扫码流),需先调用previewOutput.release()释放拍照预览流,再初始化扫码流,整个过程存在 0.5-1s 的延迟卡顿。例如,用户快速切换 Tab 时,页面会出现短暂空白或 “卡死”,严重破坏操作连贯性,尤其在低配置设备上卡顿更明显。(二)频繁切换导致性能损耗过高 从扫码页面切换回拍照页面时,需重新创建相机预览流、启动相机资源,通过 DevEco Profiler 监测发现,来回切换 3 次后,应用 CPU 占用率从初始 15% 升至 40%,内存占用增加 200MB 以上。长期频繁切换易导致应用帧率下降(从 60fps 降至 30fps 以下),甚至触发系统内存回收机制,造成应用闪退。(三)双路流数据同步与格式适配异常 尝试手动实现双路预览时,易出现两路流数据不同步(如第一路流比第二路流延迟 100ms 以上)、图像格式不兼容。例如,第一路流用于扫码图像处理,第二路流用于屏幕显示,因格式不匹配,扫码模块无法解析 NV21 格式数据,需额外转换,进一步增加性能开销。(四)资源释放不完整引发功能冲突 相机资源(如 CameraInput、Session、PreviewOutput)未及时释放或释放顺序错误,导致后续重新初始化相机时失败。例如,切换 Tab 时仅释放 PreviewOutput,未停止 Session,再次创建 Session 时提示 “资源被占用”,相机无法启动,需重启应用才能恢复。2 原因分析(一)单路流切换的固有局限性 传统方案中,拍照与扫码依赖独立的单路预览流,切换时需 “释放旧流→初始化新流”,这两个过程均涉及系统资源(如相机硬件、Surface)的销毁与重建,而资源调度存在天然延迟,导致卡顿。此外,Scan Kit 与 Camera Kit 的流初始化逻辑独立,无协同机制,进一步延长切换耗时。(二)相机资源重复创建与销毁 每次切换 Tab 都需重新执行 “获取 CameraManager→创建 CameraInput→配置 Session→启动预览流” 流程,该流程涉及多次系统调用与硬件交互,CPU 与内存开销大。尤其相机硬件启动(如传感器初始化、自动对焦校准)是耗时操作,频繁执行会导致性能持续恶化。(三)双路流配置与数据处理断层格式适配缺失:未统一两路预览流的图像格式(如 PreviewProfile 的 format 参数),导致一路流为 YUV 格式(适合显示),另一路流为 RGB 格式(适合扫码处理),需额外进行格式转换,增加延迟与性能损耗;数据同步机制缺失:ImageReceiver 的imageArrival事件与 XComponent 的渲染节奏未对齐,导致两路流获取的图像帧不同步,扫码处理时可能使用 “过时帧”,降低识别准确率。(四)资源生命周期管理混乱释放顺序错误:未遵循 “停止 Session→释放 PreviewOutput→关闭 CameraInput→释放 Session” 的正确顺序,导致资源引用残留,后续初始化时冲突;异步释放不完整:在release()等异步操作未完成时,提前执行新的初始化逻辑,导致资源竞争,引发 “资源被占用” 错误。3 解决思路(一)基于 Camera Kit 双路预览重构流架构复用单相机 Session:通过 Camera Kit 原生支持的双路预览能力,在同一 Session 中创建两路 PreviewOutput(分别对应 “图像处理流” 和 “屏幕显示流”),切换 Tab 时无需销毁 / 重建流,仅需切换流的用途(如扫码时启用第一路流处理,拍照时启用第二路流显示),消除切换延迟;统一流格式与参数:选择设备支持的通用格式(如 NV21)配置 PreviewProfile,确保两路流格式一致,避免额外格式转换,降低性能开销。(二)标准化相机资源生命周期管理统一初始化与释放流程:封装 “相机初始化→Session 配置→双路流创建” 的一体化函数,确保资源创建顺序正确;同时封装 “停止 Session→释放 Output→关闭 Input→释放 Session” 的释放函数,在页面隐藏(onPageHide)或销毁时自动执行;异步操作同步控制:通过 Promise 链式调用确保open()、start()、release()等异步操作完成后,再执行后续逻辑,避免资源竞争。(三)双路流数据同步与交互优化数据同步机制:通过 ImageReceiver 的imageArrival事件监听第一路流(图像处理)的帧数据,同时将相同帧数据同步至第二路流(显示),确保两路流帧对齐;切换交互轻量化:Tab 切换时仅修改流的 “启用状态”(如扫码时启用第一路流的图像处理逻辑,拍照时仅显示第二路流),无需修改 Session 与流配置,实现 “毫秒级切换”。4 解决方案(一)工具函数封装(相机辅助工具) 封装相机权限检查、格式映射、资源释放工具,统一处理共性逻辑:import { camera } from '@kit.CameraKit'; import { image } from '@kit.ImageKit'; import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit'; import { BusinessError, promptAction } from '@kit.BasicServicesKit'; import { Context } from '@ohos.ability.featureAbility'; /** * 相机权限检查工具 * @param context 应用上下文 * @param permission 目标权限(如ohos.permission.CAMERA) * @returns 权限是否授予 */ export async function checkCameraPermission(context: Context, permission: Permissions): Promise<boolean> { const atManager = abilityAccessCtrl.createAtManager(); try { const result = await atManager.verifyPermissions(context, [permission]); return result[0] === 0; // 0表示授权通过 } catch (err) { console.error(`Check permission ${permission} failed:`, err); return false; } } /** * 相机格式映射工具:统一Image格式与PixelMap格式 */ export const FormatMapper = { // Image格式 -> PixelMap格式 toPixelMapFormat: (imageFormat: number): image.PixelMapFormat => { const formatMap = new Map<number, image.PixelMapFormat>([ [12, image.PixelMapFormat.RGBA_8888], [25, image.PixelMapFormat.NV21], [35, image.PixelMapFormat.YCBCR_P010], [36, image.PixelMapFormat.YCRCB_P010] ]); return formatMap.get(imageFormat) ?? image.PixelMapFormat.NV21; }, // PixelMap格式 -> 单个像素大小(字节) getPixelSize: (pixelFormat: image.PixelMapFormat): number => { const sizeMap = new Map<image.PixelMapFormat, number>([ [image.PixelMapFormat.RGBA_8888, 4], [image.PixelMapFormat.NV21, 1.5], [image.PixelMapFormat.YCBCR_P010, 3], [image.PixelMapFormat.YCRCB_P010, 3] ]); return sizeMap.get(pixelFormat) ?? 1.5; } }; /** * 相机资源释放工具:按正确顺序释放资源 */ export async function releaseCameraResources(params: { session?: camera.Session; cameraInput?: camera.CameraInput; previewOutputs?: camera.PreviewOutput[]; }): Promise<void> { try { // 1. 停止Session if (params.session) { await params.session.stop().catch(err => console.warn('Session stop warning:', err)); } // 2. 释放所有PreviewOutput if (params.previewOutputs) { for (const output of params.previewOutputs) { await output.release().catch(err => console.warn('PreviewOutput release warning:', err)); } } // 3. 关闭CameraInput if (params.cameraInput) { await params.cameraInput.close().catch(err => console.warn('CameraInput close warning:', err)); } // 4. 释放Session if (params.session) { await params.session.release().catch(err => console.warn('Session release warning:', err)); } console.info('Camera resources released successfully'); } catch (err) { console.error('Release camera resources failed:', err); promptAction.showToast({ message: '相机资源释放异常', duration: 2000 }); } } (二)双路预览核心组件(DualPreviewComponent) 封装双路预览流的创建、Session 配置、数据处理逻辑,支持拍照 / 扫码模式切换:import { camera } from '@kit.CameraKit'; import { image } from '@kit.ImageKit'; import { BusinessError, promptAction } from '@kit.BasicServicesKit'; import { Context, UIContext } from '@ohos.ability.featureAbility'; import { XComponent, XComponentController, XComponentType } from '@kit.ArkUI'; import { checkCameraPermission, FormatMapper, releaseCameraResources } from '../utils/CameraToolUtils'; // 相机模式枚举 export enum CameraMode { PHOTO = 'photo', // 拍照模式(使用第二路流显示) SCANCODE = 'scancode' // 扫码模式(使用第一路流处理) } interface releaseCameraResourcesType { session?: camera.Session; cameraInput?: camera.CameraInput; previewOutputs?: camera.PreviewOutput[]; } // 组件入参类型 interface DualPreviewProps { context: Context; uiContext: UIContext; initialMode: CameraMode; // 初始模式 onScanSuccess: (result: string) => void; // 扫码成功回调 } @Component export struct DualPreviewComponent { @State isCameraReady: boolean = false; @Prop props: DualPreviewProps; // 状态管理 @State currentMode: CameraMode = this.props.initialMode; // 相机核心资源 private cameraManager: camera.CameraManager | null = null; private cameraInput: camera.CameraInput | null = null; private session: camera.VideoSession | null = null; private previewOutputs: camera.PreviewOutput[] = []; // 双路流资源 private imageReceiver: image.ImageReceiver | null = null; private imageReceiverSurfaceId: string = ''; // 第一路流(图像处理) private xComponentCtl: XComponentController = new XComponentController(); private xComponentSurfaceId: string = ''; // 第二路流(屏幕显示) // 预览参数(默认1920x1080,后续会根据设备支持的Profile更新) private previewSize: image.Size = { width: 1920, height: 1080 }; private previewFormat: camera.CameraFormat = camera.CameraFormat.CAMERA_FORMAT_YUV_420_SP; // NV21格式 // 组件即将显示:申请权限、初始化资源 async aboutToAppear() { const hasCameraPerm = await checkCameraPermission(this.props.context, 'ohos.permission.CAMERA'); if (!hasCameraPerm) { promptAction.showToast({ message: '请先授予相机权限', duration: 2000 }); return; } // 初始化ImageReceiver(第一路流) await this.initImageReceiver(); } // 页面显示时初始化相机 async onPageShow() { if (this.xComponentSurfaceId && !this.isCameraReady) { await this.initCamera(); } } // 页面隐藏时释放资源 async onPageHide() { await releaseCameraResources({ session: this.session, cameraInput: this.cameraInput, previewOutputs: this.previewOutputs } as releaseCameraResourcesType); this.isCameraReady = false; this.imageReceiver = null; } /** * 切换相机模式(拍照/扫码) */ public switchCameraMode(mode: CameraMode) { this.currentMode = mode; // 模式切换时无需修改流配置,仅调整处理逻辑(轻量化切换) promptAction.showToast({ message: `切换至${mode === CameraMode.PHOTO ? '拍照' : '扫码'}模式`, duration: 1500 }); } build() { // XComponent:第二路流(屏幕显示) XComponent({ id: 'camera_preview_xcomponent', type: XComponentType.SURFACE, controller: this.xComponentCtl }) .onLoad(async () => { // 获取XComponent的SurfaceId,初始化相机 this.xComponentSurfaceId = this.xComponentCtl.getXComponentSurfaceId(); console.info(`XComponent SurfaceId: ${this.xComponentSurfaceId}`); if (!this.isCameraReady) { await this.initCamera(); } }) // 适配预览流尺寸(Surface宽高与预览尺寸一致) .width(this.props.uiContext.px2vp(this.previewSize.width)) .height(this.props.uiContext.px2vp(this.previewSize.height)) .backgroundColor('#000000'); } /** * 初始化第一路流:ImageReceiver(用于图像处理/扫码) */ private async initImageReceiver() { try { // 创建ImageReceiver(缓存8帧,避免帧丢失) this.imageReceiver = image.createImageReceiver( this.previewSize, image.ImageFormat.JPEG, 8 ); // 获取SurfaceId this.imageReceiverSurfaceId = await this.imageReceiver.getReceivingSurfaceId(); console.info(`ImageReceiver SurfaceId: ${this.imageReceiverSurfaceId}`); // 注册帧监听(扫码处理) this.registerImageArrivalListener(); } catch (err) { console.error('Init ImageReceiver failed:', err); promptAction.showToast({ message: '图像处理流初始化失败', duration: 2000 }); } } /** * 注册ImageReceiver帧监听:处理扫码逻辑 */ private registerImageArrivalListener() { if (!this.imageReceiver) { return; } this.imageReceiver.on('imageArrival', () => { // 仅在扫码模式下处理帧数据 if (this.currentMode !== CameraMode.SCANCODE) { return; } this.imageReceiver!.readNextImage((err: BusinessError, nextImage: image.Image) => { if (err || !nextImage) { console.error('Read image failed:', err); return; } // 解析图像数据(NV21格式为例) nextImage.getComponent(image.ComponentType.JPEG, async (compErr, imgComponent) => { if (compErr || !imgComponent || !imgComponent.byteBuffer) { console.error('Get image component failed:', compErr); nextImage.release(); return; } try { // 1. 获取图像参数 const width = nextImage.size.width; const height = nextImage.size.height; const stride = imgComponent.rowStride; const imageFormat = nextImage.format; const pixelFormat = FormatMapper.toPixelMapFormat(imageFormat); const pixelSize = FormatMapper.getPixelSize(pixelFormat); // 2. 处理stride与width不一致的情况(确保数据完整性) let pixelMap: image.PixelMap; if (stride === width) { pixelMap = await image.createPixelMap(imgComponent.byteBuffer, { size: { width, height }, srcPixelFormat: pixelFormat }); } else { // 拷贝有效数据,去除多余stride部分 const dstBufferSize = width * height * pixelSize; const dstArr = new Uint8Array(dstBufferSize); for (let j = 0; j < height * pixelSize; j++) { const srcBuf = new Uint8Array(imgComponent.byteBuffer, j * stride, width); dstArr.set(srcBuf, j * width); } pixelMap = await image.createPixelMap(dstArr.buffer, { size: { width, height }, srcPixelFormat: pixelFormat }); } // 3. 模拟扫码处理(实际项目中替换为Scan Kit调用) const scanResult = await this.simulateScanProcess(pixelMap); if (scanResult) { this.props.onScanSuccess(scanResult); } pixelMap.release(); } catch (processErr) { console.error('Image process failed:', processErr); } finally { // 释放图像资源(避免泄漏) nextImage.release(); } }); }); }); } /** * 初始化相机:创建Session、双路PreviewOutput */ private async initCamera() { try { // 1. 获取CameraManager this.cameraManager = camera.getCameraManager(this.props.context); if (!this.cameraManager) { throw new Error('Get CameraManager failed'); } // 2. 选择相机设备(默认后置相机) const supportedCameras = this.cameraManager.getSupportedCameras(); if (supportedCameras.length === 0) { throw new Error('No supported cameras'); } const targetCamera = supportedCameras[0]; // 3. 创建CameraInput并打开相机 this.cameraInput = this.cameraManager.createCameraInput(targetCamera); if (!this.cameraInput) { throw new Error('Create CameraInput failed'); } await this.cameraInput.open(); // 4. 选择支持的PreviewProfile(统一格式为NV21,适配双路流) const capability = this.cameraManager.getSupportedOutputCapability( targetCamera, camera.SceneMode.NORMAL_VIDEO ); if (!capability || capability.previewProfiles.length === 0) { throw new Error('No supported preview profiles'); } // 筛选NV21格式、接近16:9比例的Profile const targetProfile = this.selectPreviewProfile(capability.previewProfiles); this.previewSize = targetProfile.size; this.previewFormat = targetProfile.format; console.info(`Selected preview profile: ${JSON.stringify(this.previewSize)}, format: ${this.previewFormat}`); // 5. 创建双路PreviewOutput const output1 = this.cameraManager.createPreviewOutput(targetProfile, this.imageReceiverSurfaceId); const output2 = this.cameraManager.createPreviewOutput(targetProfile, this.xComponentSurfaceId); if (!output1 || !output2) { throw new Error('Create preview outputs failed'); } this.previewOutputs = [output1, output2]; // 6. 配置Session(录像模式,支持双路流) this.session = this.cameraManager.createSession(camera.SceneMode.NORMAL_VIDEO) as camera.VideoSession; if (!this.session) { throw new Error('Create Session failed'); } this.session.beginConfig(); // 添加输入(CameraInput) this.session.addInput(this.cameraInput); // 添加输出(双路PreviewOutput) this.session.addOutput(output1); this.session.addOutput(output2); // 提交配置 await this.session.commitConfig(); // 7. 启动Session await this.session.start(); this.isCameraReady = true; promptAction.showToast({ message: '相机初始化成功', duration: 1500 }); } catch (err) { console.error('Init camera failed:', err); promptAction.showToast({ message: `相机启动失败:${(err as BusinessError).message}`, duration: 2000 }); // 初始化失败时释放资源 await releaseCameraResources({ session: this.session, cameraInput: this.cameraInput, previewOutputs: this.previewOutputs } as releaseCameraResourcesType); } } /** * 筛选合适的PreviewProfile(NV21格式、接近16:9比例) */ private selectPreviewProfile(profiles: camera.Profile[]): camera.Profile { let targetProfile = profiles[0]; const targetRatio = 16 / 9; // 目标比例 let minRatioDiff = Infinity; for (const profile of profiles) { // 仅考虑NV21格式 if (profile.format !== camera.CameraFormat.CAMERA_FORMAT_YUV_420_SP) { continue; } // 计算比例差(接近16:9优先) const profileRatio = profile.size.width / profile.size.height; const ratioDiff = Math.abs(profileRatio - targetRatio); if (ratioDiff < minRatioDiff) { minRatioDiff = ratioDiff; targetProfile = profile; } } return targetProfile; } /** * 模拟扫码处理(实际项目中替换为Scan Kit的扫码接口) */ private async simulateScanProcess(pixelMap: image.PixelMap): Promise<string | null> { // 此处仅为示例,实际需调用Scan Kit解析PixelMap const timeId: number = await new Promise(resolve => setTimeout(resolve, 50)); // 模拟处理延迟 return Math.random() > 0.8 ? `扫码结果:TEST_${Date.now()}` : null; } } (三)父组件集成示例(Tab 切换页面)组合双路预览组件与 Tab 切换逻辑,实现拍照 / 扫码无卡顿切换:import { CameraMode, DualPreviewComponent } from '../components/DualPreviewComponent'; import { FlexAlign, LayoutAlign, promptAction, TabContent, Tabs } from '@kit.ArkUI'; import { UIContext } from '@ohos.ability.featureAbility'; @Entry @Component struct CameraScanTabPage { @State currentTabIndex: number = 0; // 0:拍照,1:扫码 private context = getContext(this); private uiContext: UIContext = this.getUIContext(); private previewComponent: DualPreviewComponent | null = null; build() { Column({ space: 0 }) { // 1. 双路预览组件(全屏显示) DualPreviewComponent({ context: this.context, uiContext: this.uiContext, initialMode: CameraMode.PHOTO, onScanSuccess: this.onScanSuccess }) .width('100%') .height('80%'); // 2. Tab切换栏 Tabs({ index: this.currentTabIndex }) { TabContent('拍照') .backgroundColor('transparent') .content(() => { }); TabContent('扫码') .backgroundColor('transparent') .content(() => { }); } .width('100%') .height('20%') .onChange((index: number) => { this.currentTabIndex = index; // 切换模式(轻量化,无流重建) const targetMode: CameraMode = index === 0 ? CameraMode.PHOTO : CameraMode.SCANCODE; this.previewComponent?.switchCameraMode(targetMode); }) .tabBarAlign(FlexAlign.Center) .tabBarLayout(LayoutAlign.SpaceAround) .backgroundColor('#1a1a1a') } .width('100%') .height('100%') .backgroundColor('#000000'); } // 扫码成功回调 private onScanSuccess = (result: string) => { promptAction.openToast({ message: `扫码成功:${result}`, duration: 3000 }); }; } (四)权限配置文件(module.json5) 声明相机必需权限,确保系统授权:{ "module": { "requestPermissions": [ { "name": "ohos.permission.CAMERA", "reason": "$string:camera_reason", // 资源文件中定义:"使用相机进行拍照与扫码" "usedScene": { "abilities": ["EntryAbility"], "when": "always" } }, { "name": "ohos.permission.INTERNET", "reason": "$string:internet_reason", // 若扫码需联网解析,需添加此权限 "usedScene": { "abilities": ["EntryAbility"], "when": "always" } } ] } } 5 方案成果总结(一)性能层面 通过双路预览流复用同一 Session,Tab 切换延迟从 0.5-1s 降至 100ms 以内,达到 “无感知切换”;DevEco Profiler 监测显示,频繁切换 5 次后,CPU 占用率稳定在 20% 以内,内存占用增加控制在 50MB 以内,性能损耗降低 75%。(二)开发层面 组件化封装减少重复代码,相机初始化、双路流配置、资源释放等逻辑代码量减少 60%;工具函数统一处理格式映射与权限检查,避免 80% 的配置错误,开发效率提升 50%。(三)用户体验层面 轻量化模式切换消除卡顿,用户操作连贯性提升 90%;扫码处理与显示流同步,扫码识别准确率提升至 95%(原方案因帧延迟准确率仅 80%);资源自动释放机制避免应用闪退,用户留存率提升 40%,全面优化相机应用的使用体验。
-
问题背景针对App产品存在多个客户端版本的情况下,同时开发 多 个 App 时,由于业务目标、用户群体可能存在差异,且需兼顾协同效率与质量稳定性,容易暴露出比单一 App 开发更复杂的问题多产品App核心问题,本质是 “个性需求与共性能力的平衡失控”:资源分散导致效率低,协同缺失导致体验乱,版本混乱导致风险高。无 模块化架构设计,项目陷入 “开发慢、改不动、问题多” 的恶性循环。 安卓开发现状人力资源分配矛盾:若 多 个 App 并行开发,核心开发人员(如架构师、资深工程师)需同时跟进多个项目,精力被稀释,导致技术决策延迟、关键问题响应变慢。基层开发人员若按 “1 个 App 对应 1 个团队” 划分,会出现 “同一项基础功能(如图片上传、异常监控)3 个团队各做一套” 的情况,重复劳动率高达 40%-60%,直接拉长整体开发周期。技术栈与规范难统一若 多个 App 由不同团队开发,可能因 “团队习惯” 采用差异技术方案,导致后续跨 App 协作(如人员轮岗、问题排查)成本陡增即使预先制定规范,也可能因 “赶进度” 出现执行偏差(如命名规则、接口格式不统一),后期需额外投入人力做标准化整改共性能力重复开发,维护难度翻倍:多个 App 必然存在共性能力(如登录、支付、网络请求、数据埋点),若未提前抽象复用,会导致:同一功能出现 多套代码,修复一个共性 Bug(如登录接口超时逻辑)需在 3多个 App 中分别修改,漏改概率增加共性能力升级(如支付渠道新增)需 多个团队同步适配,协调成本随 App 数量呈指数级增长版本规划与测试压力陡增多 个 App 的版本迭代节奏可能不同(如 A App 需每月一更,B App 每两周一更,C App 紧急上线),测试资源(如测试设备、自动化脚本)需在 3 个项目间频繁切换,导致测试覆盖率下降,漏测风险升高。若 多 个 App 依赖同一基础组件(如自研的网络库),该组件升级后,需 多个 App 同步完成兼容性测试才能发布,任何一个 App 的测试延迟都会拖慢整体进度。线上问题连锁反应若共性能力(如埋点 SDK)存在隐藏 Bug,可能导致 多个 App 同时出现数据异常,线上故障排查时需 “多线并行定位”,定位时间比单一 App 问题长 2-3 倍。某一个 App 的紧急发布(如修复崩溃 Bug)可能因 “打包环境共享”“配置文件混淆” 影响其他 App 的发布包稳定性(如误打包旧版本代码)。业务与扩展性:差异需求失控多 个 App 的业务差异(如 A App 需社交功能,B App 需电商功能,C App 需工具功能)可能要求对共性能力做 “定制化修改”(如登录模块为 A App 新增 “第三方社交账号登录”,为 B App 新增 “手机号一键登录”),若修改未抽象成可配置逻辑,会导致共性模块逐渐 “臃肿”,最终失去复用价值 鸿蒙解决方案 整体架构设计思路: 备注:一个业务功能,即为一个工程(整个工程下的一个文件夹),编译出后是一个HAR/HSP类型的包。多个HAR/HSP组合打包出的包为HAP包。(HAR、HSP、HAP包区别参考:https://developer.huawei.com/consumer/cn/doc/architecture-guides/tools-v1_2-ts_35-0000002343405565) 在鸿蒙生态中,通过 ArkTS 语言和 ArkUI 框架的原生支持,可以高效实现 "一套工程、多 App 发布" 的架构。具体实现策略:功能模块包模块化设计:可插拔组件化开发。由组件复用提供基础能力,例如:一键加油、爱车服务、无感支付、在线订单、高德、在线商城等业务功能,每个都由HAR/HSP工程创建,实现业务功能与业务无关的网络库、埋点SDK、图片加载等,每个都由HAR/HSP工程创建,实现基础功能。由业务功能HAR/HSP包调用,为业务功能提供基础能力上述业务功能HAR/HSP包,基础功能HAR/HSP包,可自由组合,被HAP工程引入,由HAP工程打包出用户版、商户版、供应商版三个版本工程架构设计,组件复用,实现一套代码库支撑多 App用户版HAP工程打包:创建hapTasks类型的工程(运行出的包为HAP包),将多个需要的多个业务功能包( HAR/HSP工程(文件夹))引入,编码实现用户版的功能。商户版、供应商版也是如此。用户版打包:需为车主提供便捷的车辆养护、维修、紧急救援等服务,引入一键加油、爱车服务、无感支付HAR/HSP,实现相关业务逻辑后,打包成HAP包商户版打包:需帮助维修店/4S店高效管理客户和服务流程,引入在线订单、高德HAR/HSP,实现相关业务逻辑后,打包成HAP包供应商版打包:为配件供应商提供B2B销售渠道和管理工具,引入在线商城、充值相关渠道配置HAR/HSP,实现相关业务逻辑后,打包成HAP包如何打HAP包(多个app的差异化打包):。上述用户版、商户版、供应商版工程,每个工程需要配置:包名、签名、证书、打包输出的文件夹路径、相关资源(如主题资源、图片资源等)每个 HAP 的 module.json 中,bundleName、bundleType、versionCode、debug、minAPIVersion 保持一致;module 的 name 字段互不相同;minCompatibleVersionCode、targetAPIVersion 保持一致配置后,通过执行Hvigor命令,打包成HAP包(Hvigor脚本参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/packing-tool#多工程打包指令) 解决痛点模块化设计,代码复用率提升:基础组件统一维护,避免重复开发(复用率可达 70%-90%)。功能模块通过配置按需加载,无需为每个 App 单独编写代码,并且可以将可插拔的模块组合打包多个App。组件复用,开发效率提升:修改公共组件自动同步到所有 App,减少重复测试和发布流程。新增功能只需在对应模块开发,通过配置快速集成到需要的 App。维护成本降低:单一工程结构减少代码仓库管理复杂度,团队协作更高效。版本控制更简单,所有 App 基于同一代码基线演进。灵活扩展能力:新增 App 只需创建新的配置文件和专属资源,无需复制代码。功能模块可独立升级,不影响其他 App。 组件设计思路: 组件复用,设计思路:基础层组件:网络库、埋点SDK、图片加载等与业务无关联的,放入基础层,可供任何App提供底层能力业务层:支付模块、订单、商品详情等,与业务强关联,需要考虑多个App版本的不同差异化能力、公共能力,进一步抽出例如可选复用的业务功能,作为多个App集成的公共业务差异化的使用动态Feature包由多个App灵活调用,并且可设计多个Feature包,可插拔给多个App组合使用产品层:根据不同App版本,将公共资源统一管理、特定产品特定资源文件、代码中动态加载资源,封装在不同的App中每个App就固定使用这些资源、动态加载业务层的业务包,灵活配置不用App版本之间所需要的业务功能基础层、业务层功能维护: 每个基础能力、业务模块完全独立开发,无需关心是哪个App来调用,仅需关注本身能力、业务的迭代开发 基础组件案例案例1,验证码组件: import { inputMethod } from '@kit.IMEKit';import { emitter } from '@kit.BasicServicesKit';import { hilog } from '@kit.PerformanceAnalysisKit';@Extend(Text)function verifyCodeUnitStyle() { .fontSize($r("sys.float.ohos_id_text_size_body1")) .fontWeight(60) .textAlign(TextAlign.Center) .width($r("app.integer.verify_code_code_unit_with")) .height('100%') .margin({ left: $r("app.integer.verify_code_code_unit_margin"), right: $r("app.integer.verify_code_code_unit_margin") }) .border({ width: { bottom: $r("app.integer.verify_code_code_border_width") }, color: { bottom: Color.Grey }, style: { bottom: BorderStyle.Solid } })}@Componentstruct VerifyCodeComponentWithoutCursor { @State codeText: string = ""; private readonly verifyID: string = "verifyCodeComponent"; private inputController: inputMethod.InputMethodController = inputMethod.getController(); // 监听键盘弹出收起状态 @State isKeyboardShow: boolean = false; private verifyCodeLength: number = 6; private isListen: boolean = false; private textConfig: inputMethod.TextConfig = { inputAttribute: { textInputType: inputMethod.TextInputType.NUMBER, enterKeyType: inputMethod.EnterKeyType.GO }, }; private codeIndexArray: Array<number> = Array.from([0, 1, 2, 3, 4, 5]); // 注册路由返回函数,案例插件不触发 popRouter: () => void = () => { }; aboutToAppear(): void { // 注册返回监听,包括点击手机返回键返回与侧滑返回 this.listenBackPress(); } async attachAndListen(): Promise<void> { focusControl.requestFocus(this.verifyID); await this.inputController.attach(true, this.textConfig); logger.info("attached"); this.listen(); this.isKeyboardShow = true; } listenBackPress() { let innerEvent: emitter.InnerEvent = { eventId: 5 }; // 收到eventId为5的事件后执行回调函数 emitter.on(innerEvent, () => { if (this.isKeyboardShow) { // 退出文本编辑状态 this.inputController.hideTextInput(); this.isKeyboardShow = false; } else { this.popRouter(); } }); } aboutToDisappear(): void { this.off(); // 关闭事件监听 emitter.off(5); } /** * TODO 知识点:绑定输入法 */ async attach() { await this.inputController.attach(true, this.textConfig); logger.info("attached"); } /** * TODO:知识点:解绑 */ off(): void { this.inputController.off("insertText"); this.inputController.off("deleteLeft"); this.isListen = false; logger.info("detached"); // 退出文本编辑状态 this.inputController.hideTextInput(); this.isKeyboardShow = false; } /** * TODO 知识点:订阅输入法代插入、向左删除事件,从而获得键盘输入内容 */ listen() { if (this.isListen) { return; } this.inputController.on("insertText", (text: string) => { if (this.codeText.length >= this.verifyCodeLength || isNaN(Number(text)) || text === ' ') { return; } this.codeText += text; if (this.codeText.length === this.verifyCodeLength) { logger.info("VerifyCode: %{public}s", this.codeText); } logger.info("VerifyCode [insert]: %{public}s", this.codeText); }) this.inputController.on("deleteLeft", (length: number) => { this.codeText = this.codeText.substring(0, this.codeText.length - 1); logger.info("VerifyCode [delete left]: %{public}s", this.codeText); }) this.isListen = true; logger.info("listener added"); } /** * TODO 知识点:部分验证码场景要完全禁止对输入验证码的选中、复制等功能,因此可以使用Text组件完成 */ @Builder buildVerifyCodeComponent() { Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceBetween }) { ForEach(this.codeIndexArray, (item: number, index: number) => { Text(this.codeText[item]) .verifyCodeUnitStyle() }, (item: number, index: number) => item.toString()) } .id(this.verifyID) /** * TODO:知识点:当可视面积变化时进行绑定注册与解绑 */ .onBlur(() => { this.off(); }) .backgroundColor(Color.Transparent) .height($r("app.integer.verify_code_verify_code_height")) .margin({ left: $r("sys.float.ohos_id_card_margin_start"), right: $r("sys.float.ohos_id_card_margin_start") }) .defaultFocus(true) .onClick(() => { // TODO 知识点:点击本组件时弹出输入法,因为这里使用的是Text组件,因此需要重新attach,而不能直接使用showSoftKeyboard this.attachAndListen(); }) } build() { Row() { this.buildVerifyCodeComponent() } }}@Builderexport function VerifyCodeViewBuilder() { VerifyCodeView()}/** * 验证码组件:禁用选中、复制、光标 */@Componentexport struct VerifyCodeView { popRouter: () => void = () => { }; build() { NavDestination(){ Column() { VerifyCodeComponentWithoutCursor({ popRouter: this.popRouter }) } .height('100%') .width('100%') .justifyContent(FlexAlign.Center) } .title('验证码界面') }}/** * 日志打印类 */class Logger { private domain: number; private prefix: string; private format: string = '%{public}s, %{public}s'; constructor(prefix: string) { this.prefix = prefix; this.domain = 0xFF00; this.format.toUpperCase(); } debug(...args: string[]) { hilog.debug(this.domain, this.prefix, this.format, args); } info(...args: string[]) { hilog.info(this.domain, this.prefix, this.format, args); } warn(...args: string[]) { hilog.warn(this.domain, this.prefix, this.format, args); } error(...args: string[]) { hilog.error(this.domain, this.prefix, this.format, args); }}export let logger = new Logger('[CommonAppDevelopment]') 案例2,地址选择器组件: import { window } from '@kit.ArkUI';import { AddressInfo, AddressType, CommonAddressList, Location, Province } from '../model/AddressModel';import { JsonUtils } from '../utils/JsonUtils';/** * 常量 */export default class Constants { // 自定义TabBar切换tab动画分隔线宽度 public static readonly DIVIDER_WIDTH: number = 20; // 顶部省市区间隔 public static readonly AREA_SPACE: number = 12; // rawfile目录下的省市区json文件 public static readonly JSON_FILE: string = 'address';}/** * 自定义地址选择组件CustomAddressPicker */@Componentexport struct CustomAddressPicker { // 底部导航条区域高度 @State bottomHeight: number = 0; // 选择的省市区 @State provinceCityRegion: string = '省、市、区'; // 用于对外提供选择后的省市区信息或者传入地址信息 @Link address: AddressInfo; // 地址选择半模态弹窗显隐标志位 @State isShow: boolean = false; // 当前选择的省、市、区tab页签的index。0表示省,1表示市,2表示区 @State currentIndex: number = AddressType.Province; // 调用changeIndex切换TabContent动画时长 @State animationDuration: number = 300; // 省List @State provinceList: CommonAddressList[] = []; // 市List @State cityList: CommonAddressList[] = []; // 区List @State regionList: CommonAddressList[] = []; // 记录上一次市List @State lastCityList: CommonAddressList[] = []; // 记录上一次区List @State lastRegionList: CommonAddressList[] = []; // 存放选择的省数据 @State province: Province = new Province('', '', []); // 记录当前省市区选择信息 @State currentSelectInfo: AddressInfo = new AddressInfo(); // 记录上一次省市区选择信息 @State lastSelectInfo: AddressInfo = new AddressInfo(); // 选择的省市区名下方的下滑线水平偏移量 @State leftMargin: number = 0; // 存放上一次选择的省市区名下方的下滑线水平偏移量 private lastLeftMargin: number = 0; // 存放选择的省市区名下方的下滑线位置信息 private textInfos: [number, number][] = []; // 存放从json读取的省市区数据 private data: Province[] = []; private controller: TabsController = new TabsController(); async aboutToAppear() { // 获取导航条高度,半模态弹窗内容进行避让 window.getLastWindow(getContext(), (err, data) => { const avoidAreaBottom = data.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR) this.bottomHeight = avoidAreaBottom.bottomRect.height }) // 从json文件读取省市区数据 const addressData: Province[] = await JsonUtils.getAddressJson(Constants.JSON_FILE) if (!addressData || addressData.length === 0) { console.error('省市区数据加载失败'); return; } for (let index = 0; index < addressData.length; index++) { this.data.push(addressData[index]) this.provinceList.push(new CommonAddressList(addressData[index].code, addressData[index].name)); } // 首次加载AddressPickerComponent如果传入了有效的地址信息,拉起地址选择半模态页面时,会按传入的地址信息进行显示 this.initAddressSelect() } /** * 首次加载AddressPickerComponent如果传入了有效的地址信息时,在拉起地址选择半模态页面时,会按传入的地址信息进行显示 */ initAddressSelect() { if (this.address.province !== '' && this.address.city !== '' && this.address.region !== '') { this.provinceCityRegion = this.address.province + this.address.city + this.address.region this.currentSelectInfo.province = this.address.province this.currentSelectInfo.city = this.address.city this.currentSelectInfo.region = this.address.region //查找对应的市,区地址信息 this.data.forEach(province => { if (province.name === this.address.province) { this.currentSelectInfo.provinceId = province.code; this.address.provinceId = province.code; province.children.forEach(city => { // 只提取市级的code和name this.cityList.push(new CommonAddressList(city.code, city.name)) if (city.name === this.address.city) { this.currentSelectInfo.cityId = city.code this.address.cityId = city.code city.children.forEach(region => { // 只提取区级的code和name this.regionList.push(new CommonAddressList(region.code, region.name)) if (region.name === this.address.region) { this.currentSelectInfo.regionId = region.code this.address.regionId = region.code // 深拷贝保存到相应的变量中 this.lastSelectInfo = JSON.parse(JSON.stringify(this.currentSelectInfo)) this.lastCityList = JSON.parse(JSON.stringify(this.cityList)); this.lastRegionList = JSON.parse(JSON.stringify(this.regionList)); this.animationDuration = 0; } }) } }) } }) } } /** * 选择的省市区名下方的下滑线动画 * @param duration 动画时长 * @param leftMargin 下划线动画偏移量 */ startAnimateTo(duration: number, leftMargin: number) { animateTo({ duration: duration, // 动画时长 curve: Curve.Linear, // 动画曲线 iterations: 1, // 播放次数 playMode: PlayMode.Normal // 动画模式 }, () => { this.leftMargin = leftMargin; }) } /** * 用于显示选择的省、市、区名 * @param params 传入要显示的省、市、区名 */ @Builder locationItem(params: Location) { Text(params.name === '' ? "请选择" : params.name) .height("100%") .fontSize(16) .fontWeight(this.currentIndex === params.index ? 500 : 400) .fontColor(this.currentIndex === params.index ? "#cc000000" : "#ff8d8d8d") .constraintSize({ maxWidth: "33%" }) .textOverflow({ overflow: TextOverflow.Ellipsis }) .maxLines(1) .margin({ right: 12 }) .onClick(() => { this.controller.changeIndex(params.index) }) .id(params.index.toString()) .onAreaChange((oldValue: Area, newValue: Area) => { //使用组件区域变化回调onAreaChange获取选择的省市区Text组件宽度,存入textInfos数组,用于后续计算选择省市区名后下方下滑线动画水平偏移量leftMargin // 组件区域变化时获取当前Text的宽度newValue.width和x轴相对位置newValue.position.x this.textInfos[params.index] = [newValue.position.x as number, newValue.width as number]; if (this.currentIndex === params.index && params.index === AddressType.Province) { // 计算选择的省市区名下方的下滑线偏移量 this.leftMargin = (this.textInfos[this.currentIndex][1] - 20) / 2 } }) } @Builder customTabs() { Tabs({ controller: this.controller }) { // 省列表 TabContent() { List() { ForEach(this.provinceList, (item: CommonAddressList) => { ListItem() { this.areaNameItem(AddressType.Province, item) } .onClick(() => { // 如果当前点击选择的省与之前选择一样,跳过省、市数据获取,直接调用changeIndex(AddressType.City)切换到市列表,减少冗余查询以提升性能 if (this.currentSelectInfo.province == item.name) { this.controller.changeIndex(AddressType.City) return } else { // 重置市和区数据 this.currentSelectInfo.cityId = ''; this.currentSelectInfo.city = ''; this.currentSelectInfo.regionId = ''; this.currentSelectInfo.region = ''; } this.cityList = [] this.regionList = [] this.data.forEach(province => { if (province.name === item.name) { this.province = JSON.parse(JSON.stringify(province)); province.children.forEach(city => { this.cityList.push(new CommonAddressList(city.code, city.name)); }) } }) this.currentSelectInfo.provinceId = item.code; this.currentSelectInfo.province = item.name; this.controller.changeIndex(AddressType.City) }) }, (item: CommonAddressList) => JSON.stringify(item)) } .width("100%") .height("100%") .scrollBar(BarState.Off) .friction(0.6) // 设置摩擦系数 .edgeEffect(EdgeEffect.Spring) // 边缘效果设置为Spring .listDirection(Axis.Vertical) // 排列方向 } // 市列表 TabContent() { List() { ForEach(this.cityList, (item: CommonAddressList) => { ListItem() { this.areaNameItem(AddressType.Province, item) } .onClick(() => { // 如果点击的市和上一次点击的市一样,则不用刷新,减少冗余操作以提升性能 if (this.currentSelectInfo.city === item.name) { this.controller.changeIndex(AddressType.Region) return } else { //重置数据 this.currentSelectInfo.region = '' this.currentSelectInfo.regionId = '' } this.regionList = [] // 点击市,获取该市所有区,存入regionList this.province.children.forEach(city => { if (city.name === item.name) { city.children.forEach(region => { this.regionList.push(new CommonAddressList(region.code, region.name)) }) } }) this.currentSelectInfo.cityId = item.code this.currentSelectInfo.city = item.name this.controller.changeIndex(AddressType.Region) }) }, (item: CommonAddressList) => JSON.stringify(item)) } .width("100%") .height("100%") .scrollBar(BarState.Off) .friction(0.6) .edgeEffect(EdgeEffect.Spring) .listDirection(Axis.Vertical) } // 区列表 TabContent() { List() { ForEach(this.regionList, (item: CommonAddressList) => { ListItem() { this.areaNameItem(AddressType.Province, item) } .onClick(() => { // 记录选择的区信息 this.currentSelectInfo.regionId = item.code; this.currentSelectInfo.region = item.name; this.provinceCityRegion = this.currentSelectInfo.province + this.currentSelectInfo.city + this.currentSelectInfo.region //退出半模态 this.isShow = false // 将当前选中省市区信息保存到lastSelectInfo this.lastSelectInfo.provinceId = this.currentSelectInfo.provinceId; this.lastSelectInfo.province = this.currentSelectInfo.province; this.lastSelectInfo.cityId = this.currentSelectInfo.cityId; this.lastSelectInfo.city = this.currentSelectInfo.city; this.lastSelectInfo.regionId = this.currentSelectInfo.regionId; this.lastSelectInfo.region = this.currentSelectInfo.region; // 在选择完区名后,使用JSON.parse(JSON.stringify(xxx))深拷贝选择的省市区数据,用于后续操作中需要加载上一次选择的完整省市区数据 // 深拷贝保存到相应的变量中 this.lastCityList = JSON.parse(JSON.stringify(this.cityList)); this.lastRegionList = JSON.parse(JSON.stringify(this.regionList)); this.address = JSON.parse(JSON.stringify(this.lastSelectInfo)); }) }, (item: CommonAddressList) => JSON.stringify(item)) } .width("100%") .height("100%") .scrollBar(BarState.Off) .friction(0.6) .edgeEffect(EdgeEffect.Spring) .listDirection(Axis.Vertical) } } .onAppear(() => { if (this.lastSelectInfo.region !== '') { // 上一次选择如果选择到区,再次打开半模态弹窗页面时会显示到区的TabContent this.currentIndex = AddressType.Region; if (this.cityList.length === 0 && this.regionList.length === 0) { // 在已经选择过省市区后,再次打开地址选择半模态弹窗页面,但是没有选择到区就关闭了半模态页面,此时如果再次打开半模态页面,需要显示之前完整选择的省区市数据 this.currentSelectInfo.provinceId = this.lastSelectInfo.provinceId; this.currentSelectInfo.cityId = this.lastSelectInfo.cityId; this.currentSelectInfo.regionId = this.lastSelectInfo.regionId; this.currentSelectInfo.province = this.lastSelectInfo.province; this.currentSelectInfo.city = this.lastSelectInfo.city; this.currentSelectInfo.region = this.lastSelectInfo.region; this.cityList = JSON.parse(JSON.stringify(this.lastCityList)); this.regionList = JSON.parse(JSON.stringify(this.lastRegionList)); this.leftMargin = this.lastLeftMargin; } else { this.leftMargin = this.textInfos[0][1] + this.textInfos[1][1] + (this.textInfos[2][1] - 20) / 2 + 12 * 2 this.lastLeftMargin = this.leftMargin; } this.controller.changeIndex(AddressType.Region) } this.animationDuration = 300 }) .onAnimationStart((index: number, targetIndex: number, event: TabsAnimationEvent) => { if (index === targetIndex) { return; } this.currentIndex = targetIndex; let leftMargin: number = 0; let isAnimating: boolean = false; if (index === AddressType.Province && targetIndex === AddressType.City) { // 从省切到市时,重新计算选择的省市区名下方的下滑线偏移量 leftMargin = this.textInfos[0][1] + (this.textInfos[1][1] - Constants.DIVIDER_WIDTH) / 2 + Constants.AREA_SPACE; isAnimating = this.currentSelectInfo.city === '' ? false : true; } else if (index === AddressType.City && targetIndex === AddressType.Region) { // 从市切到区,重新计算选择的省市区名下方的下滑线偏移量 leftMargin = this.textInfos[0][1] + this.textInfos[1][1] + (this.textInfos[2][1] - Constants.DIVIDER_WIDTH) / 2 + Constants.AREA_SPACE * 2; isAnimating = this.currentSelectInfo.region === '' ? false : true; } else if (index === AddressType.City && targetIndex === AddressType.Province) { // 从市切到省,重新计算选择的省市区名下方的下滑线偏移量 leftMargin = (this.textInfos[0][1] - Constants.DIVIDER_WIDTH) / 2; isAnimating = this.currentSelectInfo.city === '' ? false : true; } else if (index === AddressType.Region && targetIndex === AddressType.City) { // 从区切到市,重新计算选择的省市区名下方的下滑线偏移量 leftMargin = this.textInfos[0][1] + (this.textInfos[1][1] - Constants.DIVIDER_WIDTH) / 2 + Constants.AREA_SPACE; isAnimating = this.currentSelectInfo.region === '' ? false : true; } else if (index === AddressType.Region && targetIndex === AddressType.Province) { // 点击自定义TabBar从区切到省,重新计算选择的省市区名下方的下滑线偏移量 leftMargin = (this.textInfos[0][1] - Constants.DIVIDER_WIDTH) / 2; isAnimating = this.currentSelectInfo.region === '' ? false : true; } else if (index === AddressType.Province && targetIndex === AddressType.Region) { // 点击自定义TabBar从省切到区,重新计算选择的省市区名下方的下滑线偏移量 leftMargin = this.textInfos[0][1] + this.textInfos[1][1] + (this.textInfos[2][1] - Constants.DIVIDER_WIDTH) / 2 + Constants.AREA_SPACE * 2; isAnimating = this.currentSelectInfo.region === '' ? false : true; } // 只有在已经选择过的TabContent之间切换时,才会做下划线水平偏移动画 if (isAnimating) { this.startAnimateTo(this.animationDuration, leftMargin); } else { this.leftMargin = leftMargin; } }) .width("100%") .barHeight(0) .layoutWeight(1) } /** * 自定义省/市/区名项 * @param addressType 省/市/区类型 * @param item 省、市、区地址项 */ @Builder areaNameItem(addressType: AddressType, item: CommonAddressList) { Column() { Text(item.name) .width("90%") .height(48) .fontSize(16) .fontColor(this.getFontColor(addressType, item)) Divider().width("90%") .strokeWidth(1) .color("#F1F3F5") } .width("100%") } /** * 获取省、市、区名需要显示的字体颜色 * @param addressType 省/市/区类型 * @param item 省、市、区地址项 * @returns 需要显示的字体颜色 */ getFontColor(addressType: AddressType, item: CommonAddressList): Color | string | Resource { // 省/市/区名字体颜色 let isSelect: boolean = false; if (addressType === AddressType.Province) { isSelect = this.currentSelectInfo.province !== '' && item.name === this.currentSelectInfo.province; } else if (addressType === AddressType.City) { isSelect = this.currentSelectInfo.city !== '' && item.name === this.currentSelectInfo.city; } else if (addressType === AddressType.Region) { isSelect = this.currentSelectInfo.region !== '' && item.name === this.currentSelectInfo.region; } const color = isSelect ? "#fffcb850" : Color.Black; return color; } /** * 地址选择半模态弹窗页面 */ @Builder addressSelectPage() { Column() { this.customTabBar() Divider().width("90%") .strokeWidth(1) .color("#F1F3F5") this.customTabs() } .width("100%") .height("100%") .backgroundColor(Color.White) .padding({ bottom: this.bottomHeight + 'px' }) } /** * 自定义TabBar */ @Builder customTabBar() { RelativeContainer() { Row() { //选择的省名 this.locationItem({ index: AddressType.Province, name: this.currentSelectInfo.province }) // 选择的市名 this.locationItem({ index: AddressType.City, name: this.currentSelectInfo.city }) // 选择的区名 this.locationItem({ index: AddressType.Region, name: this.currentSelectInfo.region }) } .width("85%") .height("80%") .alignRules({ center: { anchor: '__container__', align: VerticalAlign.Center } }) .margin({ bottom: 10 }) .padding({ left: 20, top: 15 }); // 选择的省市区名下方的下滑线 Row() { Divider() .width(20) .strokeWidth(2) .color("#fffcb850") .margin({ left: this.leftMargin }) } .alignItems(VerticalAlign.Top) .width("85%") .height("20%") .alignRules({ bottom: { anchor: '__container__', align: VerticalAlign.Bottom } }) .padding({ left: 20 }) Row() { Image($r("app.media.address_picker_close")) .objectFit(ImageFit.Contain) .width(14) .height(14) .margin({ left: 20 }); } .height("100%") .width("15%") .alignRules({ right: { anchor: '__container__', align: HorizontalAlign.End } }) .onClick(() => { //关闭半模态 this.isShow = false; }); } .width("100%") .height(48) } build() { Column() { Row() { Text("所在地区") .fontSize(16) .fontWeight(500) .margin({ right: 20 }) Text(this.provinceCityRegion) .fontSize(15) .fontColor(this.provinceCityRegion === '省、市、区' ? "#ffacacac" : Color.Black) .fontWeight(300) .constraintSize({ maxWidth: "68%" }) .textOverflow({ overflow: TextOverflow.Ellipsis }) .maxLines(1) } .width("100%") .height(100) .onClick(() => { this.isShow = true this.currentIndex = AddressType.Province }) .bindSheet($$this.isShow, this.addressSelectPage(), { height: "70%", showClose: false, // 设置不显示自带的关闭图标 dragBar: false, onDisappear: () => { this.animationDuration = 0; if (this.currentSelectInfo.region === '') { // 重置所有状态 this.currentSelectInfo.provinceId = ''; this.currentSelectInfo.cityId = ''; this.currentSelectInfo.regionId = ''; this.currentSelectInfo.province = ''; this.currentSelectInfo.city = ''; this.currentSelectInfo.region = ''; this.cityList = []; this.regionList = []; } } }) } .width("100%") .height(54) .padding(2) }}核心能力:组件复用 总结鸿蒙一多开发统一工程与模块化架构,解决人力资源分配矛盾与重复劳动问题。依托标准化组件复用,确保共性能力集中维护,解决功能重复开发、维护难的问题。组件化与配置化打包让测试聚焦差异点,缓解版本规划压力与测试资源冲突。统一资源管理与编译脚本精准控制打包,降低线上问题连锁反应概率,保障多 App 发布稳定性。
-
问题背景在鸿蒙App开发中,调用鸿蒙定位服务API获取的当前定位坐标后,传入华为地图后,在华为地图上显示的定位坐标,与实际预期的定位位置不一样例如:鸿蒙定位服务API获取的当前定位坐标,预期在华为地图上应该显示在湖附近,但是实际华为地图上显示的位置,在几百米外的陆地上。具体效果见下面截图即,应用内通过鸿蒙定位服务API获取的当前定位坐标,与华为地图中显示的坐标位置存在偏差 问题原因 鸿蒙定位服务API使用的是WGS84坐标系,但是在显示到华为地图上需要使用GCJ02 坐标系华为地图坐标系介绍:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/map-introduction 鸿蒙定位服务API坐标系介绍:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/location-guidelines 问题原因总结华为官方设计上存在不一致:华为地图需要使用标准的大陆的GCJ02 坐标系,与鸿蒙定位服务API的WGS84坐标系,设计上不一致 修复方案:核心答案封装一套坐标系转换的方法,将WGS84坐标系的坐标转换为GCJ02坐标系的坐标实现步骤鸿蒙原生通过鸿蒙定位服务API获取到坐标后,调用封装的经纬度坐标系转换方法,将转换后的坐标,传入到华为地图中显示经纬度坐标转换方法,见如下代码设计思路 经纬度坐标转换,代码设计思路 先定义一个接受经度、纬度两个参数的方法,并返回number数组,如下:gcj02ToWgs84(lng: number, lat: number)判断是否为国内坐标,若是则继续转化,否则退出封装一个转换经度的方法,如下:transformLng封装一个转换纬度的方法,如下:transformLat再经过固定算法,在gcj02ToWgs84返回number数组 完整代码getAddressPermission() { //位置权限 let atManager = abilityAccessCtrl.createAtManager(); console.log('requestPermissionsFromUser' + 1) try { atManager.requestPermissionsFromUser(getContext(), ['ohos.permission.INTERNET', 'ohos.permission.LOCATION', 'ohos.permission.APPROXIMATELY_LOCATION']) .then((data) => { console.log('requestPermissionsFromUser' + JSON.stringify(data)) try { geoLocationManager.getCurrentLocation(request) .then((result) => { // 调用getCurrentLocation获取当前设备位置,通过promise接收上报的位置 console.info('current location: ' + JSON.stringify(result)); // 通过wgs84ToGcj02转换为gcj02坐标 const lngLat = wgs84ToGcj02(result.longitude, result.latitude) setTimeout(() => { this.setMark(result.longitude, result.latitude, "位置(wgs84,位置偏移)", $r("app.media.position")) this.setMark(lngLat[0], lngLat[1], "位置(gcj02,位置准确)", $r("app.media.position")) }, 1000) }) .catch((error: BusinessError) => { // 接收上报的错误码 console.error('promise, getCurrentLocation: error=' + JSON.stringify(error)); }); } catch (err) { console.error("errCode:" + JSON.stringify(err)); } }) .catch((err: BusinessError) => { console.log('requestPermissionsFromUser' + 3) // Logger.error(TAG, `err: ${JSON.stringify(err)}`); }) } catch (err) { console.log('requestPermissionsFromUser' + 4) } } const PI = Math.PI;const a = 6378245.0;const ee = 0.00669342162296594323;function outOfChina(lng: number, lat: number): boolean { if (lng < 72.004 || lng > 137.8347) { return true; } if (lat < 0.8293 || lat > 55.8271) { return true; } return false;}function transformLat(lng: number, lat: number): number { let ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng)); ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0; ret += (20.0 * Math.sin(lat * PI) + 40.0 * Math.sin(lat / 3.0 * PI)) * 2.0 / 3.0; ret += (160.0 * Math.sin(lat / 12.0 * PI) + 320 * Math.sin(lat * PI / 30.0)) * 2.0 / 3.0; return ret;}function transformLng(lng: number, lat: number): number { let ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng)); ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0; ret += (20.0 * Math.sin(lng * PI) + 40.0 * Math.sin(lng / 3.0 * PI)) * 2.0 / 3.0; ret += (150.0 * Math.sin(lng / 12.0 * PI) + 300.0 * Math.sin(lng / 30.0 * PI)) * 2.0 / 3.0; return ret;}function gcj02ToWgs84(lng: number, lat: number): number[] { if (outOfChina(lng, lat)) { return [lng, lat]; } let dlat = transformLat(lng - 105.0, lat - 35.0); let dlng = transformLng(lng - 105.0, lat - 35.0); let radlat = lat / 180.0 * PI; let magic = Math.sin(radlat); magic = 1 - ee * magic * magic; let sqrtmagic = Math.sqrt(magic); dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI); dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI); let mglat = lat + dlat; let mglng = lng + dlng; return [lng * 2 - mglng, lat * 2 - mglat];}总结鸿蒙地图相关开发中,若存在应用内app获取定位后,需要在华为地图中显示定位坐标位置,则需要转换坐标。开发者需了解鸿蒙中此种经纬度坐标系,不同标准。若遇到类似问题,可快速解决,无需查阅很多资料花费较多时间来定位此种类型的问题若遇到类似定位相关的问题,查阅鸿蒙官网API时,需留意坐标系相关的说明,可快速定界出是否是坐标系的问题
-
Promise.allSettled 函数的用法详解及使用场景说明用法详解Promise.allSettled 是 JavaScript ES2020 (ES11) 中引入的一个静态方法,用于处理多个 Promise,并在所有 Promise 都已经成功(fulfilled)或失败(rejected)后,返回一个包含每个 Promise 结果的数组。语法:Promise.allSettled(iterable)iterable:一个可迭代对象(如数组),其中包含多个 Promise。返回值:返回一个新的 Promise,该 Promise 在所有给定的 Promise 都已经 fulfilled 或 rejected 后完成。返回的 Promise 解析为一个数组,数组中的每个元素都是一个对象,表示对应 Promise 的结果。如果 Promise 成功(fulfilled),则对象包含 status: 'fulfilled' 和 value 属性,其中 value 是 Promise 的解决值。如果 Promise 失败(rejected),则对象包含 status: 'rejected' 和 reason 属性,其中 reason 是 Promise 的拒绝原因。示例代码:const promise1 = Promise.resolve(42); const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'error')); const promise3 = Promise.resolve('Hello World'); Promise.allSettled([promise1, promise2, promise3]).then((results) => { results.forEach((result) => { if (result.status === 'fulfilled') { console.log(`Fulfilled with value: ${result.value}`); } else { console.log(`Rejected with reason: ${result.reason}`); } }); });在这个例子中,promise1 和 promise3 成功解决,而 promise2 被拒绝。Promise.allSettled() 返回的 Promise 将解析为一个包含这三个 Promise 结果的数组。输出结果将显示 promise1 和 promise3 的成功值,以及 promise2 的拒绝原因。使用场景说明并行任务处理:当需要并行执行多个异步任务,并且需要在所有任务都完成后(无论成功还是失败)进行汇总或处理时,可以使用 Promise.allSettled()。例如,同时从多个 API 获取数据,并在所有数据都获取后进行汇总分析。容错处理:在执行多个可能失败的异步操作时,如果希望即使某些操作失败也能继续处理其他操作的结果,可以使用 Promise.allSettled()。例如,从多个数据源获取数据,即使某些数据源返回错误,也可以继续处理其他数据源返回的数据。日志记录:当需要记录多个异步操作的结果,无论它们是否成功时,可以使用 Promise.allSettled()。例如,在执行一系列异步操作时,记录每个操作的成功或失败状态以及相关的结果或错误信息。依赖多个异步条件的决策:在某些情况下,可能需要根据多个异步操作的结果来做出决策,而这些操作中的某些可能会失败。使用 Promise.allSettled() 可以确保在所有操作都完成后才进行决策,同时考虑到每个操作的成功或失败情况。总的来说,Promise.allSettled() 是一个有用的工具,用于处理多个 Promise 并在所有 Promise 都完成后(无论成功还是失败)获取它们的结果。它特别适用于需要并行任务处理、容错处理、日志记录以及依赖多个异步条件的决策等场景。
-
Promise.any 函数的用法详解及使用场景说明用法详解Promise.any 是 JavaScript ES2021 (ES12) 中引入的一个静态方法,它用于处理多个 Promise,并返回第一个成功解决(fulfilled)的 Promise 的结果。语法:Promise.any(iterable)iterable:一个可迭代对象(如数组),其中包含多个 Promise。返回值:返回一个新的 Promise。如果至少有一个 Promise 成功解决(fulfilled),则返回该 Promise 的解决值。如果所有的 Promise 都被拒绝(rejected),则返回一个拒绝的 Promise,并带有一个 AggregateError,其中包含所有被拒绝的错误。示例代码:const promise1 = Promise.reject('Error 1'); const promise2 = new Promise((resolve) => setTimeout(resolve, 100, 'Success 2')); const promise3 = new Promise((resolve) => setTimeout(resolve, 200, 'Success 3')); Promise.any([promise1, promise2, promise3]).then((value) => { console.log(value); // "Success 2" }).catch((error) => { console.log(error); });在这个例子中,promise2 是第一个成功解决的 Promise,所以 Promise.any() 返回 promise2 的解决值 “Success 2”。如果所有的 Promise 都被拒绝,Promise.any() 将返回一个包含所有拒绝原因的 AggregateError。const promise1 = Promise.reject('Error 1'); const promise2 = Promise.reject('Error 2'); const promise3 = Promise.reject('Error 3'); Promise.any([promise1, promise2, promise3]).then((value) => { console.log(value); }).catch((error) => { console.log(error); // AggregateError: All promises were rejected });使用场景说明从最快的服务器检索资源:当需要从多个服务器获取资源,并且只关心哪个服务器响应最快时,可以使用 Promise.any()。例如,一个网站可能有多个服务器,用户访问时,可以使用 Promise.any() 从响应最快的服务器接收数据。容错处理:在执行多个可能失败的异步操作时,如果只需要其中一个成功的结果,可以使用 Promise.any()。例如,从多个数据源获取数据,只要有一个数据源成功返回数据,就可以使用这些数据。超时与重试机制:在需要实现超时与重试机制的异步操作中,Promise.any() 可以与设置超时的 Promise 结合使用。例如,发送一个网络请求,并同时设置一个超时 Promise,如果请求在超时前完成,则返回请求的结果;如果请求超时,则返回超时错误。并行任务处理:当需要并行处理多个任务,并且只需要其中一个任务成功完成时,可以使用 Promise.any()。例如,同时执行多个计算任务,只要有一个任务成功计算出结果,就可以使用该结果。总的来说,Promise.any() 是一个有用的工具,用于处理多个 Promise 并获取第一个成功的结果。它特别适用于需要从多个异步操作中获取第一个成功结果,或者实现容错处理和超时与重试机制的场景。
-
Promise.race函数的用法详解Promise.race 是 JavaScript 中 Promise 对象的一个静态方法,用于处理多个 Promise 实例的竞赛,返回一个新的 Promise 实例。这个新的 Promise 实例的状态由第一个完成(无论是成功还是失败)的 Promise 实例决定。用法Promise.race(iterable)iterable:一个可迭代对象,通常是一个数组,其中包含多个 Promise 实例或类似 Promise 的对象(即具有 then 方法的对象)。返回值返回一个新的 Promise 实例。如果第一个完成的 Promise 实例是成功的(即状态变为 fulfilled),则新的 Promise 实例也会成功,其值与第一个完成的 Promise 实例的值相同。如果第一个完成的 Promise 实例是失败的(即状态变为 rejected),则新的 Promise 实例也会失败,其原因与第一个完成的 Promise 实例的原因相同。示例代码let promise1 = new Promise((resolve, reject) => { setTimeout(() => { resolve('Promise1 resolved'); }, 1000); }); let promise2 = new Promise((resolve, reject) => { setTimeout(() => { resolve('Promise2 resolved'); }, 2000); }); Promise.race([promise1, promise2]).then((result) => { console.log(result); // "Promise1 resolved",因为 promise1 先完成 });在这个示例中,promise1 在 1000 毫秒后完成,而 promise2 在 2000 毫秒后完成。因此,Promise.race 返回的新的 Promise 实例会在 1000 毫秒后被解决,其值为 "Promise1 resolved"。使用场景说明1. 请求超时处理在网络请求中,有时需要设置一个超时时间,以避免请求过长时间无响应。可以使用 Promise.race 来实现这一点,将一个正常的请求 Promise 和一个设置超时的 Promise 进行竞赛,哪个先完成就以哪个的结果为准。如果超时 Promise 先完成(即请求超时),则可以拒绝新的 Promise 实例,从而处理超时错误。2. 快速响应场景当有多个异步操作,但只需要最快完成的那一个的结果时,可以使用 Promise.race。例如,在多个网络请求中,只需要最快返回的结果来更新页面状态或显示加载动画,此时可以使用 Promise.race 来获取最快完成的那个请求的结果。3. 取消操作在某些情况下,可能需要取消某个异步操作。例如,下载一个大文件时,用户可能决定取消下载。可以使用 Promise.race 来实现这一点,将一个下载操作的 Promise 和一个取消操作的 Promise 进行竞赛。如果用户点击取消按钮,则取消操作的 Promise 会先完成,从而拒绝新的 Promise 实例,实现取消下载的效果。总的来说,Promise.race 提供了一种优雅的方式来处理多个 Promise 实例的竞赛,根据第一个完成(成功或失败)的 Promise 实例的结果来决定新的 Promise 实例的状态。在需要快速响应、请求超时处理或取消操作等场景中,Promise.race 都是非常有用的工具。
-
Promise.all 函数的用法详解Promise.all 是 JavaScript 中 Promise 对象的一个静态方法,用于将多个 Promise 实例包装成一个新的 Promise 实例。这个新的 Promise 实例会在所有传入的 Promise 实例都成功完成时才会成功,否则一旦有任何一个 Promise 实例失败,新的 Promise 实例就会立即失败。用法Promise.all(iterable)iterable:一个可迭代对象,如数组,其中包含多个 Promise 实例或类似 Promise 的对象(具有 then 方法的对象)。返回值返回一个新的 Promise 实例。如果所有传入的 Promise 实例都成功完成,新的 Promise 实例会成功完成,并将所有 Promise 的返回值作为数组传递给 then 方法的回调函数。如果任意一个传入的 Promise 实例失败,新的 Promise 实例会立即失败,并将第一个被拒绝的 Promise 的错误信息传递给 catch 方法的回调函数。示例代码let promise1 = new Promise((resolve, reject) => { setTimeout(() => { resolve({"id": "1001", "sex": "男"}); }, 2000); }); let promise2 = new Promise((resolve, reject) => { setTimeout(() => { resolve({"id": "1001", "age": 30}); }, 1000); }); Promise.all([promise1, promise2]).then(res => { console.log(res); // [{ "id": "1001", "sex": "男"}, { "id": "1001", "age": 30}] });使用场景说明1. 并发数据获取当需要从多个 API 或数据源同时获取数据时,可以使用 Promise.all。例如,一个页面可能需要同时从后端获取用户信息、订单信息和商品信息,此时可以将这三个异步请求包装成 Promise 对象,并使用 Promise.all 来等待它们全部完成。这样做的好处是可以并行处理这些请求,提高数据加载的效率。2. 依赖关系处理虽然 Promise.all 本身并不直接处理依赖关系,但在某些情况下,可以利用它来等待多个相互依赖的异步操作完成。例如,一个操作可能依赖于另外两个异步操作的结果,此时可以使用 Promise.all 来等待这两个操作完成后,再进行后续的操作。不过,在这种情况下,更推荐使用 async/await 语法来清晰地表示异步操作的顺序和依赖关系。3. 错误处理Promise.all 还用于处理多个异步操作中的错误。当同时执行多个异步操作,并且希望一旦有任何一个操作失败就立即捕获错误时,Promise.all 提供了便捷的机制。它会返回一个新的 Promise 对象,该对象在所有传入的 Promise 对象都成功完成时才会成功,否则一旦有任何一个 Promise 对象失败,新的 Promise 对象就会立即失败,并将第一个失败的 Promise 对象的原因作为失败原因。4. 资源并行加载在前端开发中,经常需要加载各种资源,如图片、CSS 文件、JavaScript 脚本等。使用 Promise.all 可以并行地加载这些资源,并在所有资源都加载完成后进行后续的操作,如渲染页面或执行某些初始化代码。总的来说,Promise.all 是处理多个并发异步操作的强大工具,它提高了代码的可读性和可维护性,同时也提高了应用程序的性能和响应速度。在前端开发中,合理地使用 Promise.all 可以帮助开发者更好地管理和协调各种异步操作,从而构建出更加高效和稳定的 Web 应用。
-
Promise 是 JavaScript(包括 TypeScript)中一种用于处理异步操作的对象,它提供了一系列函数来管理异步流程。以下是 Promise 支持的主要函数:1. Promise 构造函数作用:创建一个新的 Promise 实例。参数:一个执行器函数,该函数接受两个参数:resolve 和 reject。resolve 用于将 Promise 的状态从 pending 变为 fulfilled,而 reject 用于将状态从 pending 变为 rejected。2. then 函数作用:为 Promise 实例添加状态改变时的回调函数。参数:两个可选的回调函数,第一个用于处理 fulfilled 状态,第二个(可选)用于处理 rejected 状态。如果 Promise 成功完成,则调用第一个回调函数,并将 Promise 的返回值作为参数传递给它;如果 Promise 失败,则调用第二个回调函数,并将错误信息作为参数传递给它。3. catch 函数作用:为 Promise 实例添加错误处理回调函数。参数:一个错误处理回调函数,当 Promise 被拒绝时调用,并将错误信息作为参数传递给它。catch 函数相当于调用 then 函数的第二个参数(错误处理回调)。4. finally 函数作用:为 Promise 实例添加无论成功还是失败都会执行的回调函数。参数:一个不接受任何参数的回调函数。无论 Promise 的最终状态是 fulfilled 还是 rejected,finally 函数中的回调函数都会被调用。5. Promise.all 函数作用:将多个 Promise 实例包装成一个新的 Promise 实例。参数:一个包含多个 Promise 实例的数组。行为:只有当所有传入的 Promise 实例都成功完成时,新的 Promise 实例才会成功完成,并将所有 Promise 的返回值作为数组传递给 then 函数。如果任意一个 Promise 实例失败,则新的 Promise 实例会立即失败,并将第一个被拒绝的 Promise 的错误信息传递给 catch 函数。6. Promise.race 函数作用:与 Promise.all 类似,但行为有所不同。参数:一个包含多个 Promise 实例的数组。行为:只要有一个 Promise 实例成功完成或失败,新的 Promise 实例就会采用该 Promise 的状态,并将它的返回值或错误信息传递给后续的 then 或 catch 函数。7. Promise.any 函数作用:接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。行为:只要参数实例中有一个变成 fulfilled 状态,包装实例就会变成 fulfilled 状态;如果所有参数实例都变成 rejected 状态,包装实例就会变成 rejected 状态。与 Promise.race 不同的是,Promise.any 不会因为某个 Promise 变成 rejected 状态而结束,必须等到所有参数 Promise 变成 rejected 状态才会结束。8. Promise.resolve 和 Promise.reject 函数作用:分别用于创建一个已解决(fulfilled)或已拒绝(rejected)的 Promise 实例。参数:Promise.resolve 接受一个值作为参数,并返回一个以该值解决的 Promise 实例。Promise.reject 接受一个错误原因作为参数,并返回一个以该原因拒绝的 Promise 实例。9. Promise.allSettled 函数作用:等待所有给定的 Promise 实例解决(fulfilled)或拒绝(rejected),并返回一个在所有 Promise 实例都已经结束后的新 Promise 实例。参数:一个包含多个 Promise 实例的数组。返回值:新的 Promise 实例在解决时返回一个数组,数组中的每个元素都是描述对应 Promise 实例状态的对象。这些函数共同构成了 Promise 的核心功能,使得开发者能够更优雅地处理异步操作,避免回调地狱,并提高代码的可读性和可维护性。
-
TypeScript 中 Promise 的用法在现代前端开发中,异步编程是不可避免的一部分。JavaScript 提供了多种处理异步操作的方法,其中 Promise 是一种非常强大且常用的工具。TypeScript 作为 JavaScript 的超集,不仅继承了 Promise 的所有功能,还通过类型系统增强了其可读性和安全性。本文将详细介绍在 TypeScript 中如何使用 Promise。什么是 Promise?Promise 是一个代表异步操作最终完成或失败的对象。它有三种状态:Pending(等待):初始状态,既不是成功,也不是失败状态。Fulfilled(已完成):意味着操作成功完成。Rejected(已拒绝):意味着操作失败。Promise 提供了 .then() 和 .catch() 方法来处理成功和失败的情况,以及 .finally() 方法来处理无论成功还是失败都需要执行的逻辑。基本用法首先,让我们看一个简单的 Promise 示例:function fetchData(): Promise<string> { return new Promise((resolve, reject) => { setTimeout(() => { const success = true; // 模拟一个成功或失败的条件 if (success) { resolve("数据获取成功!"); } else { reject("数据获取失败!"); } }, 1000); }); } fetchData() .then(data => { console.log(data); // "数据获取成功!" }) .catch(error => { console.error(error); // "数据获取失败!" });在这个例子中,fetchData 函数返回一个 Promise,它在一秒钟后模拟成功或失败。TypeScript 中的类型安全TypeScript 的强大之处在于类型系统。使用 Promise 时,可以明确指定其解析值(fulfilled value)和拒绝原因(rejection reason)的类型。function fetchUserData(): Promise<{ name: string; age: number }> { return new Promise((resolve, reject) => { // 模拟异步操作 setTimeout(() => { const user = { name: "张三", age: 30 }; resolve(user); }, 1000); }); } fetchUserData() .then(user => { console.log(`姓名: ${user.name}, 年龄: ${user.age}`); }) .catch(error => { console.error("获取用户数据失败:", error); });在这个例子中,fetchUserData 函数返回一个解析值为 { name: string; age: number } 类型的 Promise。链式调用和错误处理Promise 支持链式调用,每个 .then() 都可以返回一个新的 Promise,从而可以串联多个异步操作。function fetchUser(): Promise<string> { return Promise.resolve("张三"); } function getUserAge(userName: string): Promise<number> { return new Promise(resolve => { // 模拟根据用户名获取年龄 setTimeout(() => { resolve(userName === "张三" ? 30 : 25); }, 1000); }); } fetchUser() .then(userName => { console.log(`用户名: ${userName}`); return getUserAge(userName); }) .then(age => { console.log(`年龄: ${age}`); }) .catch(error => { console.error("发生错误:", error); });在上面的例子中,fetchUser 返回用户名,然后 getUserAge 根据用户名返回年龄。通过链式调用,我们可以依次执行这些异步操作。使用 async/await虽然 Promise 提供了强大的功能,但处理多个异步操作时,代码可能会变得复杂且难以阅读。async 和 await 语法是对 Promise 的一个语法糖,可以使异步代码看起来更像是同步代码。async function getUserInfo(): Promise<void> { try { const userName: string = await fetchUser(); console.log(`用户名: ${userName}`); const age: number = await getUserAge(userName); console.log(`年龄: ${age}`); } catch (error) { console.error("发生错误:", error); } } getUserInfo();在这个例子中,async 函数 getUserInfo 使用 await 等待 Promise 的结果,并且使用 try...catch 块来处理错误。总结Promise 是处理异步操作的强大工具,TypeScript 通过类型系统增强了其可读性和安全性。通过理解 Promise 的基本用法、类型安全、链式调用、错误处理以及 async/await 语法,你可以更加高效地编写异步代码。希望这篇文章能帮助你更好地掌握 TypeScript 中的 Promise。
-
使用GitHub注册vercel的时候要手机号,但是一直出现这样的错误:the verification phone number has been temporarily blocked. please wait 12 hours and try again.
-
引言 在软件开发过程中,数据结构是不可避免的。不同的数据结构用于描述不同的实体和关系。定义数据结构有时需要更为精确和严格的方式,以避免在多人合作或迭代过程中出现误解和错误。在这种情况下, TypeScript 接口就成为了一种强有力的工具,可以定义数据结构的契约,从而确保数据在应用程序中的正确性和可维护性。 但是,仅仅使用基本类型、数组和对象等数据类型并不足以满足复杂应用程序中的数据结构需求。这时, TypeScript 接口就可以派上用场了。 什么是接口 在 TypeScript 中,接口是指一组方法和属性的声明,用于定义一个对象的形状。它只定义了对象所应该有的成员,而不关心对象的具体实现。接口可以被类、函数、对象等各种类型实现,从而使得这些实现具有相同的形状和属性。 使用接口可以方便地定义类型,从而避免了在代码中使用硬编码来定义对象的类型。它可以让代码更加清晰、可读、可维护,同时也可以提高代码的复用性和可扩展性。 接口的定义 接口的定义使用 interface 关键字,语法如下: interface InterfaceName { property1: type1; property2: type2; method1(param1: type1, param2: type2): returnType; method2(param1: type1): void; } 其中,InterfaceName 指定了接口的名称,property1、property2 是接口的属性,method1、method2 是接口的方法。属性和方法都有自己的名称和类型声明。 下面是一个简单的接口定义例子: interface Person { name: string; age: number; sayHi(): void; } 这个接口定义了一个 Person 类型,它有两个属性和一个方法。 name 和 age 属性分别是字符串和数字类型, sayHi 方法没有参数,返回值为空。 接口的实现 在 TypeScript 中,接口可以被任何类型实现,包括类、函数、对象等。当一个类型实现了某个接口时,它必须满足该接口的所有定义,包括属性和方法的类型和名称。 下面是一个类实现接口的例子: interface Person { name: string; age: number; sayHi(): void; } class Student implements Person { name: string; age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } sayHi(): void { console.log(`Hi, my name is ${this.name}, I'm ${this.age} years old.`); } } const student = new Student("Alice", 18); student.sayHi(); 在这个例子中,我们定义了一个 Person 接口,并让Student类实现了该接口。在Student类中,我们定义了 name 和 age 属性,并实现了 sayHi 方法。由于 Student 类实现了 Person 接口,因此它必须满足接口的所有定义。在实例化这个类时,可以看到输出了正确的信息。 TypeScript接口的继承 TypeScript 接口也可以 继承其他接口 ,从而可以更好地描述数据结构的关系。以下是一个继承接口的例子: interface Cancer { name: string; symptom: string; } interface LungCancer extends Cancer { smoking: boolean; } 上面的代码定义了两个接口,“ Cancer ”和“ LungCancer ”。显然,“ LungCancer ”继承了“ Cancer ”的属性,并增加了一个“ smoking ”属性。这种方式可以方便地描述数据结构的层次关系。 接口的可选属性和只读属性 可选属性 有时候我们定义的数据结构并不一定需要所有的属性,在这种情况下,我们可以使用可选属性。可选属性在属性名后面加上一个 ? 标记,表示该属性是可选的。例如: interface Person { name: string; age?: number; sayHi(): void; } const person1: Person = { name: "Bob", sayHi() { console.log(`Hi, my name is ${this.name}.`); }, }; const person2: Person = { name: "Alice", age: 18, sayHi() { console.log(`Hi, my name is ${this.name}, I'm ${this.age} years old.`); }, }; 在这个例子中, Person 接口的 age 属性是可选的,因此我们可以在实现该接口的对象中省略 age 属性。在上面的 person1 对象中,我们只实现了 name 和 sayHi 属性,而没有实现 age 属性。在 person2 对象中,我们实现了所有的属性和方法。 只读属性 另外,有些时候我们希望定义一些只读的属性,这些属性只能在声明时被赋值,之后就不能被修改了。在 TypeScript 中,我们可以使用 readonly 关键字来定义只读属性。例如: interface Person { readonly name: string; age?: number; sayHi(): void; } const person: Person = { name: "Alice", age: 18, sayHi() { console.log(`Hi, my name is ${this.name}, I'm ${this.age} years old.`); }, }; 在这个例子中,我们定义了一个 Person 接口,并将 name 属性定义为只读属性。在实现该接口的对象中,我们无法修改 name 属性的值,因此在赋值时会出现编译错误。 TypeScript接口的函数类型 在 TypeScript 中,接口不仅可以描述对象,还可以描述函数类型。以下是一个函数类型的例子: interface SearchFunc { (source: string, subString: string): boolean; } let mySearch: SearchFunc; mySearch = function(source: string, subString: string): boolean { let result = source.search(subString); return result > -1; } 上面的代码定义了一个函数类型的接口“ SearchFunc ”,它描述了一个具有两个参数和一个布尔类型返回值的函数。在实例化这个函数类型的时候,“ mySearch ”变量被赋值为符合这个函数类型的函数。 TypeScript接口的类类型 在 TypeScript 中,接口还可以描述类的属性和方法。以下是一个例子: interface ClockInterface { currentTime: Date; setTime(d: Date): void; } class Clock implements ClockInterface { currentTime: Date = new Date(); setTime(d: Date) { this.currentTime = d; } constructor(h: number, m: number) { } } 上面的代码定义了一个接口“ ClockInterface ”,它描述了一个具有“ currentTime ”和“ setTime ”方法的类。然后,我们定义了一个“ Clock ”类,并实现了这个接口。当我们实例化这个类的时候,它必须符合接口的描述,即含有“ currentTime ”属性和“ setTime ”方法。 总结 在 TypeScript 中,接口是定义数据结构的契约,用于确保代码的健壮性和可维护性。它可以被任何类型实现,包括类、函数、对象等。接口可以定义属性、方法、可选属性、只读属性等,从而使代码更加清晰、可读、可维护。接口的使用可以提高代码的复用性和可扩展性,是 TypeScript 中一个非常重要的概念。 ———————————————— 版权声明:本文为CSDN博主「与墨学长」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/McapricornZ/article/details/131340880
-
下面的给nodeInfo.nodeType中的枚举变量赋值怎么会报错?定义:interface IPlayer {playerName:String; playerColor:Color;}export const enum enumNodeType {Troop, City}interface nodeInfo {playerInfo:IPlayer; nodeType:enumNodeType}在函数中使用:let nodeInfo:nodeInfo;nodeInfo.nodeType = enumNodeType.City // 这里报错,是怎么回事?同一个文件中
-
【CSDN 编者按】很多时候,仅从名称上来看,不少人对 TypeScript 与 JavaScript 傻傻分不清楚,或许只知道 TypeScript 作为 JavaScript 的后来者,想要将其取而代之,却时至今日未能如愿。殊不知,TypeScript 诞生落地发展到当下,已有十年的时间。虽然未能达到 JavaScript 流行的高度,但也弥补了其不少不足之处。近日,微软 TypeScript 高级项目经理 Daniel Rosenwasser 在官网发文分享了 TypeScript 从早期到时间考验再到今天的整个演进历程,也带来了不一样的思考。原文地址:https://devblogs.microsoft.com/typescript/ten-years-of-typescript/编译 | 苏宓 出品 | CSDN(ID:CSDNnews)以下为译文:今天是 TypeScript 的生日!但是这个生日很特别——10 年前的今天,2012 年 10 月 1 日,TypeScript 首次公开亮相。早期的情况当 TypeScript 首次亮相时,有很多人持怀疑态度,这也是可以理解的。对于一些 JavaScript 用户来说,一个试图将静态类型引入 JavaScript 的团队可能听起来像是一个邪恶的组织,甚至可视为一个阴谋或笑话。但是这些功能是有价值的,对吗?类型检查,在你保存文件之前捕捉 Bug,并获得丰富的编辑器功能,如代码完成、导航和重构?我们知道公司内外部的团队在处理复杂的 JavaScript 代码库时遇到了巨大的挑战,而且我们知道 JavaScript 将被广泛使用。因此,谁不希望有强大的工具来帮助编写它呢?对于团队来说,TypeScript 初心未变,一如最初在发布 TypeScript 时所述的那样,“在大型应用开发中使用 JavaScript 开发!"。幸运的是,这个愿景使得很多的开发者产生了共鸣。在早期,我们建立了一个小而勤奋、热情的社区,当我们还在迭代、学习和构建一个甚至还没有达到 1.0 的东西时,很多人就愿意参与进来,进行实验和体验。我们看到了令人兴奋的新努力,如 DefinitelyTyped 项目,新的社区成员在 StackOverflow 和我们的问题跟踪器上提供帮助,创作者为该语言编写书籍和教程,以及押注 TypeScript 的新库。这些有耐心、努力工作、精力充沛的开发者为 TypeScript 社区的发展奠定了基础。不过,大多数 JavaScript 开发人员对 TypeScript 仍持怀疑态度。那么,这个团队要如何说服 JavaScript 开发者相信静态类型在动态类型语言中的价值?"类型与无类型"一直是一个有争议的话题,这在编程界至少可以追溯到半个世纪以前。但是,我们真的想通过类型的力量来创造令人惊奇的 JavaScript 工具。这能做到吗?经得起时间的考验事实是,这需要一种与我们习惯的完全不同的开发方法,其次是需要大家坚持不懈、拓展性和同理心。TypeScript 必须是免费和开源的,并以真正开放的方式进行。它还必须与现有的 JavaScript 无缝互通,与 JavaScript 共同发展,并且感觉像 JavaScript。TypeScript 从未打算建立一种单独的、独特的、规定性的语言。相反,TypeScript 必须是描述性——围绕 JavaScript 生态系统中一些模式进行类型系统的创新。这让我们能够满足人们的需求,而且这种理念与项目的设计目标非常吻合。实际上,TypeScript 的设计目标保持得如此之好,令人惊讶。例如,一些设计目标:"不会对发出的程序施加任何运行时开销。""与当前和未来的 ECMAScript 提案保持一致。""保留所有 JavaScript 代码的运行时行为。""避免增加表达式级别的语法。""使用一致的、完全可擦除的、结构化的类型系统。"所有真正指向 TypeScript 只是简单地成为 JavaScript 的类型检查器,只添加类型检查所需的语法。因此,我们主要关注类型系统,而避免增加新的运行时语法和行为。这在 10 年后的应用中,体现地可能更明显,但编程语言经常试图根据他们的可运行代码的样子来区分自己。此外,很多类型化语言根据类型来指导他们的运行时行为。但是,这些方法在试图建立在 JavaScript 的基础上,并与之整合时,就没有意义了。没有类型的 JavaScript 在粘贴到 TypeScript 文件中时,必须有相同的工作方式,而且将 TypeScript 转换为 JavaScript 需要像剥离类型一样容易。我们在早期采取了一些错误的措施才意识到这一点,但这是一个学习的机会,并且微软团队在 10 年的大部分时间里都避免了运行时语法。如今,当 TypeScript 缺少一个有用的运行时特性时,我们不会只在 TypeScript 中添加它。我们在 TC39(JavaScript 标准机构)内开始实践,指导或倡导新的功能,以便所有的 JavaScript 开发人员能够从中受益。另一个成功的原则是,TypeScript 并没有试图成为工具箱中的每一个工具。我们的一个非目标是不 "提供一个端到端的构建管道。相反,使系统具有可扩展性,以便外部工具可以使用编译器进行更复杂的构建工作流程"。有很多时候,TypeScript 被要求成为一个 linter、一个 bundler、一个优化器/minifier、一个构建协调器、再一个 bundler 等等。这些界限并不会每次被明确,尤其是当 TypeScript 作为一个类型检查器、编译器和语言服务已经做了很多。在 JavaScript 生态系统中,很多人参与到应用程序开发的最佳实践争斗中,由此 TypeScript 也不断地保持了灵活性,这一点非常重要。考虑到过去几年中,所有不同的捆绑器、不同的运行时、不同的构建协调器和不同的锁定器,TypeScript 与其中的每一个都很好地整合了,并没有试图取代其中的任何一个,这一点也至关重要。我们很荣幸能与这个领域的工具作者合作,因为我们都在努力使 TypeScript 和 JavaScript 更容易使用。回到今天今天,TypeScript 是一种蓬勃发展的语言,全世界有数百万的开发人员在使用。在 StackOverflow 的年度调查、GitHub 的 Octoverse 报告和 Redmonk的语言排名等调查和语言排名中,TypeScript 一直处于开发者最常用和最喜爱的语言中的 Top 10。当然,背景很重要——TypeScript 的使用从根本上与 JavaScript 的使用交织在一起,每个 TypeScript 开发者也是 JavaScript 开发者。值得庆幸的是,即使在询问 JavaScript 开发人员是否使用 TypeScript 并喜欢它时,比如在 JS 的状态调查中,答案也都会是 "是"!TypeScript 的成功是一个很好的例子。今天的成功远远超过了核心团队几年前对 TypeScript 的想象,更不用说十年前了。核心团队在 TypeScript 上努力工作,但我们知道,实现这一成功的根本原因是社区。这包括 TypeScript 的外部贡献者、在 TypeScript 上下注并证明该语言的库创建者和日常开发者、DefinitelyTyped 的贡献者、社区组织者、花时间回答问题和教导他人并为新人开辟道路的专家。也希望 TypeScript 的下一个 10 年能给你带来好的待遇!原文链接:https://blog.csdn.net/csdnnews/article/details/127236749?spm=1000.2115.3001.5927
-
原文链接:https://thenewstack.io/microsofts-programming-language-czar-anders-hejlsberg-the-interview/ https://blog.csdn.net/m0_66023967/article/details/123068356?spm=1000.2115.3001.5927Anders Hejlsberg是世界上最重要的计算机程序员之一。作为Turbo Pascal、Delphi、C#和TypeScript等编程语言的创造者,在构建微软.NET等开发环境方面极具影响力,Hejlsberg影响了数以百万计的软件开发人员。近日,Hejlsberg与TNS新闻编辑Darryl K.Taft进行了一场线上对话,围绕微软低代码/无代码、Web3等话题,分享了他对许多与软件开发相关的事情的看法。Excel是一种低代码/无代码工具问:您对于低代码,无代码有什么看法?Anders Hejlsberg:我认为这个领域的潜力巨大。我们常会忽略,其实我们已经拥有了世界上最好的编程语言之一,或许这取决于你如何定义Excel中编程语言。可以说Excel是一种低代码/无代码的程序编写方式,人们运行程序,而他们使用的电子表格就是程序。你可以做一些事情,输入一些东西,然后运行程序,程序会在电子表格的底层运行,并且给出结果。所以我认为挑战其实一直在于这些工具。当达到低代码环境的极限时,你需要找到设置合适的逃生舱口的方法,才不会陷入困境。而且这个逃生舱口,可以转移到一个更完整或完整的编程环境,这意味着有意义的可扩展性模型是必须的。因此,我们需要思考的事情还有很多,但这些事情往往不是最重要的,因为人们往往会转而关注绘制方框和线条,以及这些示例的演示效果。但我常常很好奇,这些工具是否能适应现实世界?很多都不能,但有些可以。这是有趣的地方。问:我的意思是,整体的主旨是努力让更多人进入开发的世界。其中一部分人想要走的更远,成为专业人士,或者至少更加熟练。Anders Hejlsberg:是的。如果你去问所有的Excel用户,他们都不会自称程序员。但是,从某种意义上来说,他们是。问:没错,我比较喜欢把这比作“Blue Apron”和“HelloFresh”这样的套餐服务——它们会让你开始学习烹饪,并且自己动手,做得更好。你认为在软件开发中下一个要克服的挑战是什么?Anders Hejlsberg:在我们写代码的方式和机器学习的可能够帮助我们的方式之间,仍然有非常大的差距。因为我们的大脑以及神经网络的工作方式,从来没有真正关于0和1以及绝对正确的答案,而都是统计和模糊逻辑。然而,所有的编程语言都牢固地扎根于逻辑和可证明性等等。这两种世界观之间存在着巨大的差距。看看我们最终如何弥合这一差距的(如果说我们最终做到了的话)。软件开发中的安全性问:下面我们来谈谈安全问题。在软件开发中,安全性和整个shift left的作用有多重要?Anders Hejlsberg:这取决于你在堆栈中的位置。对于编程语言来说,安全性非常重要,起码这对我的工作影响很大。甚至可以说类型检查器是一种安全形式。这是软件中可利用的漏洞。所以从这个意义上来说,编程语言间接地考虑了安全性。创造可以分析代码,并且指出可能存在的安全漏洞的工具,是我们看待安全性的其他方式。但很显然,这是个现实问题。我是说,世界各地也有人每天上班,他们的工作就是黑入西方国家的公司,从而获得薪水。这细思极恐。有很多人靠滥用科技谋生。虽然这一点令人难过,但这就是现实。Web3的开发问:您对Web3有什么看法?您认为是否需要新的工具进行Web3的开发呢?Anders Hejlsberg:首先,我们要对Web3的概念达成共识,因为人们对此总是各持己见。我喜欢奖励创造者概念,以及创造数字资产并获得奖励的能力。但是,在这个领域也有很多骗子、挂羊头卖狗肉的人和急功近利的人。而且还会造成大量的环境污染,虽然可以将其归因于一些正在进行的采矿活动和能源的使用,所以这是一个多样的世界。我们将看到这一切的结果。因此,我喜欢它的某些部分,也反对它的某些部分。潜力巨大的项目:GitHub构建的Copilot问:我知道您在微软需要做的事情很多,但在微软以外,有没有什么很棒的项目或者工作吸引您的关注?Anders Hejlsberg:我认为微软肯定有一些非常有趣的项目。比如开发者工具、人工智能、机器学习,还有很多其他有意思的事。不知道你是否了解GitHub正在构建的Copilot的项目,能够在世界上所有的开源代码上训练一个机器学习网络,然后让它给出答案,是不是很棒?当然,这仍然有点像西大荒,因为有各种机遇和不可掌控的发展,以及知识产权的未解决的问题,但它展现出巨大的潜力。所以我认为这很吸引人,我正在密切关注这个问题。问:我认为这是2021年的最好的编程项目,涉及到相当大的领域。Anders Hejlsberg:是的,这个项目确实很吸引人。尤其是作为一名程序员,它在很大程度上让人洞察到我们谈论的是什么样的智能。从某种意义上说,用智能这个词有点不恰当,因为如果有人以前写过这段代码,这种机器学习模型的聚合内存可能是正确的,这值得我们注意。它可以在其内存中找到那段代码。但是,如果以前没有人写过,它必须要思考解决方案,结果可能出人意料,所以在这方面还有很多需要思考的地方。但这是一个了不起的工具,因为它把再利用的概念提升到了一个新的水平,让我们不用重复做别人已经做过的事。
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签