تنظيم تطوير تطبيقات React واسعة النطاق

يستند هذا المنشور إلى سلسلة حول تحديث الواجهة الأمامية لـ jQuery باستخدام React. من أجل فهم أسباب كتابة هذه المقالة بشكل أفضل ، يوصى بإلقاء نظرة على المقالة الأولى في هذه السلسلة. من السهل جدًا هذه الأيام تنظيم عملية تطوير تطبيق React صغير ، أو البدء من نقطة الصفر. خاصة عند استخدام تطبيق create-reaction . ستحتاج بعض المشاريع على الأرجح إلى عدد قليل من التبعيات (على سبيل المثال ، لإدارة حالة التطبيق وتدويل المشروع) ومجلد يحتوي على دليل على الأقل







srccomponents... أعتقد أن هذا هو الهيكل الذي تبدأ به معظم مشاريع React. عادة ، ومع ذلك ، مع نمو عدد تبعيات المشروع ، يواجه المبرمجون زيادة في عدد المكونات والمخفضات والآليات الأخرى القابلة لإعادة الاستخدام المدرجة في هيكلها. في بعض الأحيان يصبح كل شيء غير مريح للغاية ويصعب إدارته. ما العمل ، على سبيل المثال ، إذا لم يعد واضحًا سبب الحاجة إلى بعض التبعيات ، وكيف تتلاءم معًا؟ أو ماذا لو جمع المشروع الكثير من المكونات بحيث يصعب إيجاد العنصر المناسب بينها؟ ماذا تفعل إذا احتاج المبرمج إلى العثور على مكون معين تم نسيان اسمه؟



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



في وقت كتابة هذا التقرير ، لدينا حوالي 1200 ملف JavaScript في مشروعنا. 350 منهم مكونات. الكود هو 80٪ وحدة تم اختبارها. نظرًا لأننا ما زلنا نلتزم بالاتفاقيات التي أبرمناها ونعمل في إطار هيكل المشروع الذي تم إنشاؤه مسبقًا ، فقد قررنا أنه سيكون من الجيد مشاركة كل هذا مع عامة الناس. هذه هي الطريقة التي جاء بها هذا المقال. سنتحدث هنا عن تنظيم تطوير تطبيق React واسع النطاق ، وما الدروس التي تعلمناها من تجربة العمل عليه.



كيف أنظم الملفات والمجلدات؟



لقد وجدنا طريقة لتنظيم مواد الواجهة الأمامية في React بشكل ملائم بعد المرور بعدة مراحل من المشروع. في البداية ، كنا في طريقنا لاستضافة مواد المشروع في نفس المستودع حيث تم تخزين كود الواجهة الأمامية المستند إلى jQuery. ومع ذلك ، نظرًا لمتطلبات بنية المجلد المفروضة على المشروع بواسطة إطار عمل الواجهة الخلفية الذي نستخدمه ، فإن هذا الخيار لم يعمل بالنسبة لنا. بعد ذلك ، فكرنا في نقل كود الواجهة الأمامية إلى مستودع منفصل. نجح هذا النهج في البداية بشكل جيد ، ولكن بمرور الوقت بدأنا في التفكير في إنشاء أجزاء أخرى للعميل من المشروع ، على سبيل المثال ، واجهة أمامية تعتمد على React Native. هذا جعلنا نفكر في مكتبة المكونات. نتيجة لذلك ، قمنا بتقسيم المستودع الجديد إلى مستودعين منفصلين. كان أحدهما لمكتبة مكونة ، والآخر كان لواجهة React الأمامية الجديدة.على الرغم من أننا اعتقدنا في البداية أن هذه الفكرة كانت ناجحة ، إلا أن تنفيذها أدى إلى تعقيد خطير في إجراء مراجعة الكود. أصبحت العلاقة بين التغييرات في مستودعاتنا غير واضحة. نتيجة لذلك ، قررنا التبديل مرة أخرى إلى تخزين الكود في مستودع واحد ، ولكنه أصبح الآن مستودعًا أحاديًا.



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



قمنا بإعداد مستودعنا الأحادي باستخدام مساحات عمل الغزل باستخدام التكوين التالي في الملف الجذر package.json:



"workspaces": [
    "app/*",
    "lib/*",
    "tool/*"
]


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



  • app: جميع الحزم في هذا المجلد مرتبطة بتطبيقات الواجهة الأمامية مثل Karify frontend وبعض الواجهات الداخلية الأخرى. يتم أيضًا تخزين مواد القصص القصيرة لدينا هنا .
  • lib: -, , . , , . , , typography, media primitive.
  • tool: , , Node.js. , , , . , , webpack, , ( « »).


تحتوي جميع حزمنا ، بغض النظر عن المجلد الذي تم تخزينها فيه ، على مجلد فرعي srcومجلد اختياريًا bin. في srcحزمة المجلدات وتخزينها في الدلائل appو libقد تحتوي على بعض من المجلدات الفرعية التالية:



  • actions: يحتوي على وظائف لإنشاء الإجراءات التي يمكن تمرير قيم الإرجاع الخاصة بها إلى وظائف الإرسال من reduxأو useReducer.
  • components: يحتوي على مجلدات المكونات مع الكود والترجمات واختبارات الوحدة واللقطات والتاريخ (إذا كان ينطبق على مكون معين).
  • constants: يخزن هذا المجلد القيم التي لم تتغير في بيئات مختلفة. يتم تخزين المرافق هنا أيضًا.
  • fetch: هذا هو المكان الذي يتم فيه تخزين تعريفات الأنواع لمعالجة البيانات المستلمة من واجهة برمجة التطبيقات الخاصة بنا ، بالإضافة إلى الإجراءات غير المتزامنة المقابلة المستخدمة لتلقي مثل هذه البيانات.
  • helpers: , .
  • reducers: , redux useReducer.
  • routes: , react-router history.
  • selectors: , redux-, , API.


تسمح لنا بنية المجلد هذه بكتابة رمز معياري حقًا ، حيث إنها تخلق نظامًا واضحًا لتقسيم المسؤوليات بين المفاهيم المختلفة التي تحددها تبعياتنا. يساعدنا هذا في البحث في المستودع عن المتغيرات والوظائف والمكونات ، علاوة على ذلك ، بغض النظر عما إذا كان الشخص الذي يبحث عنها يعرف بوجودها أم لا. علاوة على ذلك ، يساعدنا ذلك في الاحتفاظ بالحد الأدنى من المحتوى في مجلدات منفصلة ، مما يسهل بدوره العمل معهم.



عندما بدأنا في تطبيق بنية المجلد هذه ، واجهنا تحديًا يتمثل في ضمان تطبيق متسق لمثل هذا الهيكل. عند العمل مع حزم مختلفة ، قد يرغب المطور في إنشاء مجلدات مختلفة في مجلدات هذه الحزم ، وتنظيم الملفات في هذه المجلدات بطرق مختلفة. على الرغم من أنه ليس دائمًا أمرًا سيئًا ، إلا أن مثل هذا النهج غير المنظم سيؤدي إلى الارتباك. لمساعدتنا في تطبيق الهيكل أعلاه بشكل منهجي ، أنشأنا ما يمكن تسميته "نظام الملفات linter". سنتحدث عن هذا الآن.



كيف تضمن التطبيق الإلزامي لدليل الأنماط؟



سعينا جاهدين لتحقيق التوحيد في هيكل الملفات والمجلدات في مشروعنا. أردنا تحقيق نفس الشيء بالنسبة للكود. بحلول ذلك الوقت ، كانت لدينا بالفعل تجربة ناجحة في حل مشكلة مماثلة في إصدار jQuery من المشروع ، ولكن كان لدينا الكثير لتحسينه ، خاصة عندما يتعلق الأمر بـ CSS. نتيجة لذلك ، قررنا إنشاء دليل نمط من البداية والتأكد من استخدامه مع الوبر. تم التحكم في القواعد التي لا يمكن فرضها باستخدام linter أثناء مراجعة الكود.



يتم إنشاء linter في مستودع أحادي بنفس الطريقة كما هو الحال في أي مستودع آخر. هذا جيد لأنه يسمح لك بفحص المستودع بأكمله في مسار واحد. إذا لم تكن معتادًا على linters ، أوصي بإلقاء نظرة على ESLint و Stylelint . نحن نستخدمها بالضبط.



أثبتت JavaScript linter أنها مفيدة بشكل خاص في المواقف التالية:



  • ضمان استخدام المكونات التي تم إنشاؤها مع وضع إمكانية الوصول إلى المحتوى في الاعتبار بدلاً من نظيراتها في HTML. عند إنشاء دليل النمط ، قدمنا ​​عدة قواعد تتعلق بإمكانية الوصول إلى الروابط والأزرار والصور والرموز. ثم احتجنا إلى تطبيق هذه القواعد في الكود والتأكد من أننا ، في المستقبل ، لن ننساها. لقد فعلنا ذلك باستخدام قاعدة عناصر رد الفعل / الحظر من eslint-plugin-response .


فيما يلي مثال لما يبدو عليه الأمر:



'react/forbid-elements': [
    'error',
    {
        forbid: [
            {
                element: 'img',
                message: 'Use "<Image>" instead. This is important for accessibility reasons.',
            },
        ],
    },
],






بالإضافة إلى فحص JavaScript و CSS ، لدينا أيضًا "نظام ملفات" خاص بنا. هو الذي يضمن الاستخدام الموحد لهيكل المجلد الذي اخترناه. نظرًا لأن هذه أداة أنشأناها بأنفسنا ، إذا قررنا التبديل إلى بنية مجلد مختلفة ، فيمكننا دائمًا تغييرها وفقًا لذلك. فيما يلي أمثلة على القواعد التي نتحكم فيها عند العمل مع الملفات والمجلدات:



  • فحص بنية المجلد المكونات: ضمان أن هناك دائما ملف index.tsو .tsx.file مع نفس اسم المجلد.
  • التحقق من صحة الملف package.json: التأكد من وجود ملف واحد لكل حزمة وأن الخاصية privateمضبوطة trueعلى منع النشر العرضي للحزمة.


أي نوع نظام يجب أن تختار؟



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



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



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



نظرًا للاتجاه الذي تطور فيه TypeScript في السنوات الأخيرة ، كان هذا الانتقال سهلاً للغاية بالنسبة لنا. كان الانتقال من TSLint إلى ESLint مفيدًا بشكل خاص بالنسبة لنا .



كيف أختبر الكود؟



عندما بدأنا العمل في المشروع ، لم يكن واضحًا لنا أي أدوات اختبار نختارها. إذا كنت تفكر في ذلك الآن، وأود أن أقول ذلك، من أجل وحدة واختبار التكامل، فمن الأفضل استخدام المزاح و السرو، على التوالي . هذه الأدوات موثقة جيدًا وسهلة الاستخدام. المؤسف الوحيد هو أن cypress لا يدعم Fetch API ، والشيء السيئ هو أن واجهة برمجة التطبيقات لهذه الأداة ليست مصممة لاستخدام بنية غير متزامنة / انتظار . بعد أن بدأنا في استخدام السرو ، لم نفهم هذا على الفور. لكني أتمنى أن يتحسن الوضع في المستقبل القريب.



في البداية ، كان من الصعب علينا إيجاد أفضل طريقة لكتابة اختبارات الوحدة. مع مرور الوقت، وقد حاولنا النهج مثل قطة الاختبار ، اختبار العارض ، الضحلة العارض . لقد جربنا مكتبة الاختبار . انتهى بنا الأمر إلى عرض ضحل ، استخدمنا لاختبار إخراج المكون ، واستخدمنا عرض الاختبار لاختبار المنطق الداخلي للمكونات.



نعتقد أن مكتبة الاختبار هي حل جيد للمشاريع الصغيرة. لكن حقيقة أن هذا النظام يعتمد على عرض DOM له تأثير كبير على الأداء المعياري. علاوة على ذلك ، نعتقد أن النقداختبار اللقطة باستخدام عرض السطح غير ذي صلة عندما يتعلق الأمر بالمكونات "العميقة" جدًا. بالنسبة لنا ، تبين أن اللقطات مفيدة جدًا في التحقق من جميع الخيارات الممكنة لإخراج المكونات. ومع ذلك ، لا ينبغي أن يكون رمز المكون معقدًا بشكل مفرط ؛ يجب أن تسعى جاهدًا لجعله مناسبًا للقراءة. يمكن toJSONتحقيق ذلك من خلال جعل المكونات صغيرة وتحديد طريقة لمدخلات المكونات التي لا تتعلق باللقطة.



لكي لا ننسى اختبارات الوحدة ، قمنا بإعداد عتبة تغطية الكود عن طريق الاختبارات... مع الدعابة ، من السهل جدًا القيام بذلك ، وليس هناك الكثير للتفكير فيه. يكفي فقط وضع مؤشر لتغطية الكود العالمي عن طريق الاختبارات. لذلك ، في بداية العمل ، حددنا هذا الرقم بنسبة 60٪. بمرور الوقت ، مع نمو تغطية الاختبار لقاعدة الشفرة الخاصة بنا ، زدناها إلى 80٪. نحن راضون عن هذا المؤشر ، لأننا لا نعتقد أنه من الضروري السعي من أجل تغطية الكود بنسبة 100٪ مع الاختبارات. إن تحقيق هذا المستوى من تغطية الكود بالاختبارات لا يبدو واقعيًا بالنسبة لنا.



كيف يمكن تبسيط إنشاء مشاريع جديدة؟



عادة بداية العمل على التطبيق رد فعل بسيط جدا: ReactDOM.render(<App />, document.getElementById(‘#root’));. ولكن في حالة احتياجك إلى دعم SSR (عرض جانب الخادم ، عرض الخادم) ، تصبح هذه المهمة أكثر تعقيدًا. أيضًا ، إذا كانت تبعيات تطبيقك تتضمن أكثر من مجرد React ، فقد يحتاج العميل والخادم إلى استخدام معلمات مختلفة. على سبيل المثال ، نستخدم تفاعل intl للتدويل ، وإعادة رد الفعل لإدارة الحالة العالمية ، وجهاز توجيه رد الفعل للتوجيه ، و redux-saga لإدارة الإجراءات غير المتزامنة . هذه التبعيات تحتاج إلى بعض التخصيص. يمكن أن تكون عملية تكوين هذه التبعيات معقدة.



استند حلنا لهذه المشكلة على أنماط التصميم " الإستراتيجية " و " المصنع التجريدي ". اعتدنا على إنشاء فئتين مختلفتين (استراتيجيتان مختلفتان): واحدة لتكوين العميل والأخرى لتكوين الخادم. تلقت كلتا هاتين الفئتين معلمات للتطبيق الذي تم إنشاؤه ، والتي تضمنت الاسم والشعار والمخفضات والمسارات واللغة الافتراضية والملحمات (لـ redux-saga) وما إلى ذلك. يمكن أخذ المخفضات والمسارات والملاحم من حزم مختلفة من مستودعنا الأحادي. يتم استخدام هذا التكوين بعد ذلك لإنشاء متجر redux ، sagas middleware ، كائن محفوظات جهاز التوجيه. يتم استخدامه أيضًا لتحميل الترجمات وتقديم التطبيق. على سبيل المثال ، فيما يلي توقيعات استراتيجيات العميل والخادم:



type BootstrapConfiguration = {
  logo: string,
  name: string,
  reducers: ReducersMapObject,
  routes: Route[],
  sagas: Saga[],
};
class AbstractBootstrap {
  configuration: BootstrapConfiguration;
  intl: IntlShape;
  store: Store;
  rootSaga: Task;
abstract public run(): void;
  abstract public render<T>(): T;
  abstract protected createIntl(): IntlShape;
  abstract protected createRootSaga(): Task;
  abstract protected createStore(): Store;
}
//   
class WebBootstrap extends AbstractBootstrap {
  constructor(config: BootstrapConfiguration);
  public render<ReactNode>(): ReactNode;
}
//   
class ServerBootstrap extends AbstractBootstrap {
  constructor(config: BootstrapConfiguration);
  public render<string>(): string;
}


وجدنا هذا الفصل بين الاستراتيجيات مفيدًا ، نظرًا لوجود بعض الاختلافات في إعداد التخزين والملاحم وكائنات التدويل والتاريخ ، اعتمادًا على البيئة التي يتم فيها تنفيذ الكود. على سبيل المثال ، يتم إنشاء متجر redux على العميل باستخدام بيانات محملة مسبقًا من الخادم وباستخدام ملحق redux-devtools . لا حاجة لأي من هذا على الخادم. مثال آخر هو كائن التدويل الذي يحصل ، على العميل ، على اللغة الحالية من navigator.languages ، وعلى الخادم من عنوان HTTP Accept-Language .



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



كيف تحافظ على جودة شفرتك على مستوى عالٍ؟



Linters والاختبارات والتحقق من النوع - كل هذا له تأثير مفيد على جودة الكود. ولكن يمكن للمبرمج أن ينسى بسهولة إجراء الفحوصات المناسبة قبل تضمين الكود في الفرع master. أفضل طريقة هي تشغيل هذه الفحوصات تلقائيًا. يفضل بعض الأشخاص القيام بذلك في كل التزام باستخدام خطافات Git.، والذي لا يسمح لك بالالتزام حتى يجتاز الكود جميع الفحوصات. لكننا نعتقد أنه مع هذا النهج ، يتدخل النظام كثيرًا في عمل المبرمج. بعد كل شيء ، على سبيل المثال ، قد يستغرق العمل في فرع معين عدة أيام ، ولن يتم التعرف عليه طوال هذه الأيام على أنه مناسب للإرسال إلى المستودع. لذلك ، نتحقق من الالتزامات باستخدام نظام التكامل المستمر. يتم فحص رمز الفروع المرتبطة بطلبات الدمج فقط. يسمح لنا هذا بتجنب إجراء الفحوصات المضمونة عدم اجتيازها ، نظرًا لأننا غالبًا ما نطلب تضمين نتائج عملنا في الكود الرئيسي للمشروع عندما نتأكد من أن هذه النتائج قادرة على اجتياز جميع عمليات الفحص.



يبدأ تدفق التحقق التلقائي من الكود بتثبيت التبعيات. ويتبع ذلك فحص النوع ، وتشغيل linters ، وإجراء اختبارات الوحدة ، وبناء التطبيق ، وإجراء اختبارات السرو. يتم تنفيذ جميع هذه المهام تقريبًا بشكل متوازٍ. في حالة حدوث خطأ في أي من هذه الخطوات ، ستفشل عملية السحب بالكامل ولا يمكن تضمين الفرع المقابل في رمز المشروع الرئيسي. فيما يلي مثال على نظام مراجعة التعليمات البرمجية العامل.





التحقق التلقائي من الكود كانت



الصعوبة الرئيسية التي واجهناها أثناء إعداد هذا النظام هي تسريع تنفيذ عمليات التحقق. هذه المهمة لا تزال ذات صلة. لقد أجرينا الكثير من التحسينات والآن أصبحت كل هذه الفحوصات مستقرة في حوالي 20 دقيقة. ربما يمكن تحسين هذا المؤشر من خلال إجراء موازٍ لتنفيذ بعض اختبارات السرو ، لكنه يناسبنا حاليًا.



النتيجة



إن تنظيم تطوير تطبيق React واسع النطاق ليس بالمهمة السهلة. لحلها ، يحتاج المبرمج إلى اتخاذ العديد من القرارات ، وتحتاج الكثير من الأدوات إلى التهيئة. في الوقت نفسه ، لا توجد إجابة واحدة صحيحة لمسألة كيفية تطوير مثل هذه التطبيقات.



نظامنا يناسبنا حتى الآن. نأمل أن يساعد الحديث عنها المبرمجين الآخرين الذين يواجهون نفس المهام التي واجهناها. إذا قررت اتباع مثالنا ، فتأكد أولاً من أن ما تمت مناقشته هنا مناسب لك ولشركتك. الأهم من ذلك ، السعي لتحقيق بساطتها. لا تفرط في تعقيد التطبيقات ومجموعات الأدوات المستخدمة لإنشائها.



كيف ستتعامل مع مهمة تنظيم تطوير مشروع React واسع النطاق؟






All Articles