1. 分层架构逻辑模型解析
HarmonyOS应用的分层架构主要包括三个层次:产品定制层products、基础特性层features和公共能力层commons,为开发者构建了一个清晰、高效、可扩展的设计架构。
- 不同设备意味着不同的入口 。products是个入口,可能包含手机 + 平板 + 2in1 + 手表 + Car
- Hap-entry
- Hsp-共享包
- Har-静态共享包
products-hap包
features-hap包和hsp包
commons-hsp包
三层: 三个大的文件夹. UI界面适配的。可复用的业务逻辑、组合各个模块
- 产品定制层:产品定制层专注于满足不同设备或使用场景(如应用)的个性化需求,包括UI设计、资源和配置,以及针对特定场景的交互逻辑和功能特性。产品定制层的功能模块独立运作,同时依赖基础特性层和公共能力层来实现具体功能。作为应用的入口,产品定制层是用户直接互动的界面。为满足特定产品需求,产品定制层可灵活地调整和扩展,从而满足各种使用场景。
- 基础特性层:基础特性层位于公共能力层之上,用于存放基础特性集合,例如相对独立的功能UI和业务逻辑实现。该层的每个功能模块都具有高内聚、低耦合、可定制的特点,以支持产品的灵活部署。基础特性层为上层的产品定制层提供稳健且丰富的基础功能支持,包括UI组件、基础服务等。同时依赖于下层的公共能力层为其提供通用功能和服务。为了增强系统的可扩展性和维护性,基础特性层将功能进行模块化处理。例如,一个应用的底部导航栏中的每个选项都可能是一个独立的业务模块。
- 公共能力层:公共功能层用于存放公共基础能力,集中了例如公共UI组件、数据管理、外部交互以及工具库等的共享功能。应用可以共享和调用这些公共能力。公共能力层为上层的基础特性层和产品定制层提供稳定可靠的功能支持,确保整个应用的稳定性和可维护性。公共能力层包括但不限于以下组成:
-
- 公共UI组件:这些组件被设计成通用且高度可复用的,确保在不同的应用程序模块间保持一致的用户体验。公共UI组件提供了标准化且友好的界面,帮助开发者快速实现常见的用户交互需求,例如提示、警告、加载状态显示等,从而提高开发效率和用户满意度。
- 数据管理:负责应用程序中数据的存储和访问,包括应用数据、系统数据等,提供了统一的数据管理接口,简化数据的读写操作。通过集中式的数据管理方式不仅使得数据的维护更为简单,而且能够保证数据的一致性和安全性。
- 外部交互:负责应用程序与外部系统的交互,包括网络请求、文件I/O、设备I/O等,提供统一的外部交互接口,简化应用程序与外部系统的交互。开发者可以更为方便地实现应用程序的网络通信、数据存储和硬件接入等功能,从而加速开发流程并保证程序的稳定性和性能。
- 工具库:提供一系列常用工具函数和类,例如字符串处理、日期时间处理、加密解密、数据压缩解压等,帮助开发者提高效率和代码质量。
Hap-entry
Hsp-共享包
Har包-静态共享包
1.1. 开发模型
图2 分层架构开发模型
- 产品定制层产品定制层的各个子目录会被编译成一个Entry类型的HAP,作为应用的主入口。该层主要针对跨多种设备,为各种设备形态集成相应的功能和特性。产品定制层被划分为多个功能模块,每个功能模块都针对特定的设备或使用场景设计,并根据具体的产品需求进行功能及交互的定制开发。说明
-
- 在产品定制层,开发者可以从不同设备对应应用的UX设计和功能两个维度,结合具体的业务场景,选择一次编译生成相同或者不同的HAP(或其组合)。
- 通过使用定制多目标构建产物的定制功能,可以将应用所对应的HAP编译成各自的.app文件,用于上架到应用市场。
- 基础特性层在基础特性层中,功能模块根据部署需求被分为两类。对于需要通过Ability承载的功能,可以设计为Feature类型的HAP,而对于不需要通过Ability承载的功能,根据是否需要实现按需加载,可以选择设计为HAR模块或者HSP模块,编译后对应HAR包或者HSP包。
- 公共能力层公共能力层的各子目录将被编译成HAR包,而他们只能被产品定制层和基础特性层所依赖,不允许存在反向依赖。该层旨在提取模块化公共基础能力,为上层提供标准接口和协议,从而提高整体的复用率和开发效率。
1.2. 部署模型
图3 分层架构部署模型(不同设备的定制)
应用程序(.app文件)在流水线或应用市场上被解包为n * Entry类型的HAP + n * Feature类型的HAP,根据设备类型和使用场景将应用部署到不同类型的设备上,实现多端的统一用户体验。
说明
当Entry类型的HAP和Feature类型的HAP被分发并部署到相应的设备时,他们所依赖的HSP也会一同被分发并部署到相应的设备上。
在部署模型中,每个Entry类型的HAP代表了应用的入口点,而Feature类型的HAP则包含了应用的特定功能模块。允许应用能够以模块化的方式适配和部署,从而满足不同设备和场景的需求。
该部署模型不仅优化了应用的组织结构,也为保持应用在各种设备和场景中的一致性提供了支持。通过按照设备类型和使用场景来区分和部署不同的HAP,能确保无论在何种设备或场景中,用户都能获得统一且高质量
腾讯的面试官问了个这么个问题:har会多份拷贝,hsp不会,但是hsp不会进行资源共享,有什么方案吗?
腾讯用hsp包了一个har,向外暴露
1.3. 单层项目架构
ets
├── common // 通用模块
│ ├── builders // - 通用组件 @Builder
│ ├── components // - 通用组件 @Component
│ ├── constants // - 常量数据
│ ├── images // - 图片资源
│ └── utils // - 工具类
├── entryability // 入口UIAbility
│ └── EntryAbility.ts
├── models // - 数据模型
├── pages // - 页面组件
│ ├── HomePage.ets
│ └── Index.ets
└── views // - 页面对应自定义组件
├── HomePage
└── Index
三层项目架构官方代码实例
1.4. 三层架构
project-root
├── commons // 基础层(通用组件、工具类、资源)
│ └── basic // - 通用模块
├── features // 需求层(业务组件)
│ ├── home // - 需求模块
│ └── mine // - 需求模块
└── products // 入口层(页面)
└── phone // - 设备模块
2. 创建项目-用git管理
使用DevEcoSudio创建一个空项目
gitee创建仓库
复制地址
创建推送成功。
3. 创建一个静态har包, 导入素材和资源
本次项目采用单层架构 + har公共包的模式进行开发
- 新建一个common目录, 在该目录下新建一个静态模块
Static Library
建好了
- 下载该media文件夹,将所有的图标素材拖动到静态资源包下的 src/main/resources/base/media下
- 导入色值
{
"color": [
{
"name": "start_window_background",
"value": "#FFFFFF"
},
{
"name": "primary",
"value": "#FE6A3D"
},
{
"name": "primary_disabled",
"value": "#FADCD9"
},
{
"name": "success",
"value": "#27BA9B"
},
{
"name": "warning",
"value": "#FFAB2E"
},
{
"name": "danger",
"value": "#FF4C4C"
},
{
"name": "text_primary",
"value": "#2A2929"
},
{
"name": "text_secondary",
"value": "#818181"
},
{
"name": "text_placeholder",
"value": "#C2C1C1"
},
{
"name": "border",
"value": "#D9D9D9"
},
{
"name": "background_divider",
"value": "#EEEEEE"
},
{
"name": "background_page",
"value": "#F4F4F4"
},
{
"name": "black",
"value": "#000000"
},
{
"name": "white",
"value": "#FFFFFF"
},
{
"name": "gray_1",
"value": "#F7F8FA"
},
{
"name": "gray_2",
"value": "#F2F3F5"
},
{
"name": "gray_3",
"value": "#EBEDF0"
},
{
"name": "gray_4",
"value": "#DEDEE0"
},
{
"name": "gray_5",
"value": "#C8C9CC"
},
{
"name": "gray_6",
"value": "#969799"
},
{
"name": "gray_7",
"value": "#646566"
},
{
"name": "gray_8",
"value": "#323233"
},
{
"name": "btn_plain",
"value": "#FFE0DD"
},
{
"name": "btn_gray",
"value": "#E6E6E6"
},
{
"name": "upload_panel",
"value": "#f2f2f2"
}
]
}
4. 修改项目名称和图标
- 修改项目名称
- 通过生成图标工具创建app图标
生成后复制替换,删除多余项
5. 公共静态包(HAR)基础目录搭建
在har包中建立如下目录
- 按照客户端开发的需要我们要在项目中建立如下结构
- 在项目中添加上述的目录,并统一新建一个index.ets文件
- 清空har包下index.ets文件内容
6. createSubWindow广告展示页
广告页的思路
-华为有广告业务,但是我们不用- ad模块
想自定义广告-
场景: app启动-有广告需求,就打开广告页,没有的话就去登录或者主页
腾讯体育的广告- 启动有广告页,退到后台的情况下,再次进入前台也会有广告
分析- 广告页作为一个app启动的首页,应该是在我们应用启动就进去的。
- 有的app有的需要广告页,有的不需要,搞个配置呗!!!
- 通过首选项配置存储我们的一些常用配置,比如要不要广告页,还有广告页的路由地址,点击广告页跳转的链接,广告页倒计时的秒数
- 在入口处进行判断是否需要广告页,需要的话,跳转广告页-广告页根据设置的参数进行渲染
- 有的同学可能会问,广告页能不能设置-因为运营人员肯定不能每次都去改我们底层的代码-这里我还可以设置成动态的-就是初始化的时候通过请求去读一下云端的请求,然后把我们的图片和一些参数配置下来,这样每次你启动app就是运营人员给你配置的广告和设置了
- 新建一个关于广告类的数据模型-basic/models/advert.ets
- 在utils中新建一个关于读取首选项的类,用来读取和设置首选项的广告设置
//读写广告配置的class
import { USER_SETTING,USER_SETTING_AD }from '../constants'
import { preferences } from '@kit.ArkData'
import { Context } from '@ohos.abilityAccessCtrl'
import { AdvertClass } from '../../../../Index'
//默认广告
const defaultAd:AdvertClass={
showAd:true,
adTime:5,
adImg:$r("app.media.start")
}
export class UserSettingClass{
//此时需要上下文和读写方法
context:Context
constructor(ctx:Context) {
this.context=ctx
}
//获取仓库
getStore(){
return preferences.getPreferencesSync(this.context,{
name:USER_SETTING
})
}
//设置用户的广告配置
async setUserAd(ad:AdvertClass){
const store =this.getStore()//拿到仓库
store.putSync(USER_SETTING_AD,JSON.stringify(ad))//放入仓库
await store.flush()//写入磁盘
}
//读取用户的广告配置
getUserAd(){
const store =this.getStore()//拿到仓库
return JSON.parse(store.getSync(USER_SETTING_AD,JSON.stringify(defaultAd)) as string) as AdvertClass//拿到广告
}
}
- 上面还用到了两个常量,我们同样需要在constants目录下定义一个文件专门用来记录-setting
entry模块-oh-package.json5
- 在ability中引入该har包依赖,就可以在entry中用我刚才的那些东西了
ability中判断
这里我们模拟了一个请求,给了一个默认广告, 写入首选项-正常加载主页
- 在pages下新建Start目录,下面新建Start的page页面
import { UserSettingClass, AdvertClass } from '@hm/basic'
@Entry
@Component
struct Start {
userSetting: UserSettingClass = new UserSettingClass(getContext(this))
@State
adObj: AdvertClass = {
showAd: false,
adTime: 0
}
timer: number = -1
async aboutToAppear() {
this.adObj = await this.userSetting.getUserAd()
this.timer = setInterval(() => {
if(this.adObj.adTime === 0) {
clearInterval(this.timer)
return
}
this.adObj.adTime--
}, 1000)
}
build() {
Stack({ alignContent: Alignment.TopEnd }) {
Image(this.adObj.adImg).objectFit(ImageFit.Cover)
Text(`${this.adObj.adTime}秒后跳过`)
.padding({ left: 10, right: 10 })
.margin({ right: 20, top: 20 })
.height(30)
.fontSize(14)
.borderRadius(15)
.backgroundColor($r("app.color.background_page"))
.textAlign(TextAlign.Center)
}.height('100%').width('100%')
}
}
- 实现Start页的页面结构及倒计时逻辑
- 使用子窗口模式加载广告
我们可以使用windowStage的createSubWindow来实现在当前页面上创建一个窗口
- 广告页在广告结束时,关闭广告
Start/Start页面
实现点击跳过广告
效果:
实现每次打开都有广告:
7. 检查token跳登录页
由于司机端是必须要求用户登录的,所以我们需要在跳转到主页前检查是否有token,有token的话跳转到主页,没有token的话跳转到登录
Entry模块
- 新建登录页- pages/Login/Login
Har包basic
- 在广告页跳转前检查token,有的话跳转到主页 Index,没有的话跳转到Login
- 声明一个token_key的常量-constants/user.ets
这里需要在首选项中在声明两个方法
Entry模块,实现开启app判断有无token跳转页面
8. 登录页界面
Entry模块:登录信息属于业务的数据,所以内容留存在当前的entry模块中
pages/Login/Login.ets
//登录页面
@Entry
@Component
struct Login {
@State showLoading: boolean = false//控制进度条显示
@Styles
loginStyle() {
.backgroundColor('#fff')
.border({ color: $r('app.color.background_divider'), width: { bottom: 1 } })
.width('100%')
.height(58)
.borderRadius(0)
}
build() {
Column() {
// 顶部标题
Text("鲨鱼快递").fontColor($r('app.color.text_primary')).fontSize(18).height(25)
// 账号登录
Row() {
Text('账号登录').fontColor($r('app.color.text_primary')).fontSize(24).fontWeight(FontWeight.Bold)
Row() {
Text("手机号登录").fontColor($r('app.color.primary')).fontSize(16).fontWeight(FontWeight.Bold)
Image($r("app.media.ic_angle")).width(10).height(10).margin({ left: 5 })
}
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ top: 50, bottom: 50 })
// 用户名输入框
TextInput({ placeholder: '请输入账号' })
.loginStyle()
// 密码框
TextInput({ placeholder: '请输入密码' })
.loginStyle()
.type(InputType.Password) // 密码框
.showPasswordIcon(true) // 显示密码按钮
// 登录按钮
Button({ type: ButtonType.Capsule }) {
Row() {
if (this.showLoading) {
LoadingProgress().width(20).height(20).margin({ right: 12 }).color($r('app.color.white'))
}
Text('登录').fontColor($r('app.color.white'))
}
}
.backgroundColor($r('app.color.primary_disabled'))
.width('100%')
.height(50)
.margin({ top: 50 })
}