HarmonyOS 实战——万字分析并学习 JsFACard 项目
在上一篇中学习原子化服务的文 HarmonyOS 实战——认识服务卡片及运行第一个服务卡片 的后半部分学习了如何运行官方提供的案例,运行效果如下:
这篇文就官方提供的 JsFACard 源码进行学习。JsFACard 的命名是因为这个项目主要是基于 JavaScript 进行实现的 FA(Feature Ability,元服务,代表有界面的 Ability,用于与用户进行交互) 卡片服务(Card 的由来)。
完成了 JsFACard 案例学习应该能够掌握以下技能:
项目结构
主要的内容,即 src 目录下的内容如下:
也就是下面的结构:
|- src
| |- main
| | |- java
| | | |- 存储其他相关对象的目录
| | | | |- ...
| | | |- MainAbility
| | | |- MyApplication
| | |- js
| | | |- card
| | | | |- common
| | | | |- i18n
| | | | |- pages
| | | | | |- index
| | | | | | |- index.css
| | | | | | |- index.hml
| | | | | | |- index.json
| | | |- default
| | | | |- ...
| | | | |- app.js
| | | |- jscardtemplate
| | | | |- 结构一样
| | | |- jsmusictemplate
| | | | |- 结构一样
| | |- resources
| | |- config.json
| |- ohosTest
基础结构相对而言还是比较简单,理解起来也不是非常的复杂,不过刚开始看的时候不知道还需要写 java 就有些蒙逼。不过最终还是下载了 JsFACard 才算搞明白,原来使用 JavaScript 开发不代表纯 JavaScript 开发这个道理。
上文内容所包含的参考资料有:
源码学习
这是官方提供的案例,下载地址在:https://gitee.com/openharmony/app_samples/tree/master/UI/JsFACard。
config.json
完整的配置文件可以在案例中的 JsFACard / entry / src / main / config.json 看到网址在:https://gitee.com/openharmony/app_samples/blob/master/UI/JsFACard/entry/src/main/config.json。
config.json 的大体结构如下:
可以看出来,config.json 有三个最大的,不可缺省 的模块:
module
这是重点,module 部分管理所有当前 HAP(HarmonyOS Ability Package) 的配置信息。每个 HAP 是 Ability 的部署包,Ability 为 应用/服务 的基本组成单位,我的理解是某个功能的具体实现。
这里会结合项目结构对 module 中的内容进行分析和学习。
-
package,不可缺省 package 是 HAP 的包结构名称,在应用内应保证唯一性,建议与 HAP 的工程目录保持一致。 例如说这个项目的 package 值是 ohos.samples.jsfacard ,与 HAP 的工程目录是保持一致的(毕竟官方自己建议这么实施)。 -
name,不可缺省 HAP 的类名,前缀需要与同级的 package 标签指定的包名一致,也可采用 . 开头的命名方式。 也就是使用绝对定位和相对定位的关系,原本的值使用的是相对定位,也就是采用 . 开头的命名方式:.MainAbility 。这种情况下,系统运行时回去寻找 package 下的类名进行打包。 本机测试采用绝对定位的方式,也就是 ohos.samples.jsfacard.MainAbility 一样可以运行。 -
mainAbility 表示 HAP 包的入口 ability 名称,如果存在 page 类型的 ability 就不可缺省。 案例情况下,name 和 mainAbility 指向的是同一个类——ohos.samples.jsfacard.MainAbility 。 -
deviceType,不可缺省 当前 应用/服务 可运行的设备,接受的参数为字符串类型。预设的时候只勾选了手机,所以这里的值是 ["phone"] 。 -
distro,不可缺省 HAP 发布的具体描述,包含的信息有:
-
deliveryWithInstall,不可缺省,建议设置为 true 当前 HAP 是否支持随应用安装。 设置 false 就代表了安装应用不会安装当前 HAP 的意思?不是很明白这个特性是什么意思。 -
moduleName 模块名称,这点抬头看最上面即可: 值是 entry -
moduleType 表示 HAP 的类型,目前只能在 entry 和 feature 中选择。 -
installationFree 免安装特性,JsFACard 默认值是 false,感觉设置为 true 也可以吧。 -
abilities,可缺省 数组格式,其中每个元素表示一个提供的服务,整个数组代表着所有提供的服务。 -
js,可缺省 数组格式,表示基于 JS UI 框架开发的 JS 模块集合,其中的每个元素代表一个 JS 模块的信息。 在 JsFACard 之中,除了 default 是默认程序的 UI 之外,其余每一个 JS 对象所对应的都是一个 forms,也就是卡片服务:
abilities
JsFACard 项目里值包含了一个对象,对象的中的键值对包含:
-
skills 表示 Ability 能够接收的 Intent 的特征,一般使用的时候系统预定义内容。JsFACard 中的配置使用的就是系统预定义内容,具体配置如下: {
"skills": [
{
"entities": ["entity.system.home"],
"actions": ["action.system.home"]
}
]
}
-
name 表示 ability 的名称。 鉴于这只是一个 Demo 项目,并且项目的入口和服务的入口是一样的,所以这里的值依旧是 .MainAbility -
icon 图标,注意这里的值使用的是 $media:icon ,注意看提示: icon 接受值的格式就是 $media:some-value 这样一个格式。 资源(resources) 下存放资源是有一定程度上的固定格式的。例如说 resources 下存放资源是需要有两级目录,一级子目录为 base 目录 和 限定词目录,二级子目录为资源目录,图解如下: resources
|---base // 默认存在的目录
| |---element
| | |---string.json
| |---media
| | |---icon.png
|---en_GB-vertical-car-mdpi // 限定词目录示例,需要 开发者自行创建
| |---element
| | |---string.json
| |---media
| | |---icon.png
|---rawfile // 默认存在的目录
因为 base 是系统默认存在的目录,当资源目录中没有与设备状态匹配的限定词目录时,会自动引用该目录中的资源文件。以 $media:some-value 为例,系统会自动匹配名为 some-value 的多媒体文件。如果出现多个名字相同的资源,则会默认匹配第一个资源。 更多细节可以参考:资源文件的分类 中的具体条款。 -
description 特性和 icon 相似,格式依旧是 $string:some-value ,并且会自动匹配第一条数据。 -
formsEnabled 表示服务是否支持 卡片(forms) 功能,仅是用一 page 类 -
label 特性和 icon 和 description 相似,格式依旧是 $string:some-value ,并且会自动匹配第一条数据。 -
type 表示服务的类型:
-
forms 服务卡片的属性,仅在 "formsEnabled": true 时才会起效。接受数据为数组,数组中的每一个对象就是一个服务卡片的属性。 卡片的属性就比较直截了当,没有什么特别难理解的地方: {
"jsComponentName": "jsmusictemplate",
"isDefault": true,
"formConfigAbility": "ability://ohos.samples.jsfacard.MainAbility",
"scheduledUpdateTime": "10:30",
"defaultDimension": "2*4",
"name": "jsmusictemplate",
"description": "This is a service widget",
"colorMode": "auto",
"type": "JS",
"supportDimensions": ["2*4"],
"updateEnabled": true,
"updateDuration": 1
}
-
launchType 这里的值是 standard ,表明服务可以有多个实例,适用于大多数应用场景
jsmusictemplate 的渲染效果如下:
卡片的 UI 暂且不论,上面的数据,如 This is a service widget (forms > description ) 和 Js卡片 (abilities > label ) 均来源于配置。
js
js 接受的也是数组类型,每个数组里面是一个对象:
{
"pages": ["pages/index/index"],
"name": "jsmusictemplate",
"window": {
"designWidth": 720,
"autoDesignWidth": true
},
"type": "form"
}
滚过一遍 module 和 abilities 就差不多知道这些配置都代表什么意思了。
js 数组中提供的是卡片的 UI 布局,对象中的 name 与 forms 包含对象中的 jsComponentName 。
至此,配置内容已经了解的差不多了,更多更具体的内容可以查看 应用配置文件 接下来可以开始着手了解实现的部分。
Java 部分
Java 部分的代码量不是很多,毕竟这个服务卡片的内容其实不是很多,主要的结构如下:
类
MainAbility 是主程序入口,可以选择继承 AceAbility 或 Ability ,这里选择继承的是 AceAbility ,应该是可以方便一些,毕竟根据官方文档来说,AceAbility 继承了 Ability :
public class AceAbility
extends Ability
implements IAbilityContinuation
实现 MainAbility 主要是为了对服务的生命周期进行管理,官方提供的生命周期为:
成员变量
在 MainAbility 中声明的几个成员变量有:
public class MainAbility extends AceAbility {
private static final HiLogLabel TAG = new HiLogLabel(HiLog.DEBUG, 0x0, MainAbility.class.getName());
private static final String STATUS = "status";
private static final String PLAY = "play";
private static final String PAUSE = "pause";
private static boolean isStatus = true;
}
其中包含:
方法
JsFACard 中重载的方法不是很多,大部分都是调用 super.method() 去调用父类中已经实现的方法,而非重载。直接调用父类实现的方法包含:
-
void onStart(Intent intent) 必须调用这个函数去设置 UI,在整个服务的生命周期中只能被调用一次 -
ProviderFormInfo onCreateForm(Intent intent) 调用这个函数会返回一个 ProviderFormInfo 对象,用于在 UI 上显示基础的卡片信息,以及向用户提供一个卡片服务 -
void onUpdateForm(long formId) 调用函数去通知卡片服务提供商去更新特定的卡片 -
void onDeleteForm(long formId) 调用函数去通知卡片服务提供商去删除特定的卡片
重载的方法有:
-
void onTriggerFormEvent(long formId, String message) 代码如下: public class MainAbility extends AceAbility {
@Override
protected void onTriggerFormEvent(long formId, String message) {
super.onTriggerFormEvent(formId, message);
ZSONObject zsonObject = new ZSONObject();
if (isStatus) {
zsonObject.put(STATUS, PAUSE);
isStatus = false;
} else {
zsonObject.put(STATUS, PLAY);
isStatus = true;
}
FormBindingData formBindingData = new FormBindingData(zsonObject);
try {
updateForm(formId, formBindingData);
} catch (FormException e) {
HiLog.info(TAG, "onTriggerFormEvent:" + e.getMessage());
}
}
}
这个函数会根据触发的事件要操作的行为去进行下面的操作:
-
创建新的 ZSONObject 对象 -
将对应的状态存储到 ZSONObject 对象中 -
更新成员变量 isStatus -
将 ZSONObject 对象 写入 FormBindingData 对象 中去 -
调用更新卡片的功能去将 FormBindingData 写入对应的卡片服务中去,从而实现卡片服务 这一步实现了卡片数据的交互,写入进卡片的数据有两种: "status": "play" 和 "status": "pause" ,这两个值在之后的 JS 部分中会有用。
在 JS 卡片开发指导 中对调用的 API 以及对实现有更具体的描写。
所以说 Ability 到底是不是 Controller 的一种来着,感觉有点像啊……
JavaScript 部分
JavaScript 部分内容分为两块:
-
模块入口 注意,这不是主程序入口,主程序入口依旧是 Java 中的 MainAbility 。模块目录结构如下: 还是比较直观的,pages 负责页面的不同组件,其下 hml 文件是 HML 的模板文件,负责框架;css 文件负责样式;js 文件负责行为。 app.js 负责应用级别的生命周期管理。 具体程序内容这里不会详细学习,简单的了解一下内容即可。 -
卡片服务应用 这里以 jsmusictemplate 为例,主要是因为 jsmusictemplate 实现的功能比其他两个卡片服务更多一些。 jsmusictemplate 的目录结构如下: 可以看到,卡片服务和 FA 服务的结构是非常相似的,最大的区别在于 pages 下存在一个 .json 文件,而非 .js 文件。 这大概是因为这个页面的逻辑比较简单,不需要其他一些默认值和函数,所以使用 .json 文件 实现会简单一些。在另一个更加复杂的项目——JS 计步器卡片中,使用的依旧是 .js 文件:
jsmusictemplate
这里主要学习的依旧是 jsmusictemplate ,先来看看这个页面长什么样的:
可以看出页面被规划成了 左边的音乐播放 和 右边的常用功能 两个部分。.hml , .css 和 .json 应该就是基于这两个模块进行实现的。
hml
鉴于 .hml 文件 是 HTML 的模板文件,基于之前的分析,实现起来应该是这样的结构:
|- container
| |- play-music
| |- shortcuts
实现的结构也是这样的:
-
音乐播放功能 其中,播放音乐的功能使用了 stack 标签 去实现,根据 文档-stack 上所描述,这个标签起到的是让元素堆叠的效果,也就是让 svg 文件堆叠在背景图片上。 stack 标签 会让后面的元素堆叠到前面的元素上,因此结构里第一个元素是背景图片,第二个元素才是播放的 icon。 注意看一下 icon 的实现代码: <image
src="/common/{{ status }}.svg"
onclick="messageEvent"
class="status-image"
></image>
这里的功能其实与 Java 部分的代码和 json 功能都有联动,{{}} 应该是 Mustache Syntax,中间的 status 属于变量名,可以获得 status 这个变量。回想一下 Java 代码中会通过 updateForm 更新的状态:"status": "play" 和 "status": "pause" ,所以这里 src 的数据有两种:"/common/play.svg" 和 "/common/pause.svg" ,common 文件夹中的确存在这两个文件: 和 状态的变更则由 messageEvent 进行触发,这里的参数是由 json 提供的,等到 json 部分再去具体化。 -
快捷键功能 即右边的搜索、播放等功能,基本结构如下: <div class="main-div medium-display-index">
<div class="wrap-div medium-display-index">
<image src="/common/ic_search.svg" class="image-div"></image>
<text class="image-text">{{ $t('strings.search') }}</text>
</div>
<div class="wrap-div medium-display-index">
<image src="/common/ic_favor.svg" class="image-div"></image>
<text class="image-text">{{ $t('strings.favor') }}</text>
</div>
<div class="wrap-div small-display-index">
<image src="/common/ic_ranking.svg" class="image-div"></image>
<text class="image-text">{{ $t('strings.ranking') }}</text>
</div>
<div class="wrap-div small-display-index">
<image src="/common/ic_recommend.svg" class="image-div"></image>
<text class="image-text">{{ $t('strings.recommend') }}</text>
</div>
</div>
<div class="main-div small-display-index">
<div class="wrap-div medium-display-index">
<image src="/common/ic_ranking.svg" class="image-div"></image>
<text class="image-text">{{ $t('strings.ranking') }}</text>
</div>
<div class="wrap-div medium-display-index">
<image src="/common/ic_recommend.svg" class="image-div"></image>
<text class="image-text">{{ $t('strings.recommend') }}</text>
</div>
<div class="wrap-div small-display-index">
<image src="/common/ic_favor.svg" class="image-div"></image>
<text class="image-text">{{ $t('strings.favor') }}</text>
</div>
<div class="wrap-div small-display-index">
<image src="/common/ic_search.svg" class="image-div"></image>
<text class="image-text">{{ $t('strings.search') }}</text>
</div>
</div>
这里分别使用了两个 div 去实现不同的功能,而在不同 div 之间,元素的类名是不一样的,这是通过 CSS 去控制显示的内容,去进行布局。 只显示一个 div 和同时显示两个 div 的效果如下:
可以看到元素中都是 small-display-index 的元素被隐藏了,这是由 CSS 控制的。
css
CSS 的大部分内容都是比较常见的,除了 small-display-index 和 medium-display-index 中使用的 display-index 之前是没有见过的。
display-index 是 原子布局 中的新特性,文档中的说明是:
该适用于 div 等支持 flex 布局的容器组件中的子组件上,当容器组件在 flex 主轴上尺寸不足以显示下全部内容时,按照 display-index 值从小到大的顺序进行隐藏,具有相同 display-index 值的组件同时隐藏,默认值为 Infinity ,表示不隐藏。
更具另外一份文档,也就是 通用样式 中可以得知,display 的默认值是 flex ,所以当空间不够的时候,small-display-index 的值就会被隐藏掉。
至于官方为什么这么实现,我觉得和多端适配有关系,下面是平板上显示的效果,能看到和手机上的效果完全不一样:
这个布局看起来是完全隐藏了 small-display-index ,只显示 medium-display-index 中内容。因为在平板上的高度足够的关系,所以 medium-display-index 中的 4 个元素可以全都显示出来。
至于 small-display-index 和 medium-display-index 加起来的宽度,则通过 音乐播放 的界面去控制的。
关于布局这方面真的还需要好好学习一下。
json
json 相对而言是三个部分中最简单的部分,因为这里没有什么特别复杂的逻辑,完整的代码只有 13 行:
{
"data": {
"status": "play"
},
"actions": {
"messageEvent": {
"action": "message",
"params": {
"message": "music change status"
}
}
}
}
可以看到,在结构体中有 data 和 actions 两大模块,data 负责的就是数据,它所导出的数据被 src="/common/{{ status }}.svg" 所获取;actions 则负责事件,它所导出的数据被 onclick="messageEvent" 所获取。结合 Java 中的代码,音乐播放器中的事件流程就是这样的:
点击事件
java内部更新
触发JS状态更新
初始化
渲染
onTriggerFormEvent
更新Status
更新卡片
触发效果是这样的:
总结
至此,JsFACard 这个项目就已经掌握得差不多了,下一步学习的目标就打算学习一下另一个更加复杂的案例:JS 计步器卡片。
本文正在参与“有奖征文 | HarmonyOS 征文大赛”活动,活动链接为:https://marketing.csdn.net/p/ad3879b53f4b8b31db27382b5fc65bbc
|