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",区别在卡片配置文件里:
// 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 卡片填 "hml""window":{"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 --><divclass="container"><!-- 双花括号绑定数据 --><textclass="title">{{title}}</text><textclass="detail">{{detail}}</text><!-- click 事件,触发 JS 里的函数 --><divclass="btn-area"@click="onClickRouter"><textclass="btn-text">打开应用</text></div><!-- message 事件按钮 --><divclass="btn-area"@click="onClickMessage"><textclass="btn-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}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';constTAG='JsCardFormAbility';constDOMAIN_NUMBER=0xFF00;constDATA_STORAGE_PATH='/data/storage/el2/base/haps/form_store';// 持久化卡片信息(formId -> formName)letstoreFormInfo=async(formId:string,formName:string,tempFlag:boolean,context:common.FormExtensionContext):Promise<void>=>{constformInfo:Record<string,string|boolean|number>={'formName':formName,'tempFlag':tempFlag,'updateCount':0};try{conststorage:preferences.Preferences=awaitpreferences.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)}`);}};// 删除持久化的卡片信息letdeleteFormInfo=async(formId:string,context:common.FormExtensionContext):Promise<void>=>{try{conststorage=awaitpreferences.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){constformId=JSON.stringify(want.parameters['ohos.extra.param.key.form_identity']);constformName=JSON.stringify(want.parameters['ohos.extra.param.key.form_name']);consttempFlag=want.parameters['ohos.extra.param.key.form_temporary']asboolean;// 持久化,以便后续 updateForm 时用到 formIdstoreFormInfo(formId,formName,tempFlag,this.context);}// 返回初始数据,字段名和 HML 里 {{title}} {{detail}} 对应constobj:Record<string,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:Record<string,string>={'title':'titleOnUpdate',// 更新后的数据'detail':'detailOnUpdate'};constformData=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');constmsg: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:
// entry/src/main/ets/entryability/EntryAbility.etsimport{AbilityConstant,UIAbility,Want}from'@kit.AbilityKit';import{hilog}from'@kit.PerformanceAnalysisKit';constTAG='EntryAbility';constDOMAIN_NUMBER=0xFF00;exportdefaultclassEntryAbilityextendsUIAbility{onCreate(want:Want,launchParam:AbilityConstant.LaunchParam):void{if(want?.parameters?.params){// params 是一个 JSON 字符串,要先 parseconstparams: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逻辑是一样的。