HarmonyOS7 组件库怎么从 0 到 1 搭起来?HSP 架构设计这篇讲透
文章目录前言为什么需要自建组件库HSP 动态共享库 vs HAR 静态库组件库的工程结构实战创建一个 HSP 组件库封装一个通用 Button 组件业务模块引用组件库版本管理的小建议前言做了好几个鸿蒙项目之后我发现一个很痛的问题——同一个按钮组件在 A 项目里写了一遍B 项目里又抄了一遍改个圆角还得挨个翻文件。后来我下决心搞一套自己的组件库这一路踩了不少坑今天把经验整理出来。为什么需要自建组件库你可能会说系统组件不够用吗够用但不够统一。实际项目里产品要求所有按钮的圆角、间距、颜色必须一致系统默认的 Button 样式根本满足不了。每个页面自己写一套三个月后代码就成了一锅粥——首页的按钮圆角 8px设置页变成了 12px谁也不知道该以哪个为准。自建组件库解决的就是三个问题风格统一、开发提效、减少重复劳动。改一处全局生效新来的同事直接用现成组件不用每次都问这个按钮应该长什么样。HSP 动态共享库 vs HAR 静态库鸿蒙提供了两种共享模块HAR静态共享包和 HSP动态共享包。选哪个做组件库我两个都试过说说区别。HAR 在编译时会把代码复制一份到引用方如果多个模块都引用了同一个 HAR最终包体积会膨胀。HSP 不一样它是运行时共享的不管多少模块引用代码只保留一份。对于组件库来说HSP 更合适。原因很简单组件库会被多个业务模块引用用 HSP 能控制包体积而且组件库更新的时候业务模块不需要重新编译只要 HSP 接口不变就行。不过 HSP 有个限制——不能独立发布必须跟主工程一起打包。如果你的组件库打算发布到 OHPM 给别的团队用那还是得走 HAR。我的做法是内部组件库用 HSP对外开放的组件库用 HAR。组件库的工程结构一个好的目录结构能省掉后期很多麻烦。我摸索出来的结构是这样的MyComponentLib/ // HSP 模块根目录 ├── src/main/ │ ├── ets/ │ │ ├── components/ // 组件目录 │ │ │ ├── base/ // 基础组件 │ │ │ │ ├── AppButton.ets │ │ │ │ ├── AppTextInput.ets │ │ │ │ └── AppAvatar.ets │ │ │ ├── composite/ // 复合组件 │ │ │ │ ├── AppCard.ets │ │ │ │ └── AppFormItem.ets │ │ │ └── feedback/ // 反馈类组件 │ │ │ ├── AppToast.ets │ │ │ └── AppDialog.ets │ │ ├── tokens/ // 设计令牌 │ │ │ ├── ColorTokens.ets │ │ │ ├── SpacingTokens.ets │ │ │ └── FontTokens.ets │ │ ├── theme/ // 主题系统 │ │ │ ├── ThemeManager.ets │ │ │ └── ThemeProvider.ets │ │ ├── utils/ // 工具函数 │ │ └── Index.ets // 统一导出入口 │ ├── resources/ // 资源文件 │ │ └── base/ │ │ ├── element/ │ │ │ └── color.json │ │ └── media/ │ └── module.json5 ├── oh-package.json5 // 包配置 └── build-profile.json5几个关键点统一导出入口——所有对外暴露的组件都在Index.ets里统一 export使用方只需要一行 import 就能拿到需要的组件。千万别让使用方去 import 深层路径那样一重构就全炸了。按功能分目录——base 放基础原子组件composite 放组合组件feedback 放弹窗提示类组件。组件多了以后找起来很快。实战创建一个 HSP 组件库打开 DevEco StudioFile - New - Module选择 “Shared Library”勾选 “Dynamic”这就是 HSP 模式。给模块起个名字比如uikit。创建好之后先配置oh-package.json5{ name: myteam/uikit, version: 1.0.0, description: 团队通用 UI 组件库, main: Index.ets, author: , license: Apache-2.0, dependencies: {} }然后写Index.ets这是整个组件库的出口// Index.ets - 组件库统一导出入口// 基础组件export{AppButton}from./components/base/AppButtonexport{AppTextInput}from./components/base/AppTextInputexport{AppAvatar}from./components/base/AppAvatar// 设计令牌export{ColorTokens}from./tokens/ColorTokensexport{SpacingTokens}from./tokens/SpacingTokensexport{FontTokens}from./tokens/FontTokens// 主题export{ThemeManager,ThemeMode}from./theme/ThemeManager封装一个通用 Button 组件来直接写一个AppButton组件。它需要支持多种类型primary/secondary/text、多种尺寸、loading 状态和图标// components/base/AppButton.etsexportenumButtonType{PRIMARYprimary,SECONDARYsecondary,TEXTtext}exportenumButtonSize{SMALLsmall,MEDIUMmedium,LARGElarge}Componentexportstruct AppButton{Proplabel:stringProptype:ButtonTypeButtonType.PRIMARYPropsize:ButtonSizeButtonSize.MEDIUMPropdisabled:booleanfalseProploading:booleanfalsePropicon:Resource|nullnullonClick?:()void// 尺寸映射privatesizeConfig:Recordstring,{height:number;fontSize:number;radius:number}{small:{height:32,fontSize:13,radius:6},medium:{height:40,fontSize:15,radius:8},large:{height:48,fontSize:17,radius:10}}// 类型对应的颜色privategetTypeColors():{bg:string;text:string;border:string}{switch(this.type){caseButtonType.PRIMARY:return{bg:#0A59F7,text:#FFFFFF,border:#0A59F7}caseButtonType.SECONDARY:return{bg:#FFFFFF,text:#0A59F7,border:#0A59F7}caseButtonType.TEXT:return{bg:transparent,text:#0A59F7,border:transparent}default:return{bg:#0A59F7,text:#FFFFFF,border:#0A59F7}}}build(){Row({space:6}){if(this.loading){LoadingProgress().width(16).height(16).color(this.getTypeColors().text)}elseif(this.icon){Image(this.icon).width(18).height(18).fillColor(this.getTypeColors().text)}Text(this.label).fontSize(this.sizeConfig[this.size].fontSize).fontColor(this.getTypeColors().text).fontWeight(FontWeight.Medium)}.width(100%).height(this.sizeConfig[this.size].height).justifyContent(FlexAlign.Center).borderRadius(this.sizeConfig[this.size].radius).backgroundColor(this.disabled?#C8C8C8:this.getTypeColors().bg).border({width:this.typeButtonType.SECONDARY?1:0,color:this.disabled?#C8C8C8:this.getTypeColors().border}).opacity(this.disabled?0.6:1).onClick((){if(!this.disabled!this.loadingthis.onClick){this.onClick()}})}}业务模块引用组件库在业务模块的oh-package.json5中添加依赖{ dependencies: { myteam/uikit: file:../uikit } }然后在页面里直接用import{AppButton,ButtonType,ButtonSize}frommyteam/uikitComponentstruct LoginPage{StateisLoading:booleanfalsebuild(){Column({space:16}){Text(欢迎回来).fontSize(24).fontWeight(FontWeight.Bold).margin({top:80})AppButton({label:登录,type:ButtonType.PRIMARY,size:ButtonSize.LARGE,loading:this.isLoading,onClick:(){this.isLoadingtrue// 登录逻辑...setTimeout((){this.isLoadingfalse},2000)}})AppButton({label:注册新账号,type:ButtonType.SECONDARY,size:ButtonSize.LARGE,onClick:(){// 跳转注册页}})AppButton({label:游客模式,type:ButtonType.TEXT,size:ButtonSize.MEDIUM,onClick:(){// 游客模式进入}})}.width(100%).padding({left:24,right:24})}}跑起来之后你会看到一个干净的登录页三个按钮风格统一而且改组件库里的颜色定义所有引用处自动更新。版本管理的小建议组件库一定要做好版本管理。我的做法是用语义化版本号改 bug 升 patch1.0.1加新组件升 minor1.1.0改了不兼容的 API 升 major2.0.0。每次发版写清楚 changelog别让使用方猜你到底改了什么。搭建组件库这事儿前期投入确实大但一旦跑通后面每个项目都能省不少时间。我个人建议从项目里已经重复用了 3 次以上的组件开始抽取别一上来就想着做一个大而全的库那样容易过度设计。