一、Turborepo在移动应用开发中的应用背景

很多做移动应用开发的团队,都会遇到一个共性问题:随着业务扩张,团队里可能同时做着同一款应用的iOS版、Android版,还有基于Flutter或React Native的跨平台版本,甚至还有配套的管理后台、用户端小程序。这么多项目堆在一起,大家慢慢就会发现,好多重复的工作在做——比如每个项目都要写一遍用户登录的逻辑、统一的弹窗组件、还有埋点统计的代码,改一个公共规则,所有项目都得挨个改一遍,效率特别低。

之前很多团队的解决办法是把公共代码打包成npm包或者Pub包,单独维护,然后各个项目再依赖这个包。但这种方式有个大麻烦:改公共代码的时候,得先等公共包打包发布,再到各个项目里升级版本,改完还要挨个测试,一旦改的地方出问题,还得挨个回滚,折腾半天。

Turborepo就是为了解决这类多项目协同的痛点出现的,它本质是一个针对JavaScript/TypeScript技术栈的单仓库(Monorepo)管理工具,不需要把公共代码单独打包发布,就能让多个项目共享同一份公共代码,改完直接生效,还能自动帮你处理依赖关系、缓存构建结果,大大提升开发效率。

二、核心应用场景

2.1 跨平台移动应用的公共逻辑复用

这是Turborepo在移动开发里最常用的场景。比如一个团队同时做React Native的跨平台应用、配套的Web管理后台,两者都需要用户登录、权限校验、埋点统计的逻辑,这些逻辑可以放在一个公共的子仓库里,React Native和Web项目直接依赖这份代码,不用单独维护。

2.2 同应用多端版本的协同开发

比如团队先做了React Native版本的应用,后来又要做纯原生的iOS和Android版本,或者基于Flutter的新版本,这时候可以把公共的业务规则(比如会员等级计算、商品折扣逻辑)放在公共子仓库,不同端的项目直接引用,保证所有端的规则完全一致,不会出现同一个商品在React Native端打9折,在Android端打8折的问题。

2.3 组件库的快速迭代

如果团队有自己的UI组件库,比如统一的按钮、输入框、弹窗,这些组件需要在多个项目里用,用Turborepo可以直接在本地修改组件库的代码,所有依赖它的项目马上就能看到修改效果,不用等组件库发布新版本,测试完再统一打包就行。

三、完整应用示例

3.1 示例技术栈声明

本示例统一使用React Native + TypeScript技术栈,包含三个子项目:React Native用户端应用、React Native管理端应用、公共业务逻辑子仓库。

3.2 项目结构搭建

首先我们需要搭建一个基于Turborepo的单仓库项目,先创建根目录,然后初始化Turborepo:

# 创建根目录
mkdir turbo-mobile-demo && cd turbo-mobile-demo

# 初始化Turborepo项目,选择默认的TypeScript模板
npx create-turbo@latest

初始化完成后,项目结构大概是这样的:

turbo-mobile-demo/
├── apps/                  # 存放应用项目
│   ├── user-app/          # 用户端React Native应用
│   └── admin-app/        # 管理端React Native应用
├── packages/             # 存放公共子仓库
│   └── common/           # 公共业务逻辑
├── package.json           # 根目录的依赖和脚本
└── turbo.json             # Turborepo的配置文件

3.3 公共子仓库开发

公共子仓库packages/common用来放所有项目都要用的代码,比如用户登录逻辑、会员等级计算逻辑、埋点统计逻辑。我们先给公共子仓库加个会员等级计算的函数,代码放在packages/common/src/member.ts

// packages/common/src/member.ts
// 会员等级枚举:0=普通会员,1=银卡,2=金卡,3=钻石
export enum MemberLevel {
  NORMAL = 0,
  SILVER = 1,
  GOLD = 2,
  DIAMOND = 3
}

/**
 * 根据用户积分计算会员等级
 * @param points 用户当前积分
 * @returns 会员等级
 */
export function getMemberLevel(points: number): MemberLevel {
  if (points >= 10000) return MemberLevel.DIAMOND;
  if (points >= 5000) return MemberLevel.GOLD;
  if (points >= 1000) return MemberLevel.SILVER;
  return MemberLevel.NORMAL;
}

然后给公共子仓库加个埋点统计的函数,代码放在packages/common/src/track.ts

// packages/common/src/track.ts
/**
 * 埋点统计函数:记录用户行为
 * @param eventName 事件名称,比如'click_buy_button'
 * @param params 事件参数,比如{ productId: '123' }
 */
export function trackEvent(eventName: string, params?: Record<string, any>) {
  // 实际项目里这里会调用埋点SDK,比如友盟、神策的接口
  console.log('埋点上报:', eventName, params);
  // 这里可以加实际的埋点逻辑,比如:
  // UmengAnalytics.track(eventName, params);
}

公共子仓库开发完后,需要在它的package.json里配置导出路径,方便其他项目引用:

{
  "name": "@turbo-mobile/common",
  "version": "0.0.0",
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "dependencies": {}
}

我们再给公共子仓库加个index.ts,把所有公共代码统一导出:

// packages/common/src/index.ts
export * from './member';
export * from './track';

3.4 应用项目引用公共代码

现在我们在用户端应用apps/user-app里引用公共代码,比如在apps/user-app/src/MemberPage.tsx里,展示用户的会员等级:

// apps/user-app/src/MemberPage.tsx
import React, { useState } from 'react';
import { View, Text, Button } from 'react-native';
// 直接引用公共子仓库的代码,不用安装任何包
import { getMemberLevel, MemberLevel, trackEvent } from '@turbo-mobile/common';

const MemberPage = () => {
  // 模拟用户积分
  const [userPoints, setUserPoints] = useState(6000);
  // 计算会员等级
  const memberLevel = getMemberLevel(userPoints);

  // 点击升级按钮的逻辑
  const handleUpgrade = () => {
    // 调用公共的埋点函数,记录点击事件
    trackEvent('click_upgrade_member', { currentPoints: userPoints });
    // 模拟升级逻辑,积分加2000
    setUserPoints(prev => prev + 2000);
  };

  // 会员等级对应的显示名称
  const levelNames = {
    [MemberLevel.NORMAL]: '普通会员',
    [MemberLevel.SILVER]: '银卡会员',
    [MemberLevel.GOLD]: '金卡会员',
    [MemberLevel.DIAMOND]: '钻石会员'
  };

  return (
    <View style={{ padding: 20 }}>
      <Text style={{ fontSize: 20, marginBottom: 20 }}>
        您的会员等级:{levelNames[memberLevel]}
      </Text>
      <Text style={{ fontSize: 16, marginBottom: 30 }}>
        当前积分:{userPoints}
      </Text>
      <Button title="升级会员" onPress={handleUpgrade} />
    </View>
  );
};

export default MemberPage;

同样的,管理端应用apps/admin-app里也可以直接引用这份公共代码,比如在管理端的会员统计页面里,用同样的getMemberLevel函数计算会员等级,保证两个项目的会员规则完全一致。

3.5 Turborepo的核心配置

根目录的turbo.json是Turborepo的核心配置,用来定义任务、缓存规则等,我们可以给它加个构建和启动的配置:

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

这个配置的意思是:如果我们执行turbo build,Turborepo会先构建所有依赖的子仓库(比如先构建common),再构建应用项目;执行turbo dev的时候,会启动所有项目的开发服务,并且缓存会关闭(因为开发模式下需要实时更新)。

3.6 开发和运行

现在我们可以用Turborepo的命令来启动项目了:

# 启动所有项目的开发服务
turbo dev

如果我们修改了公共子仓库的代码,比如把getMemberLevel里的金卡积分阈值从5000改成6000,不需要发布任何包,用户端和管理端的应用马上就能看到修改效果,开发模式下的热更新会自动生效,测试完没问题后,再统一执行turbo build构建所有项目就行。

四、技术优缺点分析

4.1 优点

第一,代码复用效率高,不用单独维护公共包,改完直接生效,避免了包版本不一致的问题。比如之前改公共代码要走“修改公共包→发布→升级所有项目→测试”的流程,现在只要改完公共代码,所有项目都能马上用,至少能省一半的时间。

第二,规则一致性强,所有端的业务规则、组件样式都用同一份代码,不会出现同一个逻辑在不同端表现不一样的问题。比如会员等级计算、商品折扣、支付逻辑这些核心规则,只要改一次,所有端都同步更新,避免了跨端不一致的bug。

第三,构建速度快,Turborepo的缓存机制会把之前构建过的结果存起来,下次构建的时候直接复用,不用重新构建所有项目。比如如果只改了用户端的代码,Turborepo会发现公共子仓库和管理端的代码没改,直接用之前的构建结果,构建速度能快好几倍。

第四,依赖管理简单,所有项目的依赖都放在根目录的package.json里统一管理,不用每个项目单独装依赖,避免了不同项目依赖版本不一致的问题。比如React Native的版本,所有项目都用同一个版本,不会出现用户端用0.72,管理端用0.71的情况。

4.2 缺点

第一,技术栈限制,Turborepo主要针对JavaScript/TypeScript技术栈,原生的iOS(Swift/Objective-C)、Android(Java/Kotlin)项目不能直接用,得单独适配,或者用Bridge的方式调用公共代码,会增加复杂度。

第二,项目体积变大,所有项目都放在同一个仓库里,仓库的体积会越来越大,拉取代码的时候会比较慢,需要配置合理的.gitignore规则,排除不需要的文件,比如node_modules、构建产物等。

第三,权限管理复杂,如果团队里有人只负责用户端,有人只负责管理端,同一个仓库里的代码所有人都能改,容易出现误改的情况,需要配合代码权限工具,或者把公共子仓库单独拆分出来。

第四,学习成本,团队成员需要熟悉Turborepo的配置和使用,比如任务依赖、缓存规则、单仓库的项目结构,需要花时间学习和适应。

五、注意事项

第一,合理划分项目结构,公共子仓库和应用项目要分清楚,不要把应用项目的代码放到公共子仓库里,也不要把公共代码放到应用项目里。比如公共子仓库里只放所有项目都要用的代码,应用项目里放自己特有的代码,比如用户端的首页、管理端的订单列表等。

第二,配置合理的缓存规则,turbo.json里的缓存规则要根据项目的实际情况配置,比如开发模式下要关闭缓存,构建模式下要配置正确的输出路径,避免缓存失效或者缓存错误的结果。

第三,规范代码提交,因为所有项目都在同一个仓库里,提交代码的时候要明确说明修改的是哪个项目,比如“修改公共子仓库的会员等级逻辑”、“修改用户端的会员页面样式”,方便其他人查看和回滚。

第四,定期清理仓库,随着项目的增加,仓库的体积会越来越大,要定期清理不需要的文件,比如旧的构建产物、临时文件等,也可以用Git的压缩命令来优化仓库的体积。

第五,做好测试,虽然Turborepo能保证代码的一致性,但修改公共代码的时候还是要测试所有依赖它的项目,避免改完公共代码后,某个项目出现bug。比如改了埋点逻辑后,要测试用户端和管理端的埋点是否正常上报。

六、文章总结

Turborepo是一个非常适合多端移动应用开发的单仓库管理工具,它解决了跨端开发中公共代码复用效率低、规则不一致、构建速度慢等痛点,能大大提升团队的开发效率。

从实际应用来看,它特别适合同时开发多个JavaScript/TypeScript技术栈的移动应用项目,比如React Native跨端应用、配套的Web管理后台、小程序等,能让团队的协同开发更顺畅。

当然,它也有技术栈限制、项目体积大等缺点,需要团队根据自己的实际情况来选择。如果团队的技术栈主要是JavaScript/TypeScript,同时开发多个端的项目,Turborepo是一个非常值得尝试的工具,能帮团队节省很多开发时间,提升产品的质量。