1# 服务卡片开发指导 2 3 4## 卡片概述 5 6服务卡片(以下简称“卡片”)是一种界面展示形式,可以将应用的重要信息或操作前置到卡片,以达到服务直达、减少体验层级的目的。 7 8卡片常用于嵌入到其他应用(当前只支持系统应用)中作为其界面的一部分显示,并支持拉起页面、发送消息等基础的交互功能。 9 10卡片的基本概念: 11 12- 卡片使用方:显示卡片内容的宿主应用,控制卡片在宿主中展示的位置。 13 14- 卡片管理服务:用于管理系统中所添加卡片的常驻代理服务,包括卡片对象的管理与使用,以及卡片周期性刷新等。 15 16- 卡片提供方:提供卡片显示内容原子化服务,控制卡片的显示内容、控件布局以及控件点击事件。 17 18 19## 运作机制 20 21卡片框架的运作机制如图1所示。 22 23 **图1** 卡片框架运作机制(FA模型) 24![form-extension](figures/form-extension.png) 25 26卡片使用方包含以下模块: 27 28- 卡片使用:包含卡片的创建、删除、请求更新等操作。 29 30- 通信适配层:由OpenHarmony SDK提供,负责与卡片管理服务通信,用于将卡片的相关操作到卡片管理服务。 31 32卡片管理服务包含以下模块: 33 34- 周期性刷新:在卡片添加后,根据卡片的刷新策略启动定时任务周期性触发卡片的刷新。 35 36- 卡片缓存管理:在卡片添加到卡片管理服务后,对卡片的视图信息进行缓存,以便下次获取卡片时可以直接返回缓存数据,降低时延。 37 38- 卡片生命周期管理:对于卡片切换到后台或者被遮挡时,暂停卡片的刷新;以及卡片的升级/卸载场景下对卡片数据的更新和清理。 39 40- 卡片使用方对象管理:对卡片使用方的RPC对象进行管理,用于使用方请求进行校验以及对卡片更新后的回调处理。 41 42- 通信适配层:负责与卡片使用方和提供方进行RPC通信。 43 44卡片提供方包含以下模块: 45 46- 卡片服务:由卡片提供方开发者实现,开发者实现生命周期处理创建卡片、更新卡片以及删除卡片等请求,提供相应的卡片服务。 47 48- 卡片提供方实例管理模块:由卡片提供方开发者实现,负责对卡片管理服务分配的卡片实例进行持久化管理。 49 50- 通信适配层:由OpenHarmony SDK提供,负责与卡片管理服务通信,用于将卡片的更新数据主动推送到卡片管理服务。 51 52> **说明:** 53> 实际开发时只需要作为卡片提供方进行卡片内容的开发,卡片使用方和卡片管理服务由系统自动处理。 54 55 56## 接口说明 57 58FormAbility生命周期接口如下: 59 60| 接口名 | 描述 | 61| -------- | -------- | 62| onCreate(want: Want): formBindingData.FormBindingData | 卡片提供方接收创建卡片的通知接口。 | 63| onCastToNormal(formId: string): void | 卡片提供方接收临时卡片转常态卡片的通知接口 | 64| onUpdate(formId: string): void | 卡片提供方接收更新卡片的通知接口。 | 65| onVisibilityChange(newStatus: { [key: string]: number }): void | 卡片提供方接收修改可见性的通知接口。 | 66| onEvent(formId: string, message: string): void | 卡片提供方接收处理卡片事件的通知接口。 | 67| onDestroy(formId: string): void | 卡片提供方接收销毁卡片的通知接口。 | 68| onAcquireFormState?(want: Want): formInfo.FormState | 卡片提供方接收查询卡片状态的通知接口。 | 69| onShare?(formId: string): {[key: string]: any} | 卡片提供方接收卡片分享的通知接口。 | 70 71FormProvider类有如下API接口,具体的API介绍详见[接口文档](../reference/apis/js-apis-app-form-formProvider.md)。 72 73 74| 接口名 | 描述 | 75| -------- | -------- | 76| setFormNextRefreshTime(formId: string, minute: number, callback: AsyncCallback<void>): void; | 设置指定卡片的下一次更新时间。 | 77| setFormNextRefreshTime(formId: string, minute: number): Promise<void>; | 设置指定卡片的下一次更新时间,以promise方式返回。 | 78| updateForm(formId: string, formBindingData: FormBindingData, callback: AsyncCallback<void>): void; | 更新指定的卡片。 | 79| updateForm(formId: string, formBindingData: FormBindingData): Promise<void>; | 更新指定的卡片,以promise方式返回。 | 80 81 82formBindingData类有如下API接口,具体的API介绍详见[接口文档](../reference/apis/js-apis-app-form-formBindingData.md)。 83 84 85| 接口名 | 描述 | 86| -------- | -------- | 87| createFormBindingData(obj?: Object \| string): FormBindingData | 创建一个FormBindingData对象。 | 88 89 90## 开发步骤 91 92FA卡片开发,即基于[FA模型](fa-model-development-overview.md)的卡片提供方开发,主要涉及如下关键步骤: 93 94- [实现卡片生命周期接口](#实现卡片生命周期接口):开发FormAbility生命周期回调函数。 95 96- [配置卡片配置文件](#配置卡片配置文件):配置应用配置文件config.json。 97 98- [卡片信息的持久化](#卡片信息的持久化):对卡片信息进行持久化管理。 99 100- [卡片数据交互](#卡片数据交互):通过updateForm()更新卡片显示的信息。 101 102- [开发卡片页面](#开发卡片页面):使用HML+CSS+JSON开发JS卡片页面。 103 104- [开发卡片事件](#开发卡片事件):为卡片添加router事件和message事件。 105 106 107### 实现卡片生命周期接口 108 109创建FA模型的卡片,需实现卡片的生命周期接口。先参考[IDE开发服务卡片指南](https://developer.harmonyos.com/cn/docs/documentation/doc-guides/ohos-development-service-widget-0000001263280425)生成服务卡片模板。 110 1111. 在form.ts中,导入相关模块 112 113 ```ts 114 import formBindingData from '@ohos.app.form.formBindingData'; 115 import formInfo from '@ohos.app.form.formInfo'; 116 import formProvider from '@ohos.app.form.formProvider'; 117 import dataStorage from '@ohos.data.storage'; 118 ``` 119 1202. 在form.ts中,实现卡片生命周期接口 121 122 ```ts 123 export default { 124 onCreate(want) { 125 console.info('FormAbility onCreate'); 126 // 使用方创建卡片时触发,提供方需要返回卡片数据绑定类 127 let obj = { 128 "title": "titleOnCreate", 129 "detail": "detailOnCreate" 130 }; 131 let formData = formBindingData.createFormBindingData(obj); 132 return formData; 133 }, 134 onCastToNormal(formId) { 135 // 使用方将临时卡片转换为常态卡片触发,提供方需要做相应的处理 136 console.info('FormAbility onCastToNormal'); 137 }, 138 onUpdate(formId) { 139 // 若卡片支持定时更新/定点更新/卡片使用方主动请求更新功能,则提供方需要重写该方法以支持数据更新 140 console.info('FormAbility onUpdate'); 141 let obj = { 142 "title": "titleOnUpdate", 143 "detail": "detailOnUpdate" 144 }; 145 let formData = formBindingData.createFormBindingData(obj); 146 formProvider.updateForm(formId, formData).catch((error) => { 147 console.info('FormAbility updateForm, error:' + JSON.stringify(error)); 148 }); 149 }, 150 onVisibilityChange(newStatus) { 151 // 使用方发起可见或者不可见通知触发,提供方需要做相应的处理,仅系统应用生效 152 console.info('FormAbility onVisibilityChange'); 153 }, 154 onEvent(formId, message) { 155 // 若卡片支持触发事件,则需要重写该方法并实现对事件的触发 156 console.info('FormAbility onEvent'); 157 }, 158 onDestroy(formId) { 159 // 删除卡片实例数据 160 console.info('FormAbility onDestroy'); 161 }, 162 onAcquireFormState(want) { 163 console.info('FormAbility onAcquireFormState'); 164 return formInfo.FormState.READY; 165 }, 166 } 167 ``` 168 169> **说明:** 170> FormAbility不能常驻后台,即在卡片生命周期回调函数中无法处理长时间的任务。 171 172### 配置卡片配置文件 173 174卡片需要在应用配置文件config.json中进行配置。 175 176- js模块,用于对应卡片的js相关资源,内部字段结构说明: 177 | 属性名称 | 含义 | 数据类型 | 是否可缺省 | 178 | -------- | -------- | -------- | -------- | 179 | name | 表示JS Component的名字。该标签不可缺省,默认值为default。 | 字符串 | 否 | 180 | pages | 表示JS Component的页面用于列举JS Component中每个页面的路由信息[页面路径+页面名称]。该标签不可缺省,取值为数组,数组第一个元素代表JS FA首页。 | 数组 | 否 | 181 | window | 用于定义与显示窗口相关的配置。 | 对象 | 可缺省 | 182 | type | 表示JS应用的类型。取值范围如下:<br/>normal:标识该JS Component为应用实例。<br/>form:标识该JS Component为卡片实例。 | 字符串 | 可缺省,缺省值为“normal” | 183 | mode | 定义JS组件的开发模式。 | 对象 | 可缺省,缺省值为空 | 184 185 配置示例如下: 186 187 188 ```json 189 "js": [{ 190 "name": "widget", 191 "pages": ["pages/index/index"], 192 "window": { 193 "designWidth": 720, 194 "autoDesignWidth": true 195 }, 196 "type": "form" 197 }] 198 ``` 199 200- abilities模块,用于对应卡片的FormAbility,内部字段结构说明: 201 | 属性名称 | 含义 | 数据类型 | 是否可缺省 | 202 | -------- | -------- | -------- | -------- | 203 | name | 表示卡片的类名。字符串最大长度为127字节。 | 字符串 | 否 | 204 | description | 表示卡片的描述。取值可以是描述性内容,也可以是对描述性内容的资源索引,以支持多语言。字符串最大长度为255字节。 | 字符串 | 可缺省,缺省为空。 | 205 | isDefault | 表示该卡片是否为默认卡片,每个Ability有且只有一个默认卡片。<br/>true:默认卡片。<br/>false:非默认卡片。 | 布尔值 | 否 | 206 | type | 表示卡片的类型。取值范围如下:<br/>JS:JS卡片。 | 字符串 | 否 | 207 | colorMode | 表示卡片的主题样式,取值范围如下:<br/>auto:自适应。<br/>dark:深色主题。<br/>light:浅色主题。 | 字符串 | 可缺省,缺省值为“auto”。 | 208 | supportDimensions | 表示卡片支持的外观规格,取值范围:<br/>1 \* 2:表示1行2列的二宫格。<br/>2 \* 2:表示2行2列的四宫格。<br/>2 \* 4:表示2行4列的八宫格。<br/>4 \* 4:表示4行4列的十六宫格。 | 字符串数组 | 否 | 209 | defaultDimension | 表示卡片的默认外观规格,取值必须在该卡片supportDimensions配置的列表中。 | 字符串 | 否 | 210 | updateEnabled | 表示卡片是否支持周期性刷新,取值范围:<br/>true:表示支持周期性刷新,可以在定时刷新(updateDuration)和定点刷新(scheduledUpdateTime)两种方式任选其一,优先选择定时刷新。<br/>false:表示不支持周期性刷新。 | 布尔类型 | 否 | 211 | scheduledUpdateTime | 表示卡片的定点刷新的时刻,采用24小时制,精确到分钟。<br/>updateDuration参数优先级高于scheduledUpdateTime,两者同时配置时,以updateDuration配置的刷新时间为准。 | 字符串 | 可缺省,缺省值为“0:0”。 | 212 | updateDuration | 表示卡片定时刷新的更新周期,单位为30分钟,取值为自然数。<br/>当取值为0时,表示该参数不生效。<br/>当取值为正整数N时,表示刷新周期为30\*N分钟。<br/>updateDuration参数优先级高于scheduledUpdateTime,两者同时配置时,以updateDuration配置的刷新时间为准。 | 数值 | 可缺省,缺省值为“0”。 | 213 | formConfigAbility | 表示卡片的配置跳转链接,采用URI格式。 | 字符串 | 可缺省,缺省值为空。 | 214 | formVisibleNotify | 标识是否允许卡片使用卡片可见性通知。 | 字符串 | 可缺省,缺省值为空。 | 215 | jsComponentName | 表示JS卡片的Component名称。字符串最大长度为127字节。 | 字符串 | 否 | 216 | metaData | 表示卡片的自定义信息,包含customizeData数组标签。 | 对象 | 可缺省,缺省值为空。 | 217 | customizeData | 表示自定义的卡片信息。 | 对象数组 | 可缺省,缺省值为空。 | 218 219 配置示例如下: 220 221 222 ```json 223 "abilities": [{ 224 "name": "FormAbility", 225 "description": "This is a FormAbility", 226 "formsEnabled": true, 227 "icon": "$media:icon", 228 "label": "$string:form_FormAbility_label", 229 "srcPath": "FormAbility", 230 "type": "service", 231 "srcLanguage": "ets", 232 "formsEnabled": true, 233 "formConfigAbility": "ability://com.example.entry.EntryAbility", 234 "forms": [{ 235 "colorMode": "auto", 236 "defaultDimension": "2*2", 237 "description": "This is a service widget.", 238 "formVisibleNotify": true, 239 "isDefault": true, 240 "jsComponentName": "widget", 241 "name": "widget", 242 "scheduledUpdateTime": "10:30", 243 "supportDimensions": ["2*2"], 244 "type": "JS", 245 "updateEnabled": true 246 }] 247 }] 248 ``` 249 250 251### 卡片信息的持久化 252 253因大部分卡片提供方都不是常驻服务,只有在需要使用时才会被拉起获取卡片信息,且卡片管理服务支持对卡片进行多实例管理,卡片ID对应实例ID,因此若卡片提供方支持对卡片数据进行配置,则需要对卡片的业务数据按照卡片ID进行持久化管理,以便在后续获取、更新以及拉起时能获取到正确的卡片业务数据。 254 255 256```ts 257const DATA_STORAGE_PATH = "/data/storage/el2/base/haps/form_store"; 258async function storeFormInfo(formId: string, formName: string, tempFlag: boolean) { 259 // 此处仅对卡片ID:formId,卡片名:formName和是否为临时卡片:tempFlag进行了持久化 260 let formInfo = { 261 "formName": formName, 262 "tempFlag": tempFlag, 263 "updateCount": 0 264 }; 265 try { 266 const storage = await dataStorage.getStorage(DATA_STORAGE_PATH); 267 // put form info 268 await storage.put(formId, JSON.stringify(formInfo)); 269 console.info(`storeFormInfo, put form info successfully, formId: ${formId}`); 270 await storage.flush(); 271 } catch (err) { 272 console.error(`failed to storeFormInfo, err: ${JSON.stringify(err)}`); 273 } 274} 275 276... 277 onCreate(want) { 278 console.info('FormAbility onCreate'); 279 280 let formId = want.parameters["ohos.extra.param.key.form_identity"]; 281 let formName = want.parameters["ohos.extra.param.key.form_name"]; 282 let tempFlag = want.parameters["ohos.extra.param.key.form_temporary"]; 283 // 将创建的卡片信息持久化,以便在下次获取/更新该卡片实例时进行使用 284 // 此接口请根据实际情况实现,具体请参考:FormExtAbility Stage模型卡片实例 285 storeFormInfo(formId, formName, tempFlag); 286 287 let obj = { 288 "title": "titleOnCreate", 289 "detail": "detailOnCreate" 290 }; 291 let formData = formBindingData.createFormBindingData(obj); 292 return formData; 293 } 294... 295``` 296 297且需要适配onDestroy卡片删除通知接口,在其中实现卡片实例数据的删除。 298 299 300```ts 301const DATA_STORAGE_PATH = "/data/storage/el2/base/haps/form_store"; 302async function deleteFormInfo(formId: string) { 303 try { 304 const storage = await dataStorage.getStorage(DATA_STORAGE_PATH); 305 // del form info 306 await storage.delete(formId); 307 console.info(`deleteFormInfo, del form info successfully, formId: ${formId}`); 308 await storage.flush(); 309 } catch (err) { 310 console.error(`failed to deleteFormInfo, err: ${JSON.stringify(err)}`); 311 } 312} 313 314... 315 onDestroy(formId) { 316 console.info('FormAbility onDestroy'); 317 // 删除之前持久化的卡片实例数据 318 // 此接口请根据实际情况实现,具体请参考:FormExtAbility Stage模型卡片实例 319 deleteFormInfo(formId); 320 } 321... 322``` 323 324具体的持久化方法可以参考[数据管理开发指导](../database/app-data-persistence-overview.md)。 325 326需要注意的是,卡片使用方在请求卡片时传递给提供方应用的Want数据中存在临时标记字段,表示此次请求的卡片是否为临时卡片: 327 328- 常态卡片:卡片使用方会持久化的卡片; 329 330- 临时卡片:卡片使用方不会持久化的卡片; 331 332由于临时卡片的数据具有非持久化的特殊性,某些场景例如卡片服务框架死亡重启,此时临时卡片数据在卡片管理服务中已经删除,且对应的卡片ID不会通知到提供方,所以卡片提供方需要自己负责清理长时间未删除的临时卡片数据。同时对应的卡片使用方可能会将之前请求的临时卡片转换为常态卡片。如果转换成功,卡片提供方也需要对对应的临时卡片ID进行处理,把卡片提供方记录的临时卡片数据转换为常态卡片数据,防止提供方在清理长时间未删除的临时卡片时,把已经转换为常态卡片的临时卡片信息删除,导致卡片信息丢失。 333 334 335### 卡片数据交互 336 337当卡片应用需要更新数据时(如触发了定时更新或定点更新),卡片应用获取最新数据,并调用updateForm()接口更新主动触发卡片的更新。 338 339 340```ts 341onUpdate(formId) { 342 // 若卡片支持定时更新/定点更新/卡片使用方主动请求更新功能,则提供方需要重写该方法以支持数据更新 343 console.info('FormAbility onUpdate'); 344 let obj = { 345 "title": "titleOnUpdate", 346 "detail": "detailOnUpdate" 347 }; 348 let formData = formBindingData.createFormBindingData(obj); 349 // 调用updateForm接口去更新对应的卡片,仅更新入参中携带的数据信息,其他信息保持不变 350 formProvider.updateForm(formId, formData).catch((error) => { 351 console.info('FormAbility updateForm, error:' + JSON.stringify(error)); 352 }); 353} 354``` 355 356 357### 开发卡片页面 358 359开发者可以使用类Web范式(HML+CSS+JSON)开发JS卡片页面。生成如下卡片页面,可以这样配置卡片页面文件: 360 361![widget-development-fa](figures/widget-development-fa.png) 362 363> **说明:** 364> FA模型当前仅支持JS扩展的类Web开发范式来实现卡片的UI。 365 366- HML:使用类Web范式的组件描述卡片的页面信息。 367 368 ```html 369 <div class="container"> 370 <stack> 371 <div class="container-img"> 372 <image src="/common/widget.png" class="bg-img"></image> 373 </div> 374 <div class="container-inner"> 375 <text class="title">{{title}}</text> 376 <text class="detail_text" onclick="routerEvent">{{detail}}</text> 377 </div> 378 </stack> 379 </div> 380 ``` 381 382- CSS:HML中类Web范式组件的样式信息。 383 384 ```css 385 .container { 386 flex-direction: column; 387 justify-content: center; 388 align-items: center; 389 } 390 391 .bg-img { 392 flex-shrink: 0; 393 height: 100%; 394 } 395 396 .container-inner { 397 flex-direction: column; 398 justify-content: flex-end; 399 align-items: flex-start; 400 height: 100%; 401 width: 100%; 402 padding: 12px; 403 } 404 405 .title { 406 font-size: 19px; 407 font-weight: bold; 408 color: white; 409 text-overflow: ellipsis; 410 max-lines: 1; 411 } 412 413 .detail_text { 414 font-size: 16px; 415 color: white; 416 opacity: 0.66; 417 text-overflow: ellipsis; 418 max-lines: 1; 419 margin-top: 6px; 420 } 421 ``` 422 423- JSON:卡片页面中的数据和事件交互。 424 425 ```json 426 { 427 "data": { 428 "title": "TitleDefault", 429 "detail": "TextDefault" 430 }, 431 "actions": { 432 "routerEvent": { 433 "action": "router", 434 "abilityName": "com.example.entry.EntryAbility", 435 "params": { 436 "message": "add detail" 437 } 438 } 439 } 440 } 441 ``` 442 443 444### 开发卡片事件 445 446卡片支持为组件设置交互事件(action),包括router事件和message事件,其中router事件用于Ability跳转,message事件用于卡片开发人员自定义点击事件。关键步骤说明如下: 447 4481. 在hml中为组件设置onclick属性,其值对应到json文件的actions字段中。 449 4502. 如何设置router事件: 451 - action属性值为"router"; 452 - abilityName为跳转目标的Ability名(支持跳转FA模型的PageAbility组件和Stage模型的UIAbility组件),如目前DevEco创建的FA模型的UIAbility默认名为com.example.entry.EntryAbility; 453 - params为传递给跳转目标Ability的自定义参数,可以按需填写。其值可以在目标Ability启动时的want中的parameters里获取。如FA模型EntryAbility的onCreate生命周期里可以通过featureAbility.getWant()获取到want,然后在其parameters字段下获取到配置的参数; 454 4553. 如何设置message事件: 456 - action属性值为"message"; 457 - params为message事件的用户自定义参数,可以按需填写。其值可以在卡片生命周期函数onEvent中的message里获取; 458 459示例如下: 460 461- hml文件 462 463 ```html 464 <div class="container"> 465 <stack> 466 <div class="container-img"> 467 <image src="/common/widget.png" class="bg-img"></image> 468 </div> 469 <div class="container-inner"> 470 <text class="title" onclick="routerEvent">{{title}}</text> 471 <text class="detail_text" onclick="messageEvent">{{detail}}</text> 472 </div> 473 </stack> 474 </div> 475 ``` 476 477- css文件 478 479 ```css 480 .container { 481 flex-direction: column; 482 justify-content: center; 483 align-items: center; 484 } 485 486 .bg-img { 487 flex-shrink: 0; 488 height: 100%; 489 } 490 491 .container-inner { 492 flex-direction: column; 493 justify-content: flex-end; 494 align-items: flex-start; 495 height: 100%; 496 width: 100%; 497 padding: 12px; 498 } 499 500 .title { 501 font-size: 19px; 502 font-weight: bold; 503 color: white; 504 text-overflow: ellipsis; 505 max-lines: 1; 506 } 507 508 .detail_text { 509 font-size: 16px; 510 color: white; 511 opacity: 0.66; 512 text-overflow: ellipsis; 513 max-lines: 1; 514 margin-top: 6px; 515 } 516 ``` 517 518- json文件 519 520 ```json 521 { 522 "data": { 523 "title": "TitleDefault", 524 "detail": "TextDefault" 525 }, 526 "actions": { 527 "routerEvent": { 528 "action": "router", 529 "abilityName": "com.example.entry.EntryAbility", 530 "params": { 531 "message": "add detail" 532 } 533 }, 534 "messageEvent": { 535 "action": "message", 536 "params": { 537 "message": "add detail" 538 } 539 } 540 } 541 } 542 ``` 543