HarmonyOS 服务卡片开发之JS 卡片开发

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 逻辑是一样的。

相关推荐
Highcharts.js2 小时前
AI向量知识谱系图表创建示例代码|Highcharts网络图表(networkgraph)搭建案例
开发语言·前端·javascript·网络·信息可视化·编辑器·highcharts
程序猿追2 小时前
HarmonyOS 6.0 NEXT:基于 Map Kit 实现一款“手绘路线”骑行导航应用
华为·harmonyos
阿正的梦工坊2 小时前
React:构建用户界面的JavaScript库
javascript·react.js·ui
行走的陀螺仪2 小时前
[特殊字符] JavaScript 设计模式完全指南:从入门到精通(含20种模式)
开发语言·javascript·设计模式
胡萝卜术3 小时前
《JavaScript 语言精粹》第三章精读:对象——最基础也最容易被误解的基石
javascript
A南方故人3 小时前
vue3常用指令以及注册
前端·javascript·vue.js
轻口味3 小时前
HarmonyOS 6.1 全栈实战录 - 08 视讯巅峰:Media Kit 视频缩略图批量提取与 HDR 渲染链路实战
华为·音视频·harmonyos
想你依然心痛3 小时前
HarmonyOS 6(API 23)实战:基于悬浮导航、沉浸光感与HMAF的“医智助手“——医疗影像AI智能体辅助诊断平台
人工智能·华为·harmonyos
nashane3 小时前
HarmonyOS 6学习:卡片组件圆角白边问题的诊断与修复实战
人工智能·pytorch·深度学习·harmonyos