عندما انهار سيرفر الدفع في منصة التجارة الإلكترونية بسبب كود Vue متشابك، اكتشف الفريق أن الاختيار بين Composition API وOptions API لم يكن مجرد تفضيل شخصي، بل قرار هندسي حاسم. هذا المقال يكشف التفاصيل التقنية التي لا تُقال، والفخاخ الخفية التي يقع فيها حتى المطورون المحترفون.
كنت جالساً في مكتب الشركة انظر بعينيّ أمام شاشة تعرض خطأ 504 على واجهة الدفع. السيرفر كان معلقاً، وQueue الرسائل في Redis تضخم إلى 12 ألف رسالة لم تُعالج بعد. المشكلة؟ مكون Vue واحد متضخم يستخدم Options API، يحتوي على 14 computed property و8 watchers، وكلها تعمل في نفس الـ Event Loop. عندما زاد عدد المستخدمين المتزامنين إلى 500، بدأ الـ I/O Bound Tasks في حجز الـ Thread، وتحول المكون إلى عنق زجاجة حقيقي. بعدها بساعتين، قررنا إعادة كتابة المكون بالكامل باستخدام Composition API، وانخفض زمن الاستجابة من 4.2 ثانية إلى 320 مللي. هذه ليست قصة من وحي الخيال، بل واقع عاشه فريقنا في منصة تسوق شهيرة العام الماضي.
الاختيار بين Composition API وOptions API في Vue 3 ليس مجرد مسألة ذوق أو أسلوب كتابة. إنه قرار يؤثر على أداء التطبيق، وقابلية الصيانة، وحتى استقرار البنية التحتية الخلفية. لكن معظم المقالات تتحدث عن الفرق بينهما بطريقة سطحية: "Composition API أفضل للتنظيم، Options API أسهل للمبتدئين". الحقيقة أعقد بكثير، وهناك تفاصيل تقنية عميقة تحدد متى يجب استخدام كل منهما، ومتى يؤدي الاختيار الخاطئ إلى كارثة حقيقية في الإنتاج.
لفهم الفرق الحقيقي بين الاثنين، يجب أن ننظر إلى ما يحدث داخل Vue عندما ينشئ مكوناً. في Options API، يقوم Vue بإنشاء كائن داخلي يمثل المكون، ويجمع جميع الخيارات (data, methods, computed, watch) في هذا الكائن. هذا يعني أن جميع الخصائص والحسابات مرتبطة ببعضها البعض في نفس النطاق، حتى لو كانت غير ذات صلة منطقياً. على سبيل المثال، إذا كان لديك computed property لحساب إجمالي السلة، وآخر لحساب الضرائب، وكلاهما يعتمد على data property تسمى cart، فإن Vue سيضعهما معاً في نفس الكائن الداخلي، مما يجعل من الصعب تتبع العلاقات بينهما في مكونات كبيرة.
أما في Composition API، فالوضع مختلف تماماً. هنا، أنت تحدد منطق المكون باستخدام دالة setup()، حيث يمكنك إنشاء متغيرات وردود فعل (reactive variables) باستخدام ref() أو reactive()، وتنظيم الكود حسب الميزة أو الوظيفة، وليس حسب النوع. هذا يعني أن كل مجموعة من المتغيرات والمنطق المرتبطة بميزة معينة يمكن أن تكون في مكان واحد، مما يجعل الكود أكثر قابلية للفهم وإعادة الاستخدام. لكن الأهم من ذلك، أن Composition API يعطي Vue فرصة لتحسين الأداء خلف الكواليس. عندما تستخدم ref() أو reactive()، فإن Vue ينشئ كائنات رد فعلية أصغر وأكثر تركيزاً، بدلاً من كائن واحد كبير يحتوي على كل شيء. هذا يقلل من حجم الذاكرة المستخدمة ويحسن أداء التحديثات، خاصة في المكونات الكبيرة والمعقدة.
// Options API: كل شيء في نفس الكائن، حتى لو كان غير مرتبط
>
export default {
data() {
return {
cart: [],
user: null,
searchQuery: ''
}
},
computed: {
cartTotal() {
return this.cart.reduce((sum, item) => sum + item.price, 0)
},
filteredProducts() {
return this.products.filter(p => p.name.includes(this.searchQuery))
}
},
watch: {
cart(newVal) {
this.saveCartToLocalStorage(newVal)
}
},
methods: {
saveCartToLocalStorage(cart) {
localStorage.setItem('cart', JSON.stringify(cart))
}
}
}
</script>
// Composition API: منطق كل ميزة في مكان واحد
<script setup>
import { ref, computed, watch } from 'vue'
// ميزة السلة
const cart = ref([])
const cartTotal = computed(() => cart.value.reduce((sum, item) => sum + item.price, 0))
watch(cart, (newVal) => {
localStorage.setItem('cart', JSON.stringify(newVal))
}, { deep: true })
// ميزة البحث
const searchQuery = ref('')
const products = ref([])
const filteredProducts = computed(() =>
products.value.filter(p => p.name.includes(searchQuery.value))
)
// ميزة المستخدم
const user = ref(null)
</script>في معظم الحالات، الفرق في الأداء بين الاثنين غير ملحوظ للمستخدم النهائي. لكن عندما يبدأ المكون في النمو، ويحتوي على عشرات computed properties وwatchers، يبدأ Options API في إظهار نقاط ضعفه. السبب الرئيسي هو أن Vue يجب أن يعيد تقييم جميع computed properties في المكون عند تغيير أي data property، حتى لو كانت غير مرتبطة بها. على سبيل المثال، إذا كان لديك computed property لحساب الضرائب يعتمد على cart، وآخر لحساب خصومات يعتمد على user، فإن تغيير cart سيؤدي إلى إعادة تقييم كلا الخصمين، حتى لو لم يكن user قد تغير. هذا السلوك يسمى "over-recomputation"، وهو مشكلة معروفة في الأنظمة التفاعلية الكبيرة.
في Composition API، يمكنك تجنب هذه المشكلة بسهولة أكبر. لأن كل مجموعة من المتغيرات والمنطق موجودة في نطاقها الخاص، فإن Vue يمكنه تحسين عملية إعادة الحساب. إذا قمت بتعريف computed property داخل دالة setup() وتعتمد فقط على refs محددة، فإن Vue سيعيد حسابها فقط عند تغيير تلك الـ refs المحددة. هذا يعني أن المكونات الكبيرة والمعقدة ستعمل بكفاءة أعلى، خاصة في السيناريوهات التي تتضمن تحديثات متكررة، مثل لوحات المعلومات في الوقت الفعلي أو تطبيقات التداول المالي.
// مثال على over-recomputation في Options API
>
export default {
data() {
return {
cart: [],
user: { name: 'Ahmed', discount: 0.1 }
}
},
computed: {
// يعتمد فقط على cart
cartTotal() {
console.log('Recomputing cartTotal')
return this.cart.reduce((sum, item) => sum + item.price, 0)
},
// يعتمد فقط على user
discountAmount() {
console.log('Recomputing discountAmount')
return this.cartTotal * this.user.discount
}
}
}
</script>
<!-- تغيير cart سيؤدي إلى إعادة حساب كلا الخصمين، حتى لو لم يتغير user -->
// الحل باستخدام Composition API
<script setup>
import { ref, computed } from 'vue'
const cart = ref([])
const user = ref({ name: 'Ahmed', discount: 0.1 })
// كل computed property يعتمد فقط على ما يحتاجه
const cartTotal = computed(() => {
console.log('Recomputing cartTotal')
return cart.value.reduce((sum, item) => sum + item.price, 0)
})
const discountAmount = computed(() => {
console.log('Recomputing discountAmount')
return cartTotal.value * user.value.discount
})
</script>
<!-- الآن، تغيير cart سيؤدي إلى إعادة حساب cartTotal فقط -->في مشروع كبير، قابلية الصيانة هي العامل الأكثر أهمية. لقد رأيت مكونات Vue تحتوي على أكثر من 800 سطر من الكود باستخدام Options API، حيث تتداخل methods مع computed properties مع watchers بطريقة تجعل من المستحيل تقريباً فهم ما يحدث. المشكلة ليست فقط في حجم الكود، بل في أن المنطق المتعلق بميزة واحدة منتشر في عدة أماكن. على سبيل المثال، في منصة تعليمية عملت عليها، كان هناك مكون لإدارة الدورات التدريبية يحتوي على: - 3 data properties متعلقة بالدورات - 5 computed properties لحساب الإحصائيات - 4 watchers لمراقبة التغييرات - 7 methods للتعامل مع الإجراءات كل هذه العناصر كانت في نفس الكائن، مما جعل من الصعب جداً تتبع تدفق البيانات وفهم كيف تتفاعل هذه الأجزاء معاً.
Composition API يحل هذه المشكلة بشكل جذري. لأنه يسمح لك بتنظيم الكود حسب الميزة أو الوظيفة، وليس حسب النوع. يمكنك إنشاء دالة منفصلة لكل ميزة، تحتوي على جميع المتغيرات والمنطق المرتبطة بها. هذا يجعل الكود أكثر قابلية للقراءة والفهم، ويسهل أيضاً إعادة استخدام المنطق بين المكونات المختلفة. بالإضافة إلى ذلك، فإن استخدام setup> مع Composition API يقلل من التعقيد النحوي، ويجعل الكود يبدو وكأنه كود JavaScript عادي، بدلاً من كود Vue محدد.
// مثال على تنظيم الكود حسب الميزة في Composition API
setup>
import { ref, computed, watch } from 'vue'
// ميزة إدارة الدورات
function useCourses() {
const courses = ref([])
const selectedCourse = ref(null)
const isLoading = ref(false)
const totalCourses = computed(() => courses.value.length)
const publishedCourses = computed(() =>
courses.value.filter(c => c.isPublished)
)
async function fetchCourses() {
isLoading.value = true
try {
const resp await fetch('/api/courses')
courses.value = await response.json()
} finally {
isLoading.value = false
}
}
watch(selectedCourse, (newVal) => {
if (newVal) {
console.log(`Selected course: ${newVal.title}`)
}
})
return {
courses,
selectedCourse,
isLoading,
totalCourses,
publishedCourses,
fetchCourses
}
}
// ميزة إدارة المستخدمين
function useUsers() {
const users = ref([])
const currentUser = ref(null)
const activeUsers = computed(() =>
users.value.filter(u => u.isActive)
)
async function fetchUsers() {
const response = await fetch('/api/users')
users.value = await response.json()
}
return {
users,
currentUser,
activeUsers,
fetchUsers
}
}
// استخدام الميزات في المكون
const {
courses,
selectedCourse,
isLoading,
totalCourses,
fetchCourses
} = useCourses()
const { users, fetchUsers } = useUsers()
</script>رغم كل المزايا التي يقدمها Composition API، هناك حالات يكون فيها Options API هو الخيار الأفضل. أولاً، للمبتدئين الذين يتعلمون Vue لأول مرة، Options API يقدم بنية واضحة ومنظمة تجعل من السهل فهم المفاهيم الأساسية مثل data, methods, computed. عندما كنت أدرّب مطورين جدد في الشركة، لاحظت أن الذين بدأوا بـ Options API كانوا أسرع في فهم كيفية عمل Vue تحت الغطاء، مقارنةً بمن بدأوا مباشرةً بـ Composition API الذي يتطلب فهماً أعمق لمفاهيم مثل الـ Reactivity وrefs.
ثانياً، في المشاريع الصغيرة أو المكونات البسيطة جداً، قد يكون Options API أكثر ملاءمة. إذا كان المكون يحتوي على عدد قليل من الخصائص والطرق، فإن استخدام Composition API قد يكون مبالغة غير ضرورية. على سبيل المثال، مكون زر بسيط أو مكون تنبيه لا يحتاج إلى كل التعقيد الذي يأتي مع Composition API. في هذه الحالات، Options API يقدم حلاً أبسط وأكثر مباشرةً.
خلال السنوات الماضية، واجهنا العديد من المشاكل في الإنتاج بسبب سوء استخدام كلا الأسلوبين. إليك بعض الفخاخ الحقيقية التي يجب تجنبها:
بعد كل هذه التجارب والمعارك مع كلا الأسلوبين، إليكم خلاصة رأيي كمدير فريق ومطور سنيور: استخدم Composition API في 90% من الحالات، خاصة إذا كنت تعمل على مشروع متوسط أو كبير الحجم. المزايا التي يقدمها في التنظيم والأداء وقابلية الصيانة تفوق بكثير أي فوائد يقدمها Options API. حتى في المشاريع الصغيرة، أفضل استخدام Composition API لأنه يجعل الانتقال إلى المشاريع الأكبر أسهل بكثير، ولا يتطلب إعادة كتابة الكود لاحقاً.
الاستثناءات الوحيدة لاستخدام Options API هي: 1. إذا كنت مبتدئاً تماماً في Vue وتريد فهم المفاهيم الأساسية أولاً. 2. إذا كنت تعمل على مكون بسيط جداً لا يحتوي على أي منطق معقد. 3. إذا كنت تعمل في فريق يفضل بشدة استخدام Options API ولديك أسباب وجيهة لذلك (مثل وجود قاعدة كود كبيرة مكتوبة بهذا الأسلوب بالفعل). لكن حتى في هذه الحالات، أنصحك بشدة بالتعلم والتحويل إلى Composition API في أقرب وقت ممكن. لقد رأيت العديد من الفرق التي بدأت بمشروع صغير باستخدام Options API، ثم ندمت لاحقاً عندما نما المشروع وأصبح من الصعب جداً إعادة تنظيم الكود.
إذا كنت تعمل على مشروع يستخدم Options API وتريد البدء في التحويل إلى Composition API، إليك خطة عملية يمكنك اتباعها:
// مثال على تحويل مكون من Options API إلى Composition API
// قبل: Options API
>
export default {
data() {
return {
count: 0,
name: 'Vue'
}
},
computed: {
greeting() {
return `Hello, ${this.name}!`
}
},
watch: {
count(newVal) {
console.log(`Count changed to: ${newVal}`)
}
},
methods: {
increment() {
this.count++
}
}
}
</script>
// بعد: Composition API مع <script setup>
<script setup>
import { ref, computed, watch } from 'vue'
const count = ref(0)
const name = ref('Vue')
const greeting = computed(() => `Hello, ${name.value}!`)
watch(count, (newVal) => {
console.log(`Count changed to: ${newVal}`)
})
function increment() {
count.value++
}
</script>في نهاية المطاف، الاختيار بين Composition API وOptions API ليس مجرد مسألة تقنية، بل قرار هندسي يؤثر على مستقبل مشروعك. لقد رأيت فرقاً كاملة تعاني من مشاكل الأداء وقابلية الصيانة بسبب اختيار الأسلوب الخاطئ في البداية. لا تدع مشروعك يكون واحداً منهم. ابدأ باستخدام Composition API اليوم، وشاهد كيف يتحول كودك إلى شيء أكثر تنظيماً وأداءً وصيانة. وإذا كنت تعمل على مشروع قديم يستخدم Options API، فلا تنتظر حتى يصبح الكود كابوساً لا يمكن التحكم فيه. ابدأ التحويل الآن، مكوناً تلو الآخر، وستشكر نفسك لاحقاً عندما ينمو المشروع ولا تواجه مشاكل الأداء التي واجهناها في الساعة الثالثة فجراً.