拓冰建站拓冰建站
首页 / 资讯中心 / 正文

ArkTS+ArkUI复刻微信UI:声明式UI与状态管理实战

简介面向华为鸿蒙HarmonyOS初、中级开发者这份示例项目演示了如何用ArkTS语言搭建“微信”风格的用户界面尤其适合熟悉TypeScript或前端技术、希望快速上手ArkUI的开发者作为入门参考。资源共100个文件压缩包仅1.89MB整体轻量易得其中16个ets文件承载页面与组件核心逻辑覆盖聊天、通讯录等主要界面4个ts文件补充业务处理17个json/json5用于工程与模块配置54个png作为图标和图片素材其余还包含构建脚本、属性文件等目录划分清晰便于按模块逐一研读。目前已有785人浏览学习。通过拆解这套代码开发者可以掌握ArkTS组件写法、状态管理、样式配置及鸿蒙工程的组织方式也能直接参考其数据模拟和界面切换思路快速迁移到自己的应用原型或课程设计中减少从零搭建UI的重复工作深入理解华为生态下的原生应用设计习惯。1. 拿到「鸿蒙原生Arkts搭建的微信UI.zip」后先想清楚要学什么不少人下载这类项目压缩包第一反应是解压、跑起来、截张图。但「鸿蒙原生Arkts搭建的微信UI.zip」这个标题信息量其实很大它意味着整套界面不是 WebView 套壳不是 Android XML 迁移而是用 ArkTS 语言配合 ArkUI 声明式框架在 HarmonyOS 应用模型下从零描述出的微信风格界面。你拿到的是一份「用代码画 UI」的样本而不是一份可以直接上架的应用。这个 zip 包里真正值钱的东西是三层第一层是 ArkTS 的语法习惯包括装饰器、状态管理、UI 描述写法第二层是 ArkUI 的布局思路比如 Row、Column、List、RelativeContainer 怎么组合出微信那种复杂的会话列表和聊天气泡页第三层是工程层面的组织方式路由怎么跳、组件怎么拆、资源怎么管。适合的人群是正在学鸿蒙应用开发、想从「能跑 Hello World」跨到「能还原复杂界面」的开发者。所以这篇博文顺着标题往下讲ArkTS 这套语言范式为什么适合写 UI、一个微信风格的项目应该怎么搭、聊天列表和会话页的核心代码怎么写、最后再聊怎么把 zip 包里的代码嚼碎变成你自己的东西。2. ArkTS 和 ArkUI 凭什么能还原微信 UI先懂声明式 UI 的底子2.1 声明式 UI 和命令式 UI 的本质区别微信 Android 版用 Java/Kotlin 写界面时典型套路是findViewById拿到控件再调用setText、setOnClickListener去改界面。这是一种命令式写法你告诉系统「每一步做什么」。而 ArkTS 写鸿蒙原生界面走的是声明式路线你描述的是「界面应该长什么样」至于什么时候刷新、怎么刷新交给框架处理。看这段最小示例就能感受到差别Entry Component struct HomePage { State message: string Hello HarmonyOS; build() { Column({ space: 10 }) { Text(this.message) .fontSize(28) .fontWeight(FontWeight.Bold) Button(更新文字) .onClick(() { this.message ArkTS 状态驱动 UI 刷新; }) } .padding(20) .alignItems(HorizontalAlign.Center) } }逻辑说明Entry标注页面入口组件Component声明这是一个自定义组件State是状态装饰器——当message变量变化时所有依赖它的 UI 会自动重新渲染。按钮的onClick回调里不需要手动操作Text组件只需改数据。参数说明Column是垂直布局容器space控制子组件间距HorizontalAlign控制交叉轴对齐方式这里把子组件水平居中。这套东西和 Flutter、SwiftUI 的思维模型非常相近但装饰器和语法是 ArkTS 自己的学习成本主要集中在这些装饰器上。这个差异直接决定了你从 zip 包里读代码的方式别去找「创建控件的代码」去找「状态变量在哪、状态如何流转」。微信 UI 里所有动态效果——未读红点、消息时间、Tab 切换高亮、右上角菜单弹出——本质都是数据变化驱动的 UI 重建。2.2 状态管理State、Prop、Link、Provide/Consume 怎么选还原微信这种多页面、多处共享状态的应用「状态放哪」是最快拉开代码水平差距的地方。ArkTS 的状态装饰器分好几层选错了要么刷新不到要么过度刷新。装饰器作用范围适用场景微信 UI 对应场景State组件内部组件私有状态会话列表页的搜索框输入内容Prop父传子单向子组件呈现只读数据聊天泡泡里的文本内容Link父子双向子组件要修改父组件状态会话列表左滑删除按钮的状态Provide/Consume跨层级祖先和任意后代共享深色模式切换需要全局广播写聊天消息组件时消息内容就适合用Prop父组件传入后子组件不允许改。而「左滑出现删除按钮」这种交互滑动状态往往放在子组件内部用State删除后的列表数据变化则在父组件里处理。一个实战项目里最容易出的问题是把所有状态都塞进State导致父组件一刷新整个微信首页的会话列表全部重建滚动位置丢失。正确做法是将状态粒度拆小滑动操作、长按弹出菜单这类临时交互状态尽量局部化真正需要跨页共享的才提升到Provide。2.3 从 zip 包看项目结构用模块化支撑微信 UI 的复杂度解压一个高质量的鸿蒙 ArkTS 微信 UI 工程你会看到大致这样的目录AppScope/ app.json5 entry/ src/main/ ets/ entryability/ EntryAbility.ets pages/ Index.ets ChatListPage.ets ChatDetailPage.ets DiscoverPage.ets ProfilePage.ets components/ ChatListItem.ets ChatBubble.ets TabBarItem.ets BadgeDot.ets model/ ChatData.ets MessageData.ets resources/ base/ element/ media/逻辑说明pages放页面级组件一个页面一个文件components放可复用的 UI 零件model放数据类和类型定义。微信 UI 之所以拆出这么多组件不是因为微信设计多复杂而是聊天列表、消息气泡、底部 Tab 这些零件在多个页面复用的概率很高。你拿到 zip 包时可以做一次「重组练习」把ChatListItem、ChatBubble、BadgeDot这几个组件单独提出来试着在新项目里复用看它们的依赖是否清晰。如果一个组件文件里堆了上千行、大量逻辑写在build()里那这个 zip 的可复用性就打折扣你更多是要从里面抄思路而不是直接搬代码。3. 本地跑通微信 UI 工程DevEco Studio 下的最小启动链路3.1 建工程、配签名、跑模拟器的三个关键动作打开 DevEco Studio新建工程时选择Application→Empty Ability模板会生成一个EntryAbility和一个Index页面。要把微信 UI 跑起来接着做三件事。第一步确认module.json5里的deviceTypes包含你当前运行的设备类型预览器、模拟器、真机对应的取值不同。默认模板已经带了phone和tablet直接跑预览器没问题。第二步把入口页面指到微信 UI 的第一个页面。打开EntryAbility.ets找到onWindowStageCreate方法里面调用了windowStage.loadContent(pages/Index, ...)。如果你的 zip 包页面路径不是这个需要改成实际路径onWindowStageCreate(windowStage: window.WindowStage): void { windowStage.loadContent(pages/ChatListPage, (err) { if (err.code) { console.error(Failed to load content: ${err.code}); return; } }); }逻辑说明loadContent的第一个参数是 ets 页面在src/main/ets下的相对路径不带.ets后缀。入口页面替换成ChatListPage后App 一启动就会进入微信的聊天列表而不是模板自带的 Hello World。第三步如果是自己新建的工程需要配置签名才能跑真机。DevEco 的File → Project Structure → Signing Configs里勾选Automatically generate signature它会自动生成调试证书和 Profile。模拟器不需要额外签名预览器也不需要。提示跑不起来时先看 Build 窗口的报错ArkTS 对类型检查比普通 TypeScript 严格很多。最常见的是any类型滥用——ArkTS 在 API 9 之后限制了显式any遇到Type any is not assignable to type就去代码里找类型标注问题。3.2 底部 Tab 栏微信 UI 的骨架用 Tabs 组件搭微信底部四个 Tab 的经典布局在 ArkUI 里有两种实现方式。一种是自己写Row摆四个Column用State记录当前选中索引另一种是直接用Tabs容器组件让框架管理页面切换和滑动。实战里我推荐用Tabs组件搭底部栏原因是页面切换的生命周期、滑动边缘效果都由系统处理减少自己手写状态同步的代码量。看这段核心实现Entry Component struct MainFrame { State currentIndex: number 0; private tabsController: TabsController new TabsController(); Builder tabBuilder(index: number, title: string, normalIcon: Resource, selectedIcon: Resource) { Column({ space: 4 }) { Image(this.currentIndex index ? selectedIcon : normalIcon) .width(26) .height(26) .objectFit(ImageFit.Contain) Text(title) .fontSize(10) .fontColor(this.currentIndex index ? #07C160 : #999999) } .width(100%) .height(100%) .justifyContent(FlexAlign.Center) } build() { Tabs({ barPosition: BarPosition.End, controller: this.tabsController }) { TabContent() { ChatListPage() } .tabBar(this.tabBuilder(0, 微信, $r(app.media.icon_chat_normal), $r(app.media.icon_chat_selected))) TabContent() { DiscoverPage() } .tabBar(this.tabBuilder(1, 发现, $r(app.media.icon_discover_normal), $r(app.media.icon_discover_selected))) TabContent() { ProfilePage() } .tabBar(this.tabBuilder(2, 我, $r(app.media.icon_profile_normal), $r(app.media.icon_profile_selected))) } .onChange((index: number) { this.currentIndex index; }) } }逻辑说明Tabs的barPosition设为BarPosition.End让 Tab 栏显示在底部。tabBar方法接收一个自定义 BuilderBuilder 里通过currentIndex和遍历索引的比较来决定图标和文字颜色。这就是状态驱动的典型写法——你不需要手动遍历子组件去改颜色改一个变量四个 Tab 自动刷新。参数说明TabContent里的子组件就是各页面根组件。注意ChatListPage等页面组件必须是以Component声明的 struct不能是普通自定义函数。$r(app.media.icon_chat_normal)是资源引用语法图标文件放在resources/base/media目录下没有对应图标文件会直接编译报错。3.3 导航与路由微信的二级页面靠 Navigation 还是 router微信聊天列表点进会话详情、发现页点进朋友圈这种二级页面跳转在鸿蒙里有两套方案router模块和Navigation容器。router是传统命令式跳转router.pushUrl({ url: pages/ChatDetailPage })就能压栈Navigation是声明式导航页面栈由NavPathStack管理配合NavDestination使用。做微信 UI 这种多页面应用我倾向用Navigation因为它能做转场动画、传参类型安全、支持返回拦截。在根组件里初始化导航容器private pathStack: NavPathStack new NavPathStack(); build() { Navigation(this.pathStack) { MainFrame() } .mode(NavigationMode.Stack) .onAppear(() { this.pathStack.pushPathByName(ChatListPage, null); }) }在主页面里的跳转写法this.pathStack.pushPathByName(ChatDetailPage, { chatId: this.chatItem.id, chatName: this.chatItem.name });逻辑说明pushPathByName的第二个参数是传给目标页面的参数对象。目标页面用NavDestination接收在onReady回调里通过this.pathStack.getParamByName(ChatDetailPage)取出参数。这套机制的好处是页面之间不直接互相 import路由解耦和微信这种多页面 App 的结构匹配度更高。注意用router跳转时页面路径是字符串用Navigation时页面必须在NavDestination中注册组件名两者混用会导致路由表混乱。一个项目里只选一种方案zip 包里的代码用哪种就跟哪种。4. 微信 UI 三件套实现聊天列表、会话详情、发现页4.1 会话列表用 List 容器复刻微信首页的完整交互微信首页的会话列表拆开看核心元素是头像、昵称、最后一条消息、时间、未读红点、左滑删除、长按菜单。用 ArkUI 实现时List加ForEach是骨架SwipeAction是左滑删除的开关。先定义会话数据模型// model/ChatData.ets export class ChatData { chatId: string; name: string; avatar: string; lastMessage: string; lastTime: string; unreadCount: number; constructor(chatId: string, name: string, avatar: string, lastMessage: string, lastTime: string, unreadCount: number) { this.chatId chatId; this.name name; this.avatar avatar; this.lastMessage lastMessage; this.lastTime lastTime; this.unreadCount unreadCount; } }页面主体用List渲染Entry Component struct ChatListPage { State chatList: ChatData[] [ new ChatData(1, 产品讨论组, 张伟, 明天上午十点评审, 10:24, 3), new ChatData(2, 李婷, , 好的收到, 09:58, 0), new ChatData(3, 文件传输助手, , 这份方案你再看下, 昨天, 0) ]; Builder itemEnd(chat: ChatData) { Row({ space: 8 }) { Text(标为未读) .fontSize(14) .fontColor(#FFFFFF) Text(删除) .fontSize(14) .fontColor(#FFFFFF) } .padding({ left: 16, right: 16 }) .height(100%) .justifyContent(FlexAlign.Center) .backgroundColor(#FF3B30) } build() { List({ space: 0 }) { ForEach(this.chatList, (item: ChatData) { ListItem() { ChatListItem({ chat: item }) .onClick(() { // 跳转会话详情 }) } .swipeAction({ end: this.itemEnd(item) }) }, (item: ChatData) item.chatId) } .width(100%) .height(100%) .backgroundColor(#F5F5F5) .scrollBar(BarState.Off) } }逻辑说明ForEach的第三个参数是键值生成函数这里用chatId作为唯一键——删除、插入时 List 才知道哪些项需要重建。swipeAction的end参数指右滑或者叫左滑露出的尾部操作区Builder 里放两个按钮。参数说明ScrollBar设置为BarState.Off隐藏滚动条更贴近微信原版视觉。ChatListItem是自定义组件通过Prop接收ChatData对象。想让时间戳显示更接近微信的「10:24」「昨天」可以在ChatListItem里写一个格式化函数根据lastTime做二次处理。注意性能点会话列表数据量大的时候ForEach的键值不能是数组下标否则删除任意一项后全部重建肉眼可见掉帧。项目里如果发现滑动卡顿先检查键值生成函数。4.2 聊天气泡Row 加 Flex 对齐实现左右分栏聊天详情页是微信 UI 里最能体现实力的部分。左右气泡的布局核心是Row和Flex的排列控制发出去的消息靠右收到的消息靠左气泡内部又是一个纵向布局。先看消息数据结构和组件// model/MessageData.ets export class MessageData { msgId: string; isSelf: boolean; text: string; time: string; constructor(msgId: string, isSelf: boolean, text: string, time: string) { this.msgId msgId; this.isSelf isSelf; this.text text; this.time time; } }Component export struct ChatBubble { Prop message: MessageData; build() { Row() { if (!this.message.isSelf) { // 对方头像在左边 Image($r(app.media.avatar_other)) .width(40) .height(40) .borderRadius(6) } Column({ space: 6 }) { if (!this.message.isSelf) { Text(对方昵称) .fontSize(11) .fontColor(#999999) } Text(this.message.text) .fontSize(16) .fontColor(#333333) .padding({ left: 12, right: 12, top: 8, bottom: 8 }) .backgroundColor(this.message.isSelf ? #95EC69 : #FFFFFF) .borderRadius(8) } .alignItems(this.message.isSelf ? HorizontalAlign.End : HorizontalAlign.Start) if (this.message.isSelf) { // 自己头像在右边 Image($r(app.media.avatar_self)) .width(40) .height(40) .borderRadius(6) } } .width(100%) .justifyContent(this.message.isSelf ? FlexAlign.End : FlexAlign.Start) .padding({ left: 12, right: 12, top: 4, bottom: 4 }) } }逻辑说明核心技巧在justifyContent上——isSelf为 true 时整个Row靠右false 时靠左。头像的渲染顺序也由isSelf控制自己的头像放在Text后面对方的头像放在前面形成头像在气泡外侧的视觉效果。参数说明backgroundColor里用的#95EC69是微信聊天绿色气泡的经典色值不是纯绿。borderRadius统一用8如果需要微信那种一个小角是直角的风格可以给四个角分别用borderRadius的数组参数[8, 8, 8, 2]之类。alignItems在Column里控制气泡文字和内部元素的对齐方向。聊天详情页的滚动区域用List加scrollToIndex实现private listScroller: Scroller new Scroller(); build() { List({ scroller: this.listScroller }) { ForEach(this.messageList, (msg: MessageData) { ListItem() { ChatBubble({ message: msg }) } }, (msg: MessageData) msg.msgId) } .width(100%) .layoutWeight(1) .scrollBar(BarState.Off) // 页面 onPageShow 时滚动到底部 this.listScroller.scrollToIndex(this.messageList.length - 1, true); }逻辑说明scrollToIndex的第二个参数控制是否平滑滚动true是平滑。进入页面时滚动到底部符合微信聊天打开定位到最新消息的习惯。技巧Alt 加类型标注的Text(this.message.text)在message.text很长时会自动换行但 ArkTS 里换行受Text组件的maxLines和textOverflow影响如果不想设最大行数保持默认即可。气泡宽度自适应内容最大宽度由父容器约束。4.3 发现页Stack 布局与角标、红点发现页是微信 UI 里相对轻量的一个页面——一组列表套着图标和文字右侧一个箭头图标部分条目带红点或角标。用 ArkUI 做这页最舒服因为List加ListItem天然支持分组微信的分组效果就是多个List或者一个List加多个ListItemGroup。实现分组列表的典型写法List({ space: 8 }) { ListItemGroup({ header: this.groupHeader(社交) }) { ForEach(this.socialItems, (item: MenuItem) { ListItem() { this.menuRow(item) } }) } ListItemGroup({ header: this.groupHeader(生活服务) }) { ForEach(this.serviceItems, (item: MenuItem) { ListItem() { this.menuRow(item) } }) } }menuRow用Row摆三件套左侧图标、中间文字、右侧箭头。角标用Stack叠加实现Builder menuRow(item: MenuItem) { Row() { Image(item.icon) .width(24) .height(24) Text(item.title) .fontSize(16) .margin({ left: 12 }) Blank() if (item.badge) { Stack() { Text(item.badge) .fontSize(10) .fontColor(#FFFFFF) } .width(18) .height(18) .borderRadius(9) .backgroundColor(#FF3B30) .margin({ right: 8 }) } Image($r(app.media.icon_arrow_right)) .width(8) .height(14) } .padding({ left: 16, right: 16, top: 14, bottom: 14 }) .width(100%) .backgroundColor(#FFFFFF) }逻辑说明Blank()是一个弹性空白组件自动占据剩余空间把右侧箭头和角标推到最右边。Stack在这里的作用是给文字套一个背景圆形容器视觉上呈现红点效果。角标数字超过 9 时width和height固定为 18 会导致文字溢出正确做法是把padding加上、去掉固定宽高。参数说明ListItemGroup的header是一个 Builder显示分组标题比如「社交」「生活服务」。注意微信的发现页每组行之间没有间距组与组之间有间距用List的space控制组间距ListItem内的Row控制行内边距。发现页的亮点在于阴影和分割线。微信的凹槽式列表用ListItem的backgroundColor加borderRadius模拟上下圆角通过borderRadius的数组参数分别设置。很多 zip 工程会在这一步偷懒直接贴一张整图那这个项目对学习的价值就少了一半。5. 从 zip 到自己的工程重组、优化、验证的三步走5.1 按「页面、组件、模型」三层重构杂乱的代码从网上下载的 zip 包通常结构混乱、命名随意、注释夹杂拼音直接拿去改业务会非常痛苦。我一般会做一次重构按页面、组件、模型三层重新摆放。具体动作是先跑通原工程确认没有任何编译错误然后把每个页面的build()方法里超过 50 行的部分抽成独立组件再把页面间共享的数据类挪到model目录。这一步做完你会发现原工程里的很多「硬编码」——比如直接在ChatListItem里写死了头像图片路径——被集中到了数据模型和资源文件里这时候整个工程的维护成本才降到正常水平。5.2 深色模式与响应式布局微信 UI 在真机和双屏上的适配技巧鸿蒙原生应用从系统层面支持深色模式如果在代码里直接写死#FFFFFF这种颜色值深色模式下会亮得刺眼。推荐用Resource颜色替代硬编码Text(this.message.text) .fontColor($r(sys.color.text_primary)) .backgroundColor($r(app.color.message_bubble_self))app.color需要在resources/下定义深色和浅色两套资源而sys.color是系统提供的语义色。做微信 UI 时把背景色、文字色、分割线色全部替换成语义资源App 就会自动跟随系统深浅色切换。响应式布局方面微信 UI 主要是竖屏手机页面但List的宽度、ForEach的列表项高度要避免写死数值。能用layoutWeight(1)撑满的空间不要用固定width(100%)能让List自适应的不要给绝对高度。折叠屏上跑的时候关键看Tabs和Navigation是否跟着breakpoint变化——这一步做得好同一个代码工程能在手机和平板上都保持微信的视觉比例。5.3 验证 UI 还原度的两个手段组件截图对比和交互走查UI 还原类项目的验收标准和业务项目不同核心不是「功能完整」而是「和原版有多像」。我习惯用两条线验证。第一条是视觉线在 DevEco 的 Previewer 里分别打开还原页面和微信截图逐块对比间距、字号、色值发现差异后用 DevEco 的 Inspector 能力查看组件的实际渲染属性定位是 padding 问题还是字体问题。第二条是交互线重点走查滚动边界、左滑删除的触发阈值、气泡长按菜单的弹出位置。SwipeAction的offset默认值在不同版本上有细微差异如果你的工程跑出来的左滑删除距离太短或太长直接调SwipeAction的参数而不是在ListItem里加偏移量去补偿。最后一个实用技巧把 zip 包里所有资源文件替换成自己项目的图标前先用hvigor做一次全量编译注意看有没有resource引用失效的编译警告。很多从网上拉下来的工程图标文件夹里缺文件但编译不报错——因为$r引用是运行时解析的——等你跑到页面上才发现图标裂了。给Image组件加一个默认占位图启动时就能暴露这类隐藏的引用问题这是整理这类 zip 工程时最该先做的动作。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门