ArkTS 卡片是主流,但还有一种更老的方案------JS 卡片 ,基于 HML + CSS + JS 开发,风格跟前端三件套很像。虽然华为推荐用 ArkTS,但一些老项目还在用 JS 卡片,理解它有必要。
今天基于
JSForm项目,把 JS 卡片的开发方式讲清楚。
JS 卡片 vs ArkTS 卡片
先说区别,免得搞混:
| 对比项 | JS 卡片 | ArkTS 卡片 |
|---|---|---|
| 卡片 UI 语法 | HML + CSS + JS | ArkTS(.ets 文件) |
| 数据绑定 | {``{变量名}} 模板语法 |
@LocalStorageProp |
| 交互事件 | @click="funcName" |
postCardAction() |
| 文件位置 | js/卡片名/pages/ |
widget/pages/ |
| uiSyntax 配置 | "hml" |
"arkts" |
| 推荐程度 | 老项目维护 | 新项目首选 |
项目结构
JSForm/
└── entry/src/main/
├── ets/
│ ├── entryability/
│ │ └── EntryAbility.ets ← 主 UIAbility,处理 router 跳转
│ └── jscardformability/
│ └── JsCardFormAbility.ets ← 卡片提供方 FormExtensionAbility
├── js/
│ └── jscard/ ← JS 卡片目录(名称要和配置一致)
│ └── pages/
│ └── index/
│ ├── index.hml ← 卡片 UI(类似 HTML)
│ ├── index.css ← 卡片样式
│ └── index.js ← 卡片逻辑
└── module.json5
第一步:配置 module.json5
JS 卡片的 type 和 ArkTS 卡片一样,都是 "form",区别在卡片配置文件里:
json5
// entry/src/main/module.json5
{
"module": {
"extensionAbilities": [
{
"name": "JsCardFormAbility",
"srcEntry": "./ets/jscardformability/JsCardFormAbility.ets",
"description": "$string:JSCardFormAbility_desc",
"label": "$string:JSCardFormAbility_label",
"type": "form",
"metadata": [
{
"name": "ohos.extension.form",
"resource": "$profile:form_jscard_config" // 指向 JS 卡片配置文件
}
]
}
]
}
}
JS 卡片配置文件 resources/base/profile/form_jscard_config.json:
json
{
"forms": [
{
"name": "jscard",
"displayName": "$string:jscard_display_name",
"description": "$string:jscard_desc",
"src": "./js/jscard/pages/index/index.hml", // JS 卡片入口文件
"uiSyntax": "hml", // 关键:JS 卡片填 "hml"
"window": {
"designWidth": 720,
"autoDesignWidth": true
},
"isDefault": true,
"updateEnabled": true,
"updateDuration": 1, // 每30分钟刷新一次
"supportDimensions": ["2*2"],
"defaultDimension": "2*2"
}
]
}
第二步:写 HML 卡片页面
HML 类似简化版 HTML,支持数据绑定和事件绑定:
html
<!-- entry/src/main/js/jscard/pages/index/index.hml -->
<div class="container">
<!-- 双花括号绑定数据 -->
<text class="title">{{title}}</text>
<text class="detail">{{detail}}</text>
<!-- click 事件,触发 JS 里的函数 -->
<div class="btn-area" @click="onClickRouter">
<text class="btn-text">打开应用</text>
</div>
<!-- message 事件按钮 -->
<div class="btn-area" @click="onClickMessage">
<text class="btn-text">发送消息</text>
</div>
</div>
第三步:写 CSS 样式
css
/* entry/src/main/js/jscard/pages/index/index.css */
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 12px 16px;
background-color: #1A1A2E;
}
.title {
font-size: 16px;
color: #FFFFFF;
opacity: 0.9;
max-lines: 1;
text-overflow: ellipsis;
margin-bottom: 6px;
}
.detail {
font-size: 12px;
color: #FFFFFF;
opacity: 0.6;
max-lines: 2;
text-overflow: ellipsis;
}
.btn-area {
width: 120px;
height: 32px;
background-color: #FFFFFF;
border-radius: 16px;
margin-top: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.btn-text {
font-size: 12px;
color: #45A6F4;
}
第四步:写 JS 逻辑
JS 卡片里触发事件用的是 this.$app.$def.postCardAction,语法和 ArkTS 的 postCardAction 不同:
javascript
// entry/src/main/js/jscard/pages/index/index.js
export default {
// 初始数据
data: {
title: 'titleOnCreate', // 和 FormAbility 传的字段名对应
detail: 'detailOnCreate'
},
// 点击触发 router 事件,跳转到应用
onClickRouter() {
this.$app.$def.postCardAction({
action: 'router', // 跳转到 UIAbility
abilityName: 'EntryAbility', // 目标 UIAbility
params: {
info: 'router info', // EntryAbility.onCreate 里能拿到
message: 'router message'
}
});
},
// 点击触发 message 事件,让 FormAbility 处理
onClickMessage() {
this.$app.$def.postCardAction({
action: 'message',
params: {
detail: 'message detail' // JsCardFormAbility.onFormEvent 里能拿到
}
});
}
}
第五步:FormAbility 处理生命周期
JS 卡片和 ArkTS 卡片共用同一个 FormExtensionAbility,生命周期回调完全一样:
typescript
// entry/src/main/ets/jscardformability/JsCardFormAbility.ets
import { common, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { formBindingData, FormExtensionAbility, formProvider } from '@kit.FormKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { preferences } from '@kit.ArkData';
const TAG = 'JsCardFormAbility';
const DOMAIN_NUMBER = 0xFF00;
const DATA_STORAGE_PATH = '/data/storage/el2/base/haps/form_store';
// 持久化卡片信息(formId -> formName)
let storeFormInfo = async (
formId: string,
formName: string,
tempFlag: boolean,
context: common.FormExtensionContext
): Promise<void> => {
const formInfo: Record<string, string | boolean | number> = {
'formName': formName,
'tempFlag': tempFlag,
'updateCount': 0
};
try {
const storage: preferences.Preferences =
await preferences.getPreferences(context, DATA_STORAGE_PATH);
await storage.put(formId, JSON.stringify(formInfo));
await storage.flush();
hilog.info(DOMAIN_NUMBER, TAG, `卡片信息已持久化, formId: ${formId}`);
} catch (err) {
hilog.error(DOMAIN_NUMBER, TAG, `持久化失败: ${JSON.stringify(err as BusinessError)}`);
}
};
// 删除持久化的卡片信息
let deleteFormInfo = async (
formId: string,
context: common.FormExtensionContext
): Promise<void> => {
try {
const storage = await preferences.getPreferences(context, DATA_STORAGE_PATH);
await storage.delete(formId);
await storage.flush();
hilog.info(DOMAIN_NUMBER, TAG, `卡片信息已删除, formId: ${formId}`);
} catch (err) {
hilog.error(DOMAIN_NUMBER, TAG, `删除失败: ${JSON.stringify(err as BusinessError)}`);
}
};
export default class JsCardFormAbility extends FormExtensionAbility {
// 卡片创建时调用
onAddForm(want: Want): formBindingData.FormBindingData {
hilog.info(DOMAIN_NUMBER, TAG, 'onAddForm');
if (want.parameters) {
const formId = JSON.stringify(want.parameters['ohos.extra.param.key.form_identity']);
const formName = JSON.stringify(want.parameters['ohos.extra.param.key.form_name']);
const tempFlag = want.parameters['ohos.extra.param.key.form_temporary'] as boolean;
// 持久化,以便后续 updateForm 时用到 formId
storeFormInfo(formId, formName, tempFlag, this.context);
}
// 返回初始数据,字段名和 HML 里 {{title}} {{detail}} 对应
const obj: Record<string, string> = {
'title': 'titleOnCreate',
'detail': 'detailOnCreate'
};
return formBindingData.createFormBindingData(obj);
}
// 卡片被移除时调用
onRemoveForm(formId: string): void {
hilog.info(DOMAIN_NUMBER, TAG, 'onRemoveForm');
deleteFormInfo(formId, this.context);
}
// 定时/主动刷新时调用
onUpdateForm(formId: string): void {
hilog.info(DOMAIN_NUMBER, TAG, 'onUpdateForm');
const obj: Record<string, string> = {
'title': 'titleOnUpdate', // 更新后的数据
'detail': 'detailOnUpdate'
};
const formData = formBindingData.createFormBindingData(obj);
formProvider.updateForm(formId, formData)
.catch((error: BusinessError) => {
hilog.error(DOMAIN_NUMBER, TAG, `updateForm 失败: ${JSON.stringify(error)}`);
});
}
// 卡片触发事件时调用(来自 JS 里的 postCardAction message 事件)
onFormEvent(formId: string, message: string): void {
hilog.info(DOMAIN_NUMBER, TAG, 'onFormEvent');
const msg: Record<string, string> = JSON.parse(message);
if (msg.detail === 'message detail') {
hilog.info(DOMAIN_NUMBER, TAG, `收到卡片消息: ${msg.detail}`);
// 在这里处理业务逻辑,比如更新卡片数据
}
}
}
EntryAbility 处理 router 事件参数
JS 卡片的 router 事件触发后,参数会通过 Want.parameters.params 传给 EntryAbility:
typescript
// entry/src/main/ets/entryability/EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = 'EntryAbility';
const DOMAIN_NUMBER = 0xFF00;
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
if (want?.parameters?.params) {
// params 是一个 JSON 字符串,要先 parse
const params: Record<string, Object> =
JSON.parse(JSON.stringify(want.parameters.params));
// 读取 JS 卡片传来的参数
if (params.info === 'router info') {
hilog.info(DOMAIN_NUMBER, TAG, `收到 info: ${params.info}`);
// 根据参数决定跳转哪个页面
}
if (params.message === 'router message') {
hilog.info(DOMAIN_NUMBER, TAG, `收到 message: ${params.message}`);
}
}
}
}
完整生命周期和数据流

JS 卡片常见坑
坑1:uiSyntax 必须填 "hml" 而不是 "arkts"
这两个值不能混,写错了系统找不到卡片 UI,添加时直接报错。
坑2:HML 文件路径要和配置里的 src 完全一致
配置文件里 "src": "./js/jscard/pages/index/index.hml",就要在这个路径建文件,一个字母都不能错。
坑3:JS 卡片不支持 import 语法
JS 卡片运行在一个受限环境里,不支持 ES6 的 import/export,也不支持 node_modules,只能用原生 JS。
坑4:postCardAction 在 HML 里的写法不同
ArkTS 卡片直接调 postCardAction(this, {...}),JS 卡片要用 this.$app.$def.postCardAction({...}),少了 this 参数。
写在最后
JS 卡片说实话有点年代感了,能用 ArkTS 就别用 JS 卡片。但如果你接手了一个老项目,或者需要维护 JS 卡片代码,理解 HML + CSS + JS 这套模式是必要的。
最核心的一点:数据绑定从 FormBindingData 到 HML 的 {``{变量}} 是完全同步的,formProvider.updateForm 推数据,HML 模板自动响应,这点和 ArkTS 的 @LocalStorageProp 逻辑是一样的。