ArkTS 卡片是主流但还有一种更老的方案——JS 卡片基于 HML CSS JS 开发风格跟前端三件套很像。虽然华为推荐用 ArkTS但一些老项目还在用 JS 卡片理解它有必要。今天基于JSForm项目把 JS 卡片的开发方式讲清楚。JS 卡片 vs ArkTS 卡片先说区别免得搞混对比项JS 卡片ArkTS 卡片卡片 UI 语法HML CSS JSArkTS.ets 文件数据绑定{{变量名}}模板语法LocalStorageProp交互事件clickfuncNamepostCardAction()文件位置js/卡片名/pages/widget/pages/uiSyntax 配置hmlarkts推荐程度老项目维护新项目首选项目结构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.json5JS 卡片的type和 ArkTS 卡片一样都是form区别在卡片配置文件里// 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{forms:[{name:jscard,displayName:$string:jscard_display_name,description:$string:jscard_desc,src:./js/jscard/pages/index/index.hml,// JS 卡片入口文件uiSyntax:hml,// 关键JS 卡片填 hmlwindow:{designWidth:720,autoDesignWidth:true},isDefault:true,updateEnabled:true,updateDuration:1,// 每30分钟刷新一次supportDimensions:[2*2],defaultDimension:2*2}]}第二步写 HML 卡片页面HML 类似简化版 HTML支持数据绑定和事件绑定!-- entry/src/main/js/jscard/pages/index/index.hml --divclasscontainer!-- 双花括号绑定数据 --textclasstitle{{title}}/texttextclassdetail{{detail}}/text!-- click 事件触发 JS 里的函数 --divclassbtn-areaclickonClickRoutertextclassbtn-text打开应用/text/div!-- message 事件按钮 --divclassbtn-areaclickonClickMessagetextclassbtn-text发送消息/text/div/div第三步写 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不同// entry/src/main/js/jscard/pages/index/index.jsexportdefault{// 初始数据data:{title:titleOnCreate,// 和 FormAbility 传的字段名对应detail:detailOnCreate},// 点击触发 router 事件跳转到应用onClickRouter(){this.$app.$def.postCardAction({action:router,// 跳转到 UIAbilityabilityName:EntryAbility,// 目标 UIAbilityparams:{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生命周期回调完全一样// entry/src/main/ets/jscardformability/JsCardFormAbility.etsimport{common,Want}fromkit.AbilityKit;import{hilog}fromkit.PerformanceAnalysisKit;import{formBindingData,FormExtensionAbility,formProvider}fromkit.FormKit;import{BusinessError}fromkit.BasicServicesKit;import{preferences}fromkit.ArkData;constTAGJsCardFormAbility;constDOMAIN_NUMBER0xFF00;constDATA_STORAGE_PATH/data/storage/el2/base/haps/form_store;// 持久化卡片信息formId - formNameletstoreFormInfoasync(formId:string,formName:string,tempFlag:boolean,context:common.FormExtensionContext):Promisevoid{constformInfo:Recordstring,string|boolean|number{formName:formName,tempFlag:tempFlag,updateCount:0};try{conststorage:preferences.Preferencesawaitpreferences.getPreferences(context,DATA_STORAGE_PATH);awaitstorage.put(formId,JSON.stringify(formInfo));awaitstorage.flush();hilog.info(DOMAIN_NUMBER,TAG,卡片信息已持久化, formId:${formId});}catch(err){hilog.error(DOMAIN_NUMBER,TAG,持久化失败:${JSON.stringify(errasBusinessError)});}};// 删除持久化的卡片信息letdeleteFormInfoasync(formId:string,context:common.FormExtensionContext):Promisevoid{try{conststorageawaitpreferences.getPreferences(context,DATA_STORAGE_PATH);awaitstorage.delete(formId);awaitstorage.flush();hilog.info(DOMAIN_NUMBER,TAG,卡片信息已删除, formId:${formId});}catch(err){hilog.error(DOMAIN_NUMBER,TAG,删除失败:${JSON.stringify(errasBusinessError)});}};exportdefaultclassJsCardFormAbilityextendsFormExtensionAbility{// 卡片创建时调用onAddForm(want:Want):formBindingData.FormBindingData{hilog.info(DOMAIN_NUMBER,TAG,onAddForm);if(want.parameters){constformIdJSON.stringify(want.parameters[ohos.extra.param.key.form_identity]);constformNameJSON.stringify(want.parameters[ohos.extra.param.key.form_name]);consttempFlagwant.parameters[ohos.extra.param.key.form_temporary]asboolean;// 持久化以便后续 updateForm 时用到 formIdstoreFormInfo(formId,formName,tempFlag,this.context);}// 返回初始数据字段名和 HML 里 {{title}} {{detail}} 对应constobj:Recordstring,string{title:titleOnCreate,detail:detailOnCreate};returnformBindingData.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);constobj:Recordstring,string{title:titleOnUpdate,// 更新后的数据detail:detailOnUpdate};constformDataformBindingData.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);constmsg:Recordstring,stringJSON.parse(message);if(msg.detailmessage detail){hilog.info(DOMAIN_NUMBER,TAG,收到卡片消息:${msg.detail});// 在这里处理业务逻辑比如更新卡片数据}}}EntryAbility 处理 router 事件参数JS 卡片的 router 事件触发后参数会通过Want.parameters.params传给EntryAbility// entry/src/main/ets/entryability/EntryAbility.etsimport{AbilityConstant,UIAbility,Want}fromkit.AbilityKit;import{hilog}fromkit.PerformanceAnalysisKit;constTAGEntryAbility;constDOMAIN_NUMBER0xFF00;exportdefaultclassEntryAbilityextendsUIAbility{onCreate(want:Want,launchParam:AbilityConstant.LaunchParam):void{if(want?.parameters?.params){// params 是一个 JSON 字符串要先 parseconstparams:Recordstring,ObjectJSON.parse(JSON.stringify(want.parameters.params));// 读取 JS 卡片传来的参数if(params.inforouter info){hilog.info(DOMAIN_NUMBER,TAG,收到 info:${params.info});// 根据参数决定跳转哪个页面}if(params.messagerouter message){hilog.info(DOMAIN_NUMBER,TAG,收到 message:${params.message});}}}}完整生命周期和数据流JS 卡片常见坑坑1uiSyntax必须填hml而不是arkts这两个值不能混写错了系统找不到卡片 UI添加时直接报错。坑2HML 文件路径要和配置里的src完全一致配置文件里src: ./js/jscard/pages/index/index.hml就要在这个路径建文件一个字母都不能错。坑3JS 卡片不支持import语法JS 卡片运行在一个受限环境里不支持 ES6 的import/export也不支持 node_modules只能用原生 JS。坑4postCardAction在 HML 里的写法不同ArkTS 卡片直接调postCardAction(this, {...})JS 卡片要用this.$app.$def.postCardAction({...})少了this参数。写在最后JS 卡片说实话有点年代感了能用 ArkTS 就别用 JS 卡片。但如果你接手了一个老项目或者需要维护 JS 卡片代码理解 HML CSS JS 这套模式是必要的。最核心的一点数据绑定从FormBindingData到 HML 的{{变量}}是完全同步的formProvider.updateForm推数据HML 模板自动响应这点和 ArkTS 的LocalStorageProp逻辑是一样的。