لغة TypeScript

Monorepo والأنواع المشتركة

33 دقيقة الدرس 35 من 40

Monorepo والأنواع المشتركة

تتبنى فرق التطوير الحديثة بشكل متزايد بنيات monorepo لإدارة مشاريع متعددة ذات صلة في مستودع واحد. توفر TypeScript أدوات قوية لمشاركة الأنواع والتكوينات والكود عبر الحزم مع الحفاظ على أمان النوع الصارم وتمكين البناء التدريجي. في هذا الدرس، سنستكشف مراجع مشروع TypeScript، وبناء حزم النوع المشتركة، وتكوين أسماء مستعارة للمسار في monorepos، وأفضل الممارسات لهيكلة مشاريع TypeScript واسعة النطاق.

لماذا نستخدم TypeScript في Monorepos؟

TypeScript في monorepos يوفر مزايا فريدة:

  • أنواع مشتركة: عرّف عقود API، ونماذج المجال، وأنواع الأدوات المساعدة مرة واحدة واستخدمها عبر جميع الحزم
  • أمان النوع عبر الحدود: تأكد من اتساق النوع بين الواجهة الأمامية والخلفية وتطبيقات الجوال
  • البناء التدريجي: ابنِ فقط الحزم المتغيرة باستخدام مراجع المشروع
  • إعادة الهيكلة على نطاق واسع: أعد تسمية الأنواع والدوال عبر حزم متعددة بثقة
  • الأدوات الموحدة: شارك تكوينات TypeScript وقواعد linting ونصوص البناء

بنية Monorepo

قد يبدو monorepo نموذجي لـ TypeScript كما يلي:

بنية دليل Monorepo:
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 من بناء حزم متعددة بالترتيب الصحيح وتمكين الترجمة التدريجية:

التكوين الأساسي (tsconfig.base.json):
{
  "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 للبناء التدريجي.
حزمة الأنواع المشتركة (packages/shared-types/tsconfig.json):
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "rootDir": "./src",
    "outDir": "./dist",
    "composite": true,
    "declaration": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
حزمة المستهلك (packages/web-app/tsconfig.json):
{
  "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 (packages/shared-types/src/api/index.ts):
// أنواع 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;
}
نماذج المجال (packages/shared-types/src/models/User.ts):
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;
  };
}
التصدير الرئيسي (packages/shared-types/src/index.ts):
// تصدير جميع أنواع API
export * from './api';

// تصدير جميع النماذج
export * from './models/User';
export * from './models/Post';
export * from './models/Comment';

// تصدير أنواع الأدوات المساعدة
export * from './utils/types';

أسماء مستعارة للمسار في Monorepos

قم بتكوين أسماء مستعارة للمسار لاستيرادات أنظف عبر monorepo الخاص بك:

tsconfig.json الجذر مع تعيين المسار:
{
  "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();
}
نصيحة: عند استخدام الأسماء المستعارة للمسار، تأكد من أن bundler الخاص بك (Webpack، Vite، إلخ) أو وقت التشغيل (Node.js مع ts-node) تم تكوينه أيضًا لحل هذه الأسماء المستعارة. يمكن أن تساعد أدوات مثل tsconfig-paths في حل وقت تشغيل Node.js.

البناء باستخدام مراجع المشروع

استخدم وضع بناء TypeScript لترجمة المشاريع بترتيب التبعية:

أوامر البناء:
# بناء جميع المشاريع المشار إليها
tsc --build

# بناء مشروع محدد وتبعياته
tsc --build packages/web-app

# تنظيف مخرجات البناء
tsc --build --clean

# إجبار إعادة البناء
tsc --build --force

# وضع المراقبة للتطوير
tsc --build --watch
نصوص الحزمة (packages/web-app/package.json):
{
  "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:

مساحة عمل pnpm (pnpm-workspace.yaml):
packages:
  - 'packages/*'
مساحات عمل Yarn (package.json):
{
  "private": true,
  "workspaces": [
    "packages/*"
  ]
}
مساحات عمل npm (package.json):
{
  "private": true,
  "workspaces": [
    "packages/*"
  ]
}

إصدار النوع والتوافق

إدارة التغييرات الجذرية في الأنواع المشتركة عبر الحزم:

أنواع API ذات الإصدارات:
// 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 آمنة من حيث النوع من البداية إلى النهاية باستخدام الأنواع المشتركة:

تعريف عقد 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 والأدوات الأخرى:

تكوين ESLint المشترك (packages/eslint-config/index.js):
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: '^_' }],
  },
};
استهلاك التكوين المشترك (packages/web-app/.eslintrc.js):
module.exports = {
  extends: ['@mycompany/eslint-config'],
  rules: {
    // تجاوزات خاصة بالمشروع
  },
};

أفضل الممارسات لأنواع Monorepo

  • مصدر واحد للحقيقة: عرّف نماذج المجال مرة واحدة في الأنواع المشتركة، لا تكرر أبدًا
  • الإصدار: استخدم الإصدار الدلالي لحزمة الأنواع المشتركة؛ قم بتوصيل التغييرات الجذرية
  • مساحات الأسماء: استخدم مساحات أسماء TypeScript أو بنية المجلد لتنظيم الأنواع حسب المجال
  • التوثيق: أضف تعليقات JSDoc إلى الأنواع المشتركة للحصول على تلميحات IDE أفضل
  • التحقق من الصحة: فكر في مكتبات التحقق من الصحة في وقت التشغيل (Zod، io-ts) لحدود API
  • ترتيب البناء: استفد من مراجع المشروع لترتيب البناء الصحيح والبناء التدريجي
  • الاختبار: اكتب اختبارات للأنواع المشتركة باستخدام مكتبات الاختبار على مستوى النوع مثل tsd
تمرين:
  1. قم بإعداد monorepo مع 3 حزم: shared-types، web-app، و api-server
  2. قم بتكوين مراجع مشروع TypeScript بين الحزم
  3. أنشئ تعريفات نوع مشتركة لنماذج User و Post و Comment
  4. عرّف أنواع عقد API لعمليات CRUD على هذه النماذج
  5. قم بإعداد أسماء مستعارة للمسار للاستيرادات النظيفة عبر الحزم
  6. نفذ عميل API آمن من حيث النوع في web-app يستخدم العقود المشتركة
  7. قم بتكوين البناء التدريجي واختبر أن الحزم المتغيرة فقط تعيد البناء

ملخص

في هذا الدرس، تعلمت كيفية هيكلة مشاريع TypeScript في بنية monorepo باستخدام مراجع المشروع، وحزم النوع المشتركة، والأسماء المستعارة للمسار. استكشفت كيفية بناء تعريفات نوع قابلة لإعادة الاستخدام تضمن اتساق النوع عبر تطبيقات الواجهة الأمامية والخلفية والجوال. تعلمت أيضًا عن استراتيجيات الإصدار للأنواع المشتركة، وعقود API الآمنة من حيث النوع، وأفضل الممارسات لإدارة قواعد كود TypeScript واسعة النطاق. هذه التقنيات تمكّن الفرق من العمل على مشاريع متعددة ذات صلة بثقة، مع العلم أن TypeScript ستكتشف أخطاء النوع عبر الحزم في وقت الترجمة بدلاً من الإنتاج.