من تهيئة المشروع إلى النشر على الإنتاج، هذا الدليل العملي يفكك كل خطوة في بناء تطبيق Vue 3 احترافي: الأداء، الأمان، التوسعة، وحل المشاكل الحقيقية التي تواجهها في سوق العمل.
عندما تفتح متصفح المستخدم وتكتب عنوان موقعك، لا أحد يرى الـ 500 مللي ثانية التي يستغرقها الـ Event Loop في معالجة الـ DOM Diffing، ولا الـ 3 ميجابايت من الـ JavaScript التي تُحمّل في الخلفية. لكن المستخدم يشعر بها فوراً: الزر لا يستجيب، الصفحة تتعطل، ثم يغلق التبويب. الحقيقة هي أن بناء تطبيق Vue 3 للإنتاج ليس مجرد كتابة مكونات جميلة، بل هو هندسة نظام متكامل يتحمل الضغط، يتوسع مع المستخدمين، ولا يترك ثغرات للأمان. في هذا الدليل، سأريك بالضبط كيف نبني تطبيقاً من الصفر لا يمر فقط باختبارات QA، بل ينجح في العالم الحقيقي حيث الـ I/O Bound Tasks تبطئ السيرفر، والـ Memory Leaks تلتهم الرام، والمستخدمون يضغطون على الأزرار بسرعة أكبر مما تتوقع.
سنتجاوز النظريات الأكاديمية ونذهب مباشرة إلى ما يهم: كيف تختار الـ Bundler المناسب؟ متى تستخدم Composition API ومتى تبقى مع Options API؟ كيف تضمن أن تطبيقك لن يتجمد عند تحميل 10 آلاف صف في الجدول؟ وكيف تنشره على الإنتاج دون أن تفقد السيطرة على الـ Rollback؟ كل خطوة مدعومة بكود حقيقي قابل للتشغيل، ومشاكل حقيقية واجهتها في مشاريع مثل منصة تعليمية تعالج 50 ألف طلب يومياً، وتطبيق إدارة مالية يتعامل مع بيانات حساسة. لنبدأ من الصفر، لكن لن نبقى فيه طويلاً.
الكثير يبدأ بـ npm create vue@latest وينتهي به الأمر بمشروع مليء بالملفات التي لا يفهمها، وdependencies لا يعرف لماذا أضيفت. لكن المهندس الجيد يعرف أن كل سطر في package.json يؤثر على الأداء النهائي. مثلاً، هل تعلم أن إضافة vue-router وpinia في البداية يضيف 20 كيلوبايت إلى حجم الـ Bundle حتى لو لم تستخدمهما؟ أو أن استخدام TypeScript يضيف 15% إلى وقت البناء بسبب الـ Type Checking؟ هذه ليست تفاصيل تافهة، بل هي الفارق بين تطبيق سريع وآخر بطيء.
لنبدأ بالأساسيات الصحيحة. أولاً، نختار الـ Bundler: Vite أم Webpack؟ في عام 2024، الإجابة واضحة: Vite. لماذا؟ لأن Vite يستخدم ES Modules مباشرة في التطوير، مما يعني أن تغيير ملف واحد يعيد تحميل المكون فقط دون إعادة بناء المشروع بالكامل. في مشروع متوسط الحجم، هذا يقلل وقت التطوير من 5 ثوانٍ إلى 50 مللي ثانية. لكن Webpack لا يزال له مكان في المشاريع الكبيرة التي تحتاج إلى تحسينات متقدمة مثل Code Splitting الديناميكي أو تحليل الـ Bundle. في هذا الدليل، سنستخدم Vite لأنه الخيار الأمثل للمشاريع الجديدة.
# إنشاء مشروع Vue 3 باستخدام Vite
npm create vite@latest my-vue-app -- --template vue-ts
cd my-vue-app
npm install
# تثبيت المكتبات الأساسية للإنتاج
npm install vue-router pinia axios lodash
npm install --save-dev @types/lodash @vitejs/plugin-vue @vue/test-utils
# تثبيت أدوات الجودة
npm install --save-dev eslint prettier husky lint-staged
npx husky-init && npm install
npx lint-staged --installلاحظ أننا أضفنا TypeScript منذ البداية. قد يبدو الأمر معقداً في البداية، لكنه يوفر ساعات من تصحيح الأخطاء لاحقاً. مثلاً، عندما تكتب props في مكون، سيخبرك TypeScript فوراً إذا كان النوع غير متوافق، بدلاً من اكتشاف الخطأ في وقت التشغيل. أيضاً، أضفنا husky وlint-staged لضمان أن الكود المرسل إلى المستودع لا يحتوي على أخطاء تنسيق أو مشاكل في الـ Linting. هذه الأدوات ليست رفاهية، بل هي ضرورية في أي مشروع إنتاجي.
الـ Dev Server الافتراضي لـ Vite جيد، لكنه ليس مثالياً للإنتاج. مثلاً، لا يدعم الـ HTTPS بشكل افتراضي، وهو أمر ضروري إذا كنت تختبر ميزات مثل الـ Service Workers أو الـ Geolocation API. أيضاً، لا يأتي مع إعدادات مخصصة للـ CORS، مما قد يسبب مشاكل عند الاتصال بـ APIs خارجية. إليك كيفية تهيئته بشكل صحيح:
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
https: true, // تفعيل HTTPS
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
},
port: 3000,
strictPort: true // يمنع تغيير المنفذ إذا كان مشغولاً
},
build: {
outDir: 'dist',
emptyOutDir: true,
sourcemap: true // مهم لتصحيح الأخطاء في الإنتاج
}
})لاحظ أننا أضفنا alias لـ @ ليشير إلى مجلد src. هذا يجعل الاستيرادات أكثر نظافة وأقل عرضة للأخطاء. مثلاً، بدلاً من كتابة ../../components/Button، يمكنك كتابة @/components/Button. أيضاً، أضفنا sourcemap في إعدادات البناء، وهو أمر ضروري لتصحيح الأخطاء في الإنتاج. بدونها، سترى أخطاء تشير إلى سطور غير موجودة في الكود الأصلي، مما يجعل تصحيح الأخطاء شبه مستحيل.
الكثير يتبع بنية المشروع التي يولدها create-vue بدون تفكير: components، views، router، store. لكن هذه البنية ليست مثالية لكل مشروع. مثلاً، في تطبيق معقد يحتوي على ات المكونات، قد تجد نفسك تبحث عن مكون محدد بين مجلدات متداخلة. أو قد تجد أن مجلد views يحتوي على مكونات كبيرة ومعقدة يصعب صيانتها. الحل؟ تصميم بنية تناسب حجم وتعقيد مشروعك.
في مشاريع الإنتاج، أفضل استخدام بنية تعتمد على الميزة (Feature-based) بدلاً من النوع (Type-based). مثلاً، بدلاً من فصل جميع المكونات في مجلد components، نقسم المشروع حسب الميزات مثل auth، dashboard، settings. كل ميزة تحتوي على مكوناتها، اختباراتها، وأنماطها الخاصة. هذا يجعل المشروع أكثر قابلية للتوسع ويسهل على الفرق الكبيرة العمل على ميزات مختلفة دون تعارضات. إليك مثال على بنية مشروع متوسط الحجم:
src/
├── assets/
│ ├── fonts/
│ └── images/
├── common/
│ ├── components/ # مكونات مشتركة مثل الأزرار والنماذج
│ ├── composables/ # الـ Composable Functions المشتركة
│ ├── utils/ # دوال مساعدة
│ └── styles/ # أنماط عامة
├── features/
│ ├── auth/
│ │ ├── components/
│ │ ├── composables/
│ │ ├── stores/
│ │ ├── types/
│ │ └── index.ts
│ ├── dashboard/
│ │ ├── components/
│ │ ├── ...
│ └── settings/
├── router/
│ └── index.ts
├── stores/
│ └── index.ts
├── App.vue
└── main.tsلاحظ أننا فصلنا المكونات المشتركة في مجلد common/components، بينما المكونات الخاصة بكل ميزة موجودة داخل مجلد الميزة. هذا يجعل من السهل حذف ميزة كاملة دون التأثير على باقي المشروع. أيضاً، أضفنا مجلد types لكل ميزة لتخزين الـ Interfaces وTypes الخاصة بها، مما يجعل الكود أكثر تنظيماً وأسهل في الصيانة.
في Vue 3، لديك خياران لإدارة الحالة: استخدام Pinia (الخليفة الرسمي لـ Vuex) أو إدارة الحالة محلياً باستخدام Composition API. الخيار ليس عشوائياً، بل يعتمد على حجم وتعقيد التطبيق. مثلاً، في تطبيق صغير يحتوي على بضعة مكونات، قد لا تحتاج إلى Pinia على الإطلاق. لكن في تطبيق يحتوي على عشرات المكونات التي تشارك نفس الحالة، فإن استخدام Pinia سيوفر عليك ساعات من تصحيح الأخطاء الناتجة عن تمرير الـ Props بشكل متكرر.
لنأخذ مثالاً عملياً: تطبيق إدارة مهام يحتوي على قائمة مهام يمكن إضافتها، حذفها، وتعديل حالتها. إذا كنت تستخدم Composition API المحلي، قد ينتهي بك الأمر بمكون مثل هذا:
setup lang="ts">
import { ref } from 'vue'
interface Task {
id: number
title: string
completed: boolean
}
const tasks = ref<Task[]>([])
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 deleteTask(id: number) {
tasks.value = tasks.value.filter(task => task.id !== id)
}
</script>
<template>
<div>
<input v-model="newTask" @keyup.enter="addTask(newTask); newTask = ''" />
<ul>
<li v-for="task in tasks" :key="task.id">
<input type="checkbox" v-model="task.completed" @change="toggleTask(task.id)" />
{{ task.title }}
<button @click="deleteTask(task.id)">حذف</button>
</li>
</ul>
</div>
</template>هذا الكود يعمل بشكل جيد لتطبيق صغير، لكن ماذا لو احتجت إلى مشاركة هذه الحالة بين عدة مكونات؟ ستضطر إلى تمرير الـ Props بشكل متكرر، أو استخدام provide/inject، مما يجعل الكود معقداً وصعب الصيانة. هنا يأتي دور Pinia. إليك نفس المثال باستخدام Pinia:
// stores/tasks.ts
import { defineStore } from 'pinia'
interface Task {
id: number
title: string
completed: boolean
}
export const useTasksStore = defineStore('tasks', {
state: () => ({
tasks: [] as Task[]
}),
actions: {
addTask(title: string) {
this.tasks.push({
id: Date.now(),
title,
completed: false
})
},
toggleTask(id: number) {
const task = this.tasks.find(task => task.id === id)
if (task) task.completed = !task.completed
},
deleteTask(id: number) {
this.tasks = this.tasks.filter(task => task.id !== id)
}
}
})الآن يمكنك استخدام هذا المتجر في أي مكون بسهولة:
setup lang="ts">
import { useTasksStore } from '@/stores/tasks'
const tasksStore = useTasksStore()
const newTask = ref('')
</script>
<template>
<div>
<input v-model="newTask" @keyup.enter="tasksStore.addTask(newTask); newTask = ''" />
<ul>
<li v-for="task in tasksStore.tasks" :key="task.id">
<input type="checkbox" v-model="task.completed" @change="tasksStore.toggleTask(task.id)" />
{{ task.title }}
<button @click="tasksStore.deleteTask(task.id)">حذف</button>
</li>
</ul>
</div>
</template>الفرق واضح: الكود أصبح أكثر نظافة وأسهل في الصيانة. أيضاً، Pinia يوفر ميزات إضافية مثل الـ DevTools Integration، مما يجعل تصحيح الأخطاء أسهل بكثير. لكن تذكر أن Pinia يضيف حجماً إلى الـ Bundle، لذا استخدمه فقط عندما تحتاج إليه حقاً.
أحد أكبر الأخطاء التي أراها في تطبيقات Vue هو تحميل البيانات بشكل كامل في الذاكرة دون تفكير. مثلاً، تطبيق يعرض قائمة عملاء تحتوي على 10 آلاف صف. إذا قمت بتحميل جميع البيانات دفعة واحدة، سيستهلك التطبيق مئات الميجابايتات من الرام، وسيتجمد المتصفح عند محاولة التمرير. الحل؟ التحميل الكسول (Lazy Loading) والتقسيم (Pagination).
لنأخذ مثالاً عملياً: جدول بيانات يعرض قائمة عملاء. بدلاً من تحميل جميع البيانات دفعة واحدة، سنستخدم مكتبة مثل vue-virtual-scroller لعرض الصفوف المرئية فقط. هذا يقلل من استخدام الذاكرة بشكل كبير ويحسن أداء التمرير. إليك كيفية تنفيذه:
setup lang="ts">
import { ref, onMounted } from 'vue'
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
interface Customer {
id: number
name: string
email: string
phone: string
}
const customers = ref<Customer[]>([])
const loading = ref(false)
const page = ref(1)
const pageSize = ref(20)
const total = ref(0)
async function fetchCustomers() {
loading.value = true
try {
const resp await fetch(`https://api.example.com/customers?page=${page.value}&pageSize=${pageSize.value}`)
const data = await response.json()
customers.value = [...customers.value, ...data.items]
total.value = data.total
} catch (error) {
console.error('Failed to fetch customers:', error)
} finally {
loading.value = false
}
}
function loadMore() {
if (customers.value.length < total.value) {
page.value++
fetchCustomers()
}
}
onMounted(fetchCustomers)
</script>
<template>
<div class="customer-table">
<RecycleScroller
class="scroller"
:items="customers"
:item-size="50"
key-field="id"
v-slot="{ item }"
@bottom="loadMore"
>
<div class="customer-row">
<div>{{ item.name }}</div>
<div>{{ item.email }}</div>
<div>{{ item.phone }}</div>
</div>
</RecycleScroller>
<div v-if="loading" class="loading">جارٍ التحميل...</div>
</div>
</template>
<style scoped>
.customer-table {
height: 500px;
overflow-y: auto;
}
.customer-row {
display: flex;
padding: 10px;
border-bottom: 1px solid #eee;
}
.loading {
padding: 10px;
text-align: center;
}
</style>لاحظ أننا استخدمنا RecycleScroller لعرض الصفوف المرئية فقط، مما يقلل من عدد العناصر في الـ DOM بشكل كبير. أيضاً، استخدمنا التحميل الكسول عند الوصول إلى نهاية القائمة (loadMore). هذا يجعل التطبيق سريعاً حتى مع كميات كبيرة من البيانات. لكن تذكر أن التحميل الكسول له تحدياته الخاصة، مثل إدارة حالة التحميل وتجنب الطلبات المكررة.
Vue يستخدم الـ Virtual DOM لتحسين أداء الـ Rendering، لكنه ليس سحرياً. إذا كتبت كوداً غير فعال، سيصبح التطبيق بطيئاً حتى مع الـ Virtual DOM. مثلاً، استخدام v-for مع قوائم كبيرة بدون key، أو استخدام computed properties مع دوال ثقيلة، يمكن أن يسبب مشاكل أداء كبيرة.
إليك بعض النصائح لتحسين أداء الـ Rendering:
لنأخذ مثالاً على استخدام v-memo:
<template>
<div>
<div v-for="item in items" :key="item.id" v-memo="[item.id, item.name]">
{{ item.name }} - {{ item.value }}
</div>
</div>
</template>
setup lang="ts">
import { ref } from 'vue'
const items = ref([
{ id: 1, name: 'Item 1', value: 100 },
{ id: 2, name: 'Item 2', value: 200 },
{ id: 3, name: 'Item 3', value: 300 }
])
// تغيير قيمة عنصر دون تغيير الاسم
function updateValue() {
items.value[0].value = 400
}
</script>في هذا المثال، استخدمنا v-memo مع مصفوفة تحتوي على item.id وitem.name. هذا يعني أن العنصر لن يعاد الـ Rendering إلا إذا تغيرت قيمة id أو name. حتى إذا تغيرت قيمة value، فلن يعاد الـ Rendering، مما يحسن الأداء بشكل كبير في القوائم الكبيرة.
الكثير يعتقد أن النشر على الإنتاج هو مجرد تشغيل npm run build ثم رفع الملفات إلى السيرفر. لكن الحقيقة أكثر تعقيداً. مثلاً، كيف تضمن أن النسخة الجديدة لا تحتوي على أخطاء؟ كيف تتعامل مع الـ Rollback إذا حدث خطأ؟ كيف تضمن أن المستخدمين لا يرون صفحة بيضاء أثناء التحديث؟ هذه الأسئلة ليست نظرية، بل هي مشاكل حقيقية تواجهها في الإنتاج.
لنبدأ بالأساسيات: بناء التطبيق للإنتاج. عندما تقوم بتشغيل npm run build، يحدث الكثير خلف الكواليس: Vite يقوم بتحسين الكود، تقليل حجم الملفات، وتحليل الـ Bundle. لكن هذا ليس كافياً. إليك بعض الخطوات الإضافية التي يجب اتخاذها:
إليك مثال على كيفية إعداد ملفات البيئة:
# .env.development
VITE_API_URL=http://localhost:3000/api
VITE_APP_MODE=development
# .env.production
VITE_API_URL=https://api.example.com
VITE_APP_MODE=productionثم في الكود، يمكنك الوصول إلى هذه المتغيرات باستخدام import.meta.env:
const apiUrl = import.meta.env.VITE_API_URL
const appMode = import.meta.env.VITE_APP_MODEالنشر اليدوي خطير ومضيعة للوقت. بدلاً من ذلك، استخدم نظام CI/CD مثل GitHub Actions أو GitLab CI لنشر التطبيق تلقائياً عند كل تغيير في المستودع. إليك مثال على ملف GitHub Actions لنشر تطبيق Vue على Vercel:
name: Deploy to Vercel
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: npm install
- name: Run tests
run: npm test
- name: Build project
run: npm run build
- name: Deploy to Vercel
run: npx vercel --prod --token=${{ secrets.VERCEL_TOKEN }}
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}هذا الملف يفعل عدة أشياء مهمة: أولاً، يتحقق من الكود من المستودع. ثم، يقوم بتثبيت Node.js والـ Dependencies. بعد ذلك، يقوم بتشغيل الاختبارات لضمان أن الكود لا يحتوي على أخطاء. أخيراً، يقوم ببناء المشروع ونشره على Vercel. لاحظ أننا نستخدم secrets لتخزين الـ Tokens الحساسة، مما يمنع تسربها في سجلات الـ CI.
عند نشر نسخة جديدة من التطبيق، هناك احتمال أن المستخدمين يرون صفحة بيضاء إذا حدث خطأ أثناء التحميل. الحل؟ استخدام استراتيجيات النشر الذكية مثل الـ Blue-Green Deployment أو الـ Canary Releases. لكن أبسط حل هو استخدام ميزة الـ Service Worker لتخزين النسخة السابقة من التطبيق مؤقتاً.
إليك كيفية إعداد Service Worker باستخدام Workbox:
// vite.config.ts
import { defineConfig } from 'vite'
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
VitePWA({
registerType: 'autoUpdate',
includeAssets: ['favicon.ico', 'apple-touch-icon.png'],
manifest: {
name: 'My Vue App',
short_name: 'VueApp',
description: 'My Awesome Vue App',
theme_color: '#ffffff',
icons: [
{
src: 'pwa-192x192.png',
sizes: '192x192',
type: 'image/png'
},
{
src: 'pwa-512x512.png',
sizes: '512x512',
type: 'image/png'
}
]
},
workbox: {
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: {
maxEntries: 10,
maxAgeSeconds: 60 * 60 * 24 // 24 ساعة
}
}
},
{
urlPattern: /\.(?:png|jpg|jpeg|svg|gif)$/i,
handler: 'CacheFirst',
options: {
cacheName: 'image-cache',
expiration: {
maxEntries: 50,
maxAgeSeconds: 60 * 60 * 24 * 7 // أسبوع
}
}
}
]
}
})
]
})هذا الإعداد يفعل عدة أشياء: أولاً، يسجل Service Worker تلقائياً عند تحميل الصفحة. ثم، يحدد استراتيجيات التخزين المؤقت للملفات الثابتة وAPIs. مثلاً، يستخدم NetworkFirst للتخزين المؤقت لـ APIs، مما يعني أنه سيحاول أولاً الحصول على البيانات من الشبكة، وإذا فشل، سيستخدم النسخة المخزنة مؤقتاً. أما بالنسبة للصور، يستخدم CacheFirst للحصول على الصور من التخزين المؤقت أولاً، مما يحسن أداء التحميل.
الأمان ليس شيئاً تضيفه في النهاية، بل هو جزء من عملية التطوير منذ البداية. مثلاً، إذا كنت تستخدم Vue مع API خارجي، كيف تضمن أن البيانات المرسلة والمستقبلة آمنة؟ كيف تمنع هجمات XSS وCSRF؟ كيف تخزن الـ Tokens الحساسة بشكل آمن؟ هذه ليست أسئلة نظرية، بل هي مشاكل حقيقية تواجهها في الإنتاج.
لنبدأ بأهم قاعدة: لا تثق أبداً بالبيانات القادمة من المستخدم. حتى إذا كنت تستخدم Vue، الذي يحمي من XSS بشكل افتراضي، يجب أن تكون حذراً عند عرض البيانات الديناميكية. مثلاً، إذا كنت تعرض محتوى من API خارجي، يجب تطهيره قبل عرضه. إليك كيفية استخدام مكتبة DOMPurify لتطهير المحتوى:
setup lang="ts">
import { ref, onMounted } from 'vue'
import DOMPurify from 'dompurify'
const userC ref('')
onMounted(async () => {
const response = await fetch('https://api.example.com/user-content')
const data = await response.json()
userContent.value = DOMPurify.sanitize(data.content)
})
</script>
<template>
<div v-html="userContent"></div>
</template>لاحظ أننا استخدمنا v-html لعرض المحتوى، لكننا قمنا بتطهيره أولاً باستخدام DOMPurify. هذا يمنع هجمات XSS عن طريق إزالة أي أكواد JavaScript ضارة من المحتوى.
عند الاتصال بـ API خارجي، يجب أن تكون حذراً جداً مع الـ Tokens الحساسة. مثلاً، تخزين الـ Token في localStorage غير آمن لأنه يمكن الوصول إليه عبر JavaScript. بدلاً من ذلك، استخدم HttpOnly Cookies لتخزين الـ Tokens، مما يمنع الوصول إليها عبر JavaScript. إليك كيفية إعداد axios لاستخدام HttpOnly Cookies:
// axios.ts
import axios from 'axios'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL,
withCredentials: true // يسمح بإرسال واستقبال الكوكيز
})
// إضافة Interceptor لإضافة الـ Token إلى كل طلب
api.interceptors.request.use((config) => {
// إذا كنت تستخدم JWT في localStorage (غير موصى به)
// const token = localStorage.getItem('token')
// if (token) {
// config.headers.Authorization = `Bearer ${token}`
// }
return config
})
// إضافة Interceptor لمعالجة الأخطاء
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// إعادة التوجيه إلى صفحة تسجيل الدخول
window.location.href = '/login'
}
return Promise.reject(error)
}
)
export default apiلاحظ أننا استخدمنا withCredentials: true للسماح بإرسال واستقبال الكوكيز. هذا يعني أن السيرفر يجب أن يرسل الـ Token في كوكي HttpOnly، وليس في الـ Response Body. أيضاً، أضفنا Interceptor لمعالجة الأخطاء، مثل إعادة التوجيه إلى صفحة تسجيل الدخول إذا تلقينا خطأ 401.
هجمات CSRF تحدث عندما يرسل المهاجم طلبات نيابة عن المستخدم دون علمه. لمنع هذه الهجمات، يجب استخدام CSRF Tokens. إليك كيفية إعدادها في تطبيق Vue:
// في مكون تسجيل الدخول
setup lang="ts">
import { ref } from 'vue'
import api from '@/axios'
const email = ref('')
const password = ref('')
const csrfToken = ref('')
async function login() {
try {
// أولاً، الحصول على CSRF Token
const csrfResp await api.get('/csrf-token')
csrfToken.value = csrfResponse.data.csrfToken
// ثم، إرسال طلب تسجيل الدخول مع CSRF Token
await api.post('/login', {
email: email.value,
password: password.value
}, {
headers: {
'X-CSRF-Token': csrfToken.value
}
})
} catch (error) {
console.error('Login failed:', error)
}
}
</script>في هذا المثال، نقوم أولاً بالحصول على CSRF Token من السيرفر، ثم نرسل طلب تسجيل الدخول مع هذا Token في الـ Headers. السيرفر سيتحقق من هذا Token قبل معالجة الطلب، مما يمنع هجمات CSRF. لاحظ أن هذا يتطلب إعداد السيرفر لإصدار والتحقق من CSRF Tokens، وهو ما يجب القيام به في الـ Backend.
بناء تطبيق Vue 3 للإنتاج ليس مجرد كتابة مكونات جميلة، بل هو هندسة نظام متكامل يتحمل الضغط، يتوسع مع المستخدمين، ويبقى آمناً. من تجربتي في بناء تطبيقات تعالج آلاف الطلبات يومياً، تعلمت أن التفاصيل الصغيرة تصنع الفارق الكبير. مثلاً، استخدام v-memo في قائمة تحتوي على 10 آلاف عنصر يمكن أن يقلل وقت الـ Rendering من 500 مللي ثانية إلى 50 مللي ثانية. أو استخدام Service Worker يمكن أن يمنع المستخدمين من رؤية صفحة بيضاء أثناء التحديثات.
النصيحة الذهبية التي أتركك بها: لا تترك الإنتاج للصدفة. اختبر تطبيقك تحت ضغط حقيقي، استخدم أدوات تحليل الأداء مثل Lighthouse وWebPageTest، وراقب التطبيق بعد النشر باستخدام أدوات مثل Sentry وLogRocket. أيضاً، لا تخف من إعادة بناء أجزاء من التطبيق إذا أصبحت معقدة جداً. في النهاية، الهدف هو بناء تطبيق يعمل بشكل جيد للمستخدمين، وليس تطبيقاً يتبع أفضل الممارسات دون تفكير.
إذا كنت تريد خطوة تالية، ابدأ بتحليل أداء تطبيقك الحالي باستخدام Lighthouse، وحدد نقطة ضعف واحدة (مثل حجم الـ Bundle أو وقت التحميل) واعمل على تحسينها. ستندهش من الفرق الذي يمكن أن تصنعه التفاصيل الصغيرة.