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