HarmonyOS ArkTS 实战重构:从单文件 到模块化拆分的完整过程
引子:代码写完能跑就行?没那么简单
星空运势应用第一版写完的时候,所有代码塞在一个 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.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;
}
}

为什么先拆数据?
- 数据类没有 UI 依赖,拆出去不会影响任何渲染逻辑
- 数据类定义清晰,拆分后每个文件的职责一目了然
- 其他文件可以
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 };
关键变化:
- 从
@Builder变成@Component struct WheelView - 转盘相关的状态(
wheelAngle、spinning、resultIdx)从 Index 移到了 WheelView 里 - 转盘相关的方法(
doSpin、doReveal、wheelPos)也跟着移过来了 - 通过
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.ets 和 TarotData.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.json 的 paths 字段。
坑 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 配置等核心知识点。但如果要上架应用商店,还需要补充状态管理库、路由管理、错误处理、日志系统等内容。建议在此基础上逐步扩展,而不是一次性做完所有功能。
更多推荐

所有评论(0)