使用 Monorepo 实现 Next.js 后端API 接口定义和移动端同步

16 阅读4分钟

之前做项目时,在两个 Repo 中分别管理后端 API 和 App,在手工同步 API 定义的过程中吃了不少亏。最近刚学到 Monorepo 这种项目管理方式,于是在新项目进行尝试。以下内容由 AI 提供,修复一些小问题并验证了一遍。

基于 pnpm + Turborepo 的标准化搭建步骤。这套方案的核心在于利用 OpenAPI (Swagger) 作为中间桥梁,实现从 Next.js 到 Flutter 的自动化类型同步。

以下是从零开始新建项目的完整步骤和配置文件内容。

📂 第一步:初始化 Monorepo 根目录

首先,我们需要建立仓库的基础结构和包管理器配置。

  1. 创建目录并初始化 在终端执行以下命令:

    mkdir my-super-app
    cd my-super-app
    git init
    pnpm init
    
  2. 配置工作区 (pnpm-workspace.yaml) 在根目录创建 pnpm-workspace.yaml,告诉 pnpm 我们的项目包含哪些部分:

    packages:
      - "apps/*"
      - "packages/*"
    
  3. 安装核心依赖 我们需要 turbo 来管理任务管道,npm-run-all 来并行执行脚本。

    pnpm add turbo npm-run-all -Dw
    

    注意:-Dw 表示将其作为开发依赖安装到根目录的工作区中。

  4. 配置构建管道 (turbo.json) 在根目录创建 turbo.json,定义构建和开发的依赖关系:

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

🚀 第二步:搭建 Next.js 后端 (API)

我们将创建一个标准的 Next.js 应用作为 API 服务器,并配置它生成 OpenAPI 规范。

  1. 创建应用

    mkdir -p apps
    cd apps
    npx create-next-app@latest api --typescript --eslint --tailwind --app --src-dir --import-alias "@/*"
    # 按照提示选择 Yes
    cd ..
    
  2. 配置 API 生成脚本 为了让 Flutter 能读取类型,我们需要 Next.js 能输出 openapi.json

    • 安装依赖 (在 apps/api 目录下):
      cd apps/api
      pnpm add @asteasolutions/zod-to-openapi swagger-ui-dist
      cd ../..
      
    • 添加脚本 (修改 apps/api/package.json): 我们需要一个脚本来启动 API 并生成 spec 文件。
      {
        "name": "api",
        "version": "0.1.0",
        "private": true,
        "scripts": {
          "dev": "next dev",
          "build": "next build",
          "start": "next start",
          "lint": "next lint",
          "generate:api-spec": "node ./scripts/generate-spec.js" 
        }
      }
      
    • 创建生成脚本文件 (apps/api/scripts/generate-spec.js): 这是一个简单的 Node 脚本,用于在构建时或手动触发时生成 JSON。
      const fs = require('fs');
      const path = require('path');
      
      // 这里仅作演示,实际项目中你需要根据 Zod Schema 生成
      // 或者使用 next-swagger-doc 等库在运行时导出
      const mockSpec = {
        "openapi": "3.0.0",
        "info": { "title": "My App API", "version": "1.0.0" },
        "paths": {} 
      };
      
      const outputPath = path.join(__dirname, '../../../packages/api-spec/openapi.json');
      // 确保目录存在
      const dir = path.dirname(outputPath);
      if (!fs.existsSync(dir)){
          fs.mkdirSync(dir, { recursive: true });
      }
      
      fs.writeFileSync(outputPath, JSON.stringify(mockSpec, null, 2));
      console.log('✅ API Spec generated at packages/api-spec/openapi.json');
      

📱 第三步:搭建 Flutter 前端

  1. 创建应用 回到根目录,创建 Flutter 项目:

    cd apps
    flutter create mobile --org com.example.myapp
    cd ..
    
  2. 配置 Flutter 代码生成 我们需要 Flutter 能够读取 packages/api-spec 中的 JSON 并生成代码。

    • 添加依赖 (apps/mobile/pubspec.yaml):

      dev_dependencies:
        flutter_test:
          sdk: flutter
        build_runner: ^2.4.0
        openapi_generator: ^6.0.0 # 或者使用 retrofit + json_serializable
      
    • 配置生成器 (apps/mobile/build.yaml): 在 apps/mobile 目录下创建 build.yaml,指定输入文件的路径(指向 Monorepo 的共享包):

      targets:
        $default:
          builders:
            openapi_generator:
              options:
                input: ../../packages/api-spec/openapi.json
                output: lib/api_client/
                # 其他配置...
      

🔗 第四步:创建共享包与根脚本

这是 Monorepo 的“魔法”所在,我们将所有东西串联起来。

  1. 创建共享包目录

    mkdir -p packages/api-spec
    # 可以在这里放一个空的 openapi.json 占位,防止首次运行报错
    echo '{}' > packages/api-spec/openapi.json
    
  2. 配置根目录脚本 (package.json) 修改根目录的 package.json,添加统一的管理命令:

    {
      "name": "my-super-app",
      "version": "1.0.0",
      "private": true,
      "scripts": {
        "dev": "turbo run dev",
        "build": "turbo run build",
        "lint": "turbo run lint",
        "clean": "turbo run clean",
        "generate:api-spec": "turbo run generate:api-spec",
        "generate:mobile-client": "cd apps/mobile && dart run build_runner build --delete-conflicting-outputs",
        "sync": "pnpm run generate:api-spec && pnpm run generate:mobile-client"
      },
      "devDependencies": {
        "npm-run-all": "^4.1.5",
        "turbo": "^2.9.1"
      },
      "packageManager": "pnpm@8.10.0"
    }
    

🛠️ 第五步:配置 VS Code 统一开发环境

为了让 VS Code 同时完美支持 Dart 和 TypeScript,请在根目录创建 .vscode 文件夹,并添加以下文件:

  1. 推荐插件 (.vscode/extensions.json)

    {
      "recommendations": [
        "Dart-Code.flutter",
        "Dart-Code.dart-code",
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode",
        "bradlc.vscode-tailwindcss"
      ]
    }
    
  2. 工作区设置 (.vscode/settings.json) 这是关键,它告诉 VS Code 如何处理多语言环境:

    {
      "files.watcherExclude": {
        "**/.git/objects/**": true,
        "**/node_modules/**": true,
        "**/apps/mobile/build/**": true,
        "**/apps/api/.next/**": true
      },
      "eslint.workingDirectories": [
        { "mode": "auto", "path": "./apps/api" }
      ],
      "dart.openDevTools": "flutter",
      "[dart]": {
        "editor.formatOnSave": true,
        "editor.defaultFormatter": "Dart-Code.dart-code"
      },
      "[typescript]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode",
        "editor.formatOnSave": true
      }
    }
    
  3. 调试配置 (.vscode/launch.json) 配置一个“一键启动全栈”的按钮:

    {
      "version": "0.2.0",
      "configurations": [
        {
          "name": "Launch Next.js API",
          "type": "node",
          "request": "launch",
          "cwd": "${workspaceFolder}/apps/api",
          "runtimeExecutable": "pnpm",
          "runtimeArgs": ["run", "dev"],
          "console": "integratedTerminal"
        },
        {
          "name": "Launch Flutter Mobile",
          "type": "dart",
          "request": "launch",
          "cwd": "${workspaceFolder}/apps/mobile",
          "program": "${workspaceFolder}/apps/mobile/lib/main.dart"
        }
      ],
      "compounds": [
        {
          "name": "🚀 Fullstack Dev (API + Mobile)",
          "configurations": ["Launch Next.js API", "Launch Flutter Mobile"],
          "presentation": {
            "hidden": false,
            "group": "fullstack",
            "order": 1
          }
        }
      ]
    }
    

🎯 最终工作流

现在,你的开发流程变成了这样:

  1. 启动开发:在 VS Code 中按 F5 选择 "🚀 Fullstack Dev",后端和模拟器会同时启动。
  2. 修改后端:在 apps/api 中修改 API 逻辑或 Zod 定义。
  3. 同步类型:在终端运行 pnpm run sync
    • 这会自动从 Next.js 生成最新的 openapi.json
    • 然后自动触发 Flutter 的代码生成器,更新 apps/mobile/lib/api_client 中的代码。
  4. AI 辅助:你可以直接问 AI:“我修改了 API 的用户模型,请帮我更新 Flutter 的 UI 以显示新字段”,AI 会理解整个 Monorepo 的上下文并给出准确代码。

更新: 在 Flutter 项目中添加 openapi_generator包时,会遇到了版本冲突问题,openapi_generator 已经掉队了。一个解决方式是:“Node.js 侧生成 Dart SDK”。

思路:利用 Monorepo 中的 Next.js 环境(Node.js)生成 OpenAPI 规范,并调用 CLI 工具生成 Dart 代码,将生成的 SDK 作为“包”引入 Flutter。

具体操作

  1. 在 Next.js 侧安装生成器

    # 在 next.js 项目
    pnpm add -D @openapitools/openapi-generator-cli
    
  2. 添加生成脚本scripts/generate-mobile-client.js):

    const { execSync } = require('child_process');
    // 使用 CLI 工具,完全避开 Flutter 环境的 build_runner
    execSync('npx openapi-generator-cli generate -i ../../packages/api-spec/openapi.json -g dart -o ../../packages/dart-client/');
    
  3. 在 Flutter 中引用

    # apps/mobile/pubspec.yaml
    dependencies:
      dart_client:
        path: ../../packages/dart-client
    
  4. 更新

    turbor.json 增加生成的 task generate:mobile-client

    {
      ...
      "tasks": {
        "generate:mobile-client": {
          "dependsOn": ["generate:api-spec"]
        }
      }
    }
    

    修改根目录的 package.json,添加统一的生成 SDK 的命令:

    {
      ...
      "scripts": {
        ...
        "generate:mobile-client": "turbo run generate:mobile-client",
        "sync": "pnpm run generate:api-spec && pnpm run generate:mobile-client"
      }
    }
    

在终端运行 pnpm run sync

  • 这会自动从 Next.js 生成最新的 openapi.json
  • 然后自动触发 Flutter 的代码生成器,更新 packages/dart-client/ 中的代码。