چگونه پروژه های React را به گونه ای سازماندهی کنیم که مقیاس پذیر، ساختارمند، مدولار، سازگار و منطقی باشد.
به نظر می رسد نحوه ساختار برنامه های React راه صحیح تا زمانی که React وجود داشته است اخیراً موضوع داغ بوده است. نظر رسمی React در مورد آن این است که نظری ندارد. این عالی است، به ما آزادی کامل می دهد تا هر کاری را که می خواهیم انجام دهیم. و همچنین بد است. این منجر به نظرات بسیار متفاوت و بسیار قوی در مورد ساختار مناسب برنامه React می شود، که حتی باتجربه ترین توسعه دهندگان نیز گاهی اوقات احساس می کنند به خاطر آن در یک گوشه به آن فکر می کنند.
البته من هم نظر محکمی در مورد این موضوع دارم. چیزی که امروز میخواهم به اشتراک بگذارم سیستمی است که در آن به خوبی کار میکند:
محیطی با دهها تیم به هم متصل در یک مخزن که روی یک محصول کار میکنند
در یک محیط پرسرعت از یک استارتاپ کوچک تنها با چند مهندس
یا حتی برای پروژه های یک نفره
فقط به یاد داشته باشید، مانند کد دزدان، همه اینها بیشتر همان چیزی است که شما آن را "راهنماها" می نامید تا قوانین واقعی.
چه چیزی از قرارداد ساختار پروژه نیاز داریم
من نمیخواهم در ابتدا به جزئیات این موضوع بپردازم که چرا ما به چنین قراردادی نیاز داریم: اگر به این مقاله رسیدید احتمالاً قبلاً تصمیم گرفتهاید که به آن نیاز دارید. چیزی که من می خواهم کمی در مورد آن صحبت کنم، قبل از پرش به راه حل ها، چیزی است که یک قرارداد ساختار پروژه را عالی می کند.
تکرارپذیری
قرارداد کد باید به اندازه کافی قابل درک و آسان باشد تا توسط هر یک از اعضای تیم بازتولید شود، از جمله کارآموزی که اخیراً با حداقل تجربه React به آن ملحق شده است. اگر روش کار در مخزن شما نیاز به سنیور دارد، چند ماه آموزش و بحث های عمیق فلسفی بر سر هر ثانیه روابط عمومی دارد. خب، احتمالاً سیستم واقعاً زیبایی خواهد بود، اما در هیچ جای دیگری جز روی کاغذ وجود نخواهد داشت.
استنتاج
شما می توانید یک کتاب بنویسید و چند فیلم در مورد "روش کار در مخزن ما" فیلمبرداری کنید. احتمالاً حتی میتوانید همه اعضای تیم را متقاعد کنید که آن را بخوانند و تماشا کنند (اگرچه احتمالاً این کار را نخواهید کرد). واقعیت همچنان پابرجاست: اکثر مردم قرار نیست هر کلمه ای از آن را حفظ کنند، حتی اگر اصلاً باشد. برای اینکه این قرارداد واقعاً کار کند، باید آنقدر واضح و شهودی باشد، به طوری که افراد در تیم به طور ایدهآل قادر به مهندسی معکوس آن با خواندن کد باشند. در دنیای کامل، مانند نظرات کد، شما حتی نیازی به نوشتن آن در جایی ندارید - خود کد و ساختار مستندات شما خواهند بود.
استقلال
یکی از مهمترین الزامات دستورالعملهای ساختار کدنویسی برای چندین نفر، و بهویژه تیمهای متعدد، ایجاد راهی برای توسعهدهندگان است که به طور مستقل عمل کنند. آخرین چیزی که می خواهید چندین توسعه دهنده است که روی یک فایل کار می کنند یا تیم هایی که دائماً به مسئولیت یکدیگر حمله می کنند.
بنابراین، دستورالعملهای ساختار کدگذاری ما باید چنین ساختاری را فراهم کند، جایی که تیمها بتوانند به طور مسالمت آمیز در یک مخزن همزیستی داشته باشند.
بهینه سازی شده برای refactoring
آخرین مورد، اما در دنیای فرانت اند مدرن، مهمترین مورد است. Frontend امروز فوق العاده روان است. الگوها، چارچوب ها و بهترین شیوه ها دائما در حال تغییر هستند. علاوه بر این، انتظار میرود که امروزه ویژگیها را سریع ارائه کنیم. نه، سریع و بعد از یک ماه دوباره آن را به طور کامل بنویسید. و سپس شاید دوباره آن را بنویسید.
بنابراین برای قرارداد کدنویسی ما بسیار مهم است که ما را مجبور نکند که کد را در مکانی دائمی و بدون هیچ راهی برای جابجایی آن "چسب کنیم". باید کارها را به گونه ای سازماندهی کند که بازسازی کاری باشد که به طور معمول و روزانه انجام شود. بدترین کاری که یک قرارداد می تواند انجام دهد این است که بازسازی را چنان سخت و زمان بر کند که همه از آن وحشت داشته باشند. در عوض، باید به سادگی نفس کشیدن باشد.
اکنون که الزامات کلی خود را برای قرداد ساختار پروژه داریم، وقت آن است که به جزئیات بپردازیم. بیایید با تصویر بزرگ شروع کنیم و سپس به جزئیات بپردازیم.
سازماندهی خود پروژه: تجزیه
اولین و مهمترین بخش سازماندهی یک پروژه بزرگ که با اصولی که در بالا تعریف کردیم همسو است، "تجزیه" است: به جای اینکه آن را به عنوان یک پروژه یکپارچه در نظر بگیریم، می توان آن را ترکیبی از ویژگی های کم و بیش مستقل در نظر گرفت.بحث قدیمی «مونولیت» در مقابل «میکروسرویس»، فقط در یک برنامه React. با این رویکرد، هر ویژگی اساساً به نوعی یک "نانوسرویس" است که از بقیه ویژگی ها جدا شده و از طریق یک "API" خارجی (معمولاً فقط React props) با آنها ارتباط برقرار می کند.
حتی فقط پیروی از این طرز فکر، در مقایسه با رویکرد سنتی تر «پروژه React»، تقریباً همه چیز را از لیست ما در بالا به شما می دهد: اگر تیم ها/افراد بتوانند به طور موازی روی ویژگی ها کار کنند اگر آنها را به عنوان یک برنچ از « جعبه های سیاه» پیاده کنند،به یکدیگر متصل شده اند. اگر تنظیم درست باشد، برای هر کسی نیز باید کاملاً واضح باشد، فقط برای تطبیق با تغییر ذهن نیاز به کمی تمرین دارد. اگر نیاز به حذف یک ویژگی دارید، فقط می توانید آن را «وصل کنید» یا آن را با ویژگی دیگری جایگزین کنید. یا اگر نیاز به بازسازی داخلی یک ویژگی دارید، می توانید آن را انجام دهید. و تا زمانی که "API" عمومی آن فعال بماند، هیچ کس خارج از آن حتی متوجه آن نمی شود.
من یک کامپوننت React را توصیف می کنم، اینطور نیست؟ خب، مفهوم یکسان است و این باعث میشود که React برای این طرز فکر عالی باشد. من یک "ویژگی" را تعریف می کنم تا آن را از "کامپوننت" متمایز کنم، به عنوان "مجموعه ای از کامپوننت و عناصر دیگر که در یک عملکرد کامل از دیدگاه کاربر نهایی به هم گره خورده اند".
حال، چگونه این را برای یک پروژه سازماندهی کنیم؟ مخصوصاً با توجه به اینکه در مقایسه با میکروسرویسها، باید pipeline بسیار کمتری داشته باشد: در پروژهای با صدها ویژگی، استخراج همه آنها در میکروسرویسهای واقعی تقریباً غیرممکن خواهد بود. کاری که میتوانیم بجای آن انجام دهیم، استفاده از معماری monorepo چند پکیج است: برای سازماندهی و جداسازی ویژگیهای مستقل به عنوان پکیج ها عالی است. پکیج مفهومی است که باید برای هر کسی که هر چیزی را از npm
نصب کرده است آشنا باشد. و monorepo - فقط یک مخزن است، که در آن کد منبع پکیج های متعددی دارید که با هم هماهنگ هستند، ابزارها، اسکریپت ها، وابستگی ها و گاهی اوقات یکدیگر را به اشتراک می گذارند.
بنابراین مفهوم ساده است: پروژه React => تقسیم آن به ویژگی های مستقل => قرار دادن آن ویژگی ها در پکیج ها.
اگر هرگز با مونورپو راهاندازی محلی کار نکردهاید و حالا، بعد از اینکه به «package» و «npm» اشاره کردم، از ایده انتشار پروژه خصوصیتان ناراحت هستید: نباشید. نه انتشار و نه منبع باز الزامی برای وجود monorepo و برای توسعه دهندگان برای استفاده از مزایای آن نیست. از منظر کد، یک پکیج فقط یک پوشه است که دارای فایل package.json
با برخی خصوصیات است. سپس آن پوشه از طریق Symlinks Node به پوشه node_modules
پیوند داده می شود، جایی که پکیج های "سنتی" نصب می شوند. این پیوند توسط خود ابزارهایی مانند Yarn یا Npm انجام می شود: «work space» نامیده می شود و هر دو از آن پشتیبانی می کنند. و آنها پکیج ها را در کد محلی شما مانند هر پکیج دیگری که از npm دانلود می شود قابل دسترسی می کنند.
به این صورت خواهد بود:
/packages
/my-feature
/some-folders-in-feature
index.ts
package.json // this is what defines the my-feature package
/another-feature
/some-folders-in-feature
index.ts
package.json // this is what defines the another-feature package
و در package.json
آن دو فیلد مهم را دارم:
{
"name": "@project/my-feature",
"main": "index.ts"
}
جایی که فیلد "name"، بدیهی است، نام پکیج است،اساساً name مستعار این پوشه، که از طریق آن به کد موجود در مخزن دسترسی خواهد داشت. و "main" نقطه ورود اصلی به پکیج است، یعنی زمانی که چیزی شبیه به آن را می نویسم کدام فایل وارد می شود.
import { Something } from '@project/my-feature';
چندین مخزن عمومی پروژه های معروف وجود دارد که از رویکرد monorepo چند پکیج استفاده می کنند: Babel، React، Jest برای نام بردن از چند مورد.
چرا پکیج ها به جای پوشه ها
در نگاه اول، رویکرد پکیج ها به نظر میرسد که «فقط ویژگیهای خود را به پوشهها تقسیم کنید، مسئله مهم چیست» و به نظر پیشگامانه نیست. با این حال، چند چیز جالب وجود دارد که پیکیج ها می توانند به ما بدهند، که پوشه های ساده نمی توانند.
نام مستعار. با پیکیج ها، می توانید به ویژگی خود با نام آن اشاره کنید، نه با مکان آن. این را مقایسه کنید:
import { Button } from '@project/button';
با این رویکرد "سنتی" تر:
import { Button } from '../../components/button';
در اولین import ، واضح است - من از یک کامپوننت "Button" عمومی پروژه خود استفاده می کنم، نسخه سیستم های طراحی من.
در مورد دوم، چندان واضح نیست - این دکمه چیست؟ آیا این دکمه عمومی «سیستمهای طراحی» است؟ یا شاید بخشی از این ویژگی است؟ یا یک ویژگی "up"؟ آیا حتی می توانم از آن در اینجا استفاده کنم، شاید برای موارد استفاده بسیار خاص نوشته شده است که در ویژگی جدید من کار نمی کند؟
ما این مورد را در یک مقاله جداگانه به صورت کامل بررسی کردیم.
اگر چندین پوشه "utils
" یا "common
" در مخزن خود داشته باشید، حتی بدتر می شود. بدترین کابوس کد من و اکثریت به این صورت است:
import { bla } from '../../../common';
import { blabla } from '../../common';
import { blablabla } from '../common';
با پیکیج ها می تواند چیزی شبیه به این باشد:
import { bla } from '@project/button/common';
import { blabla } from '@project/something/common';
import { blablabla } from '@project/my-feature/common';
فوراً آشکار می شود که چه چیزی از کجا می آید و چه چیزی به کجا تعلق دارد. و به احتمال زیاد، کد "مشترک" "my-feature" فقط برای استفاده داخلی این ویژگی نوشته شده است، هرگز قرار نبود خارج از ویژگی استفاده شود، و استفاده مجدد از آن در جای دیگر ایده بدی است. با پکیج ها، فوراً آن را خواهید دید.
تفکیک نگرانی ها با توجه به اینکه همه ما به پکیج ها از npm و آنچه نشان میدهند عادت کردهایم، زمانی که فوراً بهعنوان «پکیج» نوشته میشود، فکر کردن به ویژگی شما به عنوان یک ماژول مجزا با API عمومی خود بسیار آسانتر میشود.
یه نگاهی به این بنداز:
import { dateTimeConverter } from '../../../../button/something/common/date-time-converter';
در مقابل :
import { dateTimeConverter } from '@project/button';
اولین مورد احتمالاً در تمام import ها در اطراف خود گم می شود و بدون توجه می لغزد و کد شما را به The Big Ball of Mud تبدیل می کند. دومی فورا و به طور طبیعی چند ابرو را بالا می برد: مبدل تاریخ به زمان؟ از یک دکمه؟ واقعا؟ که طبیعتاً مرزهای واضح تری بین ویژگی ها/پکیج های مختلف ایجاد می کند.
پشتیبانی داخلی شما نیازی به اختراع چیزی ندارید، اکثر ابزارهای مدرن مانند IDE، تایپ اسکریپت، لینتینگ یا باندلرها از پکیج های خارج از جعبه پشتیبانی می کنند.
Refactoring یک نسیم است. با ویژگی های جدا شده در پکیج ها، بازسازی مجدد لذت بخش می شود. آیا می خواهید محتوای پکیج خود را تغییر دهید؟ ادامه دهید، می توانید آن را به طور کامل دوباره بنویسید، تا زمانی که API ورودی را یکسان نگه دارید، بقیه مخزن حتی متوجه آن نمی شوند. آیا می خواهید پکیج خود را به مکان دیگری منتقل کنید؟ اگر یک پوشه را تغییر ندهید، فقط آن را بکشید و رها کنید، بقیه مخزن تحت تأثیر قرار نمیگیرد. آیا می خواهید نام پکیج را تغییر دهید؟ فقط یک رشته را در پروژه جستجو و جایگزین کنید، نه بیشتر.
نقاط ورود صریح اگر میخواهید واقعاً طرز فکر «تنها API عمومی برای مصرفکنندگان» را بپذیرید، میتوانید در مورد آنچه دقیقاً از یک پکیج در دسترس مصرفکنندگان خارجی است، کاملاً مشخص باشید. برای مثال، میتوانید تمام import «عمیق (deep)» را محدود کنید، چیزهایی مانند project/button/some/deep/path@
را غیرممکن کنید و همه را مجبور کنید که فقط از API عمومی تعریفشده در فایل index.ts
استفاده کنند. برای نمونههایی از نحوه عملکرد آن، به نقاط ورود پکیج و اسناد export پکیج نگاهی بیندازید.
نحوه تقسیم کد به پکیج ها
بزرگترین چیزی که مردم در معماری چند پکیج با آن دست و پنجه نرم می کنند، این است که چه زمانی برای استخراج کد در یک پکیج مناسب است؟ آیا هر ویژگی کوچک باید یکی باشد؟ یا شاید پکیج ها فقط برای چیزهای بزرگ مانند یک صفحه کامل یا حتی یک برنامه هستند؟
در تجربه من، در اینجا تعادل وجود دارد. شما نمی خواهید هر چیز کوچکی را در یک پکیج استخراج کنید: در نهایت فقط با یک لیست مسطح از صدها پکیج کوچک تنها یک فایلی بدون ساختار مواجه خواهید شد که در ابتدا هدف از معرفی آنها را نادیده می گیرد. در عین حال، شما نمیخواهید پکیج شما خیلی بزرگ شود: تمام مشکلاتی را که ما در اینجا میخواهیم حل کنیم، فقط در داخل آن پکیج، برطرف خواهید کرد.
در اینجا برخی از مرزها وجود دارد که من معمولاً از آنها استفاده می کنم:
نوع "سیستم طراحی" مواردی مانند دکمه ها، دیالوگ های ماژول،layout و غیره، همه باید پکیج باشند.
ویژگیها در برخی از مرزهای رابط کاربری «طبیعی» کاندیدهای خوبی برای یک پکیج هستند - مثلاً چیزی که در یک modal dialog در یک کشو، در یک پانل کشویی و غیره وجود دارند.
ویژگیهای «اشتراکپذیر» - آنهایی که میتوانند در مکانهای مختلف استفاده شوند.چیزی که می توانید آن را به عنوان یک "ویژگی" جدا شده با مرزهای واضح، منطقی و به طور ایده آل در رابط کاربری قابل مشاهده توصیف کنید.
نحوه تقسیم کد به کامپوننت ها، بسیار مهم است که یک پکیج تنها مسئول یک چیز مفهومی باشد. پکیج ای که یک Button
، CreateIssueDialog
و DateTimeConverter
را export می کند، خیلی کارها را همزمان انجام می دهد و باید تقسیم شود.
نحوه سازماندهی پکیج ها
اگرچه می توان فقط یک لیست مسطح از همه پکیج ها ایجاد کرد، و برای انواع خاصی از پروژه ها کارساز خواهد بود، برای محصولات بزرگ با رابط کاربری سنگین احتمالاً کافی نخواهد بود. دیدن چیزی مانند «tooltip» و «صفحه تنظیمات» پکیجهایی که کنار هم قرار گرفتهاند من را به هم میریزد. یا بدتر - اگر پکیجهای "backend" و "frontend" را با هم دارید. این نه تنها کثیف است، بلکه خطرناک است: آخرین چیزی که می خواهید این است که به طور تصادفی مقداری کد "بک اند" را در پکیج نرم افزاری خود وارد کنید.
ساختار واقعی مخزن تا حد زیادی به این بستگی دارد که دقیقاً محصولی که پیادهسازی میکنید چیست (یا حتی تعداد محصولات موجود است)، آیا شما فقط Backend یا Frontend دارید، و احتمالاً در طول زمان به طور قابل توجهی تغییر خواهد کرد و تکامل خواهد یافت. خوشبختانه، این مزیت بزرگ پکیجها است: ساختار واقعی کاملاً مستقل از کد است، میتوانید هفتهای یک بار بدون هیچ عواقبی آنها را بکشید و رها کنید و دوباره ساختار دهید.
با توجه به اینکه هزینه "اشتباه" در ساختار بسیار کم است، حداقل در ابتدا نیازی به فکر کردن به آن نیست. اگر پروژه شما فقط frontend است، حتی می توانید با یک لیست ثابت شروع کنید:
/packages
/button
...
/footer
/settings
...
و در طول زمان آن را به چیزی شبیه به این تبدیل کنید:
/packages
/core
/button
/modal
/tooltip
...
/product-one
/footer
/settings
...
/product-two
...
یا، اگر یک Backend دارید، می تواند چیزی شبیه به این باشد:
/packages
/frontend
... // the same as above
/backend
... // some backend-specific packages
/common
... // some packages that are shared between frontend and backend
جایی که در "common
" کدی را قرار می دهید که بین فرانت و بک اند به اشتراک گذاشته می شود. معمولاً برخی از تنظیمات، ثابتها، ابزارهای لوداشمانند، انواع common
خواهد بود.
چگونه یک پکیج را ساختاربندی کنیم
برای خلاصه کردن بخش بزرگ بالا: "از monorepo استفاده کنید، ویژگی ها را در پکیج ها استخراج کنید".اکنون به قسمت بعدی می رویم، نحوه سازماندهی خود پکیج. سه چیز در اینجا برای من مهم است: نامگذاری قرارداد، تفکیک پکیج به لایه های مجزا، و سلسله مراتب دقیق.
قرارداد نامگذاری
همه دوست دارند چیزها را نام ببرند و در مورد اینکه دیگران چقدر در نامگذاری چیزها بد هستند بحث می کنند، اینطور نیست؟ برای کاهش زمان تلف شده در موضوعات بیپایان نظرات GitHub و آرام کردن افراد مبتلا به OCD مرتبط با کد، بهتر است فقط یک بار برای همه روی یک قرارداد نامگذاری توافق کنید.
استفاده از کدام یک به نظر من واقعاً مهم نیست، تا زمانی که در طول پروژه به طور مداوم دنبال شود. اگر ReactFeatureHere.ts
و react-feature-here.ts
را در یک مخزن دارید، یک بچه گربه در جایی گریه می کند.
من معمولا از این استفاده میکنم:
/my-feature-name
/assets // if I have some images, then they go into their own folder
logo.svg
index.tsx // main feature code
test.tsx // tests for the feature if needed
stories.tsx // stories for storybooks if I use them
styles.(tsx|scss) // I like to separate styles from component's logic
types.ts // if types are shared between different files within the feature
utils.ts // very simple utils that are used *only* in this feature
hooks.tsx // small hooks that I use *only* in this feature
اگر یک ویژگی دارای چند کامپوننت کوچکتر باشد که مستقیماً به index.tsx
وارد می شوند، به شکل زیر هستند:
/my-feature-name
... // the same as before
header.tsx
header.test.tsx
header.styles.tsx
... // etc
یا به احتمال زیاد، فوراً آنها را در پوشه ها استخراج می کنم و به این شکل می شوند:
/my-feature-name
... // index the same as before
/header
index.tsx
... // etc, exactly the same naming here
/footer
index.tsx
... // etc, exactly the same naming here
رویکرد قدیمیتر برای توسعه مبتنی بر کپی پیست بسیار بهینهتر است: هنگام ایجاد یک ویژگی جدید با ساختار کپی پیست از ویژگی نزدیک، تنها کاری که باید انجام دهید این است که نام تنها یک پوشه را تغییر دهید. نام همه فایل ها دقیقاً یکسان خواهد بود. بعلاوه، ایجاد یک مدل ذهنی از پکیج، تغییر شکل دادن و جابجایی کد (در بخش بعدی) آسانتر است.
لایه ها در یک پکیج
یک پکیج معمولی با یک ویژگی پیچیده دارای چند "layout" متمایز است: حداقل لایه "UI" و لایه "داده". در حالی که احتمالاً می توان همه چیز را با هم ترکیب کرد، من همچنان به آن توصیه می کنم: رندر کردن دکمه ها و واکشی داده ها از بک اند نگرانی های بسیار متفاوتی هستند. جداسازی آنها به پکیج ساختار و قابلیت پیش بینی بیشتری می دهد.
و برای اینکه پروژه از نظر معماری و کد نسبتاً سالم بماند، نکته مهم این است که بتوانید لایههایی را که برای برنامه شما مهم هستند شناسایی کنید، رابطه بین آنها را ترسیم کنید و همه اینها را به روشی سازماندهی کنید. که با هر ابزار و چارچوبی که استفاده می شود، همسو می شود.
اگر امروز یک پروژه React را از ابتدا اجرا می کردم، با Graphql برای دستکاری داده ها و وضعیت React خالص برای مدیریت state (یعنی بدون Redux یا هر کتابخانه دیگری)، لایه های زیر را داشتم:
لایه "data" - کوئری ها، mutation و چیزهای دیگری که مسئول اتصال به منابع داده خارجی و تبدیل آن هستند. فقط توسط لایه UI استفاده می شود، به هیچ لایه دیگری بستگی ندارد.
لایه "shared" - ابزارهای مختلف، توابع، قلابها، اجزای کوچک، انواع و ثابتها که در کل پکیج توسط تمام لایههای دیگر استفاده میشوند. به هیچ لایه دیگری بستگی ندارد.
لایه "UI" - اجرای واقعی ویژگی. به لایههای «data» و «shared» بستگی دارد، هیچکس به آن وابسته نیست/
خودشه!
اگر از یک کتابخانه مدیریت خارجی استفاده می کردم، احتمالاً لایه "state" را نیز اضافه می کردم. این یکی احتمالاً پلی بین "data" و "ui" خواهد بود و بنابراین از لایه های "shared" و "data" و "UI" از "state" به جای "data" استفاده می کند.
و از نقطه نظر جزئیات پیاده سازی، همه لایه ها پوشه های سطح بالایی در یک پکیج هستند:
/my-feature-package
/shared
/ui
/data
index.ts
package.json
با استفاده از هر لایه از همان قرارداد نامگذاری که در بالا توضیح داده شد. بنابراین لایه "data" شما چیزی شبیه به این خواهد بود:
/data
index.ts
get-some-data.ts
get-some-data.test.ts
update-some-data.ts
update-some-data.test.ts
برای پکیجهای پیچیدهتر، ممکن است آن لایهها را از هم جدا کنم، در حالی که هدف و ویژگیهای آنها را حفظ کنم. لایه "data" می تواند به عنوان مثال به "کوئری ها" ("دریافت کننده ها") و "جهش" ("تنظیم کننده ها") تقسیم شود، و آنها می توانند در پوشه "data" زندگی کنند یا به سمت بالا حرکت کنند:
/my-feature-package
/shared
/ui
/queries
/mutations
index.ts
package.json
یا میتوانید چند لایه فرعی از لایه «shared» استخراج کنید، مانند «types» و «shared-ui
» (که این لایه فرعی را فوراً به «UI» نوع btw تبدیل میکند، زیرا چیزی به جز «UI» وجود ندارد. می تواند از اجزای UI استفاده کند).
/my-feature-package
/shared-ui
/ui
/queries
/mutations
/types
index.ts
package.json
تا زمانی که میتوانید به وضوح هدف هر «زیر لایه» را تعریف کنید، مشخص کنید که کدام «زیر لایه» متعلق به کدام «لایه» است و میتوانید آن را برای همه اعضای تیم تجسم و توضیح دهید - همه چیز کار میکند!
سلسله مراتب دقیق در لایه ها
قطعه نهایی پازل که این معماری را قابل پیش بینی و نگهداری می کند، یک سلسله مراتب سخت در لایه ها است. این به ویژه در لایه UI قابل مشاهده است زیرا در برنامه های React معمولاً پیچیده ترین لایه است.
بیایید، برای مثال، اسکلت بندی کردن یک صفحه ساده را با یک هدر و یک footer شروع کنیم. ما فایل "index.ts
" را خواهیم داشت - فایل اصلی، جایی که صفحه در کنار هم قرار می گیرد، و اجزای "header.ts
" و "footer.ts
".
/my-page
index.ts
header.ts
footer.ts
اکنون، همه آنها کامپوننت های خود را خواهند داشت که من می خواهم آنها را در پروژه های خود قرار دهم. برای مثال، «header» دارای اجزای «search-bar
» و «send-feedback
» خواهد بود. به روش مسطح "سنتی" برای سازماندهی برنامه ها، آنها را در کنار یکدیگر قرار می دهیم، اینطور نیست؟ چیزی شبیه این خواهد بود:
/my-page
index.ts
header.ts
footer.ts
search-bar.ts
send-feedback.ts
و سپس، اگر بخواهم همان دکمه «send-feedback» را به کامپوننت footer اضافه کنم، دوباره آن را از «send-feedback.ts
» به «footer.ts
» وارد می کنم، درست است؟ پس از همه، این نزدیکی است و طبیعی به نظر می رسد.
متأسفانه، اتفاقی که اخیرا افتاد، این است که ما بدون توجه به آن، مرزهای بین لایههای خود ("UI" و "share") را نقض کردیم. اگر بخواهم اجزای بیشتر و بیشتری را به این ساختار مسطح اضافه کنم، و احتمالاً این کار را خواهم کرد، برنامه های واقعی بسیار پیچیده هستند، احتمالاً چند برابر بیشتر آنها را نقض خواهم کرد. این کار این پوشه را به "Ball Of Mud" کوچک خودش تبدیل می کند، جایی که کاملاً غیرقابل پیش بینی است که کدام کامپوننت به کدام یک بستگی دارد. و در نتیجه، گرهگشایی همه اینها و استخراج چیزی از این پوشه، زمانی که زمان بازسازی فرا میرسد، ممکن است به یک تمرین بسیار سخت تبدیل شود.
در عوض، می توانیم این لایه را به صورت سلسله مراتبی ساختار دهیم. قوانین عبارتند از:
فقط فایلهای اصلی (یعنی «index.ts
») در یک پوشه میتوانند کامپوننت های فرعی (ماژولهای فرعی) داشته باشند و میتوانند آنها را وارد کنند.
شما می توانید فقط از "فرزندان" import کنید، نه از "همسایگان"
شما نمی توانید یک سطح را رد کنید و فقط می توانید از کودکان مستقیم وارد کنید
یا اگر بصری را ترجیح می دهید، فقط یک درخت است:
و اگر بخواهید مقداری کد را بین سطوح مختلف این سلسله مراتب به اشتراک بگذارید (مانند کامپوننت ارسال بازخورد )، فوراً خواهید دید که قوانین سلسله مراتب را نقض می کنید، زیرا هر کجا آن را قرار دهید، باید وارد کنید. یا از طرف والدین یا از طرف همسایگان. بنابراین، در عوض، در لایه "shared" استخراج و از آنجا وارد می شود.
به این شکل خواهد بود:
/my-page
/shared
send-feedback.ts
/ui
index.ts
/header
index.ts
search-bar.ts
/footer
index.ts
به این ترتیب لایه UI (یا هر لایه ای که در آن قانون اعمال می شود) فقط به یک ساختار درختی تبدیل می شود که در آن هر شاخه مستقل از هر شاخه دیگری است. استخراج هر چیزی از این پکیج اکنون بسیار آسان است: تنها کاری که باید انجام دهید این است که یک پوشه را به یک مکان جدید بکشید و رها کنید. و مطمئناً میدانید که هیچ یک از کامپوننت های درخت رابط کاربری تحت تأثیر آن قرار نمیگیرد، مگر کامپوننت که واقعاً از آن استفاده میکند. تنها چیزی که ممکن است لازم باشد به علاوه لایه "shared" باشد.\
سپس برنامه کامل با لایه داده به شکل زیر خواهد بود:
چند لایه به وضوح تعریف شده، که کاملا محصور شده و قابل پیش بینی هستند.
/my-page
/shared
send-feedback.ts
/data
get-something.ts
send-something.ts
/ui
index.ts
/header
index.ts
search-bar.ts
/footer
index.ts
React در برابر لانه سازی توصیه می کند
اگر React Docs را در ساختار پروژه پیشنهادی بخوانید، خواهید دید که React در واقع در برابر بیش از حد تودرتو توصیه می کند. توصیه رسمی این است که «خود را به حداکثر سه یا چهار پوشه تو در تو در یک پروژه محدود کنید». و این توصیه برای این رویکرد نیز بسیار مرتبط است: اگر پکیج شما بیش از حد تودرتو شود، نشانه واضحی است که ممکن است لازم باشد در مورد تقسیم آن به پکیجهای کوچکتر فکر کنید. 3-4 سطح تودرتو، در تجربه من، حتی برای ویژگی های بسیار پیچیده کافی است.
با این حال، زیبایی معماری پکیجها در این است که میتوانید پکیجهای خود را با هر مقداری که نیاز دارید بدون محدود کردن این محدودیت سازماندهی کنید - هرگز به پکیج دیگری از طریق مسیر نسبی آن اشاره نمیکنید، فقط با نام آن. پکیجای با نام project/change-setting-dialog@
که در مسیر پکیجها change-settings-dialog/
زندگی میکند یا در /packages/product/features/settings-page/change-setting-dialog
پنهان است، به آن ارجاع داده میشود. به عنوان project/change-setting-dialog@ صرف نظر از موقعیت فیزیکی آن.
ابزار مدیریت Monorepo
صحبت در مورد monorepo چند پکیج برای معماری شما غیرممکن است بدون اینکه حداقل کمی به ابزارهای مدیریت monorepo دست بزنید. بزرگترین مشکل معمولاً مدیریت وابستگی درون آن است. تصور کنید، اگر برخی از پکیجهای monorepo شما از یک وابستگی خارجی استفاده میکنند، برای مثال lodash.
/my-feature-one
package.json // this one uses lodash@3.4.5
/my-other-feature
package.json // this one uses lodash@3.4.5
اکنون lodash یک نسخه جدید به نام lodash@4.0.0 منتشر می کند و می خواهید پروژه خود را به آن منتقل کنید. شما باید آن را در همه جا به طور همزمان به روز کنید: آخرین چیزی که می خواهید این است که برخی از پکیج ها در نسخه قدیمی باقی بمانند، در حالی که برخی از پکیج های جدید استفاده می کنند. اگر از npm یا نخ های قدیمی استفاده می کنید، این یک فاجعه است: آنها چندین نسخه (نه دو، چندتا) از lodash را در سیستم شما نصب می کنند که منجر به افزایش زمان نصب و ساخت می شود و اندازه پکیج های شما از بین می رود. سقف بدون اشاره به لذت توسعه یک ویژگی جدید زمانی که از دو نسخه متفاوت از یک کتابخانه در سراسر پروژه استفاده می کنید.
اگر قرار است پروژه شما در npm و منبع باز منتشر شود، من قصد ندارم از چه چیزی استفاده کنید: احتمالاً چیزی مانند لرنا کافی است، اما این موضوع کاملاً متفاوت است.
با این حال، اگر مخزن شما خصوصی باشد، همه چیز جالب تر می شود. زیرا تنها چیزی که شما واقعاً برای کارکرد این معماری به آن نیاز دارید، پکیجهای «همنام» است، نه چیزی بیشتر. یعنی فقط یک پیوند اولیه که هم Yarn و هم Npm از طریق ایده فضاهای کاری ارائه می کنند. به نظر می رسد این است. شما فایل "root" package.json را دارید که در آن فضاهای کاری (یعنی پکیج های محلی خود) را اعلام می کنید:
{
"private": true,
"workspaces": ["packages/**"]
}
و سپس دفعه بعد که yarn install را اجرا میکنید، تمام پکیجهای پکیجهای پوشه به پکیجهای «مناسب» تبدیل میشوند و از طریق نامشان در پروژه شما در دسترس خواهند بود. این کل تنظیمات monorepo است!
در مورد وابستگی ها. اگر همین وابستگی را در چند پکیج داشته باشید چه اتفاقی می افتد؟
/packages
/my-feature-one
package.json // this one uses lodash@3.4.5
/my-other-feature
package.json // this one uses lodash@3.4.5
هنگامی که yarn install
را اجرا می کنید، آن پکیج را به روت node_modules
"proper" می کند:
/node_modules
lodash@3.4.5
/packages
/my-feature-one
package.json // this one uses lodash@3.4.5
/my-other-feature
package.json // this one uses lodash@3.4.5
این دقیقاً همان وضعیتی است که اگر فقط lodash@3.4.5 را فقط در root package.json اعلام کنید. چیزی که من میگویم این است، و احتمالاً من به خاطر آن، از جمله خودم دو سال پیش، توسط نابداران اینترنت زنده به گور خواهم شد: نیازی نیست که هیچ یک از وابستگیها را در پکیجهای محلی خود اعلام کنید. همه چیز فقط می تواند به root package.json برود. و فایل های package.json شما در پکیج های محلی فقط فایل های json بسیار سبک خواهند بود که فقط فیلدهای "name" و "main" را مشخص می کنند.
تنظیم بسیار ساده تر برای مدیریت، به خصوص اگر تازه شروع کرده اید.
ساختار پروژه React برای مقیاس: نمای کلی نهایی
اوه، این متن زیادی بود. و حتی این فقط یک مرور کوتاه است: خیلی چیزهای دیگر را می توان در مورد موضوع گفت! بیایید حداقل آنچه قبلاً گفته شد را مرور کنیم:
تجزیه کلید مقیاس موفقیت آمیز برنامه react شما است. پروژه خود را نه به عنوان یک "پروژه" یکپارچه، بلکه به عنوان ترکیبی از جعبه سیاه مستقل مانند "ویژگی ها" با API عمومی خود برای مصرف کنندگان در نظر بگیرید. در واقع همان بحث "یکپارچه" در مقابل "خدمات ریز" است.
معماری Monorepo برای آن عالی است. ویژگی های خود را در پکیج ها استخراج کنید. پکیج های خود را به گونه ای سازماندهی کنید که بهترین کار را برای پروژه شما دارد.
لایههای درون یک پکیج برای اینکه ساختاری به آن ببخشند مهم هستند. احتمالاً حداقل لایه «داده»، لایه «UI» و لایه «اشتراکگذاری شده» خواهید داشت. پکیج به نیاز شما می تواند بیشتر معرفی کند، فقط باید مرزهای مشخصی بین آنها وجود داشته باشد.
ساختار سلسله مراتبی یک پکیج جالب است. بازسازي را آسانتر ميكند، شما را مجبور ميكند كه مرزهاي واضحتري بين لايهها داشته باشيد، و شما را مجبور ميكند كه در صورت بزرگتر شدن پکیجتان، آنها را به لايههاي كوچكتر تقسيم كنيد.
مدیریت وابستگی در monorepo یک موضوع پیچیده است، اما اگر پروژه شما خصوصی است، نیازی به نگرانی در مورد آن ندارید. فقط تمام وابستگی های خود را در root package.json اعلام کنید و همه پکیج های محلی را از آنها دور نگه دارید.
نتیجه
این همه برای امروز است. امیدواریم بتوانید برخی از این اصول (یا حتی همه آنها!) را در برنامه های خود اعمال کنید و فوراً شاهد پیشرفت هایی در توسعه روزانه خود باشید!