Monorepo والأنواع المشتركة
Monorepo والأنواع المشتركة
تتبنى فرق التطوير الحديثة بشكل متزايد بنيات monorepo لإدارة مشاريع متعددة ذات صلة في مستودع واحد. توفر TypeScript أدوات قوية لمشاركة الأنواع والتكوينات والكود عبر الحزم مع الحفاظ على أمان النوع الصارم وتمكين البناء التدريجي. في هذا الدرس، سنستكشف مراجع مشروع TypeScript، وبناء حزم النوع المشتركة، وتكوين أسماء مستعارة للمسار في monorepos، وأفضل الممارسات لهيكلة مشاريع TypeScript واسعة النطاق.
لماذا نستخدم TypeScript في Monorepos؟
TypeScript في monorepos يوفر مزايا فريدة:
- أنواع مشتركة: عرّف عقود API، ونماذج المجال، وأنواع الأدوات المساعدة مرة واحدة واستخدمها عبر جميع الحزم
- أمان النوع عبر الحدود: تأكد من اتساق النوع بين الواجهة الأمامية والخلفية وتطبيقات الجوال
- البناء التدريجي: ابنِ فقط الحزم المتغيرة باستخدام مراجع المشروع
- إعادة الهيكلة على نطاق واسع: أعد تسمية الأنواع والدوال عبر حزم متعددة بثقة
- الأدوات الموحدة: شارك تكوينات TypeScript وقواعد linting ونصوص البناء
بنية Monorepo
قد يبدو monorepo نموذجي لـ TypeScript كما يلي:
my-monorepo/ ├── packages/ │ ├── shared-types/ # أنواع TypeScript المشتركة │ │ ├── src/ │ │ │ ├── api/ # أنواع طلب/استجابة API │ │ │ ├── models/ # نماذج المجال │ │ │ ├── utils/ # أنواع الأدوات المساعدة │ │ │ └── index.ts # التصدير الرئيسي │ │ ├── package.json │ │ └── tsconfig.json │ ├── web-app/ # تطبيق الواجهة الأمامية │ │ ├── src/ │ │ ├── package.json │ │ └── tsconfig.json │ ├── api-server/ # API الخلفية │ │ ├── src/ │ │ ├── package.json │ │ └── tsconfig.json │ └── mobile-app/ # تطبيق الجوال │ ├── src/ │ ├── package.json │ └── tsconfig.json ├── tsconfig.base.json # تكوين TypeScript الأساسي ├── package.json # package.json الجذر └── pnpm-workspace.yaml # تكوين مساحة العمل (أو lerna.json، إلخ)
مراجع مشروع TypeScript
تمكن مراجع المشروع TypeScript من بناء حزم متعددة بالترتيب الصحيح وتمكين الترجمة التدريجية:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"composite": true,
"incremental": true
}
}
composite: true مطلوب عند استخدام مراجع المشروع. يمكّن TypeScript من إنشاء ملفات إعلان .d.ts و .tsbuildinfo للبناء التدريجي.
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"composite": true,
"declaration": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@shared-types/*": ["../shared-types/src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"],
"references": [
{ "path": "../shared-types" }
]
}
بناء حزمة أنواع مشتركة
أنشئ تعريفات نوع قابلة لإعادة الاستخدام يمكن استهلاكها من قبل حزم متعددة:
// أنواع API الأساسية
export interface ApiResponse<T> {
data: T;
message?: string;
success: boolean;
}
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
meta: {
total: number;
page: number;
perPage: number;
totalPages: number;
};
links: {
first: string;
last: string;
prev: string | null;
next: string | null;
};
}
export interface ApiError {
message: string;
errors?: Record<string, string[]>;
statusCode: number;
timestamp: string;
}
// أنواع API المستخدم
export interface LoginRequest {
email: string;
password: string;
}
export interface LoginResponse {
token: string;
refreshToken: string;
expiresIn: number;
user: User;
}
export interface RegisterRequest {
email: string;
username: string;
password: string;
firstName: string;
lastName: string;
}
export interface User {
id: number;
email: string;
username: string;
firstName: string;
lastName: string;
avatar?: string;
role: UserRole;
createdAt: string;
updatedAt: string;
}
export enum UserRole {
ADMIN = 'admin',
USER = 'user',
MODERATOR = 'moderator',
GUEST = 'guest',
}
export interface UserProfile extends User {
bio?: string;
website?: string;
location?: string;
social?: {
twitter?: string;
github?: string;
linkedin?: string;
};
}
export interface UserSettings {
userId: number;
theme: 'light' | 'dark' | 'auto';
language: string;
notifications: {
email: boolean;
push: boolean;
sms: boolean;
};
privacy: {
profileVisibility: 'public' | 'private' | 'friends';
showEmail: boolean;
showLocation: boolean;
};
}
// تصدير جميع أنواع API export * from './api'; // تصدير جميع النماذج export * from './models/User'; export * from './models/Post'; export * from './models/Comment'; // تصدير أنواع الأدوات المساعدة export * from './utils/types';
أسماء مستعارة للمسار في Monorepos
قم بتكوين أسماء مستعارة للمسار لاستيرادات أنظف عبر monorepo الخاص بك:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@shared-types/*": ["packages/shared-types/src/*"],
"@web-app/*": ["packages/web-app/src/*"],
"@api-server/*": ["packages/api-server/src/*"],
"@mobile-app/*": ["packages/mobile-app/src/*"]
}
}
}
// بدلاً من الاستيرادات النسبية:
// import { User } from '../../../shared-types/src/models/User';
// استخدم أسماء مستعارة للمسار نظيفة:
import { User, UserRole, LoginRequest } from '@shared-types/models/User';
import { ApiResponse, PaginatedResponse } from '@shared-types/api';
// في كود التطبيق الخاص بك
async function fetchUser(id: number): Promise<ApiResponse<User>> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
tsconfig-paths في حل وقت تشغيل Node.js.
البناء باستخدام مراجع المشروع
استخدم وضع بناء TypeScript لترجمة المشاريع بترتيب التبعية:
# بناء جميع المشاريع المشار إليها tsc --build # بناء مشروع محدد وتبعياته tsc --build packages/web-app # تنظيف مخرجات البناء tsc --build --clean # إجبار إعادة البناء tsc --build --force # وضع المراقبة للتطوير tsc --build --watch
{
"name": "@mycompany/web-app",
"version": "1.0.0",
"scripts": {
"build": "tsc --build",
"build:watch": "tsc --build --watch",
"clean": "tsc --build --clean",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@mycompany/shared-types": "workspace:*"
}
}
إدارة حزمة مساحة العمل
قم بتكوين مدير الحزم الخاص بك لدعم monorepo:
packages: - 'packages/*'
{
"private": true,
"workspaces": [
"packages/*"
]
}
{
"private": true,
"workspaces": [
"packages/*"
]
}
إصدار النوع والتوافق
إدارة التغييرات الجذرية في الأنواع المشتركة عبر الحزم:
// packages/shared-types/src/api/v1/User.ts
export namespace UserApiV1 {
export interface User {
id: number;
name: string;
email: string;
}
export interface CreateUserRequest {
name: string;
email: string;
password: string;
}
}
// packages/shared-types/src/api/v2/User.ts
export namespace UserApiV2 {
export interface User {
id: number;
firstName: string; // تغيير جذري: تقسيم الاسم
lastName: string;
email: string;
username: string; // حقل جديد
}
export interface CreateUserRequest {
firstName: string;
lastName: string;
username: string;
email: string;
password: string;
}
}
// يمكن للمستهلكين استيراد إصدارات محددة
import { UserApiV1 } from '@shared-types/api/v1/User';
import { UserApiV2 } from '@shared-types/api/v2/User';
عقود RPC/API آمنة من حيث النوع
عرّف عقود API آمنة من حيث النوع من البداية إلى النهاية باستخدام الأنواع المشتركة:
// packages/shared-types/src/contracts/user.contract.ts
export interface UserContract {
// تعريفات نقطة النهاية مع أنواع الطلب/الاستجابة
'GET /users': {
request: {
query: {
page?: number;
perPage?: number;
search?: string;
};
};
response: PaginatedResponse<User>;
};
'GET /users/:id': {
request: {
params: {
id: number;
};
};
response: ApiResponse<User>;
};
'POST /users': {
request: {
body: RegisterRequest;
};
response: ApiResponse<User>;
};
'PUT /users/:id': {
request: {
params: {
id: number;
};
body: Partial<User>;
};
response: ApiResponse<User>;
};
'DELETE /users/:id': {
request: {
params: {
id: number;
};
};
response: ApiResponse<{ deleted: boolean }>;
};
}
// عميل API آمن من حيث النوع
type ApiClient<Contract> = {
[K in keyof Contract]: (
request: Contract[K]['request']
) => Promise<Contract[K]['response']>;
};
// الاستخدام في الواجهة الأمامية
const userApi: ApiClient<UserContract> = {
'GET /users': async (req) => {
const { page, perPage, search } = req.query;
const response = await fetch(
`/api/users?page=${page}&perPage=${perPage}&search=${search}`
);
return response.json();
},
// ... نقاط نهاية أخرى
};
ملفات التكوين المشتركة
مركز تكوينات ESLint و Prettier والأدوات الأخرى:
module.exports = {
parser: '@typescript-eslint/parser',
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
rules: {
'@typescript-eslint/explicit-function-return-type': 'warn',
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
},
};
module.exports = {
extends: ['@mycompany/eslint-config'],
rules: {
// تجاوزات خاصة بالمشروع
},
};
أفضل الممارسات لأنواع Monorepo
- مصدر واحد للحقيقة: عرّف نماذج المجال مرة واحدة في الأنواع المشتركة، لا تكرر أبدًا
- الإصدار: استخدم الإصدار الدلالي لحزمة الأنواع المشتركة؛ قم بتوصيل التغييرات الجذرية
- مساحات الأسماء: استخدم مساحات أسماء TypeScript أو بنية المجلد لتنظيم الأنواع حسب المجال
- التوثيق: أضف تعليقات JSDoc إلى الأنواع المشتركة للحصول على تلميحات IDE أفضل
- التحقق من الصحة: فكر في مكتبات التحقق من الصحة في وقت التشغيل (Zod، io-ts) لحدود API
- ترتيب البناء: استفد من مراجع المشروع لترتيب البناء الصحيح والبناء التدريجي
- الاختبار: اكتب اختبارات للأنواع المشتركة باستخدام مكتبات الاختبار على مستوى النوع مثل
tsd
- قم بإعداد monorepo مع 3 حزم: shared-types، web-app، و api-server
- قم بتكوين مراجع مشروع TypeScript بين الحزم
- أنشئ تعريفات نوع مشتركة لنماذج User و Post و Comment
- عرّف أنواع عقد API لعمليات CRUD على هذه النماذج
- قم بإعداد أسماء مستعارة للمسار للاستيرادات النظيفة عبر الحزم
- نفذ عميل API آمن من حيث النوع في web-app يستخدم العقود المشتركة
- قم بتكوين البناء التدريجي واختبر أن الحزم المتغيرة فقط تعيد البناء
ملخص
في هذا الدرس، تعلمت كيفية هيكلة مشاريع TypeScript في بنية monorepo باستخدام مراجع المشروع، وحزم النوع المشتركة، والأسماء المستعارة للمسار. استكشفت كيفية بناء تعريفات نوع قابلة لإعادة الاستخدام تضمن اتساق النوع عبر تطبيقات الواجهة الأمامية والخلفية والجوال. تعلمت أيضًا عن استراتيجيات الإصدار للأنواع المشتركة، وعقود API الآمنة من حيث النوع، وأفضل الممارسات لإدارة قواعد كود TypeScript واسعة النطاق. هذه التقنيات تمكّن الفرق من العمل على مشاريع متعددة ذات صلة بثقة، مع العلم أن TypeScript ستكتشف أخطاء النوع عبر الحزم في وقت الترجمة بدلاً من الإنتاج.