چگونه سیستم تب مدولار را با نمای جانبی ساختیم

هنگام راهاندازی Tabs، ما یک وبلاگ درباره تصمیمات طراحی پشت برگهها نوشتهایم. این بار به جنبه فنی تب ها در acreom می پردازیم.
نمایش مطالب در برگه ها
ساختار برگه ها که در زیر نشان داده شده است، به دلیل سادگی آن انتخاب شده است. همیشه فقط از یک کانتینر تشکیل شده است که گروه های Tab را کنترل می کند، یک یا چند گروه Tab، که هر کدام شامل یک یا چند Tab است. برگه ها حاوی محتوای واقعی هستند که می توانید با آن تعامل داشته باشید.
ظرف
Container یک عملکرد ساده دارد – همه گروههای Tab موجود را رندر کنید. ظاهر ما از Vue.js استفاده می کند، بنابراین بخش اصلی کل کامپوننت اساساً به این صورت است:
<TabGroup
v-for="(groupId, index) in tabGroupsIds"
:id="groupId"
:ref="`tabGroup-${groupId}`"
:key="groupId"
class="group"
:style="getTabStyle(index)"
/>
توجه داشته باشید که ما داده های گروه Tab را مستقیماً ارسال نمی کنیم، بلکه فقط شناسه گروه Tab را ارسال می کنیم. این یکی از بهینهسازیهای واکنشپذیری است که ما برای جلوگیری از رندر شدن مجدد برنامه اجرا کردهایم – بهینهسازیها را بعداً در این وبلاگ توضیح میدهیم.
گروه برگه
گروه Tab، مشابه عنصر Container، عملکردی برای ارائه محتوای Tab دارد. عملکردهای دیگری نیز دارد، مانند نمایش همپوشانی هنگام کشیدن یک برگه روی آن، اما ما به جزئیات آن ها نمی پردازیم. کد گروه Tab مربوطه به شکل زیر است:
<component
:is="tabComponent"
v-if="activeTabId"
:id="activeTabId"
:key="activeTabId"
:entity-id="activeTabEntityId"
:group-id="group.id"
:width="groupWidth"
/>
ما از حلکننده کامپوننت Tab استفاده میکنیم تا همیشه مولفه برگه مربوطه نمایش داده شود:
get tabComponent() {
return this.$componentsRepository.getTab(this.activeTabType)
}
گروه برگه ممکن است شامل 1..n برگه باشد، اما همیشه فقط یک برگه – برگه فعال را ارائه می دهد.
Tab
برگه حاوی محتوای واقعی است. تب های مختلفی در acreom وجود دارد – برای مثال تب ویرایشگر، برگه وظایف، برگه تقویم.
Tab به طور مستقیم با داده های ذخیره شده مربوط به برگه خاص کار می کند. به این ترتیب، واکنشپذیری برگه تضمین میشود، در حالی که از رندر مجدد غیرضروری سایر بخشهای برنامه جلوگیری میشود.
گردش داده ها
با نمایش توضیح داده شده، می توانیم به بخش جالب تر – لایه داده – برویم. ما تمام داده های مورد استفاده برای رندر کردن برگه ها را در فروشگاه vuex خود ذخیره می کنیم.
هنگامی که برای اولین بار شروع به نمونه سازی برگه ها کردیم، یک ساختار ساده ایجاد کردیم. این شامل آرایه گروهی حاوی اشیاء گروهی بود که هر کدام دارای ترتیب، عرض بر حسب پیکسل، activeTabId و آرایه برگهها بودند.
ما اشیاء برگهها را مستقیماً روی شی گروه ذخیره میکردیم و هر شیء برگه حاوی شناسه موجودیتی است که باید نمایش دهد، ترتیب در گروه خود و دادهها – فراداده خاص برای یک برگه معین.
ساختار گروه ها به این صورت بود:
groups: [
{
id: <groupId1>
order: 0,
width: 400,
activeTabId: <tabId2>,
tabs: [
{
id: <tabId1>,
entityId: <uuidv4>,
order: 0,
data: { ...tabSpecificData },
}, {
id: <tabId2>,
entityId: <uuidv4>,
order: 1,
data: { ...tabSpecificData }.
},
]
},
{
id: <groupId2>,
…
],
activeGroupId: <groupId1>,
activeTabId: <tabId2>
این راه حل به اندازه کافی برای نمونه سازی خوب عمل کرد، اما ذاتاً از دو جهت دارای نقص بود:
- ساختار مسطح نبود و در نتیجه هنگام به روز رسانی هر داده پیچیدگی بالاتری داشت
- باعث ایجاد رندرهای مجدد از همه گروهها و برگهها میشود، زیرا هر تغییری در عناصر آرایه گروهها قابل مشاهده در کل آرایه است.
هرچه برگه ها یا گروه های بیشتری داشته باشید، هزینه رندر مجدد بیشتر می شود. برنامه دچار تاخیر می شد و کل تجربه (توسعه) دردناک بود.
به حداقل رساندن رندرهای مجدد
راه حل واضح این بود که ساختار را مسطح کنیم و شاخص هایی ایجاد کنیم که ساختار را به روشی غیر مرتبط تر توصیف کنند. ما همه تب ها را به یک شی جداگانه منتقل کردیم، جایی که هر تب با شناسه خود ایندکس می شود. همچنین آرایه گروه ها را به شی گروهی تغییر دادیم، هر گروه را با شناسه آن نمایه کردیم و آرایه تب ها را با هم از شیء حذف کردیم.
انشعاب نهادها بلافاصله دستاوردهای عملکرد قابل توجهی را به همراه داشت. تغییر داده ها در گروه Tab یا Tab دیگر در آرایه قابل مشاهده نیست، بلکه فقط روی هر یک از تب ها یا شی گروه ها فعال می شود. سپس یک فهرست TabsInGroup اضافه کردیم که برگه ها را به گروهی که به آن تعلق دارند متصل می کرد. بر اساس این شاخص، برگه ها برای هر گروه ارائه می شوند.
تنظیم عرض صحیح
عرض تب در پیکسل های ذخیره شده نیز به یک مشکل تبدیل شد. در تغییر اندازه هر برنامه، عرض همه گروهها باید مجدداً محاسبه و در فروشگاه بهروزرسانی میشد، که باعث رندر مجدد برای هر گروه میشد، و در نتیجه کل محتوا دوباره ارائه میشد.
عرض ذخیره شده را از پیکسل به درصدی از عرض محتوایی که هر گروه باید بگیرد تغییر دادیم. به جای اینکه js برای مدیریت پهنای گروه ها پاسخگو باشد، به css اجازه می دهیم کارهای سنگین را انجام دهد.
groups: {
<groupId1>: {
id: <groupId1>,
width: 34.615,
order: 0,
activeTab: <tab2>,
},
<groupId2>: {
id: <groupId2>,
width: 65.385,
order: 0,
activeTab: <tab3>,
},
},
tabs: {
<tabId1>: {
id: <tabId1>,
order: 0,
entityId: <uuidv4>,
data: { ...tabSpecificData },
},
<tabId2>: {
id: <tabId2>,
order: 500,
entityId: <uuidv4>,
data: { ...tabSpecificData },
},
},
listTabs: [<tabId1>, <tabId2>, <tabId3>],
listGroups: [<groupId1>, <groupId2>],
tabsInGroup: {
<groupId1>: [<tabId1>, <tabId2>],
<groupId2>: [<tabId3>],
},
activeGroupId: <groupId1>,
activeTabId: <tabId2>
نتیجه
دسترسی به داده ها با فرآیند زیر ساده شده است:
- کامپوننت Container همه گروه های موجود را با استفاده از فهرست لیست گروپ ها بازیابی می کند
- جزء TabGroup برای هر گروه ایجاد می شود
- TabGroup همه برگههایی را که باید از فهرست TabsInGroup و شناسه خود پیگیری کند، بازیابی میکند.
این کار کل رندر را ساده میکند، زیرا مؤلفهها تنها زمانی دوباره ارائه میشوند که قابل مشاهده مستقیم آنها فعال شود. تنها کاری که باید انجام دهیم این است که برگهها و گروهها را بهگونهای بهروزرسانی کنیم که قابل مشاهدهها را روی دادههایی که مستقیماً به آنها تعلق دارند، فعال کنند.
به روز رسانی برگه ها
هر بهروزرسانی که روی یک برگه اتفاق میافتد، فقط شیء برگه مربوطه را بهروزرسانی میکند، و قابل مشاهده برای شی و شاخص آن را فعال میکند. در مورد ما، برگهها، نمایهسازی با tabId، و فهرستبندی TabsInGroup توسط groupId که برگه به آن تعلق دارد، خواهد بود.
مراجع گروهها در تماس تغییر نمیکنند، بنابراین Container و TabGroupهایی که شامل برگه اصلاحشده نیستند، دوباره ارائه نمیشوند. TabGroup حاوی برگهها دوباره رندر میشود، اما از آنجایی که برگهای که اصلاح میشود به احتمال زیاد فعال است، مشکل بزرگی نیست زیرا به هر حال دوباره رندر میشود.
حفظ نظم
تنها مشکلی که باقی مانده بود سفارش بود. در هر مرتبسازی مجدد برگهها، حتی هنگام مرتبسازی مجدد برگههای غیرفعال، کل گروه و برگه فعال دوباره رندر میشوند – ناشی از تغییر ترتیب در همه برگههای یک گروه.
رویکرد تنظیم ترتیب به فهرست برگه در آرایه tabGroup به خوبی کار نمی کرد. ما در مورد راه حل متفاوتی تصمیم گرفتیم، جایی که ترتیب ابتدا به یک عدد دلخواه تنظیم می شود (ما افزایش های 500 را انتخاب کردیم، اما واقعاً مهم نیست). هنگام مرتب سازی مجدد برگه ها، میانگین سفارشات برگه قبلی و بعدی به عنوان سفارش جدید انتخاب می شود. هنگام مرتب کردن مجدد برگه در ابتدا یا در انتها، 500 از اولین (آخرین) ترتیب آیتم کم می شود (یا اضافه می شود).
استفاده از این رویکرد به ما اجازه داد تا به جای همه آنها فقط ترتیب برگه های تک را به روز کنیم. آنچه این رویکرد را مجاز می کند دو فرض است:
- شما نمی توانید همان برگه ها را صدها بار مرتب کنید
- شما بارها و بارها برگه های مختلف را دقیقاً به همان ترتیب قرار نمی دهید.
باز کردن یک برگه
هنگام باز کردن یک برگه، رویدادهای زیر رخ می دهد (در نمودار نشان داده شود):
- برگه به فروشگاه برگه ها اضافه می شود
- شناسه برگه فعال در فروشگاه به روز می شود
- رندر برگه فعال فعال می شود
- هنگام رندر:
- برگه قدیمی باز می شود و میانبرها و شنوندگان ثبت شده آن حذف می شود
- برگه جدید داده های برگه خاص را بارگیری می کند
- برگه جدید نصب می شود و میانبرها و شنوندگان خاص خود را ثبت می کند
مدیریت برگه ها
وقتی جدا می شوید چه اتفاقی می افتد؟ یک گروه جدید ایجاد میشود، ترتیب آن تنظیم میشود تا بین گروهی که تقسیم میکنید و گروه بعدی قرار بگیرد، و سپس برگه در فهرست TabsGroup به گروه جدید منتقل میشود.
همین امر در مورد جابجایی برگه ها بین گروه ها نیز صدق می کند، تنها کاری که باید انجام دهیم این است که tabId در TabsGroup را از یک گروه به گروه دیگر منتقل کنیم. در صورتی که آخرین برگه باشد، گروهی که متعلق به آن بود را پس از انتقال به سادگی حذف می کنیم.
واکنش پذیری و رندر مجدد
این گیف نشان میدهد که وقتی تب فعال تغییر میکند، رابط کاربری چگونه دوباره رندر میشود. هر سه گروه برگه چشمک می زنند و رندر مجدد را نشان می دهند اما فقط محتویات برگه مربوطه را دوباره رندر می دهند.
امکانات بی پایان
افزودن برگههای جدید اکنون ساده است – تنها چیزی که باید اضافه کنید این است که جزء برگهای را که در داخل صفحه نمایش Tab نمایش داده میشود، اضافه کنید و آن را ثبت کنید تا حلکننده بتواند نوع برگه جدید را انتخاب کند.
وقتی ادغام Jira را معرفی کردیم، تنها کاری که باید انجام میدادیم این بود که مولفه JiraAppTab.vue حاوی منطق را برای رندر کردن مسائل Jira و فایل index.ts با منطق ثبتکننده حلکننده اضافه کنیم:
registerEntityComponents(JiraIntegrationDataType.ISSUE, {
tab: (_context: Context, _tabId: string) => () =>
import('~/components/entities/jira/JiraAppTab.vue'),
}
این امکان گسترش تقریباً بدون دردسر را فراهم می کند. هنگامی که ما (یا هر شخص دیگری) میخواهیم یک نوع برگه جدید اضافه کنیم، آنها این فایلها را (به همراه هر فایل دیگری که ممکن است برای عملکرد صحیح مؤلفه .vue لازم باشد) و voila ارائه میکنند! – یک برنامه جدید متولد شده است.
اکنون ممکن است واضح باشد که ما زمینههای محکمی را برای توسعهپذیری آینده ایجاد کردهایم – هم توسط ما و هم توسط جامعه در قالب برنامههای انجمن (افزونهها)، که یکی از ویژگیهای درخواستی است.
این وبلاگ بخشی از هفته برنامه نویسی acreom است. حتما ببینید و دنبال کنید. ما را نیز بررسی کنید توییتر، یا به انجمن Discord ما بپیوندید تا در جریان باشید.