摘要 :在全渠道业务时代,团队往往需要同时维护「微信小程序/H5 裂变端」与「iOS/Android/HarmonyOS Next 高性能 App」。本文将分享一套基于 pnpm workspace 的 Monorepo 架构实战方案,通过 「UI 各自为政,业务下沉共享」 的分层思想,利用 TypeScript 接口隔离与适配器模式,实现业务契约、API 服务与状态流的高效复用。
一、 背景与痛点:多端业务的"分"与"合"
在很多中大型前端与移动端团队中,多端建设通常会经历两个极端:
-
极端一:各自为政,重复造轮子
- 小程序团队用 Taro / 原生开发,App 团队用 React Native / Flutter;
- 两端对接同一套后端 REST API,但在各自代码库里重复定义了成百上千个 TypeScript Interface、枚举、格式化函数和 API 请求方法;
- 一旦后端字段微调或鉴权逻辑变更,极易出现**"这端修了,那端漏了"**的低级 Bug。
-
极端二:过度追求"一套代码通吃所有端"
- 试图用一套跨端框架强行抹平小程序与 Native App 的 UI 和导航;
- 结果是:在小程序上打包体积过大、分包困难;在 Native App 上手势卡顿、原生效能大打折扣,最终落入"多端不讨好"的困境。
💡 破局思路:逻辑共享,UI 隔离
经过多次架构演进,我们得出一个核心结论:
真正值得复用的是「业务契约、数据处理、状态流与后端交互逻辑」,而不是强行跨端的 UI 组件与页面路由。
基于此,我们搭建了这套基于 pnpm workspace 的 App Monorepo 架构:
less
┌──────────────────────────────────────┐
│ 后端服务 / REST API │
└──────────────────┬───────────────────┘
│
▼
┌──────────────────────────────────────┐
│ packages/shared (@app/shared) │
│ (业务契约 / 常量 / 纯工具 / API工厂) │
└───────────┬──────────────┬───────────┘
│ │
(HttpClient 适配) (HttpClient 适配)
│ │
▼ ▼
┌───────────────────────────┐ ┌───────────────────────────┐
│ apps/miniapp │ │ apps/rnoh-app │
│ Taro 4 + React + Sass │ │ RN 0.82 + RNOH + NativeWind │
├───────────────────────────┤ ├───────────────────────────┤
│ 微信小程序 / H5 / 字节 │ │ iOS / Android / 纯血鸿蒙 │
└───────────────────────────┘ └───────────────────────────┘
二、 Monorepo 整体架构设计
整个工程划分为 apps/ (应用宿主)与 packages/(公共能力包)两大维度:
less
app-monorepo/
├── apps/
│ ├── miniapp/ # 📦 小程序/H5(Taro 4.2 + React 18 + Vite)
│ └── rnoh-app/ # 📦 跨端App(RN 0.82 + 鸿蒙RNOH 0.82 + NativeWind)
├── packages/
│ └── shared/ # 📦 零平台依赖的纯 TS 业务核心层 (@app/shared)
├── package.json # 根工程脚本与配置
├── pnpm-workspace.yaml # 工作区配置
└── pnpm-lock.yaml # 统一锁定文件
多端技术栈矩阵
| 维度 | apps/miniapp |
apps/rnoh-app |
packages/shared |
|---|---|---|---|
| 目标平台 | 微信/抖音/支付宝小程序、H5 | HarmonyOS Next、Android、iOS | 跨端通用纯 TS |
| 内核框架 | Taro 4.2.1 (React 18) | React Native 0.82.1 (React 19) | TypeScript 5.x |
| 鸿蒙底座 | - | RNOH 0.82.30 (C-API Architecture) | - |
| UI/样式 | Taro Components + Sass | Ant Design RN 5 + NativeWind 4 | - |
| 状态管理 | Redux / React-Redux | Redux Toolkit + Redux Persist | Redux Toolkit Slices |
| 网络层实现 | Taro.request (适配接口) |
Axios + 拦截器 (适配接口) |
抽象 HttpClient 契约 |
三、 核心设计模式落地:如何优雅下沉业务?
1. 明确边界:什么该共享?什么该隔离?
- ✅ 坚决下沉到
packages/shared的内容 :types/:统一 API 响应结构ApiResponse<T>、分页定义PaginatedResponse<T>、实体模型;constants/:API 地址、HTTP 状态码、全局枚举;utils/:时间日期格式化、树形算法、字典映射等纯函数;http/:平台无关的HttpClient契约接口;services/:工厂模式 API 服务函数;store/slices/:Redux Toolkit Slices 业务状态流。
- ❌ 严禁放入
shared的内容 :- 平台特定的 UI(Taro
<View>vs RN<View>); - 平台特定的路由(Taro 页面栈 vs React Navigation);
- 平台特有的原生 API(如相机、本地文件沙箱等)。
- 平台特定的 UI(Taro
2. 接口隔离与适配器模式:解耦网络底层
小程序端只能用 Taro.request,而 App 端基于 Axios。如何让两端共享同一套 API 请求代码?
第一步:在 shared 层定义抽象契约
typescript
// packages/shared/src/http/types.ts
export interface RequestConfig {
headers?: Record<string, string>;
timeout?: number;
noLoading?: boolean;
params?: Record<string, unknown>;
}
export interface HttpClient {
get<T>(path: string, config?: RequestConfig): Promise<ApiResponse<T>>;
post<T>(path: string, body?: unknown, config?: RequestConfig): Promise<ApiResponse<T>>;
put<T>(path: string, body?: unknown, config?: RequestConfig): Promise<ApiResponse<T>>;
delete<T>(path: string, config?: RequestConfig): Promise<ApiResponse<T>>;
}
第二步:在 shared 层使用工厂模式封装 Service
typescript
// packages/shared/src/services/baseService.ts
import type { HttpClient } from '../http/types';
import type { UserInfo, UploadResult, AppVersion } from '../types/api.types';
export function createBaseService(http: HttpClient) {
return {
getUserInfo: () =>
http.get<UserInfo>('/sys/user/info'),
uploadFile: (formData: FormData) =>
http.post<UploadResult>('/file/upload', formData),
getLastAppVersion: () =>
http.post<AppVersion>('/sys/versionApp/getLastAppVersion'),
};
}
第三步:各应用端提供自己的 Adapter
小程序端适配器(Taro):
typescript
// apps/miniapp/src/adapters/http.ts
import Taro from '@tarojs/taro';
import type { HttpClient, ApiResponse } from '@app/shared';
export function createTaroHttpClient(baseURL: string): HttpClient {
return {
async get<T>(path, config) {
const res = await Taro.request<ApiResponse<T>>({
url: baseURL + path,
method: 'GET',
header: config?.headers,
data: config?.params,
});
return res.data;
},
async post<T>(path, body, config) {
const res = await Taro.request<ApiResponse<T>>({
url: baseURL + path,
method: 'POST',
header: config?.headers,
data: body,
});
return res.data;
},
// ...put, delete
};
}
React Native / 鸿蒙端适配器(Axios):
typescript
// apps/rnoh-app/src/adapters/http.ts
import { apiClient } from '../services/apiClient'; // 内部集成了 Token 拦截、原生 Loading
import type { HttpClient } from '@app/shared';
// 现有封装好的 Axios 实例直接契合 HttpClient 接口
export const httpClient: HttpClient = apiClient;
各端在业务层调用完全一致:
typescript
// 在页面中无论小程序还是 App,享受完全一致的类型推导与方法调用
import { createBaseService } from '@app/shared';
const baseService = createBaseService(httpClient);
const res = await baseService.getUserInfo();
console.log(res.data.username); // 完整的 TypeScript 强类型提示!
3. 跨端状态流(Redux Slices)直接共享
由于两端均拥抱 React 生态,我们将 Redux Toolkit Slices 定义在 @app/shared 中:
typescript
// packages/shared/src/store/slices/userSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { UserInfo } from '../../types/api.types';
export interface UserState {
token: string | null;
userInfo: UserInfo | null;
}
const initialState: UserState = {
token: null,
userInfo: null,
};
export const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
setToken: (state, action: PayloadAction<string>) => {
state.token = action.payload;
},
setUserInfo: (state, action: PayloadAction<UserInfo>) => {
state.userInfo = action.payload;
},
logout: (state) => {
state.token = null;
state.userInfo = null;
},
},
});
export const { setToken, setUserInfo, logout } = userSlice.actions;
export const userReducer = userSlice.reducer;
- App 端 :引入
userReducer并配合redux-persist实现 Native 原生 AsyncStorage 本地持久化; - 小程序端 :引入
userReducer配合 Taro 存储,共享同一套 Action 语义。
四、 鸿蒙跨端(RNOH 0.82)深度集成与工程化细节
在 apps/rnoh-app 中,我们接入了 React Native 0.82(新架构 C-API) 与 RNOH 0.82.30 ,直接面向 HarmonyOS Next(纯血鸿蒙) 进行真机编译。
1. 鸿蒙 C-API 与 NativeWind 适配
-
样式层采用 TailwindCSS 3.4 + NativeWind 4 ,通过
react-native-css-interop将原子类高效映射为 Native Style; -
鸿蒙原生代码生成(Codegen):
bashreact-native codegen-harmony \ --cpp-output-path ./harmony/entry/src/main/cpp/generated \ --rnoh-module-path ./harmony/entry/oh_modules/@rnoh/react-native-openharmony
2. 依赖管理与补丁机制
多端开发不可避免会遇到部分 npm 包在鸿蒙端存在细微兼容性问题。我们通过 patch-package + HAR 自动重打包脚本 配合 postinstall 实现自动化治理:
json
// apps/rnoh-app/package.json
{
"scripts": {
"postinstall": "patch-package && bash scripts/repack-ohos-hars.sh"
}
}
五、 开发工作流与实战收益
1. 极简的开发体验
得益于 pnpm workspace:* 软链接机制,修改 packages/shared 代码无需经过打包发版,多端热重载即刻生效:
bash
# 1. 根目录统一安装依赖
pnpm install
# 2. 启动微信小程序监听
pnpm dev:miniapp
# 3. 启动 React Native / 鸿蒙 Metro 调试
pnpm dev:rnoh
2. 落地成效与业务收益
| 对比指标 | 传统独立双仓库 | Monorepo + 共享架构 |
|---|---|---|
| API/类型重复率 | 100% 冗余编写 | 0 重复,单处修改全局同步 |
| 多端一致性风险 | 高(容易漏改、字段拼错) | 极低(编译期 TS 严格校验) |
| 新业务接入效率 | 小程序与 App 各写一遍业务层 | 仅需写一次 shared,各端只需画 UI |
| 团队协作 | 跨端团队数据割裂 | 共享同一套业务模型与工程规范 |
六、 总结与展望
在移动端全渠道开发的背景下,Monorepo 不是为了"全盘大一统",而是为了"最大化复用确定性逻辑,最大化保留端原生优势"。
通过这套架构:
- 我们让
@app/shared保持极致纯粹(纯 TypeScript、零宿主耦合); - 借助
HttpClient适配器模式 隔离底层网络库差异; - 让
miniapp与rnoh-app各自发挥 Taro 与 React Native/RNOH 在宿主环境下的极致性能。
如果你也在面临小程序与跨端 App(尤其是近期备受关注的鸿蒙 Next)的协同开发挑战,不妨尝试这套架构方案!
📦 完整开源项目地址 :GitHub - kuma0605/app-monorepo (欢迎 Star ⭐️ 支持)
💬 欢迎在评论区讨论:你们团队在处理多端业务(小程序 / iOS / Android / 鸿蒙)时采用了怎样的架构方案?遇到了哪些跨端痛点?