HarmonyOS 实战——万字分析并学习 JsFACard 项目
Posted GoldenaArcher
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HarmonyOS 实战——万字分析并学习 JsFACard 项目相关的知识,希望对你有一定的参考价值。
HarmonyOS 实战——万字分析并学习 JsFACard 项目
在上一篇中学习原子化服务的文 HarmonyOS 实战——认识服务卡片及运行第一个服务卡片 的后半部分学习了如何运行官方提供的案例,运行效果如下:
![](https://image.cha138.com/20210829/96c668ed790b46ce949a1da3ca7fdaf2.jpg)
这篇文就官方提供的 JsFACard 源码进行学习。JsFACard 的命名是因为这个项目主要是基于 JavaScript 进行实现的 FA(Feature Ability,元服务,代表有界面的 Ability,用于与用户进行交互) 卡片服务(Card 的由来)。
完成了 JsFACard 案例学习应该能够掌握以下技能:
-
了解原子服务的基础结构
即 src 的项目结构
-
了解基础的卡片服务的配置
即通过修改 config.json 完成卡片服务的配置
-
了解卡片服务的结构
-
实现卡片服务的数据交换和状态更新
通过 json/js 实现状态管理,以及通过 Java 代码中的
onTriggerFormEvent
方法 实现卡片状态的更新
项目结构
主要的内容,即 src 目录下的内容如下:
![](https://image.cha138.com/20210829/566b1f4eaf4d4db3ac9f9980db31da6f.jpg)
也就是下面的结构:
|- src
| |- main # 主要内容
| | |- java # java 依旧会别用做为主要的实例管理模块
| | | |- 存储其他相关对象的目录
| | | | |- ...
| | | |- MainAbility # 主程序入口,DevEco Studio生成
| | | |- MyApplication # DevEco Studio生成,不需变更
| | |- js # 主要的呈现部分
| | | |- card # 卡片1号
| | | | |- common # 存放公共资源文件
| | | | |- i18n # 多语言支持
| | | | |- pages # 存放所有组件页面
| | | | | |- index # 入口,包含对应文件
| | | | | | |- index.css
| | | | | | |- index.hml
| | | | | | |- index.json
| | | |- default # 主程序,非 卡片
| | | | |- ... # 结构基本一致,除了没有 index.json 文件,取而代之的是 index.js
| | | | |- app.js # 用于全局javascript逻辑和应用生命周期管理
| | | |- jscardtemplate # 卡片2号
| | | | |- 结构一样
| | | |- jsmusictemplate # 卡片3号
| | | | |- 结构一样
| | |- resources # 共享资源
| | |- config.json # 配置文件
| |- ohosTest # 测试部分,这里不会赘述
基础结构相对而言还是比较简单,理解起来也不是非常的复杂,不过刚开始看的时候不知道还需要写 java 就有些蒙逼。不过最终还是下载了 JsFACard 才算搞明白,原来使用 JavaScript 开发不代表纯 JavaScript 开发这个道理。
上文内容所包含的参考资料有:
-
这篇文章讲述了
app.js
的用处 -
卡片服务始终还是依赖于 FA 而进行实现,换言之,不了解 FA 就无法实现卡片服务
-
这个案例是 JS 计步器卡片 这个案例,代码更加复杂一些,所以刚开始没有选择这个案例进行学习
-
服务卡片的文件组织
源码学习
这是官方提供的案例,下载地址在: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 有三个最大的,不可缺省 的模块:
-
app
表示应用的全局配置信息,同一个应用程序中,app 中的信息必须保持一致。
这里不会赘述。
-
deviceConfig
应用在具体设备上的配置信息,这里不会赘述。
-
module
这块是重点,会结合文档详细学习一下。
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
表示服务的类型:
-
page,基于 page 模板开发的 FA,用于提供与用户交互的能力。
也是这里用到的服务,其余的服务类型不多赘述。
-
…
-
-
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 的渲染效果如下:
![](https://image.cha138.com/20210829/e68f6ec500914e299561b19862f4a9fb.jpg)
卡片的 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;
}
其中包含:
-
HiLogLabel
日志类的辅助工具,用于定义日志类型、服务域名和标签
-
STATUS,PLAY,PAUSE
三个是静态常量,用于定义状态
-
isStatus
定义当前组件的状态,结合其他几个常量,应该是用来控制音乐的播放。
方法
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(); // 主要目的就是为了更新 isStatus 的状态 和更新 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 的效果如下:
-
保留父元素为
medium-display-index
即,保留第一个 div
-
保留父元素为
small-display-index
即,保留第二个 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 中的代码,音乐播放器中的事件流程就是这样的: