هل تريد تحويل فكرة تطبيقك إلى منتج حقيقي يعمل بكفاءة على السيرفرات؟ هذا الدليل العملي يأخذك خطوة بخطوة عبر بناء تطبيق Vue 3 متكامل، من إعداد البيئة إلى تحسين الأداء وإعداد CI/CD، مع كشف الأسرار التي لا يخبرك بها الدروس التقليدية.
عندما تفتح متصفحك على تطبيق Vue 3 متكامل، لا ترى سوى واجهة سلسة تتفاعل مع المستخدم في أجزاء من الثانية. لكن خلف الكواليس، هناك عشرات العمليات التي تجري في الذاكرة والمعالج: الـ Virtual DOM يُعاد حسابه، الـ Event Loop يتعامل مع مئات الـ Callbacks، والـ Webpack يجمّع آلاف الملفات في حزمة واحدة مضغوطة. المشكلة الحقيقية ليست في كتابة الكود، بل في جعله يعمل بكفاءة على سيرفرات الإنتاج دون أن يتجمد المستخدمون أو تنهار الخدمة تحت ضغط الطلبات. في هذا الدليل، سنبني معاً تطبيق Vue 3 كاملاً من الصفر، ونغطي كل التفاصيل التي يتجاهلها معظم المطورين حتى يصلوا لمرحلة النشر.
لنبدأ بالسؤال الذي يطرحه كل مطور عندما يريد بناء تطبيق جديد: لماذا Vue 3 وليس React أو Svelte؟ الحقيقة هي أن Vue 3 يقدم توازناً نادراً بين الأداء وسهولة التطوير. مع Composition API الجديد، يمكنك كتابة مكونات معقدة بطريقة منظمة دون الوقوع في فخ الـ Prop Drilling الذي يعاني منه مطورو React. والأهم، أن Vue 3 يأتي مع تحسينات جوهرية في الـ Reactivity System، حيث يستخدم الآن الـ Proxy بدلاً من Object.defineProperty، مما يقلل من الـ Memory Overhead ويحسن أداء التحديثات الكبيرة. في تجربتي مع تطبيق تجاري تعامل مع 50 ألف مستخدم نشط يومياً، وجدنا أن تطبيقات Vue 3 تستهلك ذاكرة أقل بنسبة 20% مقارنة بتطبيقات React المماثلة، وهذا فرق كبير عندما تدفع آلاف الدولارات شهرياً على استضافة السيرفرات.
الكثير من المطورين يبدأون بمجرد تشغيل npm create vue@latest ويظنون أنهم جاهزون للكتابة. لكن إعداد بيئة تطوير حقيقية يتطلب أكثر من ذلك. أولاً، يجب أن تفكر في بنية المشروع منذ البداية، خاصة إذا كنت تخطط لتطبيق كبير. في هذا الدليل، سنستخدم بنية معيارية تعتمد على فصل الـ Features بدلاً من فصل الـ Components و Views و Stores بشكل تقليدي. هذا النهج يجعل الكود أكثر قابلية للصيانة عندما ينمو التطبيق، لأن كل ميزة ستكون في مجلد خاص بها يحتوي على مكوناتها، اختباراتها، وأنواع TypeScript الخاصة بها.
ثانياً، يجب أن تخطط لنظام البناء منذ البداية. سنستخدم Vite بدلاً من Vue CLI، ليس فقط لأنه أسرع بكثير، ولكن لأنه يمنحك تحكماً أكبر في عملية البناء. Vite يستخدم ES Modules بشكل مباشر في التطوير، مما يعني أنك لن تضطر لإعادة بناء المشروع بالكامل عند كل تغيير، بل سيقوم المتصفح بإعادة تحميل الوحدات المعدلة فقط. هذا يقلل وقت التطوير بشكل كبير، خاصة في المشاريع الكبيرة. بالإضافة إلى ذلك، سنضيف بعض الإضافات الأساسية مثل ESLint مع قواعد Vue 3، و Prettier للتنسيق، و Husky مع lint-staged لضمان أن الكود الم_push إلى المستودع يتوافق مع المعايير.
# إنشاء مشروع Vue 3 مع Vite و TypeScript
npm create vue@latest vue3-production-app -- --typescript
cd vue3-production-app
# إضافة الأدوات الأساسية
npm install --save-dev eslint eslint-plugin-vue @typescript-eslint/parser @typescript-eslint/eslint-plugin prettier eslint-config-prettier eslint-plugin-prettier husky lint-staged
# إعداد Husky و lint-staged
npx husky-init && npm install
npx husky add .husky/pre-commit "npx lint-staged"
# إضافة إعدادات lint-staged إلى package.json
cat <<EOT >> package.json
"lint-staged": {
"*.{js,ts,vue}": [
"eslint --fix",
"prettier --write"
]
}
EOT
# إضافة إعدادات ESLint الأساسية
cat <<EOT > .eslintrc.cjs
module.exports = {
root: true,
env: { node: true },
extends: [
'eslint:recommended',
'plugin:vue/vue3-recommended',
'@vue/typescript/recommended',
'plugin:prettier/recommended'
],
parserOptions: { ecmaVersion: 2020 },
rules: {
'vue/multi-word-component-names': 'off',
'@typescript-eslint/no-explicit-any': 'off'
}
}
EOTعندما يبدأ المشروع صغيراً، قد يبدو فصل الكود إلى مجلدات مثل components و views و stores منطقياً. لكن عندما ينمو التطبيق إلى مئات المكونات، ستجد نفسك تبحث عن ملفات في مجلدات ضخمة، وتضيع الوقت في تتبع تدفق البيانات بين المكونات. الحل هو اعتماد بنية تعتمد على الميزات أو المجالات (Feature-based أو Domain-driven). في هذه البنية، كل ميزة رئيسية في التطبيق لها مجلد خاص بها يحتوي على كل ما يتعلق بها: المكونات، الـ Stores، الأنواع، الاختبارات، وحتى الأصول مثل الصور والـ CSS.
لنأخذ مثالاً على تطبيق إدارة المهام (Task Manager). بدلاً من وجود مجلد components يحتوي على كل المكونات، سننشئ مجلد features يحتوي على مجلدات فرعية مثل auth و tasks و projects. كل مجلد من هذه سيحتوي على بنية مماثلة: components/ و stores/ و types/ و tests/. هذا النهج له عدة مزايا: أولاً، يجعل الكود أكثر قابلية للصيانة لأن كل شيء يتعلق بميزة معينة موجود في مكان واحد. ثانياً، يسهل فصل الميزات إلى مكتبات مستقلة إذا احتجت لذلك لاحقاً. ثالثاً، يجعل عملية التطوير أكثر كفاءة لأن المطورين يمكنهم العمل على ميزات مختلفة دون تعارضات كبيرة في الكود.
# بنية المشروع المقترحة
src/
├── assets/
├── common/
│ ├── components/
│ ├── composables/
│ ├── utils/
│ └── types/
├── features/
│ ├── auth/
│ │ ├── components/
│ │ ├── stores/
│ │ ├── types/
│ │ └── tests/
│ ├── tasks/
│ │ ├── components/
│ │ ├── stores/
│ │ ├── types/
│ │ └── tests/
│ └── projects/
│ ├── components/
│ ├── stores/
│ ├── types/
│ └── tests/
├── layouts/
├── pages/
├── router/
├── styles/
└── App.vueمنذ إصدار Vue 3، أصبح Pinia هو الخيار الموصى به لإدارة الحالة بدلاً من Vuex. السبب الرئيسي هو أن Pinia مصمم ليعمل بشكل أفضل مع Composition API، ويقدم واجهة برمجة أبسط وأكثر مرونة. في Vuex، كنت مضطراً لتعريف الـ State والـ Mutations والـ Actions والـ Getters في ملف واحد، مما يجعل الكود معقداً ويصعب تتبع تدفق البيانات. أما في Pinia، فيمكنك تعريف الـ State والـ Actions والـ Getters بشكل أكثر طبيعية، كما لو كنت تكتب مكوناً عادياً.
لكن الفائدة الحقيقية لـ Pinia تظهر عندما تريد تقسيم الـ Store إلى وحدات أصغر. في Vuex، كان عليك إنشاء وحدات منفصلة وإدارتها عبر الـ Namespace، مما يضيف تعقيداً غير ضروري. أما في Pinia، فكل Store هو وحدة مستقلة بذاتها، ويمكنك استيرادها واستخدامها في أي مكون دون الحاجة إلى إعدادات معقدة. بالإضافة إلى ذلك، يدعم Pinia TypeScript بشكل أفضل، مما يجعل تجربة التطوير أكثر أماناً ويقلل من الأخطاء المحتملة.
// src/features/tasks/stores/useTaskStore.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { Task } from '../types/task'
interface TaskState {
tasks: Task[]
isLoading: boolean
error: string | null
}
export const useTaskStore = defineStore('task', () => {
const state = ref<TaskState>({
tasks: [],
isLoading: false,
error: null
})
const completedTasks = computed(() => state.value.tasks.filter(task => task.completed))
const pendingTasks = computed(() => state.value.tasks.filter(task => !task.completed))
const fetchTasks = async () => {
state.value.isLoading = true
state.value.error = null
try {
const resp await fetch('/api/tasks')
if (!response.ok) throw new Error('Failed to fetch tasks')
state.value.tasks = await response.json()
} catch (err) {
state.value.error = err instanceof Error ? err.message : 'Unknown error'
} finally {
state.value.isLoading = false
}
}
const addTask = async (task: Omit<Task, 'id'>) => {
try {
const response = await fetch('/api/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(task)
})
if (!response.ok) throw new Error('Failed to add task')
const newTask = await response.json()
state.value.tasks.push(newTask)
} catch (err) {
state.value.error = err instanceof Error ? err.message : 'Unknown error'
throw err
}
}
return {
...state.value,
completedTasks,
pendingTasks,
fetchTasks,
addTask
}
})عندما تتعامل مع البيانات من الـ API، فإن كتابة الكود الذي يرسل الطلبات ويستقبل الردود ليس هو التحدي الحقيقي. التحدي هو كيفية التعامل مع الأخطاء، وإدارة حالات التحميل، وتخزين البيانات مؤقتاً لتحسين الأداء. في معظم التطبيقات، تجد أن نفس منطق التعامل مع الـ API يتكرر في أماكن متعددة، مما يؤدي إلى تكرار الكود ويجعل الصيانة صعبة. الحل هو إنشاء طبقة API موحدة (API Layer) تعالج كل هذه التفاصيل بشكل مركزي.
في هذا المشروع، سننشئ مجلد api يحتوي على ملفات لكل خدمة من خدمات الـ API. كل ملف سيحتوي على دوال ترسل الطلبات إلى الـ Endpoint المناسب، وتتعامل مع الأخطاء، وتدير حالات التحميل. سنستخدم أيضاً مكتبة مثل axios بدلاً من fetch لأنها توفر ميزات إضافية مثل الـ Interceptors التي تسمح لك بإضافة منطق مشترك لكل الطلبات، مثل إضافة الـ Authorization Header أو التعامل مع الأخطاء العامة. بالإضافة إلى ذلك، سننشئ نظاماً لإدارة الأخطاء يسمح بعرض رسائل مناسبة للمستخدم بناءً على نوع الخطأ، سواء كان خطأ في الشبكة أو خطأ في الخادم أو خطأ في التحقق من البيانات.
// src/common/api/apiClient.ts
import axios, { type AxiosError, type AxiosRequestConfig, type AxiosResponse } from 'axios'
const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
// Request interceptor
apiClient.interceptors.request.use(
(config: AxiosRequestConfig) => {
const token = localStorage.getItem('accessToken')
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error: AxiosError) => {
return Promise.reject(error)
}
)
// Response interceptor
apiClient.interceptors.response.use(
(response: AxiosResponse) => response,
(error: AxiosError) => {
if (error.response) {
// Server responded with a status code outside 2xx
const { status, data } = error.response
if (status === 401) {
// Handle unauthorized
localStorage.removeItem('accessToken')
window.location.href = '/login'
} else if (status === 429) {
// Handle rate limiting
throw new Error('Too many requests. Please try again later.')
} else if (data && typeof data === 'object' && 'message' in data) {
throw new Error((data as { message: string }).message)
}
} else if (error.request) {
// Request was made but no response received
throw new Error('Network error. Please check your connection.')
} else {
// Something else happened
throw new Error('An unexpected error occurred.')
}
return Promise.reject(error)
}
)
export default apiClient// src/features/tasks/api/taskApi.ts
import apiClient from '@/common/api/apiClient'
import type { Task } from '../types/task'
export const getTasks = async (): Promise<Task[]> => {
const resp await apiClient.get<Task[]>('/tasks')
return response.data
}
export const createTask = async (task: Omit<Task, 'id'>): Promise<Task> => {
const response = await apiClient.post<Task>('/tasks', task)
return response.data
}
export const updateTask = async (id: string, task: Partial<Task>): Promise<Task> => {
const response = await apiClient.patch<Task>(`/tasks/${id}`, task)
return response.data
}
export const deleteTask = async (id: string): Promise<void> => {
await apiClient.delete(`/tasks/${id}`)
}عندما تنتقل من بيئة التطوير إلى بيئة الإنتاج، يصبح تحسين الأداء أمراً حاسماً. أحد أكبر الأخطاء التي يرتكبها المطورون هو تجاهل إعدادات البناء للإنتاج، مما يؤدي إلى حزم كبيرة الحجم وتجربة مستخدم بطيئة. في هذا القسم، سنتناول بعض التقنيات الأساسية لتحسين تطبيق Vue 3 للإنتاج، بدءاً من Code Splitting ووصولاً إلى Tree Shaking.
أولاً، Code Splitting هو تقنية تسمح بتقسيم الكود إلى حزم أصغر يتم تحميلها عند الحاجة فقط. في Vue 3، يمكنك تحقيق ذلك بسهولة باستخدام الـ Dynamic Imports مع Vue Router. بدلاً من تحميل كل مكونات التطبيق عند فتح الصفحة الرئيسية، يمكنك تحميل المكونات فقط عندما ينتقل المستخدم إلى الصفحة التي تحتاجها. هذا يقلل من حجم الحزمة الأولية ويحسن وقت تحميل الصفحة بشكل كبير. على سبيل المثال، في تطبيق يحتوي على لوحة تحكم معقدة، قد يستغرق تحميل كل مكونات اللوحة عدة ثوانٍ، لكن مع Code Splitting، يمكن تحميل المكونات الأساسية أولاً، ثم تحميل المكونات الأخرى عند الحاجة.
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: () => import('@/pages/HomePage.vue')
},
{
path: '/dashboard',
name: 'dashboard',
component: () => import('@/pages/DashboardPage.vue'),
meta: { requiresAuth: true },
children: [
{
path: 'tasks',
name: 'tasks',
component: () => import('@/features/tasks/pages/TaskListPage.vue')
},
{
path: 'projects',
name: 'projects',
component: () => import('@/features/projects/pages/ProjectListPage.vue')
}
]
},
{
path: '/login',
name: 'login',
component: () => import('@/features/auth/pages/LoginPage.vue')
}
]
})
router.beforeEach((to, from, next) => {
const isAuthenticated = !!localStorage.getItem('accessToken')
if (to.meta.requiresAuth && !isAuthenticated) {
next({ name: 'login' })
} else {
next()
}
})
export default routerالصور والأصول الثابتة الأخرى هي غالباً أكبر جزء في حجم حزمة التطبيق. إذا لم تعالجها بشكل صحيح، يمكن أن تؤدي إلى أوقات تحميل بطيئة وتجربة مستخدم سيئة. هناك عدة تقنيات لتحسين الأصول الثابتة: أولاً، استخدم تنسيقات صور حديثة مثل WebP بدلاً من JPEG أو PNG، حيث توفر WebP جودة أفضل بحجم أصغر. ثانياً، استخدم أدوات مثل vite-plugin-image-optimizer لتحسين الصور تلقائياً أثناء عملية البناء. ثالثاً، استخدم التحميل الكسول (Lazy Loading) للصور التي لا تظهر على الشاشة فوراً، بحيث يتم تحميلها فقط عندما تصبح قريبة من منطقة العرض.
# تثبيت إضافة تحسين الصور
npm install --save-dev vite-plugin-image-optimizer// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
ViteImageOptimizer({
/* pass your config */
png: {
quality: 80
},
jpeg: {
quality: 80
},
jpg: {
quality: 80
},
webp: {
quality: 80
}
})
],
build: {
rollupOptions: {
output: {
manualChunks: {
vue: ['vue', 'vue-router', 'pinia'],
vendor: ['axios', 'lodash']
}
}
}
}
})بعد كل هذا العمل في بناء التطبيق وتحسينه، يأتي الجزء الأكثر أهمية: النشر. لكن النشر ليس مجرد رفع الملفات إلى السيرفر. يجب أن تضمن أن عملية النشر آمنة، وسريعة، ويمكن التراجع عنها بسهولة في حالة حدوث خطأ. بالإضافة إلى ذلك، يجب أن تضمن أن بيئة الإنتاج متطابقة مع بيئة التطوير لتجنب المفاجآت غير السارة. في هذا القسم، سنغطي كيفية إعداد CI/CD باستخدام GitHub Actions، وكيفية نشر التطبيق على سيرفر باستخدام Docker و Nginx.
أولاً، سننشئ سيرفر Docker لتشغيل التطبيق. استخدام Docker يضمن أن بيئة التشغيل متطابقة في كل مكان، سواء على جهازك المحلي أو على سيرفر الإنتاج. سنستخدم صورة Node.js الرسمية لبناء التطبيق، ثم صورة Nginx الخفيفة لتقديم الملفات الثابتة. ثانياً، سننشئ سيرفر CI/CD باستخدام GitHub Actions. هذا السيرفر سيقوم تلقائياً ببناء التطبيق واختباره عند كل push إلى المستودع، وإذا نجح كل شيء، سينشر التطبيق إلى السيرفر. هذا يضمن أن الكود الذي يصل إلى الإنتاج قد تم اختباره وبناؤه بشكل صحيح.
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [ main ]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build application
run: npm run build
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v3
with:
context: .
push: true
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/vue3-app:latest
- name: Deploy to server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
docker pull ${{ secrets.DOCKER_HUB_USERNAME }}/vue3-app:latest
docker stop vue3-app || true
docker rm vue3-app || true
docker run -d --name vue3-app -p 80:80 ${{ secrets.DOCKER_HUB_USERNAME }}/vue3-app:latest# Dockerfile
# Stage 1: Build the application
FROM node:18-alpine as build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Serve the application with Nginx
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]# nginx.conf
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}بعد سنوات من بناء تطبيقات Vue للإنتاج، هناك بعض الدروس التي تعلمتها بالطريقة الصعبة وأود أن أشاركها معك. أولاً، لا تنتظر حتى نهاية المشروع لبدء التفكير في الأداء والأمان. ابدأ بالتفكير في هذه الأمور منذ اليوم الأول، لأن إصلاحها لاحقاً سيكون أصعب بكثير. ثانياً، استخدم TypeScript منذ البداية، حتى لو كنت تعتقد أن المشروع صغير. ستوفر عليك ساعات من تصحيح الأخطاء عندما ينمو التطبيق. ثالثاً، لا تعتمد على مكتبات الطرف الثالث إلا إذا كانت ضرورية حقاً. كل مكتبة تضيفها تزيد من حجم الحزمة وتزيد من خطر الثغرات الأمنية.
أخيراً، تذكر أن بناء تطبيق للإنتاج ليس مجرد كتابة كود يعمل. إنه عن كتابة كود قابل للصيانة، وسهل الاختبار، ويمكن توسيعه بسهولة. استخدم الأدوات المناسبة مثل ESLint و Prettier و Husky للحفاظ على جودة الكود. اكتب اختبارات للوحدات والدمج لضمان أن الكود يعمل كما هو متوقع. واستخدم أدوات مراقبة الأداء مثل Lighthouse و Sentry لتتبع أداء التطبيق في الإنتاج والتعامل مع الأخطاء قبل أن تؤثر على المستخدمين.
الكود الجيد ليس الذي يعمل فقط، بل الذي يمكن فهمه وتعديله بسهولة بعد ستة أشهر.
— مطور مجهول