تخطّي إلى المحتوى الرئيسي

دليل الترحيل

تتناول هذه الصفحة أمرين: مخطّط المفاهيم العام للانتقال من Vue (lh-ui-plus / lh-ui-vue2) إلى React، والفروق المتعمَّدة بين تنفيذ هذه المكتبة ونُسخ Vue.

جدول مخطّط المفاهيم​

Vue (lh-ui-plus)React (@lh-ui/react)
defineProps<T>()interface LhXxxProps + معاملات افتراضية مفكوكة
withDefaults(...)قيم افتراضية مفكوكة (دون defaultProps)
emits + emit('change', [val, e])onChange?: (value, event) => void
v-model="x"value={x} onChange={...}
v-model:visible="x"visible={x} onVisibleChange={...}
defineExpose({ ... })forwardRef + useImperativeHandle
<slot>children
<slot name="footer">prop footer?: ReactNode
الفتحة المسمَّاة #headerprop header?: ReactNode
provide(...) / inject(...)Context.Provider / useContext
يجب أن تكون provide عبارة عن computedيجب تغليف قيمة Context بـ useMemo
computed(() => ...)useMemo(() => ..., [deps])
watch(src, cb)useEffect(() => cb(), [src])
watch(src, cb, { immediate: true })useEffect (يُنفَّذ effect مرة واحدة فورًا بطبيعته)
onMounted / onUnmountedsetup / cleanup في useEffect
<Transition name="lh-x-fade">useTransition(visible, duration) يقود الأسماء نفسها
<Teleport to="body">createPortal(..., document.body)
ref="<name>"useRef<HTMLDivElement>(null)
nextTick()flushSync() أو effect (غالبًا لا حاجة)
app.use(LhUI)لا شيء — استيراد مسمَّى عند الحاجة
<LhXxx v-bind="obj"><LhXxx {...obj}>

التحكم وعدم التحكم​

تنقسم modelValue / update:modelValue في Vue إلى مجموعتي props في React:

<!-- Vue -->
<LhInput v-model="text" />
// React: متحكَّم
<LhInput value={text} onChange={(val) => setText(val)} />

// React: غير متحكَّم
<LhInput defaultValue="قيمة افتراضية" />

الجمع بينهما (تمرير value وdefaultValue معًا) يُطبع تحذيرًا في وضع التطوير، وذلك يتوافق مع دلالات النماذج الأصلية في React.

استدعاءات الأحداث​

توحَّد جميع أحداث props في تسمية onXxx، ويُوحَّد معامل الاستدعاء في معاملين (value, event):

<!-- Vue -->
<LhSelect @change="onChange" />
// React
<LhSelect onChange={(value, option) => console.log(value, option)} />

الحركة الانتقالية​

إن <Transition> في Vue تصريحية، بينما تستخدم نسخة React hookًا ذاتي التطوير useTransition لقيادة الأسماء نفسها:

import { useTransition } from '@lh-ui/react'

const { mounted, phase } = useTransition(visible, 300)
// أسماء الأصناف: `lh-dialog-fade-${phase}` —— phase ∈ enter-from / enter-active / enter-to

أسماء الأصناف مطابقة تمامًا لـ SCSS في share، والمظهر البصري متماثل إطارًا بإطار مع نسخة Vue.

الطبقات المنبثقة والنقل​

يقابل <Teleport to="body"> في React كلٌّ من createPortal، وتُعلَّق مربعات الحوار وقوائم Select المنسدلة وTooltip جميعها على document.body. ويُحتسَب هندسة التموضع عبر useLhPopper (بمشاركة @lh-ui/share/core/shared/popper مع نسخة Vue).

ref الحتمي​

يقابل defineExpose في React كلٌّ من forwardRef + useImperativeHandle:

<!-- Vue -->
<LhForm ref="formRef" />
// React
const formRef = useRef<LhFormInstance>(null)
<LhForm ref={formRef} />
<formRef.current?.validate() />

التكوين العام​

<!-- Vue -->
<LhConfigProvider :size="size">
<App />
</LhConfigProvider>
// React
<LhConfigProvider size={size}>
<App />
</LhConfigProvider>

فروق التنفيذ بين هذه المكتبة ونُسخ Vue​

الفروق التالية تعديلات مقصودة في الشكل Reactي، يُرجى الانتباه إليها عند الترحيل. وكل بند مُسجَّل داخل المستودع.

1. حقن الأنماط عند الحاجة​

على خلاف lh-ui-plus (حقن كامل)، تتوافق هذه المكتبة مع lh-ui-vue2: لا يُحقِن مدخل الحزمة أنماطًا، بل يستورِد المستخدم صراحةً theme.css + style/<name>.

2. سلسلة البناء مماثلة لـ vue2​

vite lib mode + preserveModules + tsc يُخرج d.ts مباشرةً (plus يستخدم rollup مع البحث عن المداخل المتعددة). وبفضل preserveModules يُدعَم الاستيراد عند الحاجة بطبيعة الحال، وليس في tsx وحدات SFC افتراضية، فيكون إخراج tsc لـ d.ts هو الأبسط.

3. احتياط جانب الاستهلاك في ConfigProvider​

في نسخة Vue يجعل inject(..., null) كل مكوّن يتحقق من الفراغ. أما في نسخة React فتُرجِع hook الاستهلاك كائنًا احتياطيًا بحقول undefined جميعها، فلا حاجة للتحقق من الفراغ داخل المكوّن — وهي ممارسة شائعة في React Context لأن useContext لا يمكنه تلقي قيمة افتراضية عند الاستدعاء.

4. فروق التركيب في notification / loading​

الثلاثة خدمات حتمية لكن طرق تركيبها مختلفة: يشارك message / notification الحاوية الأحادية (createRoot الكسولة + قناة action)، بينما يحتاج loading إلى التركيب على عنصر هدف (ملء الشاشة أو جزء منها) عبر createRoot لكل نسخة، حيث تحتفظ كل نسخة بجذرها ومُعالِجَي استعادة overflow / inert.

API الخاص بـ notification كائني (notification.success({ title, content })، موافق لـ Notification(options) في plus)، ويقبل أيضًا الصيغة المختصرة بالنص (يعرض سطر العنوان فقط). حركة FLIP لإفساح المكان غير مُنفَّذة: فالصنف -move الذي يوفِّره transition-group في Vue ليس له مقابل أصلي في React، وتقفز البطاقات الأخوية فورًا عند إضافة العناصر وحذفها من الطابور.

5. onChange في Input بمعاملين​

في نسخة Vue يكون @input / @change منفصلين، بينما توحِّدهما نسخة React في onChange(value, event) (إضافةً إلى onInput الذي يمرِّر حدث الإدخال الأصلي).

6. ضبط الحجم التلقائي (autosize) في Input عبر DOM من المكوّن​

تعتمد نسخة Vue على خاصية data-autosize لتحسب التوجيهات الارتفاع في طبقة DOM. أما نسخة React فتقرأ scrollHeight مباشرةً في useEffect وتكتب textarea.style.height (ضمن حدَّي minRows / maxRows) لتفادي إدخال آلية توجيهات إضافية.

7. تموضع Select عبر useLhPopper​

بعض نُسخ Vue من Select تستخدم تموضع fixed مكتوبًا يدويًا دون قلب. بينما تتخذ نسخة React نهج useLhPopper الموحَّد (الهندسة نفسها لـ Dialog / Tooltip، مع قلب التجاوز وطي التمرير ذاتهما).

8. Dialog متحكَّم به بالكامل​

يحتفظ Dialog في plus بحالة isVisible وسيطة تُزامَن ثنائيًا مع modelValue عبر watch. وأزالت نسخة React هذه الحالة: open بيد الطرف الخارجي، وكل مسارات الإغلاق تستدعي onClose فقط. فدلالة التحكم أنقى وتُزال حالة التنافس بين props والحالة الداخلية.

9. دورة حياة Dialog مقسومة إلى مجموعتي effect​

تستخدم نسخة Vue watch(visible, cb, { immediate: true }) بمعالج واحد للفتح والإغلاق. بينما تقسم نسخة React الأمر إلى «open effect» (تسجيل رصة الطبقات المنبثقة، قفل التمرير، إطلاق onOpen) و«mounted effect» (focus trap، إطلاق onOpened / onClosed)، لأن توقيت تركيب الحركة الانتقالية وتوقيت الفتح في React هما دورتا حياة مستقلتان.

10. تصحيح التحقّق غير المتزامن في Form​

تستخدم validateField في plus Promise.resolve(!message)، فلا تنتظر Promise الذي يُرجِعه validator غير المتزامن — فيُرجِع التحقّق غير المتزامن true دائمًا. وأضافت نسخة React فحص isThenable لتنتظر صراحةً اكتمال validator غير المتزامن قبل الحكم:

if (isThenable(message)) return message.then((error) => !error)

سُجِّل هذا العيب في سجل إصلاحات العائلة، وانتظر استرداد التصحيح في نسخة Vue.

11. تبسيط التحكم في Tooltip​

يملك Tooltip في plus مفتاح manual مع visible للتحكم. أما نسخة React فتنظر إلى prop visible مباشرةً: تمرير visible يجعله متحكَّمًا به، ويُعلم onVisibleChange الطرف الخارجي، دون الاحتفاظ بمفتاح manual الوسيط.

12. تحسينات في جانب React (غير متوفرة في plus بعد)​

الفروق التالية تحسينات في نسخة React، استخدمها كقدرات جديدة عند الترحيل:

  • إزالة Tag عند الإغلاق: يُزال Tag من DOM فور النقر على زر الإغلاق ثم يُطلَق onClose (plus يطلق الحدث فقط وتُزال العقدة عبر v-if الخارجي).
  • إعادة كتابة المسح في Input: زر المسح في clearable يُطلِق onChange('') أيضًا، فتتزامن الحالة المتحكَّم بها تلقائيًا (@clear في plus يطلق الحدث فقط).
  • تحقق change في Form: قواعد rule.trigger === 'change' تُطلَق مع فقاعة change من منطقة المحتوى (plus يحركها watch التفاعلي).
  • مسح الأخطاء عند إعادة التعيين في Form: يزيد resetFields() رقم إصدار داخلي، تراقبه عناصر FormItem فتُفرِغ حالة الخطأ (plus يمسحها تلقائيًا عبر السلسلة التفاعلية).
  • إظهار كلمة المرور في Input: prop showPassword يُعرِض زر عين لتبديل النص العادي عند type="password"، ولا يوجد هذا prop في plus / vue2.

13. تخصيص مكوّنات Wave C لـ React​

  • لا يمكن نقر قناع Drawer افتراضيًا: maskClosable افتراضي false (بينما يرفض canDrawerClose في core افتراضيًا)، أما closeOnClickModal في plus فافتراضي true، فيُحتاج إلى تعيينه صراحةً عند الترحيل. ولا يُفعَّل حصر التركيز إلا عند تفعيل قفل التمرير (shouldDrawerTrapFocus)، فحين لا يُقفَل التمرير يُحافَظ على تسلسل Tab لمحتوى الخلفية.
  • تحويل فتحة Dropdown إلى prop: فتحة #dropdown في plus تصبح prop بنفس الاسم dropdown في React؛ وتُسجَّل عناصر القائمة DOM عبر Context، ويسيّر roving tabindex عبر حالة activeEl المرتكزة (نسخة Vue تجمع نسخ المكوّنات الفرعية عبر getCurrentInstance).
  • تحويل Popover / Popconfirm إلى التحكم: مثل Tooltip، تمرير visible يجعله متحكَّمًا به ويُطلِق onVisibleChange للخارج، دون مفتاح manual الموجود في plus.

مرجع سريع لترحيل الأنواع​

// Vue: تُستنتَج props من withDefaults
const props = withDefaults(defineProps<LhButtonProps>(), { type: 'text' })

// React: واجهة + قيم افتراضية مفكوكة
function LhButton(props: LhButtonProps) {
const { type = 'text' } = props
}
// Vue: تعريف emit
const emit = defineEmits<{ change: [val: string, e: Event] }>()

// React: الاستدعاءات على props
interface LhXxxProps {
onChange?: (value: string, event: React.ChangeEvent<HTMLInputElement>) => void
}