第四章 ArkTS语言基础(下):类与接口、装饰器、模块化

好,咱们接着往下聊。上一章我们把ArkTS的基础类型和函数过了一遍,这一章要深入一点,聊聊类、接口、装饰器,还有模块化。这些东西在实际开发中天天用,尤其是装饰器,可以说是鸿蒙车机开发的灵魂。

4.1 类与接口:面向对象的基础

ArkTS的类,说白了就是TypeScript那一套,但加了一些鸿蒙自己的约束。我个人习惯把类当作「蓝图」,用来描述车机上的一个实体,比如一个车载应用、一个导航指令、或者一个多媒体播放器。

4.1.1 定义一个类

class CarMediaPlayer {
    private currentTrack: string = '';
    private volume: number = 50;

    // 构造函数
    constructor(track: string) {
        this.currentTrack = track;
    }

    // 方法
    public play(): void {
        console.log(`正在播放: ${this.currentTrack}`);
    }

    public setVolume(vol: number): void {
        if (vol >= 0 && vol <= 100) {
            this.volume = vol;
        } else {
            console.warn('音量范围是0-100');
        }
    }
}

嗯,这里要注意:ArkTS对访问修饰符要求比较严格。我在项目中遇到过,如果不写 privatepublic,默认就是 public,但官方推荐显式声明。你想想看,车机系统里数据安全很重要,不该暴露的属性和方法一定要藏好。

4.1.2 接口:定义契约

接口就是一份「合同」。你实现了某个接口,就必须遵守它的约定。这在多人协作开发车机应用时特别有用。

interface IMediaControl {
    play(): void;
    pause(): void;
    next(): void;
    previous(): void;
}

class MusicPlayer implements IMediaControl {
    play(): void {
        console.log('播放音乐');
    }
    pause(): void {
        console.log('暂停音乐');
    }
    next(): void {
        console.log('下一首');
    }
    previous(): void {
        console.log('上一首');
    }
}

我曾经在做一个车载音乐应用时,团队里三个人同时开发不同的播放器模块。如果没有接口约束,每个人写的播放器方法名都不一样,那后期联调就是一场灾难。接口就是用来避免这种混乱的。

我的小技巧:接口命名建议以 I 开头,比如 INavigationIVoiceControl。这是行业惯例,一看就知道是个接口。

4.2 装饰器:鸿蒙开发的灵魂

装饰器是ArkTS最核心的特性之一。说白了,它就是给类、属性、方法打上一个「标签」,让框架知道该怎么处理它们。在鸿蒙车机开发中,@Component@State 是最常用的两个。

4.2.1 @Component:声明一个UI组件

任何你想在屏幕上显示的东西,都需要用 @Component 装饰。它告诉编译器:这是一个UI组件,需要被渲染。

@Component
struct SpeedDisplay {
    build() {
        Text('当前车速: 60 km/h')
            .fontSize(24)
            .fontColor(Color.White)
    }
}

你看,@Component 后面跟着一个 struct,这是ArkTS特有的写法。我刚开始接触时也觉得奇怪,为什么不用 class?后来才明白,struct 在性能上更轻量,适合频繁刷新的UI场景。

4.2.2 @State:让数据驱动UI

车机上的数据是动态变化的,比如车速、导航距离、音乐进度。用 @State 装饰的变量,一旦值变了,UI会自动刷新。这就是所谓的「响应式编程」。

@Component
struct SpeedPanel {
    @State currentSpeed: number = 0;

    build() {
        Column() {
            Text(`当前车速: ${this.currentSpeed} km/h`)
                .fontSize(28)
            Button('加速')
                .onClick(() => {
                    this.currentSpeed += 10;
                })
        }
    }
}
核心要点:只有 @State 装饰的变量变化时,UI才会重新渲染。普通变量变了,UI不会动。这一点我踩过坑——曾经把一个状态变量忘了加 @State,调试了半天才发现UI不更新。

4.2.3 其他常用装饰器

装饰器 作用 使用场景
@Prop 父组件传给子组件的单向数据 导航信息传递
@Link 父子组件双向绑定 设置页面同步
@Watch 监听状态变化 车速超限报警
@Builder 封装重复UI片段 按钮、卡片复用

为什么会需要这么多装饰器?因为车机场景太复杂了。比如导航页面,父组件要传目的地给子组件,用 @Prop;设置页面里,音量调节需要双向同步,用 @Link。每种装饰器都有它最适合的舞台。

避坑指南:我曾经在车机项目里,把一个 @State 变量同时用在多个子组件中,结果数据一改,所有组件都重新渲染,导致界面卡顿。后来改用 @Prop 单向传递,性能明显提升。记住:@State 不要滥用,只在真正需要响应式的地方使用。

4.3 模块化与导入导出

一个车机应用少说也有几十个页面,几百个组件。如果所有代码都写在一个文件里,那维护起来就是噩梦。模块化就是用来解决这个问题的。

4.3.1 export:把东西拿出去

你想让别的文件用你的代码,就得用 export 把它暴露出去。

// utils/speed-utils.ts
export function kmhToMph(kmh: number): number {
    return kmh * 0.621371;
}

export const MAX_SPEED_LIMIT = 120;

export class SpeedCalculator {
    static isOverLimit(speed: number): boolean {
        return speed > MAX_SPEED_LIMIT;
    }
}

4.3.2 import:把东西拿进来

别的文件想用这些功能,就用 import 引入。

// pages/dashboard.ts
import { kmhToMph, SpeedCalculator } from '../utils/speed-utils';

let speedMph = kmhToMph(100);
if (SpeedCalculator.isOverLimit(100)) {
    console.warn('超速了!');
}

嗯,这里有个细节要注意:路径要用相对路径,以 ./../ 开头。我见过新手直接写文件名,结果编译报错找不到模块。

4.3.3 默认导出 vs 命名导出

导出方式 语法 导入方式 适用场景
默认导出 export default class import X from '...' 一个文件只导出一个主要功能
命名导出 export function import { X } from '...' 一个文件导出多个功能
我的建议:在车机项目中,我倾向于用命名导出。因为IDE的自动补全和重构工具对命名导出支持更好。默认导出虽然写起来少打几个字,但后期维护时容易搞混。

4.4 综合实战:一个简单的车速仪表盘

把今天学的知识串起来,写一个简单的车速仪表盘组件。

// interfaces/vehicle.ets
export interface IVehicleData {
    speed: number;
    rpm: number;
    fuelLevel: number;
}

// utils/format.ets
export function formatSpeed(speed: number): string {
    return `${speed.toFixed(0)} km/h`;
}

// components/SpeedGauge.ets
import { IVehicleData } from '../interfaces/vehicle';
import { formatSpeed } from '../utils/format';

@Component
export struct SpeedGauge {
    @State vehicleData: IVehicleData = {
        speed: 0,
        rpm: 0,
        fuelLevel: 100
    };

    build() {
        Column() {
            Text(formatSpeed(this.vehicleData.speed))
                .fontSize(48)
                .fontWeight(FontWeight.Bold)
            Text(`转速: ${this.vehicleData.rpm} rpm`)
                .fontSize(20)
            ProgressBar({ value: this.vehicleData.fuelLevel, total: 100 })
                .width('80%')
        }
        .padding(20)
        .backgroundColor('#1a1a2e')
    }
}

你看,这个例子把接口、类、装饰器、模块化全用上了。接口定义了数据结构,工具函数负责格式化,组件用 @State 管理状态,最后通过 export 暴露出去给其他页面使用。

说实话,刚开始学这些概念时,我也觉得有点抽象。但当你真正开始写一个车机应用,比如导航页面、音乐播放器、车辆设置面板,你会发现这些知识就是地基。地基打牢了,上面盖什么楼都稳。

下一章我们聊聊ArkTS的UI构建,也就是怎么用这些组件拼出漂亮的车机界面。到时候你会看到,今天学的装饰器会频繁出现,尤其是 @State@Builder,几乎每个页面都会用到。