这是一篇翻译,原文:www.thehalftimecode.com/building-a-...
github 地址: github.com/ivesfurtado...
简介
作为开发者或实习生,你可能遇到过在单个项目中管理多个应用程序或包的挑战。无论是同时开发前端应用和后端API,还是在项目的不同部分之间共享UI组件和实用函数,事情都可能很快变得难以管理。
这就是monorepo 的用武之地。通过将代码组织到一个包含多个应用和共享包的单一代码库中,你可以简化开发过程并改善协作。在本指南中,我们将带你使用 Turbopack 、Biome、Next.js 15 、Express.js 、Tailwind CSS 和 ShadCN 设置一个monorepo。我们还将使用pnpm作为包管理器来优化依赖管理。
本教程结束时,你将拥有一个功能完整的monorepo,其中包含两个应用程序(Next.js 和Express.js)和三个共享包(UI组件、TypeScript类型和实用函数)。让我们开始吧!
注意: 你可以在文章末尾找到仓库链接。如果你以前从未设置过 monorepo,我强烈建议通读整个教程,而不是仅仅 fork 仓库。
前提条件
在开始本教程之前,确保你已安装以下内容:
- Visual Studio Code(或类似的支持Biome的代码编辑器)
- VSC的Biome扩展
- NodeJS(建议通过NVM安装以便于版本管理)
什么是Monorepo?
在开始构建之前,让我们明确什么是 monorepo 以及为什么它很有用。
定义
Monorepo 是一个包含多个项目或包代码的单一代码库。它不是为每个应用程序或共享库设置单独的代码库,而是将所有内容都放在一个地方。
使用Monorepo的好处
- 代码共享: 轻松在不同应用程序之间共享代码(例如UI组件或实用函数)。
- 一致性: 在所有项目中保持依赖项和配置的一致性。
- 简化协作: 由于所有内容都在一个地方,因此开发者在项目的不同部分之间协作更加容易。
- 原子更改: 在一次提交中对多个应用程序或包进行更改。
- 集中式CI/CD: 从一个地方管理持续集成和部署流程。
在本指南中,我们将创建一个包含以下内容的 monorepo(turborepo):
- 一个Next.js 应用(前端)。
- 一个Express.js 应用(后端)。
- 用于UI组件(使用 Tailwind CSS + Shadcn/ui)、共享类型和实用函数的包。
为什么使用 pnpm 和 Turbopack ?
为了使我们的 monorepo 高效和可扩展,我们将使用两个关键工具:pnpm 进行包管理和Turbopack/Turborepo 进行快速构建。
pnpm
pnpm是npm和Yarn的替代品,提供了几个优势:
- 更快的安装: pnpm通过使用硬链接而不是复制文件来更快地安装依赖项。
- 磁盘空间效率: 它通过避免重复依赖项来节省磁盘空间。
- 工作区支持: pnpm原生支持工作区,非常适合monorepo,你可以在其中拥有多个共享依赖项的项目。
Turbopack
Turbopack 是由 Vercel 为 Next.js 引入的新打包工具。它的设计比 Webpack 快得多,尤其是在开发过程中:
- 更快的热模块替换(HMR): 当你进行更改时,Turbopack 通过仅重新加载必要的模块来加速开发。
- 优化的生产构建: Turbopack 优化你的生产构建,使其更小更快。
- 与 Next.js 15 无缝集成: Turbopack可以与 Next.js 新的应用目录结构开箱即用。
有了这些工具,让我们继续设置我们的项目结构。
项目结构概述
以下是我们最终的项目结构:
bash
monorepo/
├── .vscode/
│ ├── extensions.json
│ └── settings.json
├── apps/
│ ├── web/ # Next.js应用
│ └── server/ # Express.js应用
├── packages/
│ ├── ui/ # 共享UI组件(使用Tailwind CSS + ShadCN)
│ ├── types/ # 共享TypeScript类型
│ ├── tsconfig/ # Typescript配置
│ └── utils/ # 共享实用函数
├── .gitignore # 从git中取消跟踪文件
├── biome.json # Biome配置
├── package.json # 项目配置
├── turbo.json # Turbopack配置
└── pnpm-workspace.yaml # pnpm工作区配置
我们将项目组织成两个主要目录:
apps/
:这将包含我们的两个主要应用程序---web
(Next.js)和server
(Express.js)。packages/
:这将包含两个应用都可以使用的共享代码---ui
用于共享UI组件,types
用于TypeScript类型,utils
用于共享实用函数,以及tsconfig
用于typescript配置文件。
现在我们对结构有了一个概述,让我们开始设置 monorepo。
设置Monorepo
步骤1:使用pnpm工作区初始化Monorepo
首先,如果你还没有安装pnpm,需要全局安装:
接下来,创建你的主项目目录:
bash
mkdir monorepo && cd monorepo
初始化一个新的工作区:
这个命令在项目根目录创建一个package.json
文件。现在我们需要通过在根目录创建一个pnpm-workspace.yaml
文件来告诉pnpm哪些目录应该是工作区的一部分:
vbnet
packages:
- 'apps/*'
- 'packages/*'
这个配置告诉pnpm,apps/
或packages/
内的任何文件夹都应被视为工作区的一部分。
步骤2:配置Turbopack
接下来,我们将通过在项目根目录创建一个turbo.json
文件来配置Turbopack:
json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": [
"**/.env.*local"
],
"tasks": {
"topo": {
"dependsOn": [
"^topo"
]
},
"build": {
"dependsOn": [
"^build"
],
"outputs": [
"dist/**",
".next/**",
"!.next/cache/**"
]
},
"lint": {
"dependsOn": [
"^topo"
]
},
"format": {
"dependsOn": [
"^topo"
]
},
"lint:fix": {
"dependsOn": [
"^topo"
]
},
"format:fix": {
"dependsOn": [
"^topo"
]
},
"check-types": {},
"dev": {
"cache": false,
"persistent": true
},
"add-shadcn-component": {
"dependsOn": [
"^topo"
]
},
"clean": {
"cache": false
}
}
}
这个配置定义了Turbopack应如何处理工作区中的构建。
注意
add-shadcn-component
命令,这是一个自定义命令,将在我们的UI包中使用,以便直接从根目录轻松添加 shadcn/ui 的新组件。
步骤3:全局配置
接下来,我们将更新根目录的package.json
以添加脚本和依赖项。
json
{
"name": "monorepo",
"private": true,
"scripts": {
"changeset": "changeset",
"publish:packages": "changeset publish",
"version:packages": "turbo build && changeset version",
"add-shadcn-component": "turbo run add-shadcn-component -- --",
"build": "turbo build",
"dev": "turbo dev",
"format": "turbo format --continue --",
"format:fix": "turbo format --continue -- --write",
"lint": "turbo lint --continue --",
"lint:fix": "turbo lint --continue -- --apply",
"clean": "turbo clean"
},
"dependencies": {
"@changesets/changelog-github": "^0.5.0",
"@changesets/cli": "^2.27.1",
"turbo": "^2.1.3"
},
"devDependencies": {
"@biomejs/biome": "^1.7.2",
"typescript": "^5",
"postcss": "^8.4.27"
},
"packageManager": "[email protected]"
}
对于 Biome 配置,我们将创建一个名为biome.json
的文件:
json
{
"$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
"files": {
"ignoreUnknown": true,
"ignore": [
"node_modules/*",
"*.config.*",
"*.json",
"tsconfig.json",
".turbo",
"**/dist",
"**/out",
".next"
]
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"complexity": {
"noForEach": "off",
"noUselessFragments": "off"
},
"correctness": {
"useExhaustiveDependencies": "off",
"noUnusedImports": "warn",
"noUnusedVariables": "warn"
},
"style": {
"noParameterAssign": "off"
}
}
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
"indentStyle": "space",
"lineEnding": "lf",
"lineWidth": 120
}
}
一个非常重要的文件是.gitignore
,这是我们告诉Git我们不想跟踪哪些文件的文件。
.gitignore
# dependencies
/node_modules
/.pnp
.pnp.js
node_modules
packages/*/node_modules
apps/*/node_modules
.next
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
.pnpm-debug.log*
# other lockfiles that's not pnpm-lock.yaml
package-lock.json
yarn.lock
# local env files
.env
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# intellij
.idea
dist/**
/dist
packages/*/dist
.turbo
/test-results/
/playwright-report/
/playwright/.cache/
这个配置定义了我们的项目默认设置。现在我们已经设置了工作区配置文件,让我们继续创建我们的应用程序。
步骤3:.vscode文件夹
项目目录中的.vscode
文件夹存储了特定于 Visual Studio Code的 配置设置。这些设置允许你为你的项目或工作区需求个性化和优化 VS Code。以下是两种主要的设置类型:
- 用户设置:全局应用于系统上所有 VS Code 实例。它们非常适合你想要保持一致的设置,比如字体大小或主题。
- 工作区设置 :仅应用于当前项目。这对于项目特定的配置很有用,比如从文件浏览器中排除某些文件夹(例如
node_modules
)。
VS Code 使用 JSON 文件存储这些设置,使自定义和通过版本控制共享变得容易。为了便于管理,你可以直接在 JSON 文件中修改设置,或使用设置编辑器,它提供了一个方便的图形界面。
对于我们的项目,我们将创建两个存储这些配置的文件。首先,在根目录创建一个名为.vscode
的文件夹。然后,创建extensions.json
:
json
{
"recommendations": [
"yoavbls.pretty-ts-errors",
"bradlc.vscode-tailwindcss",
"biomejs.biome"
]
}
我们需要的最后一个配置是全局设置,所以创建一个名为settings.json
的文件:
json
{
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit",
"source.fixAll.biome": "explicit",
},
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
],
"typescript.enablePromptUseWorkspaceTsdk": true,
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.preferences.autoImportFileExcludePatterns": [
"next/router.d.ts",
"next/dist/client/router.d.ts"
],
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
}
}
创建第一个包(tsconfig)
为了创建将在我们整个monorepo中使用的typescript配置以及web和server的单独配置,我们将创建我们的tsconfig包。
bash
mkdir packages && cd packages && mkdir tsconfig && cd tsconfig
我们将有6个用于Typescript的配置文件:
- base :
base.json
- web :
next.json
- server :
express.json
- ui :
ui.json
- utils :
utils.json
- types :
types.json
首先我们将创建我们的package.json
:
json
{
"name": "@monorepo/tsconfig",
"version": "0.0.0",
"private": true,
"license": "MIT",
"publishConfig": {
"access": "public"
}
}
然后我们将创建我们的base.json
配置文件(你可以在这里找到所有tsconfig设置):
json
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"alwaysStrict": false,
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"target": "ESNext",
"lib": [
"DOM",
"DOM.Iterable",
"ESNext"
],
"noEmit": true,
"declaration": true,
"declarationMap": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"downlevelIteration": true,
"allowJs": true,
"isolatedModules": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"incremental": true,
"tsBuildInfoFile": ".tsbuildinfo"
},
"include": [
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules",
"src/tests"
]
}
现在我们将创建next.json
配置:
json
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"paths": {
"@/*": [
"./*"
]
},
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
}
}
然后我们需要创建express.json
配置文件:
json
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "ExpressJS Server",
"extends": "./base.json",
"ts-node": {
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "Node10"
}
},
"compilerOptions": {
"outDir": "./build",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"module": "ESNext"
}
}
创建一个types.json
配置文件,用于我们的共享类型包:
json
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Shared Types",
"extends": "./base.json",
"compilerOptions": {
"outDir": "./dist",
"declaration": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
},
}
此外,我们需要添加最后的配置文件ui.json
,这个文件将用于我们的共享UI包。
json
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Shared UI",
"extends": "./base.json",
"compilerOptions": {
"paths": {
"@/*": [
"./*"
]
},
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
}
}
最后,创建utils.json配置:
json
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Shared UI",
"extends": "./base.json",
"compilerOptions": {
"paths": {
"@/*": [
"./*"
]
},
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
}
}
这就是你的 tsconfig 包文件夹的样子:
恭喜,我们刚刚完成了typescript的配置。现在让我们进入激动人心的部分:创建我们的应用程序!
创建应用程序
步骤1:设置Next.js 15应用(web
)
创建并导航到apps/
目录:
bash
cd ../.. && mkdir apps && cd apps
使用pnpm创建一个新的Next.js应用:
lua
pnpm create next-app@latest web --ts --app --turbopack --no-eslint --tailwind --src-dir --skip-install --import-alias @/*
此命令将在web/
文件夹中创建一个新的Next.js应用,启用TypeScript,将Turbopack设置为默认打包器,并使用Tailwind CSS。
要将我们的tsconfig包集成到web应用中,我们需要更新默认的package.json
:
json
...,
"dependencies": {
"@monorepo/types": "workspace:*",
"@monorepo/ui": "workspace:*",
"@monorepo/utils": "workspace:*",
"react": "19.0.0-rc-02c0e824-20241028",
"react-dom": "19.0.0-rc-02c0e824-20241028",
"next": "15.0.2"
},
"devDependencies": {
"@monorepo/tsconfig": "workspace:*",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"@biomejs/biome": "^1.7.2"
}
...,
现在,更新默认的tsconfig.json
:
json
{
"extends": "@monorepo/tsconfig/next.json",
"compilerOptions": {
"plugins": [
{
"name": "next"
}
],
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"tailwind.config.ts"
],
"exclude": [
"node_modules"
]
}
添加biome.json
以便我们可以在文件夹中激活它:
json
{
"extends": ["../../biome.json"]
}
你的Next.js应用现在已经设置好了!让我们继续使用Express.js设置后端应用。
步骤2:设置Express应用(server
)
导航回到apps/
目录并创建一个Express应用:
bash
cd .. && mkdir server && cd server && pnpm init
更新你的服务器的package.json
以添加Express、其类型、cors、morgan和ts-node-dev:
json
{
"name": "server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"keywords": [],
"author": "",
"license": "ISC",
"scripts": {
"dev": "ts-node-dev --transpile-only src/server.ts"
},
"dependencies": {
"@monorepo/types": "workspace:*",
"express": "^4.21.1",
"ts-node-dev": "^2.0.0",
"cors": "2.8.5",
"morgan": "^1.10.0"
},
"devDependencies": {
"@monorepo/tsconfig": "workspace:*",
"@types/express": "^5.0.0",
"@types/morgan": "^1.9.9",
"@types/cors": "2.8.17"
}
}
将tsconfig.json
添加到服务器:
json
{
"extends": "@monorepo/tsconfig/express.json",
"include": [
"src"
],
}
在src/server.ts
中创建一个基本的Express服务器:
json
import cors from "cors";
import express from "express";
import morgan from "morgan";
const app = express();
app.use(morgan("tiny"));
app.use(express.json({ limit: "100mb" }));
app.use(
cors({
credentials: true,
origin: ["http://localhost:3000"],
}),
);
const port = process.env.PORT || 3001;
app.get("/", (_, res) => {
res.send("Hello from Express!");
});
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});
现在你已经设置了前端(Next.js)和后端(Express)应用!让我们继续创建两个应用都可以使用的共享包。
创建共享包
在这一部分,我们将创建三个共享包:一个用于UI组件(ui
),一个用于TypeScript类型(types
),一个用于实用函数(utils
)。这些包将位于packages/
目录中。
步骤1:创建utils
包
我们将创建的第一个包是用于实用函数(utils
)。要设置它:
在packages/
内创建文件夹,初始化它:
bash
cd ../.. && mkdir packages && cd packages && mkdir utils && cd utils && pnpm init && mkdir src && touch src/styles.ts
更新package.json
以添加脚本和导出:
json
{
"name": "@monorepo/utils",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"check-types": "tsc --noEmit",
"build": "tsup",
"lint": "biome lint ./src",
"format": "biome format ./src "
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"clsx": "^2.1.1",
"tailwind-merge": "^2.5.4"
},
"devDependencies": {
"@monorepo/tsconfig": "workspace:*"
},
"exports": {
".": "./src",
"./styles": "./src/styles.ts"
}
}
添加biome.json
:
json
{
"extends": [
"../../biome.json"
]
}
添加tsconfig.json
:
json
{
"extends": "@monorepo/tsconfig/utils.json",
"include": [
"**/*.ts",
],
"exclude": [
"node_modules"
],
}
我们将创建的第一个(也是唯一的)实用函数是cn
,一个用于有条件地合并tailwind类的实用函数,它在ShadCN组件中被大量使用。我们需要添加以下依赖项:
sql
pnpm add clsx tailwind-merge
在src/style.ts
中添加cn通用实用函数:
ts
import clsx, { type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
步骤2:创建ui
包(Tailwind CSS + ShadCN)
导航回到packages/
目录:
bash
cd .. && mkdir ui && cd ui && pnpm init
安装React以及Tailwind CSS(开发依赖)和ShadCN(我们将使用new york风格):
bash
pnpm add -D @types/react @types/react-dom autoprefixer postcss react tailwindcss typescript
bash
pnpm add shadcn @types/react tailwindcss-animate class-variance-authority clsx tailwind-merge @radix-ui/react-icons
按照与Next.js应用类似的步骤设置Tailwind CSS---初始化Tailwind CSS(npx tailwindcss init
)并在tailwind.config.ts
中配置它:
ts
import type { Config } from "tailwindcss";
import tailwindcssAnimate from "tailwindcss-animate";
import { fontFamily } from "tailwindcss/defaultTheme";
const config = {
darkMode: ["class"],
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
"../../packages/ui/src/**/*.{ts,tsx}",
],
prefix: "",
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px'
}
},
extend: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
}
},
borderRadius: {
lg: '`var(--radius)`',
md: '`calc(var(--radius) - 2px)`',
sm: 'calc(var(--radius) - 4px)'
},
fontFamily: {
sans: ["var(--font-sans)", ...fontFamily.sans]
},
keyframes: {
'accordion-down': {
from: {
height: '0'
},
to: {
height: 'var(--radix-accordion-content-height)'
}
},
'accordion-up': {
from: {
height: 'var(--radix-accordion-content-height)'
},
to: {
height: '0'
}
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out'
}
}
},
plugins: [tailwindcssAnimate],
} satisfies Config;
export default config;
我们还需要为Tailwind CSS 配置postcss.config.mjs
:
js
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
export default config;
由于我们也将在这个包上使用Biome,添加biome.json
:
json
{
"extends": [
"../../biome.json"
]
}
更新package.json
以添加tsconfig
、utils
包和自定义脚本:
json
{
"name": "@monorepo/ui",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"check-types": "tsc --noEmit",
"add-shadcn-component": "pnpm dlx shadcn@latest add",
"build": "tsup",
"lint": "biome lint ./src",
"format": "biome format ./src "
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@monorepo/tsconfig": "workspace:*",
"@types/react": "^18.3.12",
"@types/react-dom": "^18",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.47",
"react": "19.0.0-rc-02c0e824-20241028",
"tailwindcss": "^3.4.1"
},
"dependencies": {
"@monorepo/utils": "workspace:^",
"@radix-ui/react-accordion": "^1.2.1",
"@radix-ui/react-icons": "^1.3.1",
"@radix-ui/react-select": "^2.1.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"shadcn": "^2.1.3",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7"
},
"exports": {
"./globals.css": "./src/styles/globals.css",
"./postcss.config": "./postcss.config.mjs",
"./tailwind.config": "./tailwind.config.ts",
"./components/*": "./src/*.tsx"
}
}
创建一个tsconfig.json
文件:
json
{
"extends": "@monorepo/tsconfig/ui.json",
"include": [
"**/*.ts",
"**/*.tsx",
"tailwind.config.ts",
],
"exclude": [
"node_modules"
],
}
在src/styles/globals.css
创建一个样式文件:
css
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 47.4% 11.2%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--card: 0 0% 100%;
--card-foreground: 222.2 47.4% 11.2%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 100% 50%;
--destructive-foreground: 210 40% 98%;
--ring: 215 20.2% 65.1%;
--radius: 0.5rem;
}
.dark {
--background: 224 71% 4%;
--foreground: 213 31% 91%;
--muted: 223 47% 11%;
--muted-foreground: 215.4 16.3% 56.9%;
--accent: 216 34% 17%;
--accent-foreground: 210 40% 98%;
--popover: 224 71% 4%;
--popover-foreground: 215 20.2% 65.1%;
--border: 216 34% 17%;
--input: 216 34% 17%;
--card: 224 71% 4%;
--card-foreground: 213 31% 91%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 1.2%;
--secondary: 222.2 47.4% 11.2%;
--secondary-foreground: 210 40% 98%;
--destructive: 0 63% 31%;
--destructive-foreground: 210 40% 98%;
--ring: 216 34% 17%;
--radius: 0.5rem;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-feature-settings: "rlig" 1, "calt" 1;
}
}
ShadCN需要你创建一个components.json
(启用CLI使用):
json
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/styles/globals.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "src/",
"ui": "src/",
"utils": "@monorepo/utils/styles"
}
}
现在你可以开始在这个包中添加可重用的UI组件了!例如,要导入ShadCN按钮组件,只需在工作区根目录运行以下命令:
csharp
pnpm add-shadcn-component card
你可以在这里找到每个ShadCN组件,在这里找到基于它创建的其他组件。现在我们准备设置我们的共享类型包并集成我们已设置的所有内容!
步骤3:创建types
包
types
包将包含两个应用都可以使用的共享TypeScript类型。要创建它:
导航回到packages/
,创建文件夹并初始化它:
bash
cd .. && mkdir types && cd types && pnpm init
创建biome.json文件:
json
{
"extends": [
"../../biome.json"
]
}
创建tsconfig.json
文件:
json
{
"extends": "@monorepo/tsconfig/types.json",
"include": [
"**/*.ts",
],
"exclude": [
"node_modules"
],
}
我们将创建的第一个类型是一个简单的api客户端,这样我们可以在server
和web
之间共享类型。创建src/
文件夹,在其中创建api/
文件夹。然后创建simple-api-client.ts
:
ts
export interface GetTestResponse {
message: string;
}
export type GetTest = () => Promise<GetTestResponse>;
export interface SimpleApiClient {
getTest: GetTest;
}
更新package.json
以添加导出、脚本和devDependencies:
ts
{
"name": "@monorepo/types",
"version": "1.0.0",
"description": "",
"scripts": {
"build": "tsc",
"lint": "biome lint ./src",
"check-types": "tsc --noEmit"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@monorepo/tsconfig": "workspace:*"
},
"exports": {
".": "./src/index.ts"
}
}
现在,在src/api文件夹创建一个index.ts,并从simple-api-client.ts导出所有内容(你将使用其他文件复制它,以拥有单一导入源):
javascript
export * from "./simple-api-client";
最后,在src文件夹创建一个 index.ts 并从 api 导出所有内容:
我们的共享类型包已经全部设置好了!你的代码库应该看起来像这样:
现在,让我们继续我们教程的最后部分:集成所有内容并运行开发环境。
在本地运行你的Monorepo
现在一切都设置好了,让我们在本地运行两个应用程序!
步骤1:安装所有依赖项
要一次性安装工作区中的所有依赖项(记得切换回根目录):
pnpm install
这个命令安装两个应用(web
、server
)以及所有共享包(ui
、types
等)所需的所有依赖项。
步骤2:同时运行两个应用
arduino
pnpm turbo run dev
这个命令同时启动你的前端(Next.js )在端口3000和后端(Express)在端口3001!
Web和Server集成
为了在我们的应用和包之间创建一个简单的集成,我们将开发一个组件,该组件将使用我们在本教程前面创建的共享类型从服务器获取数据。但是,在此之前,让我们更新我们的Tailwind CSS 文件和全局样式 ,使用我们在UI
包中定义的那些。将tailwind.config.ts的内容替换为以下内容:
javascript
export * from "@monorepo/ui/tailwind.config";
现在将postcss.config.mjs
内容替换为:
javascript
export { default } from "@monorepo/ui/postcss.config";
在我们的根布局(src/app/layout.tsx
)中,更新globals.css导入以使用我们在UI包中创建的那个:
tsx
import "@monorepo/ui/globals.css";
import "./style.css";
import type { Metadata } from "next";
import localFont from "next/font/local";
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans",
weight: "100 900",
});
const geistMono = localFont({
src: "./fonts/GeistMonoVF.woff",
variable: "--font-geist-mono",
weight: "100 900",
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased dark`}>{children}</body>
</html>
);
}
这样做是为了我们可以从共享UI包控制我们的应用UI样式和配置,这样如果我们创建另一个web应用(例如管理仪表板),我们就有一致的样式!
要开始开发我们的应用程序,我们将按照以下规则组织web文件夹结构:只在一个页面中使用的组件应该在app
目录中与页面文件夹相同级别的components
文件夹内。应用共享组件应该在src/components
文件夹中。
所以,让我们在app
目录中创建一个components文件夹src/app/components
(我们将创建一个只在第一个页面使用的组件),并创建一个名为get-test.tsx
的文件,内容如下:
tsx
"use client";
import type { GetTestResponse } from "@monorepo/types";
import { Card, CardContent, CardHeader } from "@monorepo/ui/components/card";
import { cn } from "@monorepo/utils/styles";
import { useEffect, useState } from "react";
const GetTest = () => {
const [test, setTest] = useState<string>("");
useEffect(() => {
const fetchTest = async () => {
const response = await fetch("http://localhost:3001/test");
const data: GetTestResponse = await response.json();
setTimeout(() => {
setTest(data.message);
}, 3000);
};
fetchTest();
}, []);
return (
<div>
<Card>
<CardHeader>
<h1 className={cn("text-xl text-yellow-500", test !== "" && "text-green-500")}>Get Test</h1>
</CardHeader>
<CardContent>
<p>{test}</p>
</CardContent>
</Card>
</div>
);
};
export default GetTest;
看一下这个文件,我们有一个简单的fetch到我们的服务器,使用我们在共享类型中定义的类型化响应,这使我们可以轻松地使用响应。服务器路由将很快定义。首先,让我们通过在page.tsx中导入组件来完成web
部分:
tsx
import GetTest from "./components/get-test";
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<GetTest />
</div>
);
}
web设置完成后,转到server
应用并使用我们将使用的路由更新server.ts
。在app.listen
之前添加以下路由:
ts
app.get("/test", (_, res) => {
const testJson: GetTestResponse = {
message: "Hello from Express API!",
};
res.json(testJson);
});
最后,一切准备就绪,你可以使用以下命令运行整个应用程序(记得切换回根目录):
你将能够看到在页面上渲染的以下组件。
一旦组件被渲染,我们在useEffect中fetch服务器并设置状态以进行渲染(setTimeout不是必需的,它只是为了可视化状态改变),然后你将看到以下内容。
结论
恭喜!你已成功设置了一个可扩展的 monorepo,其中包含两个应用程序---一个使用 Next.js 构建的前端,使用 Tailwind CSS 样式,由 ShadCN 的可重用组件增强,以及一个 Express 后端---所有这些都使用 pnpm 工作区高效管理,以及由 Turbopack 提供的超快构建和几乎即时的 Biome 检查!
以下是你今天完成的工作:
- 在一个代码库中创建了两个独立的应用程序。
- 设置了四个包,其中三个包含可重用代码,一个用于 typescript 配置。
- 通过 pnpm 工作区使用高效的依赖管理实践将这些包链接到应用程序之间。
我们已经到达教程的结尾,现在你可以自由发挥想象力,使用最佳的 monorepo 架构创建任何你想要的东西。