
文章目录前言先搞清楚要做啥工程结构为什么用 HAP 双 HSPmodule.json5 多模块配置Navigation 路由与底部 TabBar网络层初始搭建数据层模型和仓库一些搭建阶段的建议前言这个系列我们搞一个大工程——从零搭一个完整的电商 App「鲜选商城」。名字随便起的但功能一点不含糊首页、分类、购物车、我的电商该有的核心页面咱一个不落。前面系列打了基础这次是真刀真枪干项目。我尽量把踩过的坑和关键决策都写清楚你照着做就能跑通。先搞清楚要做啥鲜选商城的核心模块就四个首页轮播图、分类导航金刚区、商品瀑布流电商标配分类左边一级分类 右边二级分类和商品列表经典联动布局购物车商品勾选、数量调整、价格计算、结算我的用户信息、订单列表、收藏、设置除了这四个 Tab 页面还有搜索页、商品详情页、订单确认页等二级页面。整体功能对标主流电商 App但做了适当裁剪保证一个人也能搞定。工程结构为什么用 HAP 双 HSP我一开始是想全塞一个 entry 模块里图省事。但写到一半发现不对——网络请求的代码到处复制UI 组件也是抄来抄去改一个地方得同步好几个文件。所以最终拆成了三个模块FreshMart/ ├── entry/ # HAP 主包页面和路由都在这 │ └── src/main/ │ ├── ets/ │ │ ├── entryability/ │ │ ├── pages/ # 所有页面 │ │ └── viewmodel/ # 页面级 ViewModel │ └── resources/ ├── lib_core/ # HSP 核心库网络层数据层工具类 │ └── src/main/ets/ │ ├── network/ # 网络请求封装 │ ├── model/ # 数据模型 │ ├── repository/ # 数据仓库 │ └── utils/ # 工具函数 └── lib_ui/ # HSP UI 库可复用组件 └── src/main/ets/ ├── components/ # 通用组件 └── theme/ # 主题色、字体等用 HSP 而不是 HAR主要是因为 HSP 是运行时共享的多个模块引用同一份代码不会膨胀包体积。对于电商 App 这种组件量大的项目这点挺重要。DevEco Studio 里新建模块的时候选「Shared Library」就行它会自动配成 HSP。module.json5 多模块配置entry 模块的配置文件是重点我把关键部分贴一下// entry/src/main/module.json5 { module: { name: entry, type: entry, description: $string:module_desc, mainElement: EntryAbility, deviceTypes: [phone], deliveryWithInstall: true, installationFree: false, pages: $profile:main_pages, abilities: [ { name: EntryAbility, srcEntry: ./ets/entryability/EntryAbility.ets, description: 鲜选商城主入口, icon: $media:app_icon, label: $string:app_name, startWindowIcon: $media:app_icon, startWindowBackground: $color:start_window_bg, exported: true, skills: [ { entities: [entity.system.home], actions: [action.system.home] } ] } ], requestPermissions: [ { name: ohos.permission.INTERNET }, { name: ohos.permission.GET_NETWORK_INFO } ] } }lib_core 和 lib_ui 的配置简单得多// lib_core/src/main/module.json5 { module: { name: lib_core, type: shared, description: $string:lib_core_desc, deviceTypes: [phone] } }type: shared就是 HSP。如果是har就是静态库区别前面说了HSP 运行时共享、不膨胀体积。别忘了在build-profile.json5的modules数组里注册这两个新模块不然构建系统找不到它们。Navigation 路由与底部 TabBar路由方案我纠结了一阵。HarmonyOS 有 router 和 Navigation 两套router 是页面级的跳转Navigation 是组件级的路由栈。对于电商 App 这种底部 Tab 大量二级页面的场景Navigation 明显更合适——它支持路由栈管理、返回手势、页面动画体验比 router 好不少。底部 TabBar 用Tabs组件实现四个 Tab 对应四个页面组件。二级页面通过NavPathStack推入路由栈返回时自动弹出。先注册路由表。在entry/src/main/resources/base/profile/下新建route_map.json{routerMap:[{name:SearchPage,pageSourceFile:src/main/ets/pages/SearchPage.ets,buildFunction:SearchPageBuilder},{name:ProductDetailPage,pageSourceFile:src/main/ets/pages/ProductDetailPage.ets,buildFunction:ProductDetailPageBuilder},{name:OrderConfirmPage,pageSourceFile:src/main/ets/pages/OrderConfirmPage.ets,buildFunction:OrderConfirmPageBuilder}]}然后在module.json5的routerMap字段指向这个文件。主入口页面这样写// entry/src/main/ets/pages/Index.etsimport{HomePage}from./HomePageimport{CategoryPage}from./CategoryPageimport{CartPage}from./CartPageimport{ProfilePage}from./ProfilePageEntryComponentstruct Index{StatecurrentTab:number0StatepathStack:NavPathStacknewNavPathStack()privatetabItems:TabItem[][{title:首页,icon:$r(app.media.tab_home),activeIcon:$r(app.media.tab_home_active)},{title:分类,icon:$r(app.media.tab_category),activeIcon:$r(app.media.tab_category_active)},{title:购物车,icon:$r(app.media.tab_cart),activeIcon:$r(app.media.tab_cart_active)},{title:我的,icon:$r(app.media.tab_profile),activeIcon:$r(app.media.tab_profile_active)},]build(){Navigation(this.pathStack){Tabs({barPosition:BarPosition.End,index:this.currentTab}){ForEach(this.tabItems,(item:TabItem,index:number){TabContent(){if(index0){HomePage()}elseif(index1){CategoryPage()}elseif(index2){CartPage()}else{ProfilePage()}}.tabBar(this.TabBarBuilder(item,index))},(item:TabItem,index:number)index.toString())}.onChange((index:number){this.currentTabindex}).barHeight(56)}.navDestination(this.PageMap).hideTitleBar(true).hideToolBar(true)}BuilderTabBarBuilder(item:TabItem,index:number){Column({space:4}){Image(this.currentTabindex?item.activeIcon:item.icon).width(24).height(24)Text(item.title).fontSize(11).fontColor(this.currentTabindex?#FF6B35:#999999)}.width(100%).height(100%).justifyContent(FlexAlign.Center).onClick((){this.currentTabindex})}BuilderPageMap(name:string,param:object){// 路由分发NavPathStack 会根据 name 匹配到这里if(nameSearchPage){// SearchPage 组件}elseif(nameProductDetailPage){// ProductDetailPage 组件}}}interfaceTabItem{title:stringicon:Resource activeIcon:Resource}几个关键点说一下NavPathStack是 Navigation 的路由栈管理器。二级页面跳转直接this.pathStack.pushPath({ name: ProductDetailPage, param: { id: 123 } })返回自动pop()不需要手动管理。TabBar 选中态用主题色#FF6B35这个橘色是我从鲜选商城的 logo 里取的后面 lib_ui 的主题模块会统一收口所有颜色值。网络层初始搭建网络层放在 lib_core 里封装一个通用的请求工具// lib_core/src/main/ets/network/HttpClient.etsimport{http}fromkit.NetworkKitconstBASE_URLhttps://api.freshmart.example.cominterfaceApiResponseT{code:numbermessage:stringdata:T}exportclassHttpClient{staticasyncgetT(url:string,params?:Recordstring,string):PromiseT{consthttpRequesthttp.createHttp()try{constresponseawaithttpRequest.request(BASE_URLurl,{method:http.RequestMethod.GET,header:{Content-Type:application/json},extraData:params,connectTimeout:15000,readTimeout:15000,})httpRequest.destroy()constresultJSON.parse(response.resultasstring)asApiResponseTif(result.code!0){thrownewError(result.message)}returnresult.data}catch(e){httpRequest.destroy()throwe}}staticasyncpostT(url:string,body:object):PromiseT{consthttpRequesthttp.createHttp()try{constresponseawaithttpRequest.request(BASE_URLurl,{method:http.RequestMethod.POST,header:{Content-Type:application/json},extraData:JSON.stringify(body),connectTimeout:15000,readTimeout:15000,})httpRequest.destroy()constresultJSON.parse(response.resultasstring)asApiResponseTif(result.code!0){thrownewError(result.message)}returnresult.data}catch(e){httpRequest.destroy()throwe}}}踩坑提醒httpRequest.destroy()一定要在finally或每个分支里都调用忘了就是内存泄漏。我一开始写在try块最后面一旦抛异常就跳过了后来老老实实每个分支都写一遍。数据层模型和仓库数据模型也放在 lib_core 里后面各页面共用// lib_core/src/main/ets/model/Product.etsexportinterfaceProduct{id:stringname:stringprice:numberoriginalPrice:numberimageUrl:stringcategory:stringsales:numberrating:numberdescription:string}exportinterfaceCategory{id:stringname:stringicon:stringchildren:SubCategory[]}exportinterfaceSubCategory{id:stringname:stringicon:stringproducts:Product[]}仓库层做一层抽象后续可以无缝切换本地 mock 和真实接口// lib_core/src/main/ets/repository/ProductRepository.etsimport{HttpClient}from../network/HttpClientimport{Product,Category}from../model/ProductexportclassProductRepository{staticasyncgetHomeProducts(page:number):PromiseProduct[]{returnHttpClient.getProduct[](/products/home?page${page})}staticasyncgetCategories():PromiseCategory[]{returnHttpClient.getCategory[](/categories)}staticasyncgetProductDetail(id:string):PromiseProduct{returnHttpClient.getProduct(/products/${id})}staticasyncsearchProducts(keyword:string,page:number):PromiseProduct[]{returnHttpClient.getProduct[](/products/search?keyword${keyword}page${page})}}一些搭建阶段的建议工程搭好之后跑一遍构建确保三个模块都能编译通过。我遇到过 lib_ui 引用 lib_core 的类型但忘了在oh-package.json5里声明依赖的问题排了半天。图标资源建议一开始就准备好至少 TabBar 的 8 个图标4 个普通 4 个选中和 App 图标。用占位图也行但别空着不然编译会报警告。路由表一定要提前规划好后面每加一个页面都要更新route_map.json。我后来养成了习惯先写路由注册再写页面代码这样不会漏。下一篇我们就开始写首页了轮播图 金刚区 瀑布流商品列表内容不少准备好了就继续。