
简介这是一份面向Android中高级开发者的组件化架构实战案例聚焦模块解耦与分层治理帮助开发者系统掌握组件化设计思想、依赖管理及独立测试方法。资源包含2000个文件以1285个Java源码涵盖各层组件实现、553个XML布局与配置文件支撑UI与Manifest组装、126个Markdown文档含架构说明与接入指南为主辅以JS/HTML前端测试页面及CSS样式资源整体包体42.05MB结构清晰、层次分明。已有206人学习下载适合正在推进组件化落地的团队成员或准备技术升级的工程师。读者可直接复用基础公共组件如网络、工具类、功能组件图片加载、地图、支付SDK封装及业务组件订单、用户模块的完整实现深入理解主工程如何通过接口通信与Gradle依赖协调多组件运行并参考配套测试方案开展单元与集成验证。1. 组件化不是拆代码而是建能力分层从基础公共组件到主工程的落地逻辑很多团队把“组件化”理解成把 UI 拆成.vue或.tsx文件再用npm publish发几个包——结果半年后发现基础组件改个边框圆角三个业务线同时报线上 bug功能组件里混着订单状态判断逻辑复用到营销活动页直接崩溃主工程node_modules里竟有 7 个版本的company/utils。这不是组件化是组件“散装化”。真正能长期演进的组件化体系核心不在“拆”而在“分层”基础公共组件原子级、无业务语义、功能组件能力聚合、跨业务可复用、业务组件领域内闭环、含轻量业务规则、主工程路由权限壳应用。本案例不讲抽象理论只呈现一线团队在中大型前端项目中如何用 Vue 3 TypeScript pnpm workspace 实现四层组件的物理隔离、依赖收敛与发布管控。适合已跑通单体应用、正面临多业务并行迭代或技术债堆积的 38 人前端小组。2. 四层组件的物理边界与依赖规则用 pnpm workspace 划清责任田组件分层不是目录命名游戏必须通过工程约束强制隔离。我们放弃 Lerna选择 pnpm workspace因其 symlink 行为更可控、pnpm link语义明确、且天然支持workspace:*版本锁定。整个结构严格遵循“下层不可引用上层同层禁止循环引用”原则。2.1 目录结构与 workspace 配置根目录pnpm-workspace.yaml定义四层包路径packages: - packages/base/** # 基础公共组件 - packages/feature/** # 功能组件 - packages/business/** # 业务组件 - apps/** # 主工程多个对应目录树packages/ ├── base/ │ ├── button/ # company/base-button │ ├── icon/ # company/base-icon │ └── tokens/ # company/base-tokensCSS 变量 TS 类型 ├── feature/ │ ├── form-builder/ # company/feature-form-builder │ ├── table-pro/ # company/feature-table-pro │ └── auth-guard/ # company/feature-auth-guard ├── business/ │ ├── order-list/ # company/business-order-list │ ├── coupon-center/ # company/business-coupon-center │ └── user-profile/ # company/business-user-profile apps/ ├── admin/ # 管理后台主工程 ├── merchant/ # 商户端主工程 └── h5/ # H5 轻应用主工程提示所有包名统一前缀company/避免 npm 名称污染base层禁止出现import { useOrderApi } from /api类业务代码feature层可引入base但禁止直接 importbusinessbusiness层可引入base和feature但禁止相互 import如order-list不得 importcoupon-center。2.2 依赖注入机制用peerDependenciesexports控制消费权关键约束靠package.json的peerDependencies和exports字段实现。以company/feature-table-pro为例{ name: company/feature-table-pro, version: 1.2.0, type: module, peerDependencies: { company/base-button: ^1.0.0, company/base-icon: ^1.0.0, vue: ^3.4.0 }, exports: { .: { import: ./dist/index.mjs, require: ./dist/index.cjs }, ./style.css: ./dist/style.css, ./types: ./types/index.d.ts } }peerDependencies强制消费者自行安装base组件避免嵌套 node_modules 导致样式/实例冲突exports明确导出路径禁止import { X } from company/feature-table-pro/src/utils这类非法深引构建脚本vite build输出dist/且types/index.d.ts由tsc --emitDeclarationOnly生成确保类型安全。2.3 构建与发布流水线四层组件的 CI 规则层级构建命令发布触发条件版本策略basepnpm buildVite所有base包变更合并到 main语义化版本手动pnpm version patch/minor/majorfeaturepnpm buildVitebase版本更新后自动触发依赖base的peerDependencies版本范围自身版本独立businesspnpm buildVitebase或feature更新后需人工确认兼容性仅当内部业务逻辑变更时发版否则复用旧版appspnpm buildVite每次 PR 合并不发布 npm仅部署静态资源注意CI 中增加pnpm dedupe步骤强制 workspace 内部依赖扁平化base层每次发布后自动运行pnpm update company/base-* --recursive更新所有下游feature包的peerDependencies锁定版本。3. 从基础公共组件到业务组件四层能力逐级封装的实战写法分层的价值体现在代码组织方式上。同一功能如“带搜索的下拉选择器”四层组件的实现粒度和职责截然不同。3.1 基础公共组件只暴露原子能力拒绝任何业务假设company/base-select是纯 UI 原子组件不处理数据获取、不内置搜索逻辑、不绑定任何状态!-- packages/base/select/src/BaseSelect.vue -- template div classbase-select :class{ is-open: isOpen } div classbase-select__trigger clicktoggle slot nametrigger :valuemodelValue{{ modelValue?.label || placeholder }}/slot BaseIcon namearrow-down classbase-select__icon / /div Transition namebase-select-fade div v-showisOpen classbase-select__dropdown slot nameoptions :optionsoptions :onSelecthandleSelect / /div /Transition /div /template script setup langts import { ref, defineProps, defineEmits } from vue import type { SelectOption } from ../types const props defineProps{ modelValue?: SelectOption | null placeholder?: string options?: SelectOption[] }() const emit defineEmits([update:modelValue, open, close]) const isOpen ref(false) const toggle () { isOpen.value !isOpen.value emit(isOpen.value ? open : close) } const handleSelect (option: SelectOption) { emit(update:modelValue, option) isOpen.value false } /script关键设计options通过slot注入而非props避免组件内部管理数据生命周期modelValue仅作受控值不主动请求 APIplaceholder是唯一文案属性无国际化逻辑。为什么这样写base层必须能被feature层任意组合。若此处内置搜索feature-searchable-select就无法复用其下拉动画和触发器结构。3.2 功能组件聚合能力解决一类通用问题company/feature-searchable-select封装搜索行为但依然保持业务中立!-- packages/feature/searchable-select/src/SearchableSelect.vue -- template BaseSelect v-modelselectedOption :placeholderplaceholder openonOpen template #trigger{ value } div classsearch-trigger BaseInput v-modelsearchQuery :placeholdersearchPlaceholder keydown.enterfetchOptions / span classtrigger-label{{ value?.label || placeholder }}/span /div /template template #options{ onSelect } div classsearch-options BaseLoading v-ifloading / BaseEmpty v-else-if!options.length searchQuery description未找到匹配项 / BaseEmpty v-else-if!options.length description暂无选项 / div v-else classoptions-list BaseOption v-foropt in options :keyopt.value :optionopt clickonSelect(opt) / /div /div /template /BaseSelect /template script setup langts import { ref, watch, onMounted } from vue import BaseSelect from company/base-select import BaseInput from company/base-input import BaseOption from company/base-option import BaseLoading from company/base-loading import BaseEmpty from company/base-empty import { useSearchApi } from ../composables/useSearchApi // 封装了 fetch debounce 的通用 hook const props defineProps{ placeholder?: string searchPlaceholder?: string apiEndpoint: string // 例如 /api/v1/products/search searchField?: string // 搜索字段名默认 q }() const emit defineEmits([update:modelValue, change]) const searchQuery ref() const selectedOption refany(null) const options refany[]([]) const loading ref(false) // 复用通用搜索 Hook不耦合具体业务 API const { fetchOptions } useSearchApi({ endpoint: props.apiEndpoint, searchField: props.searchField || q, onResult: (data) { options.value data loading.value false } }) const onOpen () { if (!options.value.length) { fetchOptions(searchQuery.value) } } watch(searchQuery, (val) { if (val.length 2) { loading.value true fetchOptions(val) } else if (val ) { options.value [] } }) /script关键设计apiEndpoint作为 prop 传入而非硬编码useSearchApi是通用 hook不感知商品/用户/订单等实体BaseOption由base层提供保证视觉一致性。参数说明searchField允许后端搜索参数名自定义如q/keyword/name避免功能组件强绑定某套 API 规范。3.3 业务组件注入领域知识完成闭环company/business-order-status-selector是订单域专用组件此时才引入业务规则!-- packages/business/order-status-selector/src/OrderStatusSelector.vue -- template SearchableSelect v-modellocalValue :placeholdert(selectStatus) :search-placeholdert(searchStatus) api-endpoint/api/v1/orders/statuses search-fieldname template #options{ onSelect } div classstatus-options StatusOption v-forstatus in statusOptions :keystatus.code :statusstatus clickonSelect(status) / /div /template /SearchableSelect /template script setup langts import { ref, watch } from vue import { useI18n } from vue-i18n import SearchableSelect from company/feature-searchable-select import StatusOption from ./StatusOption.vue // 订单状态专属 Option含状态色块、tooltip 等 import { getOrderStatusOptions } from /api/order // 业务 API返回 { code: PAID, name: 已支付, color: #52c418 } const props defineProps{ modelValue?: string }() const emit defineEmits([update:modelValue]) const { t } useI18n() const localValue ref(props.modelValue) const statusOptions ref{ code: string; name: string; color: string }[]([]) // 业务侧预加载状态列表避免每次打开都请求 const loadStatusOptions async () { statusOptions.value await getOrderStatusOptions() } onMounted(loadStatusOptions) watch(localValue, (val) { emit(update:modelValue, val) }) /script关键设计getOrderStatusOptions()是业务 API返回带颜色、文案、code 的完整状态对象StatusOption是订单域定制的渲染组件复用base的交互逻辑但覆盖视觉t()调用 i18n体现业务语言上下文。为什么不能放在 feature 层状态颜色、文案、code 映射关系是订单域契约营销活动页的优惠券状态可能完全不同强行复用会导致语义错乱。4. 主工程集成与运行时治理让四层组件真正协同工作主工程如apps/admin不是简单import { OrderStatusSelector } from company/business-order-status-selector而要建立运行时治理机制解决跨层通信、主题切换、错误降级等实际问题。4.1 主工程的组件注册策略按需加载 全局注册兜底apps/admin/src/main.ts中不全局注册所有组件而是分场景// apps/admin/src/main.ts import { createApp } from vue import App from ./App.vue import { registerBaseComponents } from company/base-register // 自动注册所有 base 组件 import { registerFeatureComponents } from company/feature-register // 自动注册常用 feature 组件 const app createApp(App) // 1. 全局注册基础组件高频、无副作用 registerBaseComponents(app) // 2. 全局注册部分功能组件如 TablePro、AuthGuard因路由守卫等需要 registerFeatureComponents(app) // 3. 业务组件按需导入避免首屏过大 // 在路由组件中 // import OrderStatusSelector from company/business-order-status-selector // components: { OrderStatusSelector } app.mount(#app)company/base-register的实现// packages/base/register/src/index.ts import type { App } from vue import BaseButton from company/base-button import BaseIcon from company/base-icon import BaseInput from company/base-input export function registerBaseComponents(app: App) { app.component(BaseButton, BaseButton) app.component(BaseIcon, BaseIcon) app.component(BaseInput, BaseInput) // ... 其他基础组件 }提示base-register包体积极小仅 2KB且不包含任何 CSS样式由company/base-tokens统一注入避免重复打包。4.2 主题切换通过 CSS Custom Properties 实现四层联动主题能力由company/base-tokens提供主工程控制开关/* packages/base/tokens/src/tokens.css */ :root { --color-primary: #1890ff; --color-success: #52c418; --border-radius-base: 4px; } [data-themedark] { --color-primary: #40a9ff; --color-success: #73d13d; --border-radius-base: 6px; }主工程在App.vue中监听主题变化!-- apps/admin/src/App.vue -- script setup langts import { onMounted, watch } from vue import { useThemeStore } from /stores/theme const themeStore useThemeStore() onMounted(() { document.documentElement.setAttribute(data-theme, themeStore.theme) }) watch( () themeStore.theme, (newTheme) { document.documentElement.setAttribute(data-theme, newTheme) } ) /script所有base组件直接使用var(--color-primary)feature组件在需要时覆盖变量如table-pro的斑马纹背景business组件可添加theme-darkclass 进行微调。无需修改任何组件源码仅靠 CSS 变量即可完成全链路主题切换。4.3 错误边界与降级策略业务组件失效时的优雅兜底当company/business-order-status-selector因网络或版本不兼容失效时主工程需提供降级方案!-- apps/admin/src/views/OrderEdit.vue -- template div classorder-edit !-- 使用 Suspense 包裹异步组件 -- Suspense template #default OrderStatusSelector v-modelorder.status / /template template #fallback div classfallback-status-selector BaseSelect v-modelorder.status :optionsfallbackStatusOptions / small classfallback-hint状态选择器加载中已切换至基础模式/small /div /template /Suspense /div /template script setup langts import { defineAsyncComponent } from vue import BaseSelect from company/base-select // 异步加载业务组件失败时 fallback 到 base const OrderStatusSelector defineAsyncComponent({ loader: () import(company/business-order-status-selector), errorComponent: { template: div classerror-boundary状态选择器加载失败/div }, timeout: 3000 }) const fallbackStatusOptions [ { value: DRAFT, label: 草稿 }, { value: PAID, label: 已支付 }, { value: SHIPPED, label: 已发货 } ] /script关键点defineAsyncComponent的timeout和errorComponent提供加载超时与错误兜底fallbackStatusOptions是主工程维护的最小可用集不依赖业务组件包。为什么有效base层永远是最稳定的当上层组件不可用时降级到base不会破坏功能完整性只是牺牲部分体验。5. 验证组件分层健康度三类必查指标与自动化脚本分层不是写完就结束必须建立可量化的健康度检查。我们用eslint-plugin-import 自定义脚本在 CI 中强制校验。5.1 依赖层级合规性用 eslint 插件拦截非法引用在packages/.eslintrc.js中配置module.exports { plugins: [import], rules: { // 禁止 business 层 import feature 层的非公开模块 import/no-internal-modules: [ error, { allow: [company/base-*/**, company/feature-*/src/index], forbid: [company/feature-*/src/**, company/business-*/src/**] } ], // 禁止 base 层 import 任何非 peerDependencies 的包 import/no-extraneous-dependencies: [ error, { devDependencies: false, optionalDependencies: false, peerDependencies: true, packageDir: ./ } ] } }5.2 构建产物分析用 source-map-explorer 定位分层泄漏在packages/feature-table-pro构建后执行npx source-map-explorer dist/index.mjs --no-border --no-browser检查输出中是否出现business/或apps/路径。若存在说明该功能组件意外引入了业务代码需立即修复。5.3 运行时组件树审计主工程启动时打印分层统计在apps/admin/src/main.ts末尾添加// 开发环境打印组件分层统计 if (import.meta.env.DEV) { const appInstance app._instance const componentTree (appInstance?.type?.components || {}) as Recordstring, any const layerStats { base: Object.keys(componentTree).filter(k k.startsWith(Base)), feature: Object.keys(componentTree).filter(k k.startsWith(Feature)), business: Object.keys(componentTree).filter(k k.startsWith(Business)) } console.group(✅ 组件分层健康度) console.log(基础组件:, layerStats.base.length) console.log(功能组件:, layerStats.feature.length) console.log(业务组件:, layerStats.business.length) console.groupEnd() }输出示例✅ 组件分层健康度 基础组件: 12 功能组件: 8 业务组件: 5提示此统计仅用于开发环境生产构建时自动移除。当business数量突增如从 5 到 20说明业务组件被过度拆分应检查是否违反“业务组件需领域闭环”原则——一个订单域不应拆出OrderHeader、OrderBody、OrderFooter三个业务组件而应是一个OrderDetailCard。组件分层的终极验证不是看目录结构多漂亮而是当base-tokens升级 CSS 变量、feature-table-pro修复分页 bug、business-order-list迭代新状态时三者能否完全独立发版、零冲突上线。做到这一点你的组件化才算真正扎根。本文还有配套的精品资源点击获取