HarmonyOS ArkTS 代码架构与模块化:从星空运势应用看单文件的边界与组件拆分策略
2026/7/19 12:34:20 网站建设 项目流程

引子:500 行代码塞一个文件,迟早要出事

星空运势应用的所有代码都写在一个Index.ets文件里,超过 500 行。功能跑起来没问题,但每次改代码都要在文件里上下翻找——改转盘的坐标要翻到 200 行,改塔罗的洗牌要翻到 400 行,改导航栏要翻到 100 行。

一开始图省事全塞一个文件,开发速度快,但维护成本越来越高。更麻烦的是,三个页面的状态都耦合在一起——改转盘的逻辑可能影响星图的渲染,改塔罗的状态可能影响导航栏的高亮。

完整效果

单文件架构的问题

优点

先把优点说清楚——单文件不是一无是处:

  1. 开发快:所有代码在一个文件里,跳转方便,不用切换文件
  2. 理解快:读代码时不需要在多个文件之间跳转
  3. 部署简单:只有一个文件需要编译和打包

对于小项目(<300 行),单文件完全够用。但超过 500 行,问题就开始显现。

缺点

1. 状态耦合

@Statepage:number=0;// 导航状态@StatewheelAngle:number=0;// 转盘状态@Statespinning:boolean=false;// 转盘状态@StateselectedSign:number=-1;// 星图状态@Statedealt:boolean=false;// 塔罗状态@StatetarotIdx:number[]=[];// 塔罗状态@StatetarotOpen:boolean[]=[];// 塔罗状态

7 个@State变量全在同一个组件里。改任何一个都可能触发整个组件的重新渲染,导致不必要的性能开销。

2. 代码查找困难

500+ 行代码在一个文件里,找某个功能的实现需要上下翻找。虽然 IDE 有搜索功能,但远不如文件级别的模块清晰。

3. 复用困难

转盘的旋转逻辑、塔罗的洗牌算法、星图的坐标计算——这些逻辑在当前应用中只能用一次。如果要做另一个应用(比如"每日塔罗"),需要复制粘贴代码。

4. 测试困难

单文件架构无法对单个功能进行单元测试。要测洗牌算法,必须加载整个应用。

@Builder vs @Component:什么时候用哪个

ArkUI 提供了两种代码复用方式:@Builder@Component。理解它们的区别是模块化的基础。

@Builder:轻量级 UI 片段

@BuilderStarDetail(){Stack(){// 星图详情的 UI}}

特点

  • 不能有@State状态
  • 不能接收@Prop@Link参数
  • 本质上是"模板函数",把一段 UI 代码封装起来
  • 调用方式和普通函数一样:this.StarDetail()

适用场景

  • UI 片段的封装(比如卡片样式、按钮样式)
  • 不需要独立状态的简单组件
  • 同一页面内的代码组织

@Component:独立组件

@Componentstruct StarPage{@PropselectedSign:number=-1;build(){Column(){// 星图页面的完整 UI}}}

特点

  • 可以有@State状态
  • 可以接收@Prop@Link@Provide/@Consume参数
  • 是独立的组件,有完整的生命周期
  • 调用方式是标签语法:<StarPage />

适用场景

  • 需要独立状态的复杂组件
  • 跨页面复用的组件
  • 需要单元测试的组件

对比表

特性@Builder@Component
状态有(@State)
参数普通参数@Prop/@Link
复用性页面内复用跨页面复用
性能轻量略重
测试困难容易
适用场景UI 片段独立组件

当前应用的选择

星空运势应用全部用@Builder,没有用@Component。这是因为三个页面都在同一个组件里,不需要跨组件通信。但如果要拆分,就需要把@Builder升级为@Component

组件拆分策略:怎么拆

拆分的时机

不是代码多就要拆,而是:

  1. 功能独立:一个功能可以独立运行(比如转盘的旋转逻辑)
  2. 状态独立:一个功能有自己的状态(比如塔罗的洗牌状态)
  3. 复用需求:一个功能要在多处使用(比如卡片样式)
  4. 维护困难:找代码要花很长时间

拆分的粒度

拆分不是越细越好。粒度太细会导致组件太多,管理困难。建议按"功能模块"拆分:

当前结构(单文件): Index.ets ├── TopNav() ├── WheelPage() ├── StarPage() ├── TarotPage() ├── StackedDeck() ├── DealtCards() ├── Readings() └── 数据定义 拆分后(按功能模块): Index.ets # 主页面,只负责路由和状态管理 ├── components/ │ ├── TopNav.ets # 导航栏组件 │ ├── WheelPage.ets # 转盘页面组件 │ ├── StarPage.ets # 星图页面组件 │ └── TarotPage.ets # 塔罗页面组件 ├── data/ │ ├── Signs.ts # 星座数据 │ ├── Tarot.ts # 塔罗数据 │ └── Colors.ts # 颜色配置 └── utils/ ├── shuffle.ts # 洗牌算法 └── math.ts # 数学工具函数

拆分的步骤

第一步:提取数据

数据是最容易拆分的,因为数据没有依赖:

// data/Signs.tsexportconstSIGNS:SignData[]=[newSignData('白羊座','♈','3.21-4.19','火','热情勇敢·行动力强',[newStarPt(100,30,3),// ...]),// ...];

第二步:提取工具函数

洗牌算法、日期种子、坐标计算——这些纯函数也可以轻松提取:

// utils/shuffle.tsexportfunctionfisherYatesShuffle(arr:number[],seed:number):number[]{constresult=[...arr];lets=seed;for(leti=result.length-1;i>0;i--){s=(s*1103515245+12345)%2147483648;constj=s%(i+1);[result[i],result[j]]=[result[j],result[i]];}returnresult;}

第三步:提取页面组件

把三个页面拆分成独立的@Component

// components/WheelPage.ets@Componentstruct WheelPage{@Propspinning:boolean=false;@PropwheelAngle:number=0;@PropresultIdx:number=-1;onSpin:()=>void=()=>{};onReveal:()=>void=()=>{};build(){Column(){// 转盘的 UI}}}

第四步:提取公共组件

导航栏、卡片样式、按钮样式——这些可以跨页面复用的组件:

// components/TopNav.ets@Componentstruct TopNav{@Proppage:number=0;onNavigate:(idx:number)=>void=()=>{};build(){Row(){// 导航栏的 UI}}}

拆分后的主页面

// Index.ets@Entry@Componentstruct Index{@Statepage:number=0;@StatewheelAngle:number=0;@Statespinning:boolean=false;@StateselectedSign:number=-1;@Statedealt:boolean=false;@StatetarotIdx:number[]=[0,1,2];@StatetarotOpen:boolean[]=[false,false,false];build(){Column(){TopNav(page:this.page,onNavigate:(idx:number)=>{this.page=idx;})Swiper(){WheelPage(spinning:this.spinning,wheelAngle:this.wheelAngle,...)StarPage(selectedSign:this.selectedSign,...)TarotPage(dealt:this.dealt,tarotIdx:this.tarotIdx,...)}.index(this.page).onChange((idx:number)=>{this.page=idx;})}}}

主页面只负责状态管理和路由,UI 逻辑全部委托给子组件。

数据与 UI 的分离原则

拆分代码时,一个重要的原则是数据与 UI 分离

  • 数据层:定义数据结构、常量、工具函数
  • UI 层:使用数据渲染界面

当前应用的问题

当前应用把数据和 UI 混在一起:

constSIGNS:SignData[]=[...];// 数据@Componentstruct Index{// UI 和状态}

虽然数据定义在组件外面,但SignDataStarPt等类定义也在同一个文件里。如果要复用这些数据,必须把整个文件复制过去。

改进方案

把数据和类定义移到独立文件:

// data/types.tsexportclassPos2D{...}exportclassStarPt{...}exportclassSignData{...}exportclassTarotInfo{...}// data/Signs.tsimport{SignData,StarPt}from'./types';exportconstSIGNS:SignData[]=[...];// data/Tarot.tsimport{TarotInfo}from'./types';exportconstTAROT:TarotInfo[]=[...];

这样每个文件职责单一,修改数据不会影响 UI,修改 UI 不会影响数据。

状态管理的优化

拆分组件后,状态管理也需要优化。

问题:状态提升

当前所有状态都在主组件里,子组件通过@Prop接收。这意味着每次状态变化,主组件都会重新渲染,然后所有子组件都会重新渲染。

解决方案:状态局部化

把状态移到需要它的组件里:

// components/WheelPage.ets@Componentstruct WheelPage{@StatewheelAngle:number=0;// 转盘角度只在转盘组件里@Statespinning:boolean=false;// 旋转状态只在转盘组件里@StateresultIdx:number=-1;// 结果只在转盘组件里build(){...}}// components/TarotPage.ets@Componentstruct TarotPage{@Statedealt:boolean=false;// 抽牌状态只在塔罗组件里@StatetarotIdx:number[]=[0,1,2];// 牌组只在塔罗组件里@StatetarotOpen:boolean[]=[false,false,false];// 翻牌只在塔罗组件里build(){...}}

这样每个组件管理自己的状态,修改转盘状态不会触发塔罗组件的重新渲染。

但有些状态需要共享

page状态需要在导航栏和 Swiper 之间共享,这种状态应该留在主组件里,通过@Prop@Link传递给子组件。

踩坑记录

坑 1:@Builder 不能有 @State

@Builder不能声明@State状态。如果需要状态,必须用@Component。这是很多新手踩的坑——以为@Builder@Component一样,结果发现无法添加状态。

坑 2:组件间的通信

拆分后,组件间需要通信。比如点击转盘的"重新占卜"按钮,需要通知塔罗组件重置状态。ArkUI 提供了@Provide/@Consume、事件回调等方式,但增加了复杂度。

坑 3:文件路径的引用

拆分后需要 import 其他文件。ArkTS 的 import 路径相对复杂,需要配置tsconfig.jsonpaths字段。新手容易在这里卡住。

坑 4:组件的生命周期

@Component有完整的生命周期(aboutToAppear、aboutToDisappear 等),@Builder没有。如果需要在组件加载时执行逻辑,必须用@Component

代码改进建议

1. 渐进式拆分

不需要一次性拆完所有代码。可以按优先级逐步拆分:

  1. 先提取数据文件(最简单)
  2. 再提取工具函数(纯函数,无依赖)
  3. 然后提取页面组件(需要处理状态传递)
  4. 最后提取公共组件(需要考虑复用性)

2. 建立目录规范

entry/src/main/ets/ ├── pages/ # 页面组件 ├── components/ # 公共组件 ├── data/ # 数据定义 ├── utils/ # 工具函数 ├── styles/ # 样式配置 └── constants/ # 常量定义

3. 使用 TypeScript 的模块系统

ArkTS 基于 TypeScript,可以充分利用 TypeScript 的模块系统:

  • export/import导出导入
  • interface定义数据结构(比class更轻量)
  • type定义类型别名
  • enum定义枚举(替代常量数组)

4. 考虑状态管理库

对于复杂应用,可以考虑引入状态管理库(比如 ArkUI 的@ohos.stateManagement),但小项目用@State就够了。

总结

星空运势应用的 500+ 行代码全部写在一个文件里,短期内可以跑,但长期维护会越来越痛苦。拆分代码的核心原则是"单一职责"——每个文件只做一件事,每个组件只管一个功能。

适用边界:这个部分适合用作 ArkTS 代码架构和模块化的入门指南,涵盖了单文件的问题、@Builder vs @Component、组件拆分策略、数据与 UI 分离、状态管理优化等核心知识点。但如果要上架应用商店,还需要补充状态管理库、路由管理、错误处理、日志系统等内容。建议在此基础上逐步扩展,而不是一次性做完所有功能。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询