找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1890

积分

0

好友

237

主题
发表于 昨天 09:19 | 查看: 6| 回复: 0

原文地址:https://medium.com/@beenakumawat002/turborepo-monorepo-in-2025-next-js-react-native-shared-ui-type-safe-api-%EF%B8%8F-6194c83adff9
原文作者: Elizabeth ✨

最终你将得到什么

  • 一个包含 Next.js(web)、React Native(mobile)以及共享 packages 的仓库
  • 使用 pnpm workspaces + Turborepo,实现快速构建与缓存
  • 跨 apps / packages 的 TypeScript project references
  • 共享 UI(React primitives + RN bindings)以及共享 API client(Zod + OpenAPI / tRPC-ready)
  • 全仓库统一使用的 ESLint / Prettier / 类 Ruff 的单一配置
  • 解决 React Native monorepo 中 Metro + symlinks 的问题
  • 使用 Changesets 进行版本管理与发布
  • GitHub Actions CI(build、test、lint),可选集成 EAS 的 mobile hook

0)整体鸟瞰 🗺️

acme/
  apps/
    web/               # Next.js 15(App Router)
    mobile/            # React Native(Expo 或 CLI)
  packages/
    ui/                # 共享 UI 组件
    api/               # 类型安全的 API client(fetch + zod)
    config/            # eslint、tsconfig、prettier
    types/             # 全局类型与 schema contracts
  .github/workflows/ci.yml
  turbo.json
  package.json
  pnpm-workspace.yaml
  tsconfig.base.json

1)初始化 workspace ⚙️

# prereqs: Node 20+, pnpm 9+, git
mkdir acme && cd acme
pnpm init -y
pnpm dlx turbo@latest init

pnpm-workspace.yaml

packages:
  - "apps/*"
  - "packages/*"

package.json

{
  "name": "acme",
  "private": true,
  "packageManager": "pnpm@9.6.0",
  "scripts": {
    "dev": "turbo run dev --parallel",
    "build": "turbo run build",
    "lint": "turbo run lint",
    "test": "turbo run test",
    "clean": "turbo run clean",
    "release": "changeset version && pnpm -r build && changeset publish"
  },
  "devDependencies": {
    "turbo": "^2.0.0",
    "changesets": "^2.26.2",
    "typescript": "^5.5.4"
  }
}

tsconfig.base.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "lib": ["ES2022", "DOM"],
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "jsx": "react-jsx",
    "baseUrl": ".",
    "paths": {
      "@acme/ui/*": ["packages/ui/src/*"],
      "@acme/api/*": ["packages/api/src/*"],
      "@acme/types/*": ["packages/types/src/*"],
      "@acme/config/*": ["packages/config/src/*"]
    }
  }
}

turbo.json

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

2)共享配置包(ESLint / Prettier / TS)🧰

pnpm -w add -D eslint prettier @typescript-eslint/parser @typescript-eslint/eslint-plugin
mkdir -p packages/config/src

packages/config/package.json

{
  "name": "@acme/config",
  "version": "0.0.0",
  "private": true,
  "type": "module",
  "exports": "./src/index.js"
}

packages/config/src/eslint.cjs

module.exports = {
  root: false,
  parser: "@typescript-eslint/parser",
  plugins: ["@typescript-eslint", "react", "react-hooks"],
  extends: [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:react/recommended",
    "plugin:react-hooks/recommended"
  ],
  settings: { react: { version: "detect" } },
  rules: {
    "@typescript-eslint/consistent-type-imports": "warn",
    "react/react-in-jsx-scope": "off"
  }
};

packages/config/src/index.js

export { default as eslint } from "./eslint.cjs";

3)类型与契约(Zod)🧾

mkdir -p packages/types/src
pnpm -w add zod

packages/types/package.json

{
  "name": "@acme/types",
  "version": "0.0.0",
  "type": "module",
  "main": "src/index.ts",
  "exports": "./src/index.ts",
  "devDependencies": { "typescript": "^5.5.4" }
}

packages/types/src/index.ts

import { z } from "zod";

export const UserId = z.string().uuid().brand<"UserId">();
export type UserId = z.infer<typeof UserId>;

export const User = z.object({
  id: UserId,
  email: z.string().email(),
  name: z.string().min(1),
});
export type User = z.infer<typeof User>;

export const CreateUser = z.object({
  email: z.string().email(),
  name: z.string().min(1),
});
export type CreateUser = z.infer<typeof CreateUser>;

4)类型安全的 API client(fetch + Zod)🌐✅

mkdir -p packages/api/src

packages/api/package.json

{
  "name": "@acme/api",
  "version": "0.0.0",
  "type": "module",
  "main": "src/index.ts",
  "exports": "./src/index.ts",
  "dependencies": { "zod": "^3.23.8", "@acme/types": "workspace:*" }
}

packages/api/src/index.ts

import { z } from "zod";
import { User, CreateUser } from "@acme/types";

const ApiError = z.object({ message: z.string() });
type ApiError = z.infer<typeof ApiError>;

async function parse<T>(res: Response, schema: z.ZodType<T>): Promise<T> {
  const text = await res.text();
  const json = text ? JSON.parse(text) : null;
  if (!res.ok) {
    const err = ApiError.safeParse(json);
    throw new Error(err.success ? err.data.message : `HTTP ${res.status}`);
  }
  const parsed = schema.safeParse(json);
  if (!parsed.success) throw new Error(parsed.error.message);
  return parsed.data;
}

export class AcmeClient {
  constructor(private base = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000") {}

  listUsers() {
    return fetch(`${this.base}/api/users`, { cache: "no-store" }).then((r) =>
      parse(r, z.array(User))
    );
  }

  createUser(input: CreateUser) {
    return fetch(`${this.base}/api/users`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(input),
    }).then((r) => parse(r, User));
  }
}

5)共享 UI 包(web + native)🎨

我们保持 primitives 的兼容性:web 使用 div ,mobile 使用 react-native 。对外暴露统一 API,并提供平台特定的入口。

mkdir -p packages/ui/src
pnpm -w add react react-native

packages/ui/package.json

{
  "name": "@acme/ui",
  "version": "0.0.0",
  "type": "module",
  "main": "src/index.web.tsx",
  "react-native": "src/index.native.tsx",
  "exports": {
    ".": {
      "import": {
        "react-native": "./src/index.native.tsx",
        "default": "./src/index.web.tsx"
      }
    }
  },
  "peerDependencies": {
    "react": ">=18.0.0",
    "react-native": ">=0.73.0 || >=0.80.0"
  }
}

packages/ui/src/Button.tsx

// platform-agnostic props
export type ButtonProps = { title: string; onPress?: () => void; className?: string };

packages/ui/src/index.web.tsx

import React from "react";
import type { ButtonProps } from "./Button";

export const Button: React.FC<ButtonProps> = ({ title, onPress, className }) => (
  <button className={className ?? "px-3 py-2 rounded bg-black text-white"} onClick={onPress}>
    {title}
  </button>
);

packages/ui/src/index.native.tsx

import React from "react";
import { Pressable, Text, StyleSheet } from "react-native";
import type { ButtonProps } from "./Button";

export const Button: React.FC<ButtonProps> = ({ title, onPress }) => (
  <Pressable onPress={onPress} style={styles.btn}>
    <Text style={styles.txt}>{title}</Text>
  </Pressable>
);

const styles = StyleSheet.create({
  btn: { paddingHorizontal: 12, paddingVertical: 8, borderRadius: 8, backgroundColor: "#000" },
  txt: { color: "#fff", fontWeight: "600" }
});

6)Next.js 应用(web)🌐

pnpm dlx create-next-app@latest apps/web --ts --eslint --tailwind --app

apps/web/tsconfig.json

{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "outDir": "dist",
    "paths": {
      "@acme/ui/*": ["../../packages/ui/src/*"],
      "@acme/api/*": ["../../packages/api/src/*"],
      "@acme/types/*": ["../../packages/types/src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}

使用共享 UI 与 API

apps/web/app/page.tsx

import { Button } from "@acme/ui";
import { AcmeClient } from "@acme/api";
import Link from "next/link";

export default async function Page() {
  const api = new AcmeClient();
  const users = await api.listUsers();

  return (
    <main className="p-8 space-y-4">
      <h1 className="text-2xl font-bold">Web Dashboard 🌐</h1>
      <ul className="list-disc pl-6">
        {users.map(u => <li key={u.id}>{u.email} — {u.name}</li>)}
      </ul>
      <Link href="/new">
        <Button title="Create user" />
      </Link>
    </main>
  );
}

简单的 route handler

apps/web/app/api/users/route.ts

import { NextResponse } from "next/server";
import { User, CreateUser } from "@acme/types";
import { z } from "zod";

const db: z.infer<typeof User>[] = [];

export async function GET() {
  return NextResponse.json(db, { headers: { "cache-control": "no-store" } });
}

export async function POST(req: Request) {
  const body = await req.json();
  const parsed = CreateUser.safeParse(body);
  if (!parsed.success) return new NextResponse("Invalid", { status: 400 });

  const newUser: z.infer<typeof User> = {
    id: crypto.randomUUID() as any,
    email: parsed.data.email,
    name: parsed.data.name
  };

  db.push(newUser);
  return NextResponse.json(newUser);
}

7)React Native 应用(Expo)📱

Expo 能显著简化 Monorepo 的配置。

pnpm dlx create-expo-app apps/mobile -t

Monorepo 相关修复

apps/mobile/metro.config.js

const { getDefaultConfig } = require("expo/metro-config");
const path = require("path");

const projectRoot = __dirname;
const workspaceRoot = path.resolve(projectRoot, "../..");

const config = getDefaultConfig(projectRoot);

// 1) 监听 monorepo 根目录,让 Metro 能识别 packages/*
config.watchFolders = [workspaceRoot];

// 2) 将 symlink 的 packages 解析到源码
config.resolver.nodeModulesPaths = [
  path.resolve(projectRoot, "node_modules"),
  path.resolve(workspaceRoot, "node_modules")
];

// 3) 允许从 packages 中加载 TS / TSX
config.resolver.sourceExts.push("cjs");

module.exports = config;

apps/mobile/tsconfig.json

{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "paths": {
      "@acme/ui/*": ["../../packages/ui/src/*"],
      "@acme/api/*": ["../../packages/api/src/*"],
      "@acme/types/*": ["../../packages/types/src/*"]
    }
  }
}

使用共享 UI 与 API

apps/mobile/App.tsx

import React, { useEffect, useState } from "react";
import { SafeAreaView, Text, View } from "react-native";
import { Button } from "@acme/ui";
import { AcmeClient } from "@acme/api";

export default function App() {
  const api = new AcmeClient("http://10.0.2.2:3000"); // Android emulator → host computer
  const [users, setUsers] = useState<{id:string;email:string;name:string}[]>([]);

  useEffect(() => {
    api.listUsers().then(setUsers).catch(console.error);
  }, []);

  return (
    <SafeAreaView>
      <View style={{padding:16}}>
        <Text style={{fontSize:20, fontWeight: "700", marginBottom:12}}>
          Mobile Dashboard 📱
        </Text>
        {users.map(u => (
          <Text key={u.id}>{u.email} — {u.name}</Text>
        ))}
        <Button title="Refresh" onPress={() => api.listUsers().then(setUsers)} />
      </View>
    </SafeAreaView>
  );
}

提示:iOS simulator 使用 http://localhost:3000 ;Android emulator 使用 http://10.0.2.2:3000 ;真机请使用你电脑的局域网 IP。


8)全仓库统一 lint / test / scripts ✅

为各 package 添加 scripts:

apps/web/package.json

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "lint": "eslint .",
    "test": "vitest",
    "clean": "rm -rf .next"
  },
  "eslintConfig": { "extends": ["@acme/config/src/eslint.cjs"] }
}

apps/mobile/package.json

{
  "scripts": {
    "dev": "expo start -c",
    "build": "echo 'Build via EAS or native tooling'",
    "lint": "eslint .",
    "test": "jest",
    "clean": "rm -rf .expo"
  },
  "eslintConfig": { "extends": ["@acme/config/src/eslint.cjs"] }
}

packages/ui/package.json

{ "scripts": { "build": "tsc -p tsconfig.json -b", "lint": "eslint .", "test": "vitest", "clean": "rimraf dist" } }

packages/api/package.json

{ "scripts": { "build": "tsc -p tsconfig.json -b", "lint": "eslint .", "test": "vitest", "clean": "rimraf dist" } }

9)使用 Changesets 进行版本管理与发布 📦

pnpm dlx changeset init

这会创建 .changeset/ 。当你修改了某个 package:

pnpm changeset
# 选择 packages、升级类型、填写说明
pnpm release

你可以将内部包发布到私有 registry(GitHub Packages / Nexus),或者仅作为 workspace 内部使用。


10)GitHub Actions CI 🧪🛠️

.github/workflows/ci.yml

name: CI
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
        with: { version: 9 }
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: 'pnpm' }
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm -r test
      - run: pnpm build

移动端 CI/CD 可单独添加 EAS workflow 或 Fastlane。


11)开发工作流 🧑‍💻

# 在仓库根目录
pnpm dev
# → Next.js 运行在 3000,Expo dev server 同时启动

得益于 Metro 配置和 pnpm workspaces,packages/ui 或 packages/api 中的任何改动都会实时同步到两个应用中。


12)常见坑位(及解决方案)🧯

  • Metro 找不到共享 packages → 确认已配置 watchFoldersnodeModulesPaths
  • react 被重复安装 → 在根目录统一版本,并在 @acme/ui 中声明为 peerDependencies
  • Path aliases 不生效 → 检查 tsconfig.base.json 以及各 app 的 tsconfig.json
  • Android 网络问题 → emulator 使用 10.0.2.2 ,真机使用电脑 IP
  • Next.js + ESM → 保持 "type": "module" ,并在 TS 中启用 verbatimModuleSyntax

13)生产环境检查清单 ✅

  • 通过 @acme/config 统一 eslint / prettier / tsconfig
  • 使用 @acme/types(Zod)集中管理类型契约
  • API client 同时校验 runtime 并推断 compile time 类型
  • UI 包同时导出 web / native 入口
  • Metro 已正确配置 symlink packages
  • Turbo pipeline 启用缓存,并在 CI 中恢复 cache
  • 环境变量策略(dev 使用 dotenv,prod 使用平台 secrets)
  • Changesets 管理版本与发布说明
  • E2E 冒烟测试(Web 使用 Playwright,Mobile 使用 Detox)——可选但强烈推荐

对于构建跨平台的现代化应用,一个高效的 React Native Monorepo 架构至关重要。本文分享的实践方案,希望能帮助你在管理复杂项目时理清思路。如果你对这类开发实践感兴趣,欢迎持续关注 云栈社区 ,获取更多工程化与架构方面的深度解析。




上一篇:技术管理者反思:10个让团队陷入内卷的“高效”管理法
下一篇:Spring AI 1.0整合DeepSeek:从零构建企业级智能客服系统实战
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-11 14:18 , Processed in 0.211775 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表