引子:代码写完能跑就行?没那么简单

星空运势应用第一版写完的时候,所有代码塞在一个 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() 主入口

主要问题

  1. 数据和 UI 混在一起:星座数据、塔罗数据、颜色配置全写在组件上面,和 UI 代码混在一起
  2. 状态耦合:7 个 @State 全在同一个组件里,改转盘可能影响星图
  3. Builder 过多:8 个 @Builder 全塞在 Index 里,找代码要翻来翻去
  4. 复用困难:转盘、星图、塔罗的逻辑无法单独复用

重构后的目录结构

重构后的项目结构变成了这样:

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.ts
export class Pos2D {
  x: number = 0;
  y: number = 0;
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

export class StarPt {
  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;
  }
}

export class SignData {
  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;
  }
}

export class TarotInfo {
  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;
  }
}

在这里插入图片描述

为什么先拆数据?

  1. 数据类没有 UI 依赖,拆出去不会影响任何渲染逻辑
  2. 数据类定义清晰,拆分后每个文件的职责一目了然
  3. 其他文件可以 import 这些类,为后续拆分打基础

第二步:提取星座数据

数据类拆完后,把 12 星座的完整数据也拆出去。这次重构还顺便给每个星座补充了优势、弱点、最佳配对:

// model/SignsData.ts
import { SignData, StarPt } from './Types';

export const SIGNS: SignData[] = [
  new SignData('白羊座', '♈', '3.21-4.19', '火', '热情勇敢·行动力强',
    [new StarPt(100,30,3), new StarPt(60,70,2), new StarPt(140,70,2),
     new StarPt(100,110,4), new StarPt(100,170,3), new StarPt(40,140,2),
     new StarPt(160,140,2)],
    '充满活力,勇于开拓,天生的领导者',
    '冲动急躁,缺乏耐心,容易半途而废',
    ['狮子座', '射手座']),
  // ... 其他 11 个星座
];

在这里插入图片描述

新增的三个字段

  • strengths:优势描述(用于星图详情的"优势"区域)
  • weaknesses:弱点描述(用于星图详情的"弱点"区域)
  • matches:最佳配对星座数组(用于星图详情的"最佳配对"区域)

第三步:提取塔罗数据

// model/TarotData.ts
import { TarotInfo } from './Types';

export const TAROT: TarotInfo[] = [
  new TarotInfo('愚者', '🃏', '新开始·冒险', '新的旅程即将开始...'),
  new TarotInfo('魔术师', '🪄', '创造力·技巧', '你拥有实现目标的所有资源...'),
  // ... 其他 19 张
];

在这里插入图片描述

第四步:提取常量配置

// model/Constants.ts
export const WHEEL_COLORS: string[] = [
  '#7B4FBF','#3B6FC2','#20A39E','#E89240',
  '#C44569','#5D5FEF','#2E9C7A','#D4594C',
  '#6B5CE7','#3D8FD1','#18A085','#E07030'
];

export const FORTUNE_LABELS: string[] = [
  '大吉','中吉','小吉','末吉','大吉','中吉',
  '小吉','大吉','中吉','末吉','小吉','中吉'
];

颜色和运势标签从 Index.ets 顶部移到了独立文件。以后要改配色,只需要改这一个文件。

第五步:提取页面组件

数据拆完后,开始拆页面组件。这是最关键的一步——把 @Builder 升级为 @Component

转盘组件

// view/WheelView.ets
import { SignData, Pos2D } from '../model/Types';
import { SIGNS } from '../model/SignsData';
import { WHEEL_COLORS, FORTUNE_LABELS } from '../model/Constants';

@Component
struct WheelView {
  @State wheelAngle: number = 0;
  @State spinning: boolean = false;
  @State resultIdx: number = -1;

  dateSeed(max: number): number {
    const d: Date = new Date();
    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; }
    const n: number = ((360 - (this.wheelAngle % 360)) % 360 + 360) % 360;
    this.resultIdx = Math.floor(n / 30) % 12;
    this.spinning = false;
  }

  wheelPos(idx: number): Pos2D {
    const a: number = -Math.PI / 2 + idx * (2 * Math.PI / 12);
    return new Pos2D(145 - 28 + 105 * Math.cos(a), 145 - 28 + 105 * Math.sin(a));
  }

  build() {
    Column() {
      // 转盘的完整 UI
    }
  }
}

export { WheelView };

关键变化

  1. @Builder 变成 @Component struct WheelView
  2. 转盘相关的状态(wheelAnglespinningresultIdx)从 Index 移到了 WheelView 里
  3. 转盘相关的方法(doSpindoRevealwheelPos)也跟着移过来了
  4. 通过 import 引用数据和常量

星图组件

// view/StarView.ets
import { SignData, StarPt, Pos2D } from '../model/Types';
import { SIGNS } from '../model/SignsData';

@Component
struct StarView {
  @State selectedSign: number = -1;
  @State showDetail: boolean = false;

  build() {
    Column() {
      // 星图页面的完整 UI
      // 包括:标题、星图详情(优势/弱点/配对)、星座网格
    }
  }
}

export { StarView };

星图组件的新增功能

重构后的星图页面比第一版丰富了很多:

星座星图
点击星座·探索你的星空

┌─────────────────────────┐
│      星点可视化           │
│   · · · · · · ·        │
│     · ♊ ·              │
│   · · · · · · ·        │
└─────────────────────────┘
  风象星座  5.21-6.21
  聪明灵活·好奇心强

  ✨ 优势
  思维敏捷,沟通能力出众,能快速适应各种环境

  ⚠️ 弱点
  注意力分散,容易半途而废,有时显得表里不一

  ❤️ 最佳配对
  天秤座  水瓶座

[收起详情]  [关闭星图]

┌──────┐ ┌──────┐ ┌──────┐
│ 白羊 │ │ 金牛 │ │ 双子 │  ← 双子座选中(金色边框)
└──────┘ └──────┘ └──────┘
...

塔罗组件

// view/TarotView.ets
import { TarotInfo } from '../model/Types';
import { TAROT } from '../model/TarotData';

@Component
struct TarotView {
  @State dealt: boolean = false;
  @State tarotIdx: number[] = [0, 1, 2];
  @State tarotOpen: boolean[] = [false, false, false];

  shuffle(): void {
    // 洗牌逻辑
  }

  doFlip(i: number): void {
    // 翻牌逻辑
  }

  build() {
    Column() {
      // 塔罗页面的完整 UI
    }
  }
}

export { TarotView };

第六步:精简 Index.ets

最后一步是精简主入口。Index.ets 现在只负责导航和路由:

// pages/Index.ets
import { WheelView } from '../view/WheelView';
import { StarView } from '../view/StarView';
import { TarotView } from '../view/TarotView';

@Entry
@Component
struct Index {
  @State page: 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.etsTarotData.ts,不需要复制粘贴代码。

4. 维护成本降低

改星座数据只需要改 SignsData.ts,改塔罗数据只需要改 TarotData.ts,不会影响其他文件。

重构中的踩坑记录

坑 1:@Builder 升级为 @Component 的参数传递

@Builder 不需要参数传递(直接访问父组件的 this),但 @Component 需要通过 @Prop@Link 传递参数。

比如转盘组件需要从外部控制旋转状态,就需要:

@Component
struct WheelView {
  @Prop spinning: boolean = false;
  @Prop wheelAngle: number = 0;
  // ...
}

但这次重构选择了让每个组件管理自己的状态,所以没有用 @Prop

坑 2:import 路径的配置

ArkTS 的 import 路径需要正确配置。'../view/WheelView' 这种相对路径在大多数情况下可以工作,但如果项目结构复杂,可能需要配置 tsconfig.jsonpaths 字段。

坑 3:export 的写法

ArkTS 的 export 语法和 TypeScript 略有不同。需要用 export { WheelView } 或者在 struct 前加 export

坑 4:共享数据的导入

多个组件都需要导入 SIGNS 数据。如果每个组件都写一遍 import,修改数据文件路径时需要改多个地方。更好的做法是在 model/ 文件夹的 index.ts 中统一导出:

// model/index.ts
export { 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 配置等核心知识点。但如果要上架应用商店,还需要补充状态管理库、路由管理、错误处理、日志系统等内容。建议在此基础上逐步扩展,而不是一次性做完所有功能。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐