ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

前端转鸿蒙开发:用ArkTS实战桌面卡片,技能平移与避坑指南

2026/9/28 12:08:14 拓冰建站 浏览量
前端转鸿蒙开发:用ArkTS实战桌面卡片,技能平移与避坑指南 前端转鸿蒙开发者这件事我去年还觉得是“要不要放弃前端”的艰难抉择直到朋友丢来一个需求帮他做一张待办事项卡片能放桌面、显示当天任务、点击跳转 App 内页。我硬着头皮用 ArkTS 从零写了一张卡片并成功把 App 提审上架。回头发现所谓鸿蒙开发尤其是卡片开发对前端来说更像“把 Vue/React 的组件化思维换一层皮再补上生命周期和系统级能力”。这篇文章用我真实的卡片实践过程讲清楚前端怎么把手上的技能平移到鸿蒙 App 开发哪些可以直接复用哪些必须推倒重来适合正在犹豫转不转、以及刚接触卡片开发的同学。1. 先说结论前端转鸿蒙不是“从零开始”而是换个框架写界面1.1 一场让我改变想法的卡片需求我做了三年多前端日常主要写 Vue 和 React背景是后台管理系统和活动页。一开始朋友找我说“帮我做个鸿蒙 App”我第一反应是抵触学一门新语言、新框架、新工具链周期太长了。但他说得挺轻松“不用做完整 App先帮我做张桌面卡片显示今天待办点一下能跳进 App 详情页就行。”我抱着试试看的心态去翻了官方文档结果发现这张“桌面卡片”的开发难度比我预想中低得多。卡片本质上是一个展示型小组件数据由后台服务注入UI 用声明式写法渲染点击事件通过一个回调通知宿主应用。这和我在前端里做侧边栏小组件、做浏览器插件的 popup 页面完全是同一套思路。我给这段经历做个总结前端转鸿蒙真正需要补的核心不是语法而是三件事——理解 Stage 模型下的生命周期、理解系统级数据刷新机制、理解 ArkTS 在 TypeScript 基础上做的各种严格限制。前端的组件化、数据驱动、事件分发这些基本功在鸿蒙开发里一个都不会浪费。1.2 前端技能与鸿蒙开发能力映射表我刚开始学的时候最喜欢做的一件事就是把前端概念往鸿蒙上套。这张映射表是我自己整理的后来带一个转岗的小弟也用这张表入门效果不错前端概念鸿蒙开发对应能力说明HTML 结构ArkTS 声明式 UIbuild 方法不再写标签用组件树描述结构CSS 布局与样式ArkUI 布局系统Row/Column/Stack/Flex链式调用属性设置样式Vue/React 组件化Component 自定义组件结构、样式、逻辑聚合在一个文件data/computed/propsState、Prop、LocalStorageProp 装饰器状态驱动 UI 更新Vuex/Pinia/ReduxAppStorage、LocalStorage、PersistentStorage全局状态与持久化事件绑定onClick组件的 onClick/onChange 等事件回调加事件监听的方式非常像页面路由router / Navigation页面跳转有声明式和命令式npm 包管理ohpm命令和 npm 很像localStoragePreferences / PersistentStorage持久化存储webpack/vite 构建hvigor DevEco Studio可视化配置为主Chrome DevToolsPreviewer Log 面板 ArkUI Inspector调试思路高度一致这张表不是说“鸿蒙 前端换皮”而是给你一个心理锚点遇到陌生概念先想想前端里有没有类似的东西再去找两者的差异点学习效率会高很多。1.3 为什么“卡片”是前端入门鸿蒙的最佳姿势如果让我给前端转鸿蒙的人一个入门路径建议我会直接说别一上来就写完整页面先做一张服务卡片。原因有三点。第一范围小。卡片最大也就是 4x4 的格子UI 代码量通常不超过 200 行写坏了大不了重来。前端同学最容易在“页面太复杂、不知道从哪下手”的时候放弃卡片天然避开了这个问题。第二约束明确。卡片对组件有白名单Web、Video、Canvas 这些重组件根本不能用你不用纠结选型也不用做复杂状态管理。这就像前端写一个纯展示的小组件数据外部注入UI 固定复杂度被人为压低了。第三成就感来得快。前端写页面要跑通路由、调接口、处理鉴权才能看到一个完整效果卡片写完长按桌面就能添加改一行代码立刻能看到变化。这种即时反馈对学习动力的帮助非常大。当然也有现实因素。现在很多鸿蒙应用都在做卡片化桌面卡片、负一屏卡片、锁屏卡片都是需求点。我从卡片切入既学了鸿蒙开发的基础链路又能直接产出可用功能这对想靠转岗吃饭的人来说很实际。2. 卡片是什么以及为什么它最适合前端转型切入2.1 服务卡片在鸿蒙里的定位服务卡片Form不是 App 里的一个页面而是挂在桌面上的一种轻量化视图。你长按桌面空白处选择“服务卡片”找到某个应用就能把它的小卡片拖到桌面上。这张卡片不需要打开 App 就能看到信息比如天气、待办、日程、运动步数。用前端的语言来类比卡片就像是把一个页面里的某个小组件“抽取”出来挂到系统桌面上独立运行。但要注意它和网页里的 iframe 不一样——卡片虽然有独立的 UI 渲染但它的生命周期和数据源都由系统的卡片框架管理开发者在 App 里通过 FormExtensionAbility 这个“扩展能力”来提供卡片内容和更新数据。卡片在鸿蒙生态里的地位挺特殊。它不像页面那样可以承载复杂交互也不像通知那样一划就没了它是用户“不开 App 就能看到信息”的关键入口。所以做卡片开发核心词是“轻”和“准”界面轻量、数据准确。2.2 提供方、使用方和 ArkTS 卡片的技术形态理解卡片要先分清两个角色提供方和使用方。提供方是你的 App。App 里继承 FormExtensionAbility负责创建卡片实例、提供初始数据、定时或被动地更新数据。使用方是系统桌面、负一屏等界面它们负责把卡片渲染出来并把用户的点击事件回传给提供方。整个流程大概是这样的用户在桌面上添加卡片系统唤起 App 里的 FormExtensionAbility调用 onAddForm 方法拿到初始数据然后把数据注入到卡片 UI 中渲染。用户点卡片上的按钮通过 postCardAction 发起一个动作这个动作可以跳转到 App 的某个页面也可以发送一个消息触发数据更新。ArkTS 卡片的意思是卡片 UI 本身用 ArkTS 语言和 ArkUI 框架来写。它和 App 内部页面的写法非常像都是 Entry Component build 方法但运行环境更受限。卡片 UI 里不能用重组件不能用复杂动画数据获取也不是直接发网络请求而是依赖提供方注入。2.3 从组件化视角看卡片前端理解起来毫无压力把卡片拆解成前端的组件模型你会发现它几乎就是一个小型前端组件。如果把卡片看作一个 Vue 组件FormExtensionAbility 返回的 FormBindingData 就是 props卡片 UI 里的 LocalStorageProp 装饰器变量相当于 databuild 方法相当于 template点击事件里的 postCardAction 相当于 methods 里调用 $emit。我当时看官方示例时脑子里自动就把它翻译成了 Vue 的语法Column 就是 flex column 容器Row 就是 flex row 容器Text 就是 span 或者带样式的 divBlank 就是 flex 布局里的 space-between 填充物。这么一映射整个卡片结构的可读性立刻上来了。这种“组件化思维平移”的好处是你不需要把鸿蒙当作一门全新的学问去背而是把它当成“前端组件模型的另一个实现”。唯一要适应的是 card 的 UI 里数据只能通过系统提供的链路流转不能像普通前端那样在组件里随便发请求、改全局状态。3. 手把手从零创建一个卡片项目3.1 环境准备与工程创建做鸿蒙开发第一件事是装 DevEco Studio。这个 IDE 是基于 IntelliJ 的界面和 Android Studio 高度相似Vue 开发者不会觉得陌生。装完之后创建一个 HarmonyOS 工程模板选择“Empty Ability”语言选 ArkTS 就好。创建工程的时候要注意模块类型。Stage 模型是现在的主流FA 模型是旧架构新项目直接选 Stage不用犹豫。API 版本我建议选你手头真机支持的版本如果手头没有真机就选最新的稳定版比如 API 12 或更高。前端同学容易忽略的一点是DevEco Studio 里创建的工程包含 entry 模块这是主 Ability 的载体之后我们加的卡片扩展能力也在 entry 模块下面不用单独建模块。创建好工程后先跑一次默认的 Hello World确保模拟器或真机连接正常。别急着写卡片如果基础环境都没通后面排查问题会很痛苦。3.2 在 module.json5 里注册卡片服务卡片不是一个独立页面它是以“ExtensionAbility”的形式注册在模块配置里的。打开 entry 模块下的 module.json5找到 extensionAbilities 数组添加一个 type 为 form 的扩展能力{ module: { extensionAbilities: [ { name: EntryFormAbility, srcEntry: ./ets/entryformability/EntryFormAbility.ets, label: $string:EntryFormAbility_label, description: $string:EntryFormAbility_desc, type: form, metadata: [ { name: ohos.extension.form, resource: $profile:form_config } ] } ] } }这里的 srcEntry 指向我们即将创建的 FormExtensionAbility 文件type 固定为 formmetadata 里的 resource 指向一个 form_config.json 配置文件。换句话说这个 JSON 文件才是卡片的“户口本”系统通过它知道卡片的名字、尺寸、刷新策略。前端同学可以把这段配置理解成 webpack 插件注册或小程序 app.json 里的页面注册。你不在配置里声明代码写得再好系统也不认。3.3 form_config.json卡片的“户口本”在 resources/base/profile 目录下创建 form_config.json。这是我当时做待办卡片的配置{ forms: [ { name: todo_card, displayName: $string:todo_card_name, description: $string:todo_card_desc, src: ./ets/widget/pages/TodoCard.ets, uiSyntax: arkts, window: { designWidth: 720, autoDesignWidth: true }, colorMode: auto, isDefault: true, updateEnabled: true, scheduledUpdateTime: 10:30, updateDuration: 1, defaultDimension: 2*2, supportDimensions: [2*2, 2*4] } ] }几个关键字段值得展开说。src 指向卡片 UI 文件这个文件后续会是一个 ArkTS 组件uiSyntax 固定写 arkts表示这张卡片用 ArkTS 渲染defaultDimension 和 supportDimensions 定义卡片支持几种尺寸。我一开始只配了 2x2后来发现用户诉求五花八门就加了 2x4让待办列表能多显示几条。window 里的 designWidth 和 autoDesignWidth 是前端的“rem 适配”思路以 720 为设计基准宽度系统自动按卡片实际宽度缩放。如果你不做这个适配同一张卡片在不同尺寸的桌面上会出现字体大小不一致的问题。updateEnabled、scheduledUpdateTime 和 updateDuration 是卡片的定时刷新策略。更新逻辑我放到下一节细说这里你只需要知道想定时刷新就打开 updateEnabled然后配置刷新周期或刷新时间点。3.4 用 ArkTS 写第一张卡片界面接下来是核心环节写卡片 UI。我在 entry 模块下创建了一个 ets/widget/pages/TodoCard.ets 文件内容大概是这样的let storage new LocalStorage(); storage.setOrCreate(todoList, [ { title: 晨会, time: 09:00, done: false }, { title: 提交周报, time: 18:00, done: false } ]); Entry(storage) Component struct TodoCard { LocalStorageProp(todoList) todoList: TodoItem[] []; readonly FULL_WIDTH_PERCENT: string 100%; readonly FULL_HEIGHT_PERCENT: string 100%; build() { Column() { Row() { Text(今日待办) .fontSize(16) .fontWeight(FontWeight.Bold) Blank() Text(查看全部 ) .fontSize(12) .fontColor(#999999) .onClick(() { this.postCardActionToRouter(); }) } .width(this.FULL_WIDTH_PERCENT) ForEach(this.todoList.slice(0, 2), (item: TodoItem) { Row() { Text(item.time) .fontSize(12) .fontColor(#666666) Text(item.title) .fontSize(14) .margin({ left: 12 }) } .width(this.FULL_WIDTH_PERCENT) .padding(8) }, (item: TodoItem) JSON.stringify(item)) } .padding(16) .width(this.FULL_WIDTH_PERCENT) .height(this.FULL_HEIGHT_PERCENT) .backgroundColor(#FFFFFF) .borderRadius(16) } postCardActionToRouter() { postCardAction(this, { action: router, abilityName: EntryAbility, params: { targetPage: TodoListPage } }); } }这段代码和前端写组件几乎没有区别。Column 和 Row 是布局容器类似 flexText 是文本Blank 占满剩余空间等同于 flex: 1ForEach 就是列表渲染onClick 就是点击事件。样式通过链式调用设置边框圆角、背景色、字体大小都是这样。这里的数据 todoList 来自 LocalStorage通过 LocalStorageProp 装饰器绑定。实际运行时FormExtensionAbility 返回的 FormBindingData 会把数据注入到这个 LocalStorage 里UI 就能拿到了。我在开发初期直接在组件里写死了一份初始数据方便先看 UI 效果。再看 postCardAction 这个函数。它是卡片里最常用的交互出口作用是把点击事件交给系统然后由系统决定是跳转页面、发消息还是拉起后台任务。上面这段代码实现的是点击“查看全部”跳到主应用的 EntryAbility并带上 targetPage 参数。4. 卡片的生命周期与数据刷新前端状态管理的另一种表达4.1 FormExtensionAbility 的生命周期卡片 UI 是“展示层”数据逻辑集中在 FormExtensionAbility 里。我创建了 entryformability/EntryFormAbility.ets 文件import { FormExtensionAbility, formBindingData, formInfo } from kit.FormKit; import { Want } from kit.AbilityKit; export default class EntryFormAbility extends FormExtensionAbility { onAddForm(want: Want): formBindingData.FormBindingData { // 创建卡片实例时触发返回初始数据 let data { todoList: loadTodoFromStorage() }; return formBindingData.createFormBindingData(data); } onUpdateForm(formId: string) { // 定时刷新、消息刷新时触发 let data { todoList: loadTodoFromStorage() }; this.formProvider.updateForm(formId, formBindingData.createFormBindingData(data)); } onRemoveForm(formId: string) { // 卡片从桌面移除时触发做清理工作 console.info(Form removed: ${formId}); } }这段代码的逻辑很直白。onAddForm 相当于前端的 created 生命周期卡片实例创建时调用返回值就是卡片 UI 拿到的初始数据。onUpdateForm 相当于一个定时触发的更新回调系统要求更新卡片时调用我们在这里重新读取最新数据然后调用 formProvider.updateForm 推送下去。onRemoveForm 相当于 beforeDestroy卡片被删时做善后清理。对于前端来说最需要扭转的认知是普通页面的生命周期由用户操作驱动比如路由跳转、组件挂载而卡片的生命周期很大一部分是由系统桌面驱动。用户把卡片拖到桌面onAddForm 被调用系统觉得该刷新了onUpdateForm 被调用。你无法控制用户什么时候删卡片但你要保证每个生命周期都能正确处理。4.2 三种刷新姿势各自有坑卡片要显示“最新数据”前端同学第一反应可能是“写个 setInterval 定时请求”。鸿蒙不让你这么干卡片的刷新机制是系统统一管理的主要有三种姿势刷新方式配置/API适用场景注意点定时刷新form_config.json 里的 updateDuration半小时级更新的信息卡片有最小粒度限制不能设太短指定时间刷新scheduledUpdateTime每天固定时间日报、提醒类卡片只能精确到时分24 小时内一次消息触发刷新卡片按钮里 postCardAction 发 message用户主动操作后刷新需要卡片 UI 配合事件驱动先说定时刷新。updateDuration 的取值和功耗限制在不同 API 版本里写法略有差异我实操时发现设成 1 并代表“每 1 分钟刷新”系统会按功耗策略做最小间隔限制。你最好用 Log 输出验证实际刷新频率别只看配置。刷得太频繁系统会直接忽略你的请求这个坑我在真机上踩过。指定时间刷新适合“每天早上更新一次”的场景。比如我的待办卡片配置了 scheduledUpdateTime 为 10:30每天早上十点半自动拉一次当天任务。它的限制是只能一天一次适合变化不频繁的内容。消息触发刷新是最灵活的方式。在卡片 UI 里放一个按钮点击后发送 message 动作系统会回调 FormExtensionAbility 的更新方法然后重新拉数据推给卡片。这种方式不受定时器频率限制用户点一下刷新一次体验最实时。4.3 postCardAction 的三种 action 怎么选postCardAction 是卡片交互的核心函数它有三个动作router、message、call。router 最常用就是卡片点击跳转到 App 页面。我在待办卡片里点击“查看全部”就跳转到主应用的待办列表页。这里的 abilityName 要和你工程里实际的主 Ability 名称完全一致比如默认就是 EntryAbility大小写都不能错。message 是“通知后台更新数据”的动作。比如我卡片上放一个“标记完成”的按钮用户点了之后先发 message 给 FormExtensionAbility后台更新数据库里的待办状态再把最新数据推给卡片。这就像前端里子组件发出一个自定义事件让父组件重新拉数据。call 是拉起一个后台任务比如拉取最新天气、执行一段耗时的数据处理。它和 message 的区别在于 call 可以启动一个独立的 ExtensionAbility 来处理任务适合计算量稍大的场景。我的经验是能跳页就用 router只更新数据就用 message需要后台多做一些事就用 call。前端同学可以把这三种动作类比成 location.href、window.postMessage 和 Web Worker 消息。4.4 数据从哪来Preferences 与 AppStorage卡片数据不能直接在 UI 里通过 fetch 拿系统的网络限制比较多而且卡片 UI 的运行环境不允许随便发异步请求。更靠谱的做法是主应用把数据写到本地存储比如 Preferences 或数据库然后 FormExtensionAbility 在 onAddForm 和 onUpdateForm 里读取再通过 FormBindingData 推给 UI。这个模型和前端很像主应用负责写 localStorage卡片只负责读。但要注意一点Preferences 的读写是异步的我在 onUpdateForm 里最初直接同步读结果拿到的是空值。改成都用 async/await 包裹后数据链路才稳定。AppStorage 和 LocalStorage 是页面和自定义组件的状态管理工具但不直接做持久化。前端的 Vuex 也不是持久化工具要配一个 localStorage 插件才能跨会话保存鸿蒙这边同理。如果你有跨应用共享的需求可以考虑 AppStorage 配合 PersistentStorage 使用但卡片场景下最简单的方案还是“Preferences 存储数据 FormExtensionAbility 推送数据”。5. 调试、上真机、避坑我花了两周才摸清的细节5.1 预览器只能看 UI交互请上真机DevEco Studio 自带的 Previewer 可以预览卡片布局这对写 UI 非常方便效果好而且能快速切换不同尺寸的卡片预览。但它只是“静态渲染”FormExtensionAbility 的生命周期回调不执行postCardAction 也不生效。我第一次做卡片时在预览器里看到 UI 一切正常沾沾自喜地以为完成了结果一上真机桌面卡片空白一片。排查了半天才发现onAddForm 返回数据时抛了个异常预览器压根不跑这段逻辑所以看不到问题。后来我养成了一个习惯任何涉及生命周期和数据注入的功能一律直接连真机验证Previewer 只用来调布局。真机调试时日志输出非常关键。我习惯在 FormExtensionAbility 的生命周期里加 Log 输出把 formId、返回的数据对象全部打出来再在 DevEco Studio 的 Log 面板里过滤关键词。这样卡片刷没刷新、数据长什么样一眼就能看到。5.2 卡片尺寸适配与布局白名单卡片尺寸的单位是“格”cell一格大约是桌面一个标准图标的大小。常见尺寸有 1x2、2x2、2x4、4x4。一张 2x2 的卡片实际可用的空间非常小所以布局策略和写网页完全不同。我的经验是小尺寸卡片只放核心信息不要堆内容。2x2 的待办卡片标题加两三条待办就够了再多就拥挤了。2x4 可以多放一条列表同时支持点击跳转。前端同学容易犯的毛病是把网页的排版习惯带进来比如在 2x2 卡片里塞一个复杂表格结果字都挤变形了。组件白名单方面Row、Column、Stack、Text、Image、Button、List、ForEach 这些常规组件在卡片里都能用但 Web、Video、Canvas、XComponent 这类重组件就完全不支持。动画也要慎用布局类的隐式动画还好复杂属性动画在卡片上可能直接被忽略。字体适配要依赖 form_config 里的 designWidth 和 autoDesignWidth。我建议 designWidth 固定用 720autoDesignWidth 打开这样卡片在不同桌面尺寸上能按比例缩放。否则真机上字体大小可能和你预览时相差很大。5.3 ArkTS 严格模式前端同学最容易破防的地方ArkTS 是 TypeScript 的超集但为了运行时性能和安全它加了很多限制。前端 TS 写得飞起的人到 ArkTS 这里会各种编译报错这是正常现象不用怀疑自己。最常见的几个破防点不能随便用 any。前端用 any 的地方在 ArkTS 里必须显式声明类型否则编译直接失败。对象字面量不能直接作为复杂类型使用必须先声明 interface 或 class。解构赋值在部分场景不支持或者行为受限我干脆改成逐个取值。比如我一开始写卡片数据源时习惯性地写let { todoList } loadData();结果编译直接报错。改成let data loadData(); let todoList data.todoList;就正常了。还有动态给对象加属性也不行。前端习惯了“先建对象再往里塞字段”在 ArkTS 里你要么一开始就把所有字段声明完整要么用 Map 结构。这些限制会让写惯了 JS 的人觉得“束手束脚”但适应一周之后你会发现类型约束带来的编译期检查实际上减少了很多运行时 bug。5.4 我踩过的坑清单最后整理一份真实的踩坑清单每一行都是我在两个月里实际遇到的问题现象根因解决办法预览器正常真机卡片空白onAddForm 返回数据时抛异常在生命周期里加日志检查返回值桌面添加卡片时找不到应用module.json5 或 form_config.json 配置错误检查 extensionAbilities 和 profile 文件路径postCardAction 跳转没反应abilityName 和主 Ability 名称不一致确认 EntryAbility 的实际名称和大小写卡片字体忽大忽小没用 designWidth 适配设置 720 autoDesignWidth定时刷新长时间不更新updateDuration 触发了功耗限制用 Log 验证实际刷新频率ArkTS 编译一堆红用了 any、解构、动态属性按严格模式逐个修正深色模式下文字看不清卡片固定白色背景但字体颜色不适配配置 colorMode 或使用系统资源色真机添加卡片后立即消失onAddForm 里数据读取超时Preferences 异步读取确保数据返回后再 createFormBindingData这里面最坑的是第一个预览器正常但真机空白。原因是预览器不执行生命周期代码导致 onAddForm 里的异常根本暴露不出来。后来我做任何卡片改动都第一时间上真机看日志预览器只当“样式确认工具”用。ArkTS 严格模式的坑也不小特别是团队里从前端转过来的同学写的代码被编译器教育两天之后基本就能养成“先定义类型、再操作变量”的习惯了。这个习惯丢进普通前端项目里也是好事类型严谨性提升了线上 bug 会少很多。最后聊几句实际的感受做完这张待办卡片我最大的体会是前端转鸿蒙没有想象中那么“伤筋动骨”。组件化、数据驱动、状态管理、事件响应这些基本功全部能平移。真正需要花时间啃的是系统的生命周期模型、数据刷新机制、ArkTS 的严格模式以及 DevEco Studio 这套工具链的使用习惯。如果你想试水我建议别去看太多理论文章直接用 DevEco Studio 创建一个工程把官方默认的卡片模板跑通然后把生命周期方法的日志打出来观察一次添加、刷新、删除的完整过程。再自己加一个按钮用 postCardAction 做一次页面跳转。这三步走完你基本就掌握卡片开发的主链路了。后面想往深了做可以研究一下数据共享、FormExtension 的多实例管理、不同尺寸卡片的动态布局切换每一块都有继续挖的空间。