引子:代码写完能跑就行?没那么简单
星空运势应用第一版写完的时候,所有代码塞在一个Index.ets里,500 多行,三个页面、数据定义、状态管理全搅在一起。功能没问题,预览效果也挺好,但每次想改点什么都要在文件里翻半天。
完整效果
重构前的问题
先看看重构前的 Index.ets 长什么样:
Index.ets(500+ 行) ├── 数据定义(Pos2D, StarPt, SignData, TarotInfo, SIGNS, TAROT, WHEEL_COLORS...) ├── @State 状态(page, wheelAngle, spinning, selectedSign, dealt, tarotIdx, tarotOpen) ├── @Builder TopNav() ├── @Builder WheelPage() ├── @Builder StarPage() ├── @Builder TarotPage() ├── @Builder StackedDeck() ├── @Builder DealtCards() ├── @Builder Readings() ├── @Builder StarDetail() └── build() 主入口主要问题:
- 数据和 UI 混在一起:星座数据、塔罗数据、颜色配置全写在组件上面,和 UI 代码混在一起
- 状态耦合:7 个
@State全在同一个组件里,改转盘可能影响星图 - Builder 过多:8 个
@Builder全塞在 Index 里,找代码要翻来翻去 - 复用困难:转盘、星图、塔罗的逻辑无法单独复用
重构后的目录结构
重构后的项目结构变成了这样:
entry/src/main/ets/ ├── pages/ │ └── Index.ets # 主入口(精简到 ~100 行) ├── view/ │ ├── WheelView.ets # 转盘页面组件 │ ├── StarView.ets # 星图页面组件 │ └── TarotView.ets # 塔罗页面组件 ├── model/ │ ├── Types.ts # 数据类定义(Pos2D, StarPt, SignData, TarotInfo) │ ├── SignsData.ts # 12 星座完整数据 │ ├── TarotData.ts # 21 张塔罗牌数据 │ └── Constants.ts # 颜色、运势标签等常量 ├── entryability/ │ └── EntryAbility.ets └── resources/核心变化:
- Index.ets 从 500+ 行精简到 ~100 行
- 三个页面各自独立成
@Component - 数据定义全部移到
model/文件夹 - 每个文件职责单一,改一个不影响另一个
拆分过程详解
第一步:提取数据类
最先拆的是数据类定义,因为数据没有依赖,拆起来最安全:
// model/Types.tsexportclassPos2D{x:number=0;y:number=0;constructor(x:number,y:number){this.x=x;this.y=y;}}exportclassStarPt{x:number=0;y:number=0;sz:number=0;constructor(x:number,y:number,sz:number){this.x=x;this.y=y;this.sz=sz;}}exportclassSignData{name:string='';emoji:string='';date:string='';elem:string='';trait:string='';stars:StarPt[]=[];// 新增字段strengths:string='';weaknesses:string='';matches:string[]=[];constructor(n:string,e:string,d:string,el:string,t:string,s:StarPt[],str:string='',wk:string='',mt:string[]=[]){this.name=n;this.emoji=e;this.date=d;this.elem=el;this.trait=t;this.stars=s;this.strengths=str;this.weaknesses=wk;this.matches=mt;}}exportclassTarotInfo{name:string='';emoji:string='';key:string='';text:string='';constructor(n:string,e:string,k:string,t:string){this.name=n;this.emoji=e;this.key=k;this.text=t;}}为什么先拆数据?
- 数据类没有 UI 依赖,拆出去不会影响任何渲染逻辑
- 数据类定义清晰,拆分后每个文件的职责一目了然
- 其他文件可以
import这些类,为后续拆分打基础
第二步:提取星座数据
数据类拆完后,把 12 星座的完整数据也拆出去。这次重构还顺便给每个星座补充了优势、弱点、最佳配对:
// model/SignsData.tsimport{SignData,StarPt}from'./Types';exportconstSIGNS:SignData[]=[newSignData('白羊座','♈','3.21-4.19','火','热情勇敢·行动力强',[newStarPt(100,30,3),newStarPt(60,70,2),newStarPt(140,70,2),newStarPt(100,110,4),newStarPt(100,170,3),newStarPt(40,140,2),newStarPt(160,140,2)],'充满活力,勇于开拓,天生的领导者','冲动急躁,缺乏耐心,容易半途而废',['狮子座','射手座']),// ... 其他 11 个星座];新增的三个字段:
strengths:优势描述(用于星图详情的"优势"区域)weaknesses:弱点描述(用于星图详情的"弱点"区域)matches:最佳配对星座数组(用于星图详情的"最佳配对"区域)
第三步:提取塔罗数据
// model/TarotData.tsimport{TarotInfo}from'./Types';exportconstTAROT:TarotInfo[]=[newTarotInfo('愚者','🃏','新开始·冒险','新的旅程即将开始...'),newTarotInfo('魔术师','🪄','创造力·技巧','你拥有实现目标的所有资源...'),// ... 其他 19 张];第四步:提取常量配置
// model/Constants.tsexportconstWHEEL_COLORS:string[]=['#7B4FBF','#3B6FC2','#20A39E','#E89240','#C44569','#5D5FEF','#2E9C7A','#D4594C','#6B5CE7','#3D8FD1','#18A085','#E07030'];exportconstFORTUNE_LABELS:string[]=['大吉','中吉','小吉','末吉','大吉','中吉','小吉','大吉','中吉','末吉','小吉','中吉'];颜色和运势标签从 Index.ets 顶部移到了独立文件。以后要改配色,只需要改这一个文件。
第五步:提取页面组件
数据拆完后,开始拆页面组件。这是最关键的一步——把@Builder升级为@Component。
转盘组件:
// view/WheelView.etsimport{SignData,Pos2D}from'../model/Types';import{SIGNS}from'../model/SignsData';import{WHEEL_COLORS,FORTUNE_LABELS}from'../model/Constants';@Componentstruct WheelView{@StatewheelAngle:number=0;@Statespinning:boolean=false;@StateresultIdx:number=-1;dateSeed(max:number):number{constd:Date=newDate();return(d.getFullYear()*397+d.getMonth()*31+d.getDate())%max;}doSpin():void{if(this.spinning){return;}this.spinning=true;this.resultIdx=-1;this.wheelAngle+=1800+this.dateSeed(360);}doReveal():void{if(!this.spinning){return;}constn:number=((360-(this.wheelAngle%360))%360+360)%360;this.resultIdx=Math.floor(n/30)%12;this.spinning=false;}wheelPos(idx:number):Pos2D{consta:number=-Math.PI/2+idx*(2*Math.PI/12);returnnewPos2D(145-28+105*Math.cos(a),145-28+105*Math.sin(a));}build(){Column(){// 转盘的完整 UI}}}export{WheelView};关键变化:
- 从
@Builder变成@Component struct WheelView - 转盘相关的状态(
wheelAngle、spinning、resultIdx)从 Index 移到了 WheelView 里 - 转盘相关的方法(
doSpin、doReveal、wheelPos)也跟着移过来了 - 通过
import引用数据和常量
星图组件:
// view/StarView.etsimport{SignData,StarPt,Pos2D}from'../model/Types';import{SIGNS}from'../model/SignsData';@Componentstruct StarView{@StateselectedSign:number=-1;@StateshowDetail:boolean=false;build(){Column(){// 星图页面的完整 UI// 包括:标题、星图详情(优势/弱点/配对)、星座网格}}}export{StarView};星图组件的新增功能:
重构后的星图页面比第一版丰富了很多:
星座星图 点击星座·探索你的星空 ┌─────────────────────────┐ │ 星点可视化 │ │ · · · · · · · │ │ · ♊ · │ │ · · · · · · · │ └─────────────────────────┘ 风象星座 5.21-6.21 聪明灵活·好奇心强 ✨ 优势 思维敏捷,沟通能力出众,能快速适应各种环境 ⚠️ 弱点 注意力分散,容易半途而废,有时显得表里不一 ❤️ 最佳配对 天秤座 水瓶座 [收起详情] [关闭星图] ┌──────┐ ┌──────┐ ┌──────┐ │ 白羊 │ │ 金牛 │ │ 双子 │ ← 双子座选中(金色边框) └──────┘ └──────┘ └──────┘ ...塔罗组件:
// view/TarotView.etsimport{TarotInfo}from'../model/Types';import{TAROT}from'../model/TarotData';@Componentstruct TarotView{@Statedealt:boolean=false;@StatetarotIdx:number[]=[0,1,2];@StatetarotOpen:boolean[]=[false,false,false];shuffle():void{// 洗牌逻辑}doFlip(i:number):void{// 翻牌逻辑}build(){Column(){// 塔罗页面的完整 UI}}}export{TarotView};第六步:精简 Index.ets
最后一步是精简主入口。Index.ets 现在只负责导航和路由:
// pages/Index.etsimport{WheelView}from'../view/WheelView';import{StarView}from'../view/StarView';import{TarotView}from'../view/TarotView';@Entry@Componentstruct Index{@Statepage:number=0;navTitle():string{if(this.page===0){return'命运转盘';}if(this.page===1){return'星座星图';}return'塔罗牌阵';}navSub():string{if(this.page===0){return'转动转盘·揭晓今日运势';}if(this.page===1){return'点击星座·探索你的星空';}return'默念问题·翻开命运之牌';}navIcon(i:number):string{if(i===0){return'🎡';}if(i===1){return'✨';}return'🃏';}navLabel(i:number):string{if(i===0){return'运势';}if(i===1){return'星图';}return'塔罗';}build(){Column(){// 导航栏Column(){Text(this.navTitle()).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#E8DFF8')Text(this.navSub()).fontSize(10).fontColor('#6B5B9A').margin({top:4})}Row(){ForEach([0,1,2],(i:number)=>{Column(){Text(this.navIcon(i)).fontSize(22).opacity(i===this.page?1:0.4).scale({x:i===this.page?1.15:1,y:i===this.page?1.15:1})Text(this.navLabel(i)).fontSize(11).fontColor(i===this.page?'#FFD700':'#5B4FAF')Divider().width(i===this.page?24:0).height(2).color('#FFD700')}.width(90).onClick(()=>{this.page=i;})})}// Swiper 页面切换Swiper(){WheelView()StarView()TarotView()}.index(this.page).indicator(false).loop(false).duration(300).onChange((idx:number)=>{this.page=idx;}).layoutWeight(1)}.width('100%').height('100%').backgroundColor('#06060F')}}Index.ets 的变化:
| 项目 | 重构前 | 重构后 |
|---|---|---|
| 代码行数 | 500+ | ~100 |
| @State 数量 | 7 个 | 1 个(page) |
| @Builder 数量 | 8 个 | 0 个 |
| 数据定义 | 内联 | import |
| 页面逻辑 | 全在 Index | 各自独立 |
重构带来的好处
1. 代码可读性提升
每个文件只做一件事:
Index.ets:导航和路由WheelView.ets:转盘逻辑StarView.ets:星图逻辑TarotView.ets:塔罗逻辑Types.ts:数据结构SignsData.ts:星座数据TarotData.ts:塔罗数据
找代码不再需要翻来翻去,打开对应的文件就行。
2. 状态隔离
转盘的旋转状态、星图的选择状态、塔罗的翻牌状态各自独立。修改转盘不会触发星图的重新渲染,修改塔罗不会影响导航栏。
3. 复用性提升
如果要做一个"每日塔罗"应用,可以直接复用TarotView.ets和TarotData.ts,不需要复制粘贴代码。
4. 维护成本降低
改星座数据只需要改SignsData.ts,改塔罗数据只需要改TarotData.ts,不会影响其他文件。
重构中的踩坑记录
坑 1:@Builder 升级为 @Component 的参数传递
@Builder不需要参数传递(直接访问父组件的this),但@Component需要通过@Prop或@Link传递参数。
比如转盘组件需要从外部控制旋转状态,就需要:
@Componentstruct WheelView{@Propspinning:boolean=false;@PropwheelAngle:number=0;// ...}但这次重构选择了让每个组件管理自己的状态,所以没有用@Prop。
坑 2:import 路径的配置
ArkTS 的 import 路径需要正确配置。'../view/WheelView'这种相对路径在大多数情况下可以工作,但如果项目结构复杂,可能需要配置tsconfig.json的paths字段。
坑 3:export 的写法
ArkTS 的 export 语法和 TypeScript 略有不同。需要用export { WheelView }或者在struct前加export。
坑 4:共享数据的导入
多个组件都需要导入SIGNS数据。如果每个组件都写一遍 import,修改数据文件路径时需要改多个地方。更好的做法是在model/文件夹的index.ts中统一导出:
// model/index.tsexport{Pos2D,StarPt,SignData,TarotInfo}from'./Types';export{SIGNS}from'./SignsData';export{TAROT}from'./TarotData';export{WHEEL_COLORS,FORTUNE_LABELS}from'./Constants';然后各组件统一从model/导入:
import{SIGNS,SignData,StarPt}from'../model';重构前后的对比
| 维度 | 重构前 | 重构后 |
|---|---|---|
| 文件数 | 1 个 | 7 个 |
| 最大文件行数 | 500+ | ~150 |
| 状态数量 | 7 个 @State | 1 + 2 + 2 + 2 |
| 数据定义 | 内联在组件顶部 | 独立 model/ 文件夹 |
| 修改成本 | 改一个可能影响全部 | 改一个只影响一个 |
| 复用性 | 无法复用 | 可独立复用 |
总结
这次重构把一个 500+ 行的单文件拆成了 7 个文件,每个文件职责单一。重构过程中还顺便给星图页面补充了优劣势和最佳配对数据,让应用内容更丰富。
适用边界:这个重构过程适合用作 ArkTS 模块化拆分的实战案例,涵盖了数据提取、组件拆分、状态隔离、import 配置等核心知识点。但如果要上架应用商店,还需要补充状态管理库、路由管理、错误处理、日志系统等内容。建议在此基础上逐步扩展,而不是一次性做完所有功能。