从零开始开发纯血鸿蒙应用之逻辑封装

news2025/1/6 2:52:49

从零开始开发纯血鸿蒙应用

  • 一、前言
  • 二、逻辑封装的原则
  • 三、实现 FileUtil
    • 1、统一的存放位置
    • 2、文件的增删改查
      • 2.1、文件创建与文件保存
      • 2.2、文件读取
      • 2.2.1、读取内部文件
      • 2.2.2、读取外部文件
    • 3、文件删除
  • 四、总结

一、前言

应用的动态,借助 UI 响应完成,所谓 UI响应,就是指对用户操作的回应。通常,UI 响应可分为纯逻辑响应和内容刷新两大类型,前者指用户触发动作发生后,不会在应用页面上有任何改变,而后者往往会产生页面内容的更新。

简单的UI响应,处理代码可以直接放在组件的事件处理方法中,鸿蒙UI组件支持的通用事件有如下:
在这里插入图片描述
复杂的响应处理,也即代码量比较多,无法通过只调用一个系统API来完成的,就需要封装在另外的方法体中,然后将对应的方法以函数参数的形式传入组件的事件处理函数中,这时候就涉及到逻辑封装了。

二、逻辑封装的原则

与 UI 封装一样,逻辑封装也有相应的原则必须遵守。于我而言,原则之一就是,能与UI实现文件独立的,就不要杂糅在 Component struct 里面,除非涉及到更新UI内容。用独立文件进行封装时,最好搞成工具类的形式,将负责相同类型处理的代码,统一放置在相同的 ets 文件中,方便进行源码管理,比如,在本工程中用到的文件读写处理,我就是专门放在了 lib_util模块的 FileUtil 中。

封装工具类的时候,应当将实现方法以静态方法的形式对外提供,所以,有必要将 类构造函数私有化,即如下:
在这里插入图片描述
要知道,很多UI响应处理都是面向过程的,因而面向对象的那一套类体实现,就不要采用了,即便类体里面存在某些需要初始化操作的字段,也应该有同样是静态方法的 init 方法去实现。

最后,每个工具类的每个方法都应该有函数头注释和日志打印,函数头注释要采用文档类型,这样在其他ets 文件中进行使用的时候,才能通过鼠标悬停获知说明信息:
在这里插入图片描述
在这里插入图片描述

三、实现 FileUtil

正如第一篇所说,本工程旨在实现一个支持通用纯文本文件浏览和编辑的纯血鸿蒙应用,因此,文件的读写操作,在本工程里面是重中之重的,下面就分享一下我在实现 FileUtil 过程中的一些考量。

1、统一的存放位置

在应用内创建的纯文本格式的文件,不论文件后缀为何,我都是统一放在沙箱目录 fileDir下的 docs 文件夹中,如此一来,便可以降低获取文件名列表的方法的复杂性。

在鸿蒙API中,允许根据指定目录和指定文件后缀,去获取文件名列表,例如本工程里面实现的 getFileNameList 方法:

 /**
   * 获取文件名列表
   * @param ctx 上下文
   * @returns 返回指定目录下的纯文本文件的文件名列表
   */
  static getFileNameList(ctx: common.UIAbilityContext) {
    const prefix: string = ctx.filesDir;
    const dir: string = DirectoryConstants.DOCUMENT_PATH;
    const path: string = `${prefix}/${dir}`;
    const option: ListFileOptions = {
      recursion: false,
      listNum: 0,
      filter: {
        suffix: ['.txt', '.log', '.csv', '.ini', '.conf', '.md', '.markdown', '.rtf', '.json',
          '.xml', '.ets', '.java', '.py',
          '.c', '.cpp', '.h', '.html']
      }
    }
    return fs.listFileSync(path, option)
  }

应用沙箱路径可以借助 UI 上下文进行获取,所以,方法参数就是一个 UI 上下文。将文件直接放在应用沙箱的一级目录,如 file 目录下,是不明智的,所以,必须另辟一个子目录进行存放,而子目录名可以记录在 lib_constants 中。

2、文件的增删改查

就像数据库一样,文件也是可以增删改查的。

2.1、文件创建与文件保存

首先,看一下文件的新增和修改:

/**
   * 保存文本数据到文件
   * @param ctx 上下文
   * @param data 待保存的数据
   * @param fileName 目标文件名
   * @returns 是否写入成功
   */
  static async saveToFile(ctx: common.UIAbilityContext, data: string, fileName: string): Promise<number> {
    const prefix: string = ctx.filesDir;
    const dir: string = DirectoryConstants.DOCUMENT_PATH;
    const path: string = `${prefix}/${dir}/${fileName.trimEnd()}`;
    const file = fs.openSync(path, fs.OpenMode.READ_WRITE|fs.OpenMode.CREATE);
    return new Promise((resolve, reject) => {
      fs.write(file.fd, data)
        .then((writeLen) => {
          Logger.info(`write ${writeLen} bytes to ${path}`, TAG)
          resolve(writeLen);
        }).catch((err: BusinessError) => {
        Logger.error(`write file failed, ${err.message}`, TAG)
        reject(err);
      }).finally(() => {
        fs.closeSync(file)
      })
    })
  }

考虑到 IO 操作通常都比较慢,所以,采用异步方法的形式进行实现,为了保障做到文件不存在时创建、存在时就写入,需要将文件以 fs.OpenMode.READ_WRITE|fs.OpenMode.CREATE 打开。

为了减少重复的目录存在性判断代码,我在 entry 模块的 util 目录下的 EntryUtil 中,专门用一个 createDirectory 方法负责目录的创建:

 static createDirectoryDocs() {
    if (EntryUtil.context) {
      const prefix = EntryUtil.context.filesDir;
      const docsLocator = `${prefix}/${DirectoryConstants.DOCUMENT_PATH}`;
      if (fs.accessSync(docsLocator)) {
        Logger.info(`${docsLocator}已存在`, TAG);
      } else {
        fs.mkdir(docsLocator)
          .then(() => {
            Logger.info(`${docsLocator}创建成功`, TAG);
          }).catch((err: BusinessError) => {
            Logger.error(`${docsLocator}创建失败: ${err.message}`, TAG);
        })
      }

    } else {
      throw new Error("context is not init");
    }
  }

并在 EntryAbility 的 onCreate 方法中调用。

回到 saveToiFile 方法,该方法会返回一个 Promise<number> 对象,这是 Typescript 或者说 Javascript 中,专门为异步方法提供的返回值类型;当文件内容成功写入目标文件中时,会将写入的字节数通过 resolve 回调函数返回给调用者,而如果写入失败,则用 reject 回调函数抛出错误。

由于是文件写入操作,所以,除了 UI 上下文外,还需要文件名和文件内容

2.2、文件读取

文件读取分为读取内部文件和外部文件两种,并且针对性地封装了相应的方法。对于内部文件,即在本应用中创建的文件,只需传入一个文件名即可,而对于外部文件、即其他应用通过系统的文件分享功能传入的文件,就需要传入完整的 file uri 才能打开。

2.2.1、读取内部文件

首先,看一下内部文件的读取实现代码:

/**
   * 读取文件内容
   * @param ctx 上下文
   * @param fileName 文件名
   * @returns 文件内容
   */
  static async getFromFile(ctx: common.UIAbilityContext, filename: string): Promise<string>{
    const prefix: string = ctx.filesDir;
    const dir: string = DirectoryConstants.DOCUMENT_PATH;
    const path: string = `${prefix}/${dir}/${filename}`;
    Logger.info(`read file from ${path}`, TAG)
    return new Promise((resolve, reject) => {
      if (fs.accessSync(path)) {
        const stat = fs.statSync(path);
        if (stat.size > 0) {
          const readTextOption: ReadTextOptions = {
            offset: 1,
            length: stat.size,
            encoding: 'utf-8'
          };
          fs.readText(path, readTextOption).then((data) => {
            Logger.info(`read ${data.length} bytes from ${path}`, TAG)
            resolve(data);
          }).catch((err: BusinessError) => {
            Logger.error(`read file failed, ${err.message}`, TAG)
            reject(err);
          })
        } else {
          resolve("");
        }
      } else {
        reject(`${filename} is not exist!`);
      }
    })
  }

一样采用异步方法的形式进行实现,在处理逻辑中,会先判断文件的存在性,如果不存在则抛错。接着利用 fs.statSync(path) 去获取文件信息,如文件大小等,该 API 的官方说明如下:
在这里插入图片描述
而 Stat 对象的组成如下:

  • ino:文件标识,通常同设备上的不同文件的INO不同。
  • mode:文件权限。
  • uid:文件所有者的ID。
  • gid:文件所有组的ID。
  • size:文件的大小,以字节为单位。仅对普通文件有效。
  • atime:上次访问该文件的时间,表示距1970年1月1日0时0分0秒的秒数。
  • mtime:上次修改该文件的时间,表示距1970年1月1日0时0分0秒的秒数。
  • ctime:最近改变文件状态的时间,表示距1970年1月1日0时0分0秒的秒数。
  • location:文件的位置,表示该文件是本地文件或者云端文件。

有了文件的 Stat 信息后,就可以利用其中的 size 字段,去设置 ReadTextOptions,该 option 是 readText 方法所必传的,readText 方法官方说明如下:
在这里插入图片描述
在这里插入图片描述

成功读取,则将文件内容通过 resolve 回调函数外传。

2.2.2、读取外部文件

外部文件的读取实现,代码如下:

/**
   * 读取其他应用分享的文件
   * @param fileUri
   * @returns
   */
  static async readExternalFile(fileUri: string): Promise<string> {
    return new Promise((resolve, reject) => {
      const file = fs.openSync(fileUri, fs.OpenMode.READ_ONLY);
      if (file) {
        Logger.info(`success open file: ${file.path}`, TAG);
        const fileStat = fs.statSync(file.fd);
        Logger.info(`file size: ${fileStat.size}`, TAG);
        const buf: ArrayBuffer = new ArrayBuffer(fileStat.size);
        fs.read(file.fd, buf)
          .then((readLen) => {
            if (readLen > 0) {
              const decoder = util.TextDecoder.create('utf-8');
              const content = decoder.decodeToString(new Uint8Array(buf));
              resolve(content);
            } else {
              reject(new Error("read file failed"))
            }
          }).then(() => {
            reject(new Error("read file failed"))
        }).finally(() => {
          fs.closeSync(file);
        })

      } else {
        Logger.error(`open file failed: ${fileUri}`);
        reject(new Error("open file failed"))
      }
    })
  }

大致上和内部文件的读取实现相同,除了参数只需传入 file uri 和使用 fs.read API 外。

fs.read 方法读取文件时,会将内容读取到一个 ArrayBuffer 中,所以,在利用 resolve 回调外传文件内容前,需要 ArrayBuffer 进行转码操作,将其转成 string 类型。

3、文件删除

在鸿蒙框架中,文件删除是通过调用 fileIo 的 unlink 或 unlinkSync 实现的,从方法名就可以看出,文件的删除实际上,只是将原本指向文件所在存储区域的指针或者链接,进行摘除和悬空,并非是将对应的存储区域用二进制零进行覆盖。

在这里插入图片描述

四、总结

其他的功能逻辑的封装,基本上跟 FileUtil 的封装大同小异,都是通过一组系统 API 的相互配合,达到功能的实现。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2270658.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ESP32 I2S音频总线学习笔记(一):初识I2S通信与配置基础

文章目录 简介为什么需要I2S&#xff1f;关于音频信号采样率分辨率音频声道 怎样使用I2S传输音频&#xff1f;位时钟BCLK字时钟WS串行数据SD I2S传输模型I2S通信格式I2S格式左对齐格式右对齐格式 i2s基本配置i2s 底层API加载I2S驱动设置I2S使用的引脚I2S读取数据I2S发送数据卸载…

CSS 中 content换行符实现打点 loading 正在加载中的效果

我们动态加载页面内容的时候&#xff0c;经常会使用“正在加载中…”这几个字&#xff0c;基本上&#xff0c;后面的 3 个点都是静态的。静态的问题在于&#xff0c;如果网络不流畅&#xff0c;加载时间比较长&#xff0c;就会给人有假死的 感觉&#xff0c;但是&#xff0c;如…

25考研王道数据结构课后习题笔记

声明&#xff1a;以下内容来自于B栈知名up主–白话拆解数据结构 回答&#xff1a;为什么要做这个&#xff0c;因为我这个学期学完了数据结构&#xff0c;而且这个数据结构是408的重头&#xff0c;为什么选择25的&#xff0c;因为这个25考研刚刚结束&#xff0c;25相对成熟&…

小程序发版后,强制更新为最新版本

为什么要强制更新为最新版本&#xff1f; 在小程序的开发和运营过程中&#xff0c;强制用户更新到最新版本是一项重要的策略&#xff0c;能够有效提升用户体验并保障系统的稳定性与安全性。以下是一些主要原因&#xff1a; 1. 功能兼容 新功能或服务通常需要最新版本的支持&…

GRAPE——RLAIF微调VLA模型:通过偏好对齐提升机器人策略的泛化能力(含24年具身模型汇总)

前言 24年具身前沿模型大汇总 过去的这两年&#xff0c;工作之余&#xff0c;我狂写大模型与具身的文章&#xff0c;加之具身大火&#xff0c;每周都有各种朋友通过CSDN私我及我司「七月在线」寻求帮助/指导(当然&#xff0c;也欢迎各大开发团队与我司合作共同交付&#xff09…

0xc0000020错误代码怎么处理,Windows11、10坏图像错误0xc0000020的修复办法

“0xc0000020”是一种 Windows 应用程序错误代码&#xff0c;通常表明某些文件缺失或损坏。这可能是由于系统文件损坏、应用程序安装或卸载问题、恶意软件感染、有问题的 Windows 更新等原因导致的。 比如&#xff0c;当运行软件时&#xff0c;可能会出现类似“C:\xx\xxx.dll …

pycharm+anaconda创建项目

pycharmanaconda创建项目 安装&#xff1a; Windows下PythonPyCharm的安装步骤及PyCharm的使用-CSDN博客 详细Anaconda安装配置环境创建教程-CSDN博客 创建项目&#xff1a; 开始尝试新建一个项目吧&#xff01; 选择好项目建设的文件夹 我的项目命名为&#xff1a;pyth…

基于Pytorch和yolov8n手搓安全帽目标检测的全过程

一.背景 还是之前的主题&#xff0c;使用开源软件为公司搭建安全管理平台&#xff0c;从视觉模型识别安全帽开始。主要参考学习了开源项目 https://github.com/jomarkow/Safety-Helmet-Detection&#xff0c;我是从运行、训练、标注倒过来学习的。由于工作原因&#xff0c;抽空…

【PDF物流单据提取明细】批量PDF提取多个区域内容导出表格或用区域内容对文件改名,批量提取PDF物流单据单号及明细导出表格并改名的技术难点及小节

相关阅读及下载&#xff1a; PDF电子物流单据&#xff1a; 批量PDF提取多个区域局部内容重命名PDF或者将PDF多个局部内容导出表格&#xff0c;具体使用步骤教程和实际应用场景的说明演示https://mp.weixin.qq.com/s/uCvqHAzKglfr40YPO_SyNg?token720634989&langzh_CN扫描…

JavaWeb开发(五)Servlet-ServletContext

1. ServletContext 1.1. ServletContext简介 1.1.1. ServletContext定义 ServletContext即Servlet上下文对象&#xff0c;该对象表示当前的web应用环境信息。 1.1.2. 获取ServletContext对象: &#xff08;1&#xff09;通过ServletConfig的getServletContext()方法可以得到…

长时间序列预测算法---Informer

目录 一、传统的 Transformer 模型二、Informer原理2.1 Attention计算2.2 “积极”的Q筛选2.2.1 KL散度2.2.2 “懒惰”的q处理 2.3 Encoder结构2.4 Decoder结构2.4.1 Transformer的Decoder操作2.4.2 Informer的Decoder操作 2.5 Informer模型的改进 三、模型应用 时间序列相关参…

点击取消按钮,console出来数据更改了,页面视图没有更新

点击取消按钮&#xff0c;console出来数据更改了&#xff0c;页面视图没有更新 前言 实现效果&#xff1a;点击取消按钮&#xff0c;页面视图全部为空&#xff0c; 遇到的问题&#xff1a; 点击取消按钮&#xff0c;console出来数据更改了&#xff0c;SchemaJson 都是默认值啦…

RFID手持机与RFID工业平板在仓储物流管理系统中的选型

概述 随着物联网技术在仓储物流管理系统中的普及&#xff0c;RFID手持机与RFID工业平板作为基于RFID技术手持式读写器的两种重要终端设备形态&#xff0c;得到了广泛应用。尽管RFID手持机与RFID工业平板都具备读写 RFID标签的基本功能&#xff0c;使用场景较为类似&#xff0c…

UML之泛化、特化和继承

在UML&#xff08;统一建模语言&#xff09;中&#xff0c;泛化&#xff08;Generalization&#xff09;和特化&#xff08;Specialization&#xff09;是面向对象思想中继承&#xff08;Inheritance&#xff09;关系的重要概念&#xff0c;它们描述类与类&#xff08;或用例与…

vue 修改vant样式NoticeBar中的图标,不用插槽可以直接用图片

使用文档中是可以直接使用图片链接的 :left-icon"require(../../assets/newImages/noticeImg.png)" <html> .... <NoticeBarmode""color"#C6C6C6"background""v-if"global_info.site_bulletin":left-icon"r…

【漫话机器学习系列】028.CP

Mallows’ Cp&#xff1a;标准化公式解析与应用 Mallows’ Cp 是一种常用的模型选择工具&#xff0c;用于在一系列候选模型中权衡拟合度和复杂性&#xff0c;帮助我们选择性能最优的模型。本文将基于其标准化公式展开详细解析&#xff0c;并探讨其应用场景、实现方法、优点与局…

vs 2022 中xml 粘贴为Class 中,序列化出来的xml 的使用

上图是visual studio 2022 中使用的粘贴功能的菜单位置 在生成的xml 中&#xff0c;有些是类似如下类型的 [System.Serializable] [System.Xml.Serialization.XmlType] public class Item {private bool isVisibleField;private bool isVisibleFieldSpecified;[System.Xml.Se…

数据库自增 id 过大导致前端时数据丢失

可以看到&#xff0c;前端响应参数是没有丢失精度的 但是在接受 axios 请求参数时出现了精度丢失 解决方案一&#xff1a;改变 axios 字符编码 axios.defaults.headers[Content-Type] application/json;charsetUTF-8; 未解决 解决方案二&#xff1a;手动使用 json.parse() …

STM32-笔记19-串口打印功能

复制项目文件夹03-流水灯&#xff0c;重命名为19-串口打印功能 打开项目 在主函数中&#xff0c;添加头文件、和串口初始化函数&#xff08;设置波特率&#xff09;和输出函数&#xff0c;如图所示&#xff1a; 软件部分就设置好了 下面是硬件部分 接线&#xff1a;使用USB…

GPU 进阶笔记(四):NVIDIA GH200 芯片、服务器及集群组网

大家读完觉得有意义记得关注和点赞&#xff01;&#xff01;&#xff01; 1 传统原厂 GPU 服务器&#xff1a;Intel/AMD x86 CPU NVIDIA GPU2 新一代原厂 GPU 服务器&#xff1a;NVIDIA CPU NVIDIA GPU 2.1 CPU 芯片&#xff1a;Grace (ARM)2.2 GPU 芯片&#xff1a;Hopper/B…