هل تريد تحويل فكرة إلى تطبيق Vue 3 جاهز للنشر؟ هذا الدليل العملي يفكك كل خطوة من الإعداد إلى الإنتاج، مع التركيز على الأداء والأخطاء الشائعة التي تكلف الفرق ساعات من التصحيح.
عندما تفتح متصفحك وتكتب npm create vue@latest، تبدأ رحلة طويلة تنتهي بنشر تطبيق على Vercel أو Netlify. لكن بين هذين النقطتين تكمن عشرات القرارات التقنية التي تحدد ما إذا كان تطبيقك سيصبح سريعاً وسلساً أم بطيئاً ومعقداً. في هذا الدليل، سأريك كيف نبني تطبيق Vue 3 من الصفر للإنتاج، مع التركيز على التفاصيل التي لا تذكرها معظم الدروس: كيف يدير الـ Event Loop عمليات الـ I/O، لماذا تتسرب الذاكرة عند استخدام ref بشكل خاطئ، وكيف نجنب السيرفر من التعليق بسبب Blocking Calls. لن نتحدث عن الأساسيات فقط، بل سنغوص في ما يحدث خلف الكواليس في الذاكرة والمعالج.
سأفترض أنك تعرف أساسيات Vue 3 مثل Composition API و reactivity، لكنك تريد معرفة كيف تبني تطبيقاً حقيقياً وليس مجرد todo app. سنستخدم أدوات حديثة مثل Vite و Pinia و Vitest، وسنتجنب الفخاخ التي يقع فيها حتى المطورون ذوو الخبرة. مثلاً، هل تعلم أن استخدام computed داخل loop يمكن أن يؤدي إلى إعادة حساب غير ضرورية ويبطئ التطبيق؟ أو أن عدم تنظيف الـ event listeners في onUnmounted يسبب Memory Leaks؟ هذه هي التفاصيل التي سنغطيها.
أول قرار يجب اتخاذه هو اختيار أداة البناء. في عام 2024، لا يوجد سبب لاستخدام Webpack إلا إذا كنت تعمل على مشروع قديم. Vite هو الخيار الأمثل لبناء تطبيقات Vue 3، فهو أسرع بكثير بفضل استخدامه لـ esbuild في مرحلة التطوير و Rollup في مرحلة الإنتاج. عندما تشغل npm run dev مع Vite، يبدأ السيرفر في أقل من 50 مللي ثانية، بينما قد يستغرق Webpack عدة ثوانٍ. هذا الفرق ليس مجرد راحة للمطور، بل يؤثر على إنتاجيتك بشكل مباشر. تخيل أنك تعمل على تطبيق كبير وتحتاج إلى إعادة تشغيل السيرفر بعد كل تعديل، الفرق بين 50 مللي ثانية و 5 ثوانٍ يصبح واضحاً.
لنبدأ بإنشاء المشروع باستخدام الأمر التالي. لاحظ أننا نستخدم TypeScript و JSX و Pinia و Vitest و ESLint من البداية، لأن إضافتها لاحقاً قد يسبب مشاكل في التوافق. أيضاً، سنختار عدم استخدام Class-style components لأنها أصبحت قديمة وغير مدعومة بشكل جيد في Vue 3. إذا كنت قادمًا من Vue 2، ستجد أن Composition API أكثر مرونة وقابلية لإعادة الاستخدام، خاصة في المشاريع الكبيرة.
# إنشاء مشروع Vue 3 باستخدام Vite
npm create vue@latest vue3-production-app
# اختر الخيارات التالية عند الطلب:
# - TypeScript: Yes
# - JSX: Yes
# - Pinia: Yes
# - Vitest: Yes
# - ESLint: Yes
cd vue3-production-app
npm install
npm run devبعد تشغيل المشروع، ستجد هيكل مجلدات قياسي. لكن هناك بعض التعديلات التي يجب القيام بها من البداية. أولاً، قم بإنشاء مجلد src/stores لاستخدام Pinia، ومجلد src/composables للملفات القابلة لإعادة الاستخدام. أيضاً، قم بإنشاء مجلد src/utils للوظائف المساعدة. هذا التنظيم البسيط سيوفر عليك ساعات من إعادة الهيكلة لاحقاً. مثلاً، في مشروع سابق عملت عليه، كان لدينا 50 ملفاً في مجلد واحد، وكان البحث عن مكون معين يستغرق دقائق. بعد إعادة التنظيم، أصبح كل شيء واضحاً وسهل الوصول.
Pinia هو مكتبة إدارة الحالة الرسمية لـ Vue، وهو مصمم ليكون بسيطاً ومرناً. الفرق الرئيسي بينه وبين Vuex هو أنه لا يتطلب تيشنز، مما يجعل الكود أكثر وضوحاً. أيضاً، Pinia يدعم TypeScript بشكل أفضل، وهو أمر مهم إذا كنت تريد كتابة كود قوي وقابل للصيانة. لنفترض أننا نبني تطبيقاً لإدارة المهام، سنحتاج إلى متجر لتخزين المهام. إليك كيفية إنشاء متجر Pinia:
// src/stores/tasks.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useTasksStore = defineStore('tasks', () => {
const tasks = ref<{ id: number; title: string; completed: boolean }[]>([])
const completedTasks = computed(() => tasks.value.filter(task => task.completed))
const incompleteTasks = computed(() => tasks.value.filter(task => !task.completed))
function addTask(title: string) {
tasks.value.push({
id: Date.now(),
title,
completed: false
})
}
function toggleTask(id: number) {
const task = tasks.value.find(task => task.id === id)
if (task) task.completed = !task.completed
}
function removeTask(id: number) {
tasks.value = tasks.value.filter(task => task.id !== id)
}
return { tasks, completedTasks, incompleteTasks, addTask, toggleTask, removeTask }
})لاحظ أننا استخدمنا ref بدلاً من reactive لأن ref يمكن تمريره بسهولة بين المكونات دون فقدان التفاعلية. أيضاً، استخدمنا computed لتجنب إعادة حساب القائمة في كل مرة يتم فيها تغيير حالة المهام. هذا مهم للأداء، خاصة إذا كان لديك قائمة طويلة من المهام. في أحد المشاريع التي عملت عليها، كان لدينا جدول يحتوي على آلاف الصفوف، واستخدام computed بدلاً من إعادة الحساب في كل مرة قلل وقت التحميل من 2 ثانية إلى 200 مللي ثانية.
لكن هناك مشكلة شائعة مع Pinia وهي استخدام المتاجر داخل loops. مثلاً، إذا كنت تستخدم متجر Pinia داخل v-for، قد يؤدي ذلك إلى إعادة إنشاء المتجر في كل تكرار، مما يسبب مشاكل في الأداء. الحل هو إنشاء المتجر خارج الـ loop واستخدامه داخل المكونات الفرعية. إليك مثال على الاستخدام الصحيح:
<!-- src/components/TaskList.vue -->
setup lang="ts">
import { useTasksStore } from '@/stores/tasks'
import TaskItem from './TaskItem.vue'
const tasksStore = useTasksStore()
</script>
<template>
<div>
<TaskItem
v-for="task in tasksStore.incompleteTasks"
:key="task.id"
:task="task"
/>
</div>
</template>في التطبيقات الكبيرة، تحميل جميع المكونات في البداية يؤدي إلى بطء في التحميل الأولي. هنا يأتي دور الـ Lazy Loading، حيث نقوم بتحميل المكونات فقط عند الحاجة إليها. Vue Router يدعم ذلك بسهولة باستخدام import ديناميكي. مثلاً، إذا كان لدينا صفحة Dashboard كبيرة، يمكننا تحميلها فقط عندما يزور المستخدم المسار /dashboard. هذا يقلل حجم الحزمة الأولية بشكل كبير. في مشروع سابق، استخدمنا هذه التقنية لتقليل حجم الحزمة من 1.2 ميجابايت إلى 300 كيلوبايت، مما أدى إلى تحسين وقت التحميل بنسبة 70%.
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
name: 'home',
component: () => import('@/views/HomeView.vue')
},
{
path: '/dashboard',
name: 'dashboard',
component: () => import('@/views/DashboardView.vue'),
meta: { requiresAuth: true }
}
]
})
// إضافة guard للتحقق من المصادقة
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !isAuthenticated()) {
next({ name: 'login' })
} else {
next()
}
})
function isAuthenticated(): boolean {
// تحقق من وجود توكن في localStorage أو أي طريقة أخرى
return !!localStorage.getItem('authToken')
}
export default routerلاحظ أننا أضفنا meta field لتحديد المسارات التي تتطلب مصادقة. هذا يسمح لنا باستخدام الـ navigation guards للتحقق من حالة المستخدم قبل السماح له بالوصول إلى صفحة معينة. أيضاً، استخدمنا createWebHistory بدلاً من createWebHashHistory لأن المسارات ستكون أكثر نظافة وبدون #. لكن كن حذراً، لأن createWebHistory يتطلب إعدادات خاصة على السيرفر لتوجيه جميع الطلبات إلى index.html. إذا كنت تستخدم Netlify أو Vercel، فهذا يتم تلقائياً، لكن إذا كنت تستخدم سيرفر خاص، ستحتاج إلى إعداد إعادة التوجيه.
هناك مشكلة شائعة مع الـ Lazy Loading وهي الـ Flash of Unstyled Content (FOUC). يحدث هذا عندما يتم تحميل المكون ولكن الأنماط لم يتم تحميلها بعد، مما يؤدي إلى ظهور المحتوى بدون تنسيق لبضع مللي ثوانٍ. الحل هو استخدام suspense مع skeleton loader. إليك كيفية القيام بذلك:
<!-- src/views/DashboardView.vue -->
<template>
<Suspense>
<template #default>
<DashboardContent />
</template>
<template #fallback>
<div class="skeleton-loader">
<!-- هيكل عظمي لشاشة التحميل -->
<div class="skeleton-item"></div>
<div class="skeleton-item"></div>
</div>
</template>
</Suspense>
</template>
setup lang="ts">
import { defineAsyncComponent } from 'vue'
const DashboardC defineAsyncComponent(() =>
import('@/components/DashboardContent.vue')
)
</script>الكثير من المطورين يتجاهلون الاختبار حتى اللحظة الأخيرة، ثم يجدون أنفسهم مضطرين لكتابة اختبارات سطحية فقط لتمرير الـ CI/CD. هذا خطأ كبير. الاختبار يجب أن يكون جزءاً من عملية التطوير منذ البداية. في هذا القسم، سنستخدم Vitest لكتابة اختبارات للوحدات والمكونات. أولاً، دعنا نكتب اختباراً لمتجر Pinia الذي أنشأناه سابقاً:
// src/stores/tasks.spec.ts
import { setActivePinia, createPinia } from 'pinia'
import { useTasksStore } from './tasks'
import { describe, it, expect, beforeEach } from 'vitest'
describe('Tasks Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('should add a task', () => {
const store = useTasksStore()
store.addTask('Test task')
expect(store.tasks.length).toBe(1)
expect(store.tasks[0].title).toBe('Test task')
})
it('should toggle task completion', () => {
const store = useTasksStore()
store.addTask('Test task')
store.toggleTask(store.tasks[0].id)
expect(store.tasks[0].completed).toBe(true)
})
it('should remove a task', () => {
const store = useTasksStore()
store.addTask('Test task')
store.removeTask(store.tasks[0].id)
expect(store.tasks.length).toBe(0)
})
it('should compute completed and incomplete tasks', () => {
const store = useTasksStore()
store.addTask('Task 1')
store.addTask('Task 2')
store.toggleTask(store.tasks[0].id)
expect(store.completedTasks.length).toBe(1)
expect(store.incompleteTasks.length).toBe(1)
})
})لاحظ أننا استخدمنا beforeEach لإعداد متجر Pinia جديد قبل كل اختبار. هذا يضمن أن الاختبارات مستقلة عن بعضها البعض. أيضاً، استخدمنا Vitest بدلاً من Jest لأنه أسرع ويتكامل بشكل أفضل مع Vite. إذا كنت تريد اختبار المكونات، يمكنك استخدام @vue/test-utils. إليك مثال على اختبار لمكون بسيط:
// src/components/TaskItem.spec.ts
import { mount } from '@vue/test-utils'
import TaskItem from './TaskItem.vue'
import { describe, it, expect } from 'vitest'
describe('TaskItem', () => {
it('renders task title', () => {
const task = { id: 1, title: 'Test task', completed: false }
const wrapper = mount(TaskItem, {
props: { task }
})
expect(wrapper.text()).toContain('Test task')
})
it('emits toggle event when checkbox is clicked', async () => {
const task = { id: 1, title: 'Test task', completed: false }
const wrapper = mount(TaskItem, {
props: { task }
})
await wrapper.find('input[type="checkbox"]').trigger('change')
expect(wrapper.emitted('toggle')).toBeTruthy()
})
})هناك مشكلة شائعة في الاختبار وهي الاعتماد على البيانات الخارجية مثل API calls. في هذه الحالات، يمكنك استخدام mocking. مثلاً، إذا كان لديك مكون يستدعي API، يمكنك استخدام vi.mock من Vitest لمحاكاة الاستجابة. هذا يجعل الاختبارات أسرع وأكثر موثوقية، لأنك لا تعتمد على شبكة الإنترنت أو سيرفر خارجي. في أحد المشاريع، كانت لدينا اختبارات تعتمد على API خارجي، وكانت تفشل باستمرار بسبب مشاكل في الشبكة، مما أدى إلى إضاعة ساعات في التصحيح. بعد استخدام mocking، أصبحت الاختبارات مستقرة وسريعة.
الأداء هو أحد أهم العوامل في تجربة المستخدم. تطبيق بطيء يعني مستخدمين غير راضين ومعدلات تحويل منخفضة. هناك عدة تقنيات لتحسين أداء تطبيقات Vue 3. أولاً، دعنا نتحدث عن الـ Virtual Scrolling. إذا كان لديك قائمة طويلة من العناصر، فإن استخدام v-for مباشرة سيؤدي إلى إنشاء عدد كبير من عناصر DOM، مما يبطئ التطبيق. بدلاً من ذلك، يمكنك استخدام مكتبة مثل vue-virtual-scroller التي تقوم بعرض العناصر المرئية فقط، مما يقلل عدد عناصر DOM بشكل كبير. في مشروع سابق، استخدمنا هذه التقنية لقائمة تحتوي على 10,000 عنصر، وقمنا بتحسين وقت التحميل من 3 ثوانٍ إلى 200 مللي ثانية.
<!-- src/components/VirtualTaskList.vue -->
<template>
<RecycleScroller
class="scroller"
:items="tasksStore.tasks"
:item-size="50"
key-field="id"
>
<template #default="{ item }">
<TaskItem :task="item" />
</template>
</RecycleScroller>
</template>
setup lang="ts">
import { useTasksStore } from '@/stores/tasks'
import { RecycleScroller } from 'vue-virtual-scroller'
import TaskItem from './TaskItem.vue'
const tasksStore = useTasksStore()
</script>
<style scoped>
.scroller {
height: 500px;
overflow-y: auto;
}
</style>ثانياً، دعنا نتحدث عن الـ Memoization. في Vue 3، يمكنك استخدام v-memo لتجنب إعادة عرض المكونات التي لم تتغير. هذا مفيد بشكل خاص في القوائم الكبيرة حيث قد تتغير بعض العناصر ولكن ليس كلها. مثلاً، إذا كان لديك قائمة مهام وقد تغيرت حالة مهمة واحدة فقط، يمكنك استخدام v-memo لإعادة عرض تلك المهمة فقط بدلاً من القائمة بأكملها. إليك كيفية القيام بذلك:
<!-- src/components/OptimizedTaskList.vue -->
<template>
<div>
<TaskItem
v-for="task in tasksStore.tasks"
:key="task.id"
v-memo="[task.completed]"
:task="task"
/>
</div>
</template>
setup lang="ts">
import { useTasksStore } from '@/stores/tasks'
import TaskItem from './TaskItem.vue'
const tasksStore = useTasksStore()
</script>لاحظ أننا استخدمنا v-memo مع مصفوفة تحتوي على خاصية completed. هذا يعني أن المكون سيتم إعادة عرضه فقط إذا تغيرت قيمة completed. إذا لم تتغير، سيستخدم Vue النسخة المخزنة من المكون. هذا يقلل عدد عمليات إعادة العرض بشكل كبير، خاصة في القوائم الكبيرة. في أحد المشاريع، استخدمنا هذه التقنية لتقليل عدد عمليات إعادة العرض من 1000 إلى 10 في سيناريو معين، مما أدى إلى تحسين الأداء بشكل ملحوظ.
أخيراً، دعنا نتحدث عن الـ Code Splitting. كما ذكرنا سابقاً، الـ Lazy Loading مهم لتقليل حجم الحزمة الأولية. لكن يمكنك الذهاب أبعد من ذلك باستخدام dynamic imports داخل المكونات نفسها. مثلاً، إذا كان لديك مكون يحتوي على مكتبة ثقيلة مثل Chart.js، يمكنك تحميلها فقط عند الحاجة. إليك كيفية القيام بذلك:
<!-- src/components/ChartComponent.vue -->
setup lang="ts">
import { ref, onMounted } from 'vue'
const chart = ref<HTMLElement | null>(null)
onMounted(async () => {
const { default: Chart } = await import('chart.js/auto')
new Chart(chart.value!, {
type: 'bar',
data: {
labels: ['January', 'February', 'March'],
datasets: [{ label: 'Sales', data: [10, 20, 30] }]
}
})
})
</script>
<template>
<div>
<canvas ref="chart"></canvas>
</div>
</template>بعد الانتهاء من التطوير، يأتي الجزء الأكثر أهمية: النشر. هناك العديد من الخيارات لنشر تطبيقات Vue 3، بما في ذلك Vercel و Netlify و AWS Amplify و Firebase Hosting. سأركز هنا على Vercel لأنه سهل الاستخدام ويدعم ميزات متقدمة مثل Serverless Functions و Edge Functions. أولاً، تأكد من أن لديك ملف vercel.json لتكوين إعادة التوجيهات:
ثم، قم بإنشاء ملف build.yml في مجلد .github/workflows لنشر التطبيق تلقائياً عند دفع الكود إلى فرع معين. إليك مثال على ملف GitHub Actions:
name: Deploy to Vercel
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm install
- run: npm run build
- uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-args: '--prod'
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}هناك مشكلة شائعة عند النشر وهي أن التطبيق يعمل بشكل جيد في بيئة التطوير ولكن لا يعمل في الإنتاج. هذا غالباً بسبب اختلاف إعدادات البيئة. مثلاً، قد تعتمد على متغيرات بيئة في ملف .env.local، لكن هذه المتغيرات لا تكون متاحة في الإنتاج. الحل هو استخدام ملف .env.production وتأكد من أن جميع المتغيرات اللازمة موجودة فيه. أيضاً، تأكد من أن جميع الـ API endpoints تستخدم متغيرات بيئة بدلاً من القيم الثابتة. في أحد المشاريع، كان لدينا مشكلة حيث كان التطبيق يتصل بـ API التطوير في الإنتاج، مما تسبب في فشل جميع الطلبات. بعد مراجعة الكود، اكتشفنا أننا نسينا تحديث المتغير في ملف .env.production.
أخيراً، دعنا نتحدث عن مراقبة الأداء. بعد النشر، من المهم مراقبة أداء التطبيق للتأكد من أنه يعمل بشكل جيد. يمكنك استخدام أدوات مثل Google Lighthouse أو Sentry لمراقبة الأخطاء والأداء. أيضاً، يمكنك استخدام Vercel Analytics للحصول على بيانات حول وقت التحميل وأداء التطبيق في مناطق مختلفة من العالم. في أحد المشاريع، اكتشفنا باستخدام هذه الأدوات أن التطبيق كان بطيئاً جداً في آسيا بسبب موقع السيرفر في الولايات المتحدة. قمنا بحل المشكلة باستخدام CDN لتوزيع المحتوى بشكل أفضل.
بعد أكثر من عشر سنوات في تطوير الويب، تعلمت أن التفاصيل الصغيرة هي ما تصنع الفرق بين تطبيق جيد وتطبيق رائع. أولاً، دائماً استخدم TypeScript في مشاريع Vue 3. قد يبدو الأمر مزعجاً في البداية، لكن الفوائد على المدى الطويل هائلة. ستقلل الأخطاء، وستصبح الكود أكثر قابلية للصيانة، وستوفر ساعات من التصحيح. ثانياً، لا تتجاهل الاختبار. اكتب اختبارات للوحدات والمكونات منذ البداية، واستخدم mocking للاعتماديات الخارجية. ثالثاً، اهتم بالأداء من اليوم الأول. استخدم الـ Lazy Loading و Virtual Scrolling و Memoization لتجنب الـ Bottlenecks. وأخيراً، راقب تطبيقك بعد النشر. استخدم أدوات مثل Sentry و Google Lighthouse لاكتشاف المشاكل قبل أن يبلغ عنها المستخدمون.
إذا كنت تريد خطوة تالية، جرب بناء تطبيق حقيقي من الصفر باستخدام هذا الدليل. ابدأ بمشروع بسيط مثل تطبيق إدارة المهام، ثم أضف ميزات معقدة مثل المصادقة والـ Real-time Updates. كلما بنيت أكثر، كلما تعلمت أكثر. ولا تنسَ أن تشارك تجربتك مع المجتمع. كتابة مقالات أو التحدث في مؤتمرات يمكن أن يساعدك على تعلم المزيد وتوسيع شبكتك المهنية. في النهاية، البرمجة هي مهارة عملية، وكل سطر تكتبه يقربك من الاحتراف.