简介:本资源是一套基于ArkTS语言开发的鸿蒙商城完整项目源码,面向鸿蒙应用开发者、前端工程师及HarmonyOS初学者,助力快速掌握ArkTS语法、分布式能力集成与商城类App工程化实践。压缩包共255个文件,总大小10.21MB,包含106个.ets(ArkTS主逻辑文件)、116个PNG/JPG/GIF/SVG等图像资源、8个json5与7个json配置文件、6个.ts类型增强脚本,以及hvigorw.bat构建脚本、HTML入口页等关键组件,覆盖UI界面(如ShopCartPage、CommodityDetail)、业务模型(LocDataModel、ShopData)、交互模块(SpecificationDialog、PayOrder)和用户中心(MinePage)等完整功能链路。已有247人学习下载,提供可直接编译运行的结构化工程,含清晰分层目录、标准化状态管理与典型鸿蒙组件调用范式,是理解ArkTS工程落地与鸿蒙商城架构设计的优质实操样本。
1. 这不是另一个“Vue商城”:ArkTS写的鸿蒙商城,为什么连按钮点击逻辑都得重写?
你打开一个鸿蒙商城源码包,看到ShopCartPage.ets和PayOrder.ets,第一反应可能是:“哦,又是购物车+支付页,套个UI框架就能跑”。但实际点开ShopCartPage.ets,你会发现它没用v-model,没有@click修饰符,甚至找不到this.$router.push—— 因为 ArkTS 不是 Vue,也不是 React,它运行在 ArkUI 框架之上,依赖的是@Builder、@Entry、@State这套声明式 UI 原语,且所有状态变更必须通过@Watch或@Track显式触发视图刷新。这个项目里 106 个.ts/.ets文件,92% 是带@Component装饰器的自定义组件,它们不依赖 DOM,也不走虚拟 DOM diff,而是由 ArkCompiler 直接编译为 Native UI 组件树。这意味着:你不能把 Vue 商城的computed逻辑直接搬过来;JSON5 配置里的themeColor字段,会直接影响Button组件的backgroundColor属性绑定;而SpecificationDialog.ets中那个弹窗,底层调用的是window.createDialog()而非document.createElement('div')。它适合两类人:正在考华为 HCIA-HarmonyOS 应用开发者认证的工程师,以及需要在真实产线中交付鸿蒙原生应用(非 WebView 壳)的团队——因为只有这类项目,才会真正暴露 ArkTS 类型系统与 HarmonyOS 分布式调度器之间的耦合细节。
2. ArkTS 与 TypeScript 的关键分叉点:从类型声明到生命周期钩子的实操差异
2.1 为什么LocDataModel.ets里export class LocDataModel必须加@Observed才能响应式更新?
在标准 TypeScript 中,类实例属性修改后,视图不会自动重绘;但在 ArkTS 中,仅靠@State无法监听嵌套对象深层变化。LocDataModel.ets定义了定位数据结构:
// LocDataModel.ets @Observed export class LocDataModel { @Property city: string = '北京'; @Property district: string = '朝阳区'; @Property timestamp: number = Date.now(); }提示:
@Observed是 ArkTS 特有装饰器,作用于类,表示该类所有@Property成员变更都会触发依赖它的@ObjectLink组件重渲染。若去掉@Observed,即使city改变,MinePage.ets中绑定的@ObjectLink locData: LocDataModel也不会刷新 UI。
对比标准 TS 写法:
// ❌ 错误:TypeScript 原生 class 无响应式能力 class LocDataModelTS { city: string = '北京'; } // 即使赋值 locData.city = '上海',UI 也无反应而 ArkTS 要求显式声明响应关系:
// MinePage.ets 中正确用法 @Entry @Component struct MinePage { @ObjectLink locData: LocDataModel; // 必须配合 @Observed 使用 build() { Column() { Text(`当前城市:${this.locData.city}`) // ✅ 自动响应 city 变更 } } }2.1.1@Observed的三个硬性约束条件
| 约束项 | 具体要求 | 违反后果 |
|---|---|---|
| 类声明位置 | 必须在.ets文件顶层导出,不能嵌套在函数或命名空间内 | 编译报错ERROR: [ARKTS] Decorator @Observed can only be applied to class declarations |
| 成员修饰 | 所有需响应的字段必须用@Property修饰(不可用public/private替代) | 字段变更不触发视图更新 |
| 实例创建方式 | 必须通过new LocDataModel()创建,不能用Object.assign或解构赋值生成新实例 | @ObjectLink无法建立有效引用链 |
2.2CommodityDetail.ets中的@Watch为何比watchEffect更严格?
CommodityDetail.ets实现商品详情页动态加载,其价格字段依赖 SKU 选择:
// CommodityDetail.ets @Entry @Component struct CommodityDetail { @State selectedSkuId: string = ''; @State price: number = 0; @State stock: number = 0; @Watch('selectedSkuId') // ✅ 正确:监听单个字段 onSkuChange() { // 根据 selectedSkuId 查询价格和库存 const sku = this.getSkuById(this.selectedSkuId); this.price = sku?.price || 0; this.stock = sku?.stock || 0; } build() { Column() { Text(`¥${this.price.toFixed(2)}`) Button('加入购物车').onClick(() => { this.addToCart(); }) } } }注意:ArkTS 的
@Watch不支持路径字符串监听(如@Watch('sku.price')),也不支持返回清理函数。它只接受字段名字符串,且回调函数内不能使用async/await(需改用setTimeout或Promise.then包裹异步逻辑)。
对比 Vue 的watchEffect:
// ❌ ArkTS 不支持 watchEffect(() => { if (this.selectedSkuId) { fetch(`/api/sku/${this.selectedSkuId}`).then(res => { this.price = res.price; // ⚠️ 此处 this.price 更新可能被忽略 }); } });正确替代方案:
@Watch('selectedSkuId') onSkuChange() { if (this.selectedSkuId) { // ✅ 必须手动处理异步结果 fetch(`/api/sku/${this.selectedSkuId}`) .then(res => res.json()) .then(data => { this.price = data.price; this.stock = data.stock; }); } }2.3ShopCartPage.ets的@Builder函数为何不能访问this?
ShopCartPage.ets中购物车列表使用@Builder封装单元格渲染逻辑:
// ShopCartPage.ets @Builder function CartItem(item: CartItemModel) { Row() { Image(item.icon).width(80).height(80) Column() { Text(item.name).fontSize(14) Text(`¥${item.price.toFixed(2)}`).fontColor(Color.Red) } // ❌ 下面这行会报错:Cannot access 'this' in @Builder function // Button('删除').onClick(() => this.removeCartItem(item.id)) } } @Entry @Component struct ShopCartPage { @State cartItems: CartItemModel[] = []; build() { List() { ForEach(this.cartItems, item => { ListItem() { CartItem(item) // ✅ 正确:传入 item 数据,不依赖 this } }, item => item.id.toString()) } } }提示:
@Builder是 ArkTS 的纯函数式 UI 构建器,设计初衷是避免闭包捕获this导致内存泄漏。所有交互逻辑(如删除)必须通过参数透传回调函数:
@Builder function CartItem( item: CartItemModel, onDelete: (id: string) => void // ✅ 显式传入回调 ) { Row() { Image(item.icon).width(80).height(80) Column() { Text(item.name).fontSize(14) Text(`¥${item.price.toFixed(2)}`).fontColor(Color.Red) } Button('删除').onClick(() => onDelete(item.id)) // ✅ 安全调用 } } // 在 build() 中使用: CartItem(item, (id) => this.removeCartItem(id))3. 从hvigorw.bat到真机调试:鸿蒙商城构建链路与资源加载机制解析
3.1hvigorw.bat不是 Gradle Wrapper:它如何驱动 ArkTS 编译流程?
项目根目录的hvigorw.bat是鸿蒙 DevEco Studio 的构建脚本封装,其本质是调用hvigor工具链(HarmonyOS Vigorous Build System)。执行hvigorw.bat -p module:entry:assembleDebug时,实际发生以下四阶段:
- 预处理阶段:扫描所有
.ets文件,提取@Entry、@Component、@Builder装饰器元数据,生成build/intermediates/ets/entry/debug/ets_meta.json - 类型检查阶段:调用
arkts-checker(基于 TypeScript 4.9 修改版)校验@State/@Prop类型兼容性,例如检测@Prop接收string但传入number会报错 - 资源索引阶段:解析
resources/base/element/color.json5等配置,将颜色值#FF0088FF编译为Resource.color.sys_color_primary常量,供Button.backgroundColor($r('app.color.primary'))调用 - Native 绑定生成:为每个
@Component生成 C++ 绑定代码,存于build/intermediates/ndk/entry/debug/src/main/cpp/,实现 JS 层与 UI 组件的零拷贝通信
注意:
hvigorw.bat默认使用build-profile.json5中定义的signingConfigs,若未配置签名证书,assembleDebug会失败并提示ERROR: [SIGN] No signing config found for module 'entry'。必须在build-profile.json5中补全:
"signingConfigs": { "default": { "storeFile": "C:/Users/xxx/keystore.jks", "storePassword": "123456", "keyAlias": "harmony", "keyPassword": "123456" } }3.2 图片资源加载为何必须用$r('app.media.xxx')而非相对路径?
项目含 116 个 PNG、3 个 JPG、1 个 GIF、1 个 SVG,全部存于resources/base/media/目录。DetailsPage.ets中加载商品主图:
// DetailsPage.ets Image($r('app.media.product_main')) // ✅ 正确:通过资源 ID 加载 .width(300) .height(300) // ❌ 错误:ArkTS 不支持文件系统路径 // Image('resources/base/media/product_main.png')这是因为鸿蒙资源系统在构建时会:
- 将
product_main.png编译为二进制资源块,存入build/intermediates/resources/entry/debug/resources.index - 生成
resource_table.json,记录product_main对应的资源 ID(如0x7f020001) - 运行时
Image($r(...))通过 ID 查表加载,而非读取文件路径
3.2.1 多分辨率图片资源适配规则表
| 资源目录 | 适用设备密度 | 示例文件 | 加载方式 |
|---|---|---|---|
resources/base/media/ | 默认(基准密度) | icon.png | $r('app.media.icon') |
resources/zh-CN/media/ | 中文语言环境 | cart_zh.png | $r('app.media.cart')(自动匹配) |
resources/phone/media/ | 手机设备 | banner_phone.jpg | $r('app.media.banner')(设备类型优先) |
resources/land/media/ | 横屏模式 | grid_land.png | $r('app.media.grid')(方向优先) |
当同时存在resources/phone/media/icon.png和resources/base/media/icon.png时,手机横屏下会优先加载resources/phone/land/media/icon.png(设备+方向组合最高优先级)。
3.3JSON5配置文件如何影响OrderListContent.ets的订单状态渲染?
OrderListContent.ets渲染订单列表时,状态文案来自resources/base/element/order_status.json5:
// resources/base/element/order_status.json5 { "WAIT_PAY": { "text": "待付款", "color": "#FF3333", "icon": "app.media.icon_wait_pay" }, "SHIPPED": { "text": "已发货", "color": "#339933", "icon": "app.media.icon_shipped" } }在OrderListContent.ets中通过@Resource装饰器注入:
@Entry @Component struct OrderListContent { @Resource orderStatusConfig: Record<string, { text: string; color: string; icon: Resource }>; build() { List() { ForEach(this.orders, order => { ListItem() { Column() { Text(this.orderStatusConfig[order.status]?.text || '未知') .fontColor(new Color(this.orderStatusConfig[order.status]?.color || '#999')) Image(this.orderStatusConfig[order.status]?.icon) } } }) } } }提示:
@Resource注入的 JSON5 对象是只读的,修改this.orderStatusConfig.WAIT_PAY.text不会生效。如需动态更新状态文案,应使用@State+@Watch组合:
@State statusTextMap: Record<string, string> = { WAIT_PAY: '待付款', SHIPPED: '已发货' }; @Watch('statusTextMap') onStatusTextChange() { // 触发重新渲染 }4.SpecificationDialog.ets的分布式能力实践:跨设备规格选择同步
4.1SpecificationDialog.ets如何利用@Concurrent实现多设备协同?
SpecificationDialog.ets是商品规格选择弹窗,其核心能力是:当用户在手机上选择“颜色:红色”,平板设备上的同一商品页自动高亮对应 SKU。这依赖 ArkTS 的@Concurrent装饰器与 HarmonyOS 的分布式数据服务(Distributed Data Service, DDS):
// SpecificationDialog.ets @Concurrent export class SpecSelectionModel { @StorageLink('selectedSpec') selectedSpec: string = ''; // 同步键名 @StorageLink('productId') productId: string = ''; constructor(productId: string) { this.productId = productId; } selectSpec(specId: string) { this.selectedSpec = specId; // ✅ 自动触发跨设备同步 } } @Entry @Component struct SpecificationDialog { @State specModel: SpecSelectionModel = new SpecSelectionModel('P1001'); build() { Column() { Button('红色').onClick(() => this.specModel.selectSpec('red')) Button('蓝色').onClick(() => this.specModel.selectSpec('blue')) Text(`当前选择:${this.specModel.selectedSpec}`) } } }注意:
@Concurrent类必须满足三个条件才能启用分布式同步:
- 类必须
export且顶层声明(不能在函数内)- 所有
@StorageLink字段必须是基础类型(string/number/boolean),不支持对象或数组- 设备需在同一 HarmonyOS 分布式网络(开启“多设备协同”开关,且登录同一华为账号)
4.2PayOrder.ets中的@Entry组件如何触发分布式任务迁移?
PayOrder.ets是支付页,当用户点击“去支付”按钮时,若当前设备电量低于 20%,系统可自动将支付流程迁移到电量充足的平板设备上。这通过@Entry的deviceType属性控制:
// PayOrder.ets @Entry({ deviceType: ['phone', 'tablet'] }) // ✅ 声明支持设备类型 @Component struct PayOrder { @State paymentMethod: string = 'huaweiPay'; build() { Column() { Button('立即支付').onClick(() => { // 调用分布式任务调度 API abilityAccessCtrl.startAbility({ want: { deviceId: '', // 空字符串表示由系统自动选择最优设备 bundleName: 'com.example.harmonyshop', abilityName: 'PayOrder', parameters: { orderId: this.orderId, method: this.paymentMethod } } }); }) } } }4.2.1 分布式任务迁移的四个必要条件
| 条件 | 检查方法 | 不满足后果 |
|---|---|---|
| 设备在线 | deviceManager.getTrustedDeviceListSync()返回非空数组 | startAbility抛出ERR_DEVICE_NOT_FOUND |
| 权限声明 | module.json5中requestPermissions包含"ohos.permission.DISTRIBUTED_DATASYNC" | 安装时报INSTALL_FAILED_PERMISSION_MODEL_ERROR |
| 签名一致 | 手机与平板安装的 APK 使用同一签名证书 | 迁移失败,日志显示ERR_SIGNATURE_NOT_MATCH |
| API 版本 | minCompatibleSDKVersion≥ 5(API 5 支持分布式任务) | startAbility静默失败,无错误提示 |
5. 真机调试避坑指南:从adb shell日志定位到@Watch失效的终极排查法
5.1adb logcat -s ArkTS输出的关键日志含义解析
当ShopCartPage.ets中购物车数量不更新时,执行:
adb logcat -s ArkTS常见输出及对策:
| 日志片段 | 含义 | 解决方案 |
|---|---|---|
ArkTS: [WATCH] Field 'cartItems' not found in component ShopCartPage | @Watch('cartItems')监听的字段名拼写错误或未声明为@State | 检查ShopCartPage.ets中是否漏写@State cartItems: CartItemModel[] = [] |
ArkTS: [BINDING] Failed to bind property 'price' of type 'number' to Text component | Text组件接收number类型,但 ArkTS 要求字符串 | 改为Text(${this.price.toFixed(2)}) |
ArkTS: [RESOURCE] Resource 'app.media.icon_cart' not found, fallback to default | 图片资源名大小写错误或未放入resources/base/media/ | 检查resources/base/media/icon_cart.png是否存在,注意 Windows 不区分大小写但鸿蒙区分 |
5.2@Watch失效的三类高频场景与修复命令
5.2.1 场景一:@Watch监听数组长度变化失败
ShopCartPage.ets中想监听购物车数量变化:
// ❌ 错误:监听 length 属性无效 @Watch('cartItems.length') onCartCountChange() { /* 不会触发 */ } // ✅ 正确:监听数组本身,配合 deep: true(ArkTS 3.0+ 支持) @Watch('cartItems', { deep: true }) onCartItemsChange() { /* 数组增删改均触发 */ }5.2.2 场景二:@Watch回调中修改@State导致无限循环
@Watch('selectedSkuId') onSkuChange() { this.price = this.getSkuPrice(this.selectedSkuId); // ✅ 安全 this.selectedSkuId = 'default'; // ❌ 危险:触发自身再次调用 onSkuChange() }修复命令(禁用递归):
@Watch('selectedSkuId') onSkuChange() { // 使用标志位防止递归 if (this.isUpdatingSku) return; this.isUpdatingSku = true; try { this.price = this.getSkuPrice(this.selectedSkuId); } finally { this.isUpdatingSku = false; } }5.2.3 场景三:@Watch在@Builder函数中失效
@Builder function CartHeader(count: number) { @Watch('count') // ❌ 错误:@Watch 不能在 @Builder 内使用 function onCountChange() {} Text(`共 ${count} 件`) }修复方案(移出@Builder):
@Entry @Component struct ShopCartPage { @State cartCount: number = 0; @Watch('cartCount') onCartCountChange() { console.info(`购物车数量更新为:${this.cartCount}`); } @Builder CartHeader() { Text(`共 ${this.cartCount} 件`) // ✅ 通过 this 访问 } build() { Column() { this.CartHeader() } } }5.3 使用hdc shell直接验证分布式数据同步状态
当SpecificationDialog.ets的@Concurrent同步失败时,用 hdc(HarmonyOS Device Connector)检查 DDS 状态:
# 连接设备 hdc list targets # 查看分布式数据服务状态 hdc shell "bm dump -a com.huawei.hms.distributeddatamgr" # 查询指定 key 的同步值(手机端执行) hdc shell "aa start -a MainAbility -b com.example.harmonyshop -e key selectedSpec" # 在平板端查看是否收到同步数据 hdc shell "cat /data/storage/el1/bundle/com.example.harmonyshop/distributed_data/selectedSpec"若返回空,则检查module.json5中是否声明了distributedNotificationEnabled: true:
"module": { "name": "entry", "type": "entry", "distributedNotificationEnabled": true, // ✅ 必须为 true "description": "$string:module_desc" }本文还有配套的精品资源,点击获取