ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ArkTS入门:从TypeScript语法到鸿蒙状态管理与声明式UI

ArkTS入门:从TypeScript语法到鸿蒙状态管理与声明式UI 1. 类型系统先打好语法地基1.1 变量声明与常量意识ArkTS是TypeScript的超集所以在入门阶段你完全可以按照TS的写法先跑起来但真正上手项目之后你会发现ArkTS在类型约束上比纯TS更严格尤其是对any类型的使用有诸多限制。这其实是个好事因为UI应用的代码往往需要多人协作类型越明确后期维护越省心。先说说变量声明。let和const是主力var能不用就不用。原因很简单var存在变量提升和函数级作用域的问题写复杂业务逻辑时容易踩坑。我见过不少新人写ArkTS代码时习惯性打var结果在循环里闭包捕获值的时候翻车排查半天才发现是作用域问题。ArkTS里推荐使用let声明可变变量const声明不可变引用。let count: number 0 const MAX_COUNT: number 100 // 推荐优先使用const const TAX_RATE: number 0.06 const BASE_URL: string https://api.example.com这里有个细节ArkTS里const修饰的引用类型比如const list: number[] []你仍然可以调用list.push(1)。const保证的是“变量指向的地址不变”不是“对象内容不可变”。这个和JS/TS完全一致但新人经常误以为const数组就不能修改导致写出不必要的防御代码。另外ArkTS官方编码规范里建议常量字符串建议用const而不是let这不仅是习惯问题还能让编译器做更多优化。实测下来用const声明的基础类型常量在华为方舟编译器下能拿到更积极的优化结果性能上有实打实的差别。1.2 基础类型与联合类型的实用姿势ArkTS支持的基础类型就是JS那套string、number、boolean、null、undefined以及ES6引入的symbol。日常开发里最常用的是前三者。但有一点需要注意ArkTS里number没有细分float、double、int统一都是浮点数这在做数值计算时要心里有数。联合类型在ArkTS里使用频率极高尤其适合表达“取值范围有限”的变量type ViewMode list | grid | card let currentMode: ViewMode list // 合法 currentMode grid // 报错不能将table分配给类型ViewMode currentMode table联合类型最大的价值是编译期检查。你写currentMode table时编译器直接报错不用等运行时报错才发现。在UI场景里这种模式可以用来控制页面状态、按钮类型、主题风格等比用普通字符串安全得多。还必须要提的是undefined和null的处理。ArkTS是严格模式下运行的函数的入参如果可能为空建议显式加上可选标记function formatUserName(name?: string): string { if (name undefined) { return 未命名用户 } return name.trim() }这里?:表示参数可选传入undefined不会报错。在ArkTS的UI回调里这类写法非常常见因为很多事件回调的参数可能为空比如onClick的回调正常情况下不会给参数但你在封装公共组件时回调函数参数的可选性一定要提前设计好否则组件之间传值会出现“莫名拿不到值”的尴尬。1.3 接口和类型别名结构化数据的核心表达接口interface在ArkTS中的地位非常核心。鸿蒙应用开发里数据模型的建模基本都靠接口完成。比如一个电商页面里的商品信息你应该先定义好接口再去写UI和业务逻辑interface Product { id: string name: string price: number description?: string tags: string[] }定义一个商品接口后你就可以在组件中放心地使用Product类型声明变量IDE会有完整的代码提示字段名打错了编译器会立刻报错。这对于字段繁多的业务模块来说绝对是提效利器。接口还支持继承在做多层数据建模时非常有用interface BaseResponse { code: number message: string } interface ProductListResponse extends BaseResponse { data: Product[] }类型别名type可以定义联合类型、交叉类型等更复杂的类型表达type ID string | number type CallbackT (value: T) void关于interface和type怎么选我的经验是定义对象形状用interface表达联合、工具类型、函数签名用type。这个问题属于高频面试点也是新人最容易纠结的点记住这个选择逻辑就够了。1.4 泛型入门写一次用多处泛型在ArkTS里的应用场景非常集中列表组件、数据请求封装、状态管理工具函数。理解泛型最简单的比喻就是“占位符”——你先不写死类型等调用的时候再指定。比如封装一个列表加载函数async function fetchListT(url: string): PromiseT[] { const response await fetch(url) const data await response.json() return data as T[] } // 使用时指定具体类型 const products await fetchListProduct(/api/products)这里T就是泛型占位符调用fetchListProduct时函数内部所有的T都会被替换成Product返回的结果就能拿到完整类型提示。使用泛型后这套请求逻辑可以被商品、订单、用户等各种列表复用不需要为每种数据类型单独写一个请求函数。ArkTS内置的ArrayT、PromiseT本身也是泛型你写ArrayProduct和Product[]等价。在UI开发中ForEach循环渲染列表时数组元素的类型明确之后循环体里的每一项都能拿到完整的类型推断这是提升开发体验的关键一环。按照官方最佳实践和社区反馈泛型用得好的代码重构起来也轻松很多——改一处类型定义所有引用点同步更新不用到处打补丁。2. 函数与面向对象真正写代码的姿势2.1 函数字面量、箭头函数和this陷阱ArkTS继承了TS的函数体系但UI开发里函数最常见的用法不是声明式函数而是函数字面量——也就是把函数当作一个值传来传去。典型场景是事件回调Button(点击) .onClick(() { console.info(按钮被点击了) })箭头函数() {}在ArkTS中是绝对的主力。它比起function() {}最大的优势在于this的绑定逻辑。普通函数的this取决于调用方式而箭头函数在定义的时候就固定了this指向外层作用域。在组件方法里写回调时这差距体现得极其明显Component struct CounterPage { State count: number 0 handleClick(): void { this.count } build() { Column() { Button(用箭头函数) .onClick(() this.handleClick()) Button(用普通函数) // 这行会出大问题this指向变了 .onClick(this.handleClick) } } }第二种写法onClick(this.handleClick)是把函数引用传出去了等真正触发点击时this已经不再指向组件实例运行时会直接报“Cannot read property count of undefined”。很多新人第一次遇到这个报错都一脸懵以为是ArkTS的bug其实是JS/TS通用的this陷阱。解决方案就是在onClick里用箭头函数包裹一层强制绑定调用时的this。函数参数的默认值和解构赋值也是高频操作interface UserInfo { name: string age: number } function printUser({ name, age }: UserInfo): void { console.info(用户${name}年龄${age}) }2.2 class与继承组件的底层骨架ArkTS组件用struct定义但struct里面跑的仍然是class那一套。实际开发中当你的组件逻辑变复杂时光靠struct可能不够你需要抽象出一些业务类来处理数据计算、网络请求等职责。ArkTS的class和TS基本一致但有几个特性需要额外关注。第一成员变量必须显式声明类型不能靠隐式推断混过去第二public、private、protected这些访问修饰符要善用尤其是在做数据管理类时没有访问控制的话团队协作时很容易出现“谁都能改数据”的混乱局面。class ShoppingCart { private items: Product[] [] private totalPrice: number 0 addItem(product: Product): void { this.items.push(product) this.calculateTotal() } private calculateTotal(): void { this.totalPrice this.items.reduce( (sum, item) sum item.price, 0 ) } getTotal(): number { return this.totalPrice } }继承在ArkTS里也常用尤其是封装基础组件时。假设你有多个页面都需要顶部导航栏可以抽一个BasePage组件在其他页面里继承它Component export struct BasePage { Prop title: string build() { Column() { Text(this.title) .fontSize(20) .fontWeight(FontWeight.Bold) } } } Component struct HomePage extends BasePage { build() { Column() { // 使用继承来的title属性 Text(this.title) // 其他内容 } } }struct继承的关键限制是build()方法的UI描述部分是可以覆盖的但装饰器状态属性是继承来的不能重复声明。另外构造函数里一定要调用super()否则运行时会报初始化错误。这个细节我在实际项目中踩过坑当时是在子组件构造时忘记传参导致父组件属性一直是默认值。2.3 只读属性与访问器ArkTS支持readonly修饰符它可以加在class属性上表示该属性只能在初始化时赋值一次。class AppConfig { readonly APP_NAME: string MyHarmonyApp readonly VERSION: string 1.0.0 }这在定义配置类时非常实用可以防止其他模块在运行过程中意外修改全局配置。另外getter和setter访问器在ArkTS里也要会写尤其是当你需要在属性变更时做数据联动class TemperatureConverter { private celsiusValue: number 0 get celsius(): number { return this.celsiusValue } set celsius(value: number) { this.celsiusValue value } get fahrenheit(): number { return this.celsiusValue * 9 / 5 32 } }fahrenheit只有getter没有setter这样外部代码只能读不能随意改保证了换算逻辑的一致性。在状态管理比较复杂的场景下这种访问器模式能让你把“计算属性”的职责收敛在类内部UI层只负责读取逻辑更清爽。3. 装饰器与状态管理ArkTS的灵魂3.1 装饰器大全家组件化开发的基石ArkTS和纯TS最大的区别就是这套装饰器体系。市面上关于ArkTS的“八股”中装饰器相关题目占据半壁江山足以说明它的核心地位。必须先理解一个核心思想装饰器是ArkTS对UI框架能力的一种声明式绑定普通变量无法触发UI刷新带装饰器的变量才具备响应式能力。我们逐个过一遍最常用的装饰器Entry标记页面入口组件一个页面只能有一个Entry组件。Component声明这是一个自定义组件它的build()方法描述了UI结构。State组件内部的状态变量变更时自动刷新相关UI。Prop接收父组件传入的值单向同步子组件内部不能修改父组件的值。Link与父组件双向同步子组件修改会直接反映到父组件。Provide/Consume跨层级传值的装饰器祖先组件用Provide提供数据任意后代组件用Consume接收。Observed/ObjectLink用于处理对象嵌套的场景让深层次对象的属性变化也能触发UI刷新。Builder装饰一个函数用于封装可复用的UI片段。举个例子一个经典的父子组件传值场景Component export struct ChildComponent { Prop title: string Link count: number build() { Column() { Text(this.title) Button(增减) .onClick(() { this.count }) } } } Entry Component struct ParentPage { State count: number 0 build() { Column() { ChildComponent({ title: 子组件标题, count: $count }) } } }这里有一个非常重要的语法细节父组件传Link时要用$前缀即$count传Prop时直接用变量名count。很多新手在这里写错把$用在Prop上或者漏掉$导致数据不同步这些都是高频编译错误。3.2 组件生命周期页面从生到死的完整过程ArkTS组件的生命周期函数不多但非常重要。按顺序来说aboutToAppear()组件即将挂载时触发适合做初始化数据和启动异步任务。onPageShow()页面每次显示时触发不只是首次进入从后台切回来也会触发。onPageHide()页面隐藏时触发比如跳转到别的页面。aboutToDisappear()组件即将销毁时触发适合清理定时器、释放资源。初学者最容易忽略的一个点aboutToAppear和onPageShow的区别。如果你只在aboutToAppear里加载数据那从其他页面返回时不会重新加载。如果你希望在每次进入页面时都拉取最新数据就必须用onPageShow。Entry Component struct HomePage { State userInfo: UserInfo | null null aboutToAppear(): void { this.loadData() } onPageShow(): void { // 每次进入页面都刷新数据 this.loadData() } }有个细节aboutToAppear里不能做过于耗时的同步操作否则页面进入会卡顿。正确的做法是加载数据用异步函数必要时加一个loading状态变量来控制UI展示。3.3 状态管理设计从单组件到跨组件刚开始用装饰器时很多人会陷入“什么变量都加State”的误区。我见过一个500行的组件里堆了30多个State变量逻辑乱成一团麻后续加需求时改一处坏三处。状态管理的核心设计原则是“就近原则”能用局部普通变量解决的就用普通变量只有需要UI响应的才用State需要父子联动的再考虑Prop和Link。跨层级共享的数据优先考虑Provide/Consume不要一层一层地手动传参那样代码冗余且难维护。这里也要重点提一下Observed和ObjectLink的适用场景。如果你有一个class对象里面嵌套了多层数据直接用State修饰对象时修改对象内部深度嵌套的属性往往不会触发UI刷新Observed class Person { name: string address: Address } Component struct PersonView { ObjectLink person: Person // ... }加了Observed的类配合子组件里的ObjectLink才能实现深层属性的细粒度刷新。这一点在实际开发中异常关键因为大部分业务数据都是多层嵌套的结构体不掌握Observed/ObjectLink状态管理做起来会非常痛苦。4. UI语法糖与页面结构把语法变成界面4.1 声明式UI的构建逻辑ArkTS的UI编写不是“一步步命令式地创建控件”而是“声明式描述界面长什么样”。这个概念需要转变一下思维你的代码就是UI的描述本身。build() { Column({ space: 10 }) { Text(Hello ArkTS) .fontSize(30) .fontColor(Color.Blue) Row({ space: 8 }) { Button(按钮A) .backgroundColor(#007DFF) Button(按钮B) .backgroundColor(#A6A6A6) } } .padding(16) }这里Column和Row是容器组件子组件按顺序排列。Column是纵向排列Row是横向排列。属性链式调用是UI设置的主要方式每个组件都有几十个可配置属性包括fontSize、fontColor、backgroundColor、padding、margin等非常接近Flutter和SwiftUI的开发体验。build方法有一些硬性要求只能有一个根组件不能写if之外的其他编程控制流语句比如for循环必须用ForEach包裹不能用console.info打断UI描述等。这些规定看似束缚实际上是为了保证UI描述的可预测性和性能。如果违反编译器会直接报错不用等到运行期。字符串模板的用法也要准确掌握Text(当前选择${this.selectedIndex})注意这里用的是反引号不是单引号。有些新手从Java或Python过来习惯用单引号拼接字符串导致变量不被解析页面上直接显示“${this.selectedIndex}”的原文。4.2 条件渲染与循环渲染页面上的逻辑分支条件渲染使用if、else if和else注意ArkTS的UI描述里不支持switch分支语句build() { Column() { if (this.loading) { LoadingProgress() } else if (this.errorMessage ! ) { Text(this.errorMessage) } else { Text(数据加载完成) } } }时机的关键点在于if条件的变量必须是State或由State派生的计算属性普通变量变化不会触发条件分支的重新评估。这也是新手经常出现的“变量改了UI没反应”问题的根源。循环渲染用的是ForEach。最基本的用法State todoList: string[] [学习ArkTS, 写代码, 发布应用] build() { Column() { ForEach(this.todoList, (item: string, index: number) { Row() { Text(${index 1}. ${item}) } }, (item: string, index: number) item index) } }ForEach第三个参数是键值生成函数用来决定列表项的复用身份。如果不写这个函数或者生成逻辑不合理在列表项插入、删除、排序时会出现复用错乱的问题导致UI显示重复项或跳变。有个一直存在的争议键值生成函数能不能直接返回${index}我的结论是如果列表是静态的、不变化的返回index没问题如果列表有增删排序必须用item的唯一标识来生成key。因为用index作为key新增一个头部数据会导致后面所有项的key都变化UI会整列重建性能不好而且可能出现错乱。这个属于ArkTS八股中的经典题目面试和实际开发都经常遇到。4.3 Builder复用UI消灭重复代码当你在多个组件里用到同一段UI结构时比如统一的卡片样式、统一的标题行直接复制粘贴虽然能解决问题但维护成本极高。ArkTS提供了Builder函数装饰器来解决这个问题Builder PageHeader(title: string) { Row() { Text(title) .fontSize(24) .fontWeight(FontWeight.Bold) } .width(100%) .padding({ left: 16, right: 16, top: 12, bottom: 12 }) .backgroundColor(#F1F3F5) }然后在build()里直接调用build() { Column() { this.PageHeader(首页) this.PageHeader(列表页) } }Builder函数可以传参参数可以是基础类型、对象甚至也可以传入UI组件。实测下来把公共UI抽成Builder后代码量能减少30%左右而且后期调整公共样式只需要改一个地方。还有Styles和Extend它们用于复用公共属性配置。区别是Styles只能用在当前组件内Extend可以定义全局的样式扩展。比如统一按钮样式Extend(Button) function primaryButton() { .backgroundColor(#007DFF) .fontColor(Color.White) .borderRadius(8) }不过这个属于进阶玩法入门阶段掌握Builder基本够用了。5. 高频八股与避坑实录别人踩过的坑你别再踩5.1 ArkTS与TypeScript的区别面试必问开发必懂这块内容在任何ArkTS相关面试中都是核心开场题实际开发中也决定了你写代码时会踩多少坑。ArkTS不是一门全新的语言它基本继承了TS的语法和类型系统但为了匹配鸿蒙的方舟编译器和UI框架做了三方面调整。一是禁用或限制部分动态特性。比如any类型在ArkTS中被严格限制官方推荐用unknown或者明确的类型取代。原因在于any会让类型检查彻底失效这对追求编译期优化和稳定性的方舟编译器来说是不可接受的。二是新增装饰器体系和声明式UI能力。State、Prop、Link这些装饰器是纯TS不具备的它们让变量拥有响应式能力数据一变UI自动刷新。三是对运行时行为做了更多编译期约束。比如对对象字面量的类型推断要求更严格不允许在运行时随意修改对象的形状。这在写习惯了纯TS的人看来会有约束感但对工程化项目来说其实是保护。5.2 常见的编译错误和运行时报错速查表根据我自己的开发经验把最常见的错误整理成一个速查表方便大家排查问题。报错现象常见原因解决方式类型“xxx”上不存在属性“yyy”接口定义不完整或拼写错误检查接口属性声明确认字段名不能将类型“string”分配给类型“number”变量类型与赋值类型不匹配明确类型转换或修改声明找不到名称“xxx”变量未声明或导入遗漏检查import和变量声明属性“value”在类型“undefined”上不存在可选链未处理使用可选链?.或空值判断Cannot read property of undefined访问了未初始化的对象属性使用空值判断或在初始化时赋默认值Link变量未初始化父组件传参时没用$符号检查传参写法确认用$绑定ForEach键值重复键值生成函数返回了相同值用唯一id生成key页面不刷新但数据已变变量没有用State修饰给需要UI响应的变量加上State子组件修改了父组件值但父不刷新Prop是单向同步子组件改了无效改用Link实现双向同步速查表不是让大家背下来的而是在遇到问题时对照排查。真正高效的做法是在写代码之前就提醒自己需要UI响应的变量用State子组件传入的用Prop或LinkUI无关的普通变量直接声明。想清楚再动手报错率直线下降。5.3 状态管理最容易被忽视的三个陷阱第一个陷阱是复杂对象不刷新。使用State修饰一个class对象直接修改该对象内部的某个属性渲染层并不会刷新。解决方式就是给该对象所在的类加Observed并在子组件中用ObjectLink接收。这个我在前面已经提到但值得再强调一次因为它是“页面数据改了但UI纹丝不动”的最常见原因。第二个陷阱是数组更新不刷新。用State声明数组调用push()添加元素后页面大概率能正常刷新但如果你直接按下标修改数组的某个元素比如this.list[0].name xxxUI不会更新。正确做法是重新生成一个新数组或者使用this.list.splice(0, 1, newItem)的方式。第三个陷阱是过度装饰。有些开发者为了保险把所有变量都加上State。这会导致组件状态过多每改一个变量都会触发UI刷新逻辑性能开销变大而且状态之间耦合度高排查问题时视野会被干扰。正确的标准是只有影响UI展示的才用State纯内部计算变量保持普通变量。5.4 我的实用调试技巧和编码建议再分享几个我自己实践下来非常有效的技巧。第一个是给组件加明确的命名。很多人写组件时习惯用通用名字比如Page、Content、Card结果项目大了以后日志打出来全是“Page update”根本不知道是哪个页面在刷新。组件命名用业务语义比如ProductCard、OrderListItem后面排查问题时能省下大量时间。第二个是在合适的位置打日志。装饰器触发UI刷新是框架行为你无法在setter里打日志观察。但可以在数据请求完成、事件回调出发时打日志确认数据确实变更了有助于快速区分到底是“数据没变”还是“UI没刷新”。第三个是善用DevEco Studio的预览器。写了UI代码后不要直接跑到模拟器里验证。先用Previewer快速预览布局效果微调属性确认布局合理后再上真机。这个习惯能极大提高开发效率尤其是排列复杂布局时能帮助你在开发阶段快速调优避免每改一个间距都得起模拟器浪费大量时间。第四个建议是保持代码格式整洁。ArkTS拥有自己的编码规范比如缩进用两个空格、组件属性换行对齐等。DevEco Studio的格式化快捷键能够把你的代码自动整理成符合规范的样子。代码整洁不只是好看也能让编译器更准确地识别代码结构避免一些隐晦的解析错误。5.5 关于“八股”的一点个人看法最近总有人讨论ArkTS的“八股文”意思是面试必问、死记硬背的知识点。我的看法是八股本身没有错错的是死记硬背而不理解背后的原理。像装饰器、this陷阱、ForEach键值规则这类问题只要你在项目里亲手写过一遍、踩过一次坑就是真正属于你的经验。建议初学者在跑通项目之后主动拆掉一个组件里的装饰器观察界面会发生什么变化这样的“破坏性实验”比背十道面试题都管用。再补充一点个人体会。学习ArkTS最忌讳的就是一上来就纠结“为什么和TS不一样”。TS本身也是一门不断演进的语言ArkTS作为它的超集加上了UI框架的约束和装饰器能力它是为了鸿蒙应用开发这个具体场景服务的。先接受这些规则熟练之后再思考背后的设计动机学习曲线会平滑很多。语法这东西写一遍比看十遍都管用我建议你把文章里的例子亲手打一遍跑通之后再换成自己的业务数据试试。等你能独立写出一个带列表、带状态管理、带页面跳转的小应用再回头看这篇文章你会发现所有内容都已经内化成自己的知识了。
返回列表