📚معرفی و نصب

Vue.js یک فریم‌ورک تدریجی (Progressive Framework) برای ساخت رابط کاربری است. برخلاف React، نیازی به build step یا JSX ندارد و می‌توان آن را دقیقاً مثل jQuery با یک تگ <script> در صفحه استفاده کرد — و بعداً در صورت نیاز به پروژه‌ای بزرگ با Vite و Single File Component ارتقا داد.

Reactivity بروزرسانی خودکار UI
🧩
کامپوننت‌محور قابل استفادهٔ مجدد
🛠
Directive ها v-if، v-for، v-model
📦
بدون build فقط یک script tag

نصب

<!-- فایل لوکال — قبل از </body> -->
<script src="vue.global.min.js"></script>

<!-- یا CDN -->
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

ساخت اولین اپ

<div id="app">{{ message }}</div>

<script>
const { createApp } = Vue;

createApp({
  data() {
    return { message: 'سلام از Vue!' };
  }
}).mount('#app');
</script>

// یا با Composition API (توصیه‌شده در Vue 3)
const { createApp, ref } = Vue;
createApp({
  setup() {
    const message = ref('سلام از Vue!');
    return { message };
  }
}).mount('#app');
ⓘ نکته: در این آموزش از ساخت global (vue.global.min.js) استفاده می‌کنیم که همهٔ APIها را روی آبجکت سراسری Vue قرار می‌دهد — دقیقاً مثل jQuery، بدون نیاز به import یا bundler.
▶ دمو: اولین اپ Vue
{{ message }}

Reactivity پایه — ref و reactive

هستهٔ Vue سیستم Reactivity آن است: هر وقت دادهٔ reactive تغییر کند، DOM به‌طور خودکار بروزرسانی می‌شود — بدون نیاز به دستکاری دستی DOM.

تابعکاربردخواندن/نوشتن
ref(value)مقادیر ساده (عدد، رشته، بولین)x.value
reactive(obj)آبجکت‌ها و آرایه‌هاobj.prop مستقیم
computed(fn)مقدار مشتق‌شده و کش‌شدهc.value
toRefs(obj)تبدیل خواص reactive به ref های جدا
const { createApp, ref, reactive } = Vue;

createApp({
  setup() {
    // ref — برای مقادیر ساده
    const count = ref(0);
    function increment() { count.value++; } // در JS با .value

    // reactive — برای آبجکت/آرایه
    const user = reactive({ name: 'علی', age: 25 });
    function birthday() { user.age++; } // بدون .value

    return { count, increment, user, birthday };
  }
}).mount('#app');

// در تمپلیت: {{ count }} و {{ user.name }} — بدون .value!
// Vue خودش در تمپلیت unwrap می‌کند
▶ دمو: Reactivity زنده
شمارنده: {{ count }} — سن {{ user.name }}: {{ user.age }}

🛠Directive های اصلی

Directiveتوضیح
v-bind:attr / :attrبایند کردن attribute (کوتاه: :)
v-on:event / @eventگوش‌دادن به رویداد (کوتاه: @)
v-if / v-else-if / v-elseرندر شرطی (حذف/اضافهٔ کامل عنصر)
v-showنمایش شرطی با display:none (همیشه رندر می‌شود)
v-for="item in list"حلقه روی آرایه/آبجکت
v-modelبایند دوطرفهٔ input/select/textarea
v-htmlدرج HTML خام (مراقب XSS باشید)
<!-- v-bind و v-on -->
<img :src="imgUrl" :alt="imgAlt">
<button @click="count++">کلیک</button>

<!-- v-if / v-show -->
<p v-if="isLoggedIn">خوش آمدید</p>
<p v-else>لطفاً وارد شوید</p>
<div v-show="visible">گاهی دیده می‌شود</div>

<!-- v-for با key منحصربه‌فرد -->
<li v-for="item in items" :key="item.id">
  {{ item.name }}
</li>

<!-- کلاس/استایل شرطی -->
<div :class="{ active: isActive, disabled: !enabled }"></div>
▶ دمو: v-if / v-for / :class

این پاراگراف با v-if کنترل می‌شود ✅

  • {{ i+1 }}. {{ f }} (جدیدترین)

📊Computed و Watch

computed — مقدار مشتق‌شده و کش‌شده

const { createApp, ref, computed } = Vue;

createApp({
  setup() {
    const first = ref('علی');
    const last  = ref('رضایی');

    // فقط وقتی first یا last تغییر کند دوباره محاسبه می‌شود
    const fullName = computed(() => first.value + ' ' + last.value);

    return { first, last, fullName };
  }
}).mount('#app');

watch — واکنش به تغییرات

const { createApp, ref, watch } = Vue;

createApp({
  setup() {
    const question = ref('');

    watch(question, (newVal, oldVal) => {
      console.log('تغییر کرد از', oldVal, 'به', newVal);
    });

    // watch روی چند منبع با آرایه
    watch([question, /* ref دیگر */], () => { /* ... */ });

    return { question };
  }
});
💡 computed در برابر watch: اگر مقداری از داده‌های دیگر مشتق می‌شود، از computed استفاده کنید (کش می‌شود). اگر باید هنگام تغییر یک مقدار یک عملیات جانبی (side effect مثل درخواست شبکه) انجام دهید، از watch استفاده کنید.
▶ دمو: نام کامل با computed + لاگ با watch
نام کامل (computed): {{ fullName }}
// تغییرات نام اینجا با watch لاگ می‌شود...

🧩کامپوننت‌ها و Props

کامپوننت‌ها بلوک‌های قابل استفادهٔ مجددِ UI هستند. داده از والد به فرزند با props منتقل می‌شود، و فرزند با emit رویداد به والد خبر می‌دهد.

const { createApp } = Vue;

// تعریف کامپوننت فرزند
const StarRating = {
  props: { modelValue: Number },   // دریافت از والد
  emits: ['update:modelValue'],  // اعلام رویدادهای خروجی
  template: `
    <span v-for="n in 5" :key="n"
          @click="$emit('update:modelValue', n)"
          :style="{color: n <= modelValue ? '#f5b301' : '#ddd'}">★</span>
  `
};

createApp({
  components: { StarRating },
  setup() {
    const rating = Vue.ref(3);
    return { rating };
  }
}).mount('#app');

// استفاده در تمپلیت:
// <star-rating v-model="rating" />
💡 جریان یک‌طرفهٔ داده: Props همیشه از والد به فرزند جریان می‌یابند. فرزند هرگز props را مستقیم تغییر نمی‌دهد — به‌جایش رویداد emit می‌کند تا والد تصمیم بگیرد.
▶ دمو: کامپوننت امتیازدهی ستاره‌ای (Props + Emit)

امتیاز فعلی (در والد): {{ rating }} / ۵

چرخهٔ حیات (Lifecycle Hooks)

Hookزمان اجرا
onMountedبعد از قرار گرفتن کامپوننت در DOM
onUpdatedبعد از هر بروزرسانی DOM ناشی از تغییر داده
onUnmountedبعد از حذف کامپوننت از DOM (پاک‌سازی)
onBeforeMountدرست قبل از رندر اول
const { createApp, onMounted, onUnmounted } = Vue;

const Timer = {
  template: `<div>زمان سپری‌شده: {{ seconds }} ثانیه</div>`,
  setup() {
    const seconds = Vue.ref(0);
    let timer;

    onMounted(() => {
      timer = setInterval(() => seconds.value++, 1000);
      console.log('کامپوننت mount شد — تایمر شروع شد');
    });

    onUnmounted(() => {
      clearInterval(timer); // جلوگیری از memory leak
      console.log('کامپوننت unmount شد — تایمر پاک شد');
    });

    return { seconds };
  }
};
⚠ مهم: هر چیزی که در onMounted راه‌اندازی می‌کنید (تایمر، event listener، subscription) باید در onUnmounted پاک‌سازی شود تا نشتی حافظه رخ ندهد.
▶ دمو: mount/unmount یک تایمر زنده
کامپوننت در حال حاضر mount نشده است.
// رویدادهای چرخهٔ حیات اینجا نمایش داده می‌شود...

📝فرم‌ها و v-model

v-model بایند دوطرفه بین input و داده ایجاد می‌کند — تغییر در ورودی داده را عوض می‌کند و برعکس.

<!-- متن -->
<input v-model="name">

<!-- چک‌باکس (بولین) -->
<input type="checkbox" v-model="agree">

<!-- select -->
<select v-model="city">
  <option value="tehran">تهران</option>
  <option value="shiraz">شیراز</option>
</select>

<!-- radio -->
<input type="radio" value="m" v-model="gender">
<input type="radio" value="f" v-model="gender">

<!-- Modifiers مفید -->
<input v-model.trim="name">      <!-- حذف فاصلهٔ اضافه -->
<input v-model.number="age">    <!-- تبدیل به عدد -->
<input v-model.lazy="q">       <!-- sync روی change نه input -->
▶ دمو: فرم ثبت‌نام با v-model
ثبت شد: {{ name }} — {{ city }} ✓
{{ error }}
state زنده: {{ { name, city, agree } }}

⚖️Composition API در برابر Options API

Vue 3 از دو سبک نوشتن پشتیبانی می‌کند. هر دو معتبرند؛ Composition API برای پروژه‌های بزرگ و استفادهٔ مجدد منطق (composables) برتری دارد.

ویژگیOptions APIComposition API
سازمان‌دهیبر اساس نوع (data, methods, computed)بر اساس ویژگی/قابلیت منطقی
استفادهٔ مجدد منطقMixins (محدودیت‌دار)Composables (توابع ساده)
یادگیریساده‌تر برای مبتدینیاز به آشنایی با ref/reactive
TypeScriptپشتیبانی متوسطپشتیبانی عالی

همان کامپوننت با دو سبک

// Options API
createApp({
  data() {
    return { count: 0 };
  },
  methods: {
    increment() { this.count++; }
  },
  computed: {
    doubled() { return this.count * 2; }
  }
});

// Composition API — همان منطق
createApp({
  setup() {
    const count = ref(0);
    const doubled = computed(() => count.value * 2);
    function increment() { count.value++; }
    return { count, doubled, increment };
  }
});
ⓘ توصیهٔ رسمی: برای پروژه‌های جدید و متوسط/بزرگ، تیم Vue استفاده از Composition API با <script setup> (در پروژه‌های مبتنی بر build) را توصیه می‌کند. برای اسکریپت‌های ساده و صفحات کوچک، Options API همچنان کاملاً مناسب است.

🔥بهترین شیوه‌ها

۱. همیشه key منحصربه‌فرد در v-for

// ✗ بد — بدون key، Vue نمی‌تواند تغییرات را بهینه دنبال کند
<li v-for="item in items">{{ item.name }}</li>

// ✓ خوب — key پایدار (نه index، مگر لیست هرگز مرتب/فیلتر نشود)
<li v-for="item in items" :key="item.id">{{ item.name }}</li>

۲. هرگز v-if و v-for را روی یک عنصر ننویسید

// ✗ بد — اولویت v-if بالاتر است و به item دسترسی ندارد
<li v-for="item in items" v-if="item.active"></li>

// ✓ خوب — فیلتر با computed، یا wrapper با template
<li v-for="item in activeItems" :key="item.id"></li>

۳. Props را در فرزند تغییر ندهید

// ✗ بد — جهش مستقیم props (خطا در حالت strict)
props: ['modelValue'],
methods: { update() { this.modelValue = 5; } }

// ✓ خوب — emit به والد و اجازه بده والد تصمیم بگیرد
this.$emit('update:modelValue', 5);
موضوعVue.jsVanilla JS
رندر لیستv-for + :keyinnerHTML دستی یا createElement
بایند دوطرفه فرمv-modelevent listener + مقداردهی دستی
بروزرسانی UIخودکار (reactive)دستی با هر تغییر داده
💡 نکات مهم:
  • نام کامپوننت‌ها را با PascalCase یا kebab-case ثابت نگه دارید
  • props را با props: { title: String } تایپ کنید تا خطاهای زودهنگام دریافت کنید
  • منطق قابل استفادهٔ مجدد را در composable (تابعی با پیشوند use) جدا کنید
  • از computed برای مقادیر مشتق‌شده و از watch فقط برای side effect استفاده کنید

🏆پروژه کامل — لیست کارها (Todo App)

ترکیب ref + v-model + v-for + computed + methods در یک اپ واقعی کوچک:

const { createApp, ref, computed } = Vue;

createApp({
  setup() {
    const newTodo = ref('');
    const todos = ref([
      { id: 1, text: 'یادگیری Vue', done: true },
      { id: 2, text: 'ساخت پروژه', done: false }
    ]);

    function addTodo() {
      if (!newTodo.value.trim()) return;
      todos.value.push({ id: Date.now(), text: newTodo.value, done: false });
      newTodo.value = '';
    }
    function removeTodo(id) {
      todos.value = todos.value.filter(t => t.id !== id);
    }
    const remaining = computed(() => todos.value.filter(t => !t.done).length);

    return { newTodo, todos, addTodo, removeTodo, remaining };
  }
}).mount('#app');
▶ دمو: لیست کارهای تعاملی و کامل
{{ t.text }}

{{ remaining }} کار باقی‌مانده از {{ todos.length }}

🚀زمین بازی کد (Playground)

کد را همین‌جا ویرایش کن و نتیجه را زنده ببین. یک مثال را از منوی بالا انتخاب کن، تب‌های HTML و JS را تغییر بده، و دکمهٔ ▶ اجرا را بزن (یا فقط تایپ کن تا خودکار اجرا شود). از console.log() برای دیدن خروجی در پایین استفاده کن.

💡 این محیط کاملاً جداست (iframe ایزوله)؛ هر چه می‌خواهی امتحان کن — صفحهٔ آموزش خراب نمی‌شود. نسخهٔ Vue.js همین صفحه (۳.۵.۳۹) به‌صورت آفلاین داخل تمرین بارگذاری می‌شود.