درود دوستان. زمانی که برنامه رشد می‌کنه و بزرگ میشه، یکی از دغدغه‌های هر توسعه‌دهنده اینه که چطوری ویژگی‌ها و ساختار برنامه رو مدیریت کنه. توی برنامه‌های Vue یکی از بهترین راه‌های مدیریت کردن ساختار برنامه استفاده از کامپوننت‌هاست. با کامپوننت‌ها می‌تونیم قسمت‌های بی‌ارتباط هر برنامه رو جدا کنیم تا هم راحت‌تر قابل توسعه باشن و هم قابل استفاده مجدد. اما گاهی اوقات با کامپوننت‌هایی مواجه می‌شیم که به مرور زمان بزرگ و بزرگ‌تر شدن.

فرض کنیم صفحه‌ای داریم که مربوط به مدیریت کردن پست‌ها هست. برای این صفحه لازم داریم دسته‌بندی‌ها و تگ‌ها رو هم لود کنیم. چنین کامپوننتی برای اون می‌سازیم:

<template>
  <!-- View -->
</template>

<script>
export default {
  mounted() {
    this.getPosts();
    this.getCategories();
    this.getTags();
  },
  data() {
    return {
      posts: [],
      categories: [],
      tags: [],
      searchQuery: '',
    }
  },
  methods: {
    getPosts() { ... },
    filterPostsByCategory() { ... },
    filterPostsByUser() { ... },
    searchPost() { ... },

    getCategories() { ... },
    filterCategories() { ... },

    getTags() { ... },
    filterTags() { ... },
  }
}
</script>

توی این کامپوننت که از نوشتن آپشن‌های computed و watch صرف نظر کردیم. همونطور که می‌بینیم، کدهای مربوط به تگ‌ها و دسته‌بندی‌ها کنار هم قرار گرفتن. یکی از مشکلاتی که به وجود میاد اینه که کدهای نامربوط کنار هم قرار می‌گیرن که باعث میشه خوانایی و همچنین توسعه و اضافه کردن ویژگی‌های جدید سخت بشه. شاید راهی که به ذهنمون برسه این باشه که کامپوننت رو تقسیم کنیم به کامپوننت‌های کوچیک‌تر. اما این راه همیشه مناسب و قابل اجرا نیست. مثلاً بیشتر حجم این کامپوننت مربوط به کدهای قسمت mounted یا data و methods و watch هست که جدا کردن این قسمت‌ها و منتقل کردن اونها به فایل‌های مجزا میسر نیست، مگر با روش‌های غیراستاندارد.

پس راه استاندارد و تمیز چیه؟ 🤔 اگه از ورژن ۳ فریم‌ورک Vue استفاده می‌کنیم قابلیتی به اسم Composition API رو داریم 👌

 

Composition API چیه؟ 🤔

این مهمترین ویژگی اضافه شده به Vue 3 هست که با اون می‌تونیم کدهای یک کامپوننت بزرگ رو به بخش‌های کوچیک‌تر تقسیم کنیم تا بتونیم اون‌ها رو به صورت مجزا توسعه بدیم.

در مقابل، ما Options API رو داریم که به روش سنتی‌ای میگن که توی مثال بالا بررسی کردیم.

Composition یعنی چی؟ 🤔

شاید با درک این واژه، بهتر بتونیم این ویژگی فریم‌ورک رو درک کنیم:

کامپوزیشن یک مفهوم کلی توی دنیای برنامه‌نویسی هست و به معنی نحوه قرار گرفتن و ترکیب‌شدن اعضای کوچیک برای ساختن یک ساختار بزرگ هست

 

چطوری کامپوننت‌ها رو با Composition API بنویسیم؟

توی ویو ۳ و تو کامپوننت‌ها، در کنار آپشن‌هایی مثل mounted ،data و methods، ما یک آپشن دیگه داریم به اسم setup. ما قراره کدهامون رو توی این آپشن بنویسیم:

export default {
  setup() {
  
  },

  // the "rest" of the component
  mounted() {},
  data() {},
}

این آپشن به صورت یک متد هست و کدهایی که توی اون می‌نویسیم قبل از ساخته شدن کامپوننت و قبل از متدهایی مثل mounted و beforeCreate اجرا میشن:

توی متد setup می‌تونیم همه آپشن‌هایی که توی روش سنتی مثل data و methods و mounted و watch داشتیم رو داشته باشیم. با توجه به اینکه setup زودتر از قسمت دیگه‌ای اجرا میشه، ما نمی‌تونیم به بقیه اعضای کامپوننت مثل data و methods دسترسی داشته باشیم.

قبل از اینکه مثالمون رو با Composition API بنویسیم، می‌خوایم ببینیم چه چیزهایی می‌تونیم توی setup تعریف کنیم.

 

تعریف کردن پراپرتی توی setup

یک پراپرتی که قبلاً اون رو توی data تعریف می‌کردیم، توی setup به صورت زیر تعریف میشه (خط ۵):

import { ref } from 'vue'

export default {
  setup() {
>>     const x = ref(1);
  }
}

توی خط ۵ ما با استفاده از تابع درونی ویو به اسم ref داریم یک پراپرتی می‌سازیم. این تابع برای ساختن پراپرتی‌هایی استفاده میشه که reactive هستن. پراپرتی‌های reactive پراپرتی‌هایی هستن که وقتی مقدار اونها تغییر می‌کنه، فریم‌ورک متوجه میشه و واکنش نشون میده که باعث میشه برنامه پویایی داشته باشه. ما قبلاً پراپرتی‌هایی توی data می‌نوشتیم به صورت خودکار reactive می‌شدن.

هر نوع مقداری (آرایه، متن، آبجکت و ...) می‌تونه توی ref قرار بگیره.

این کد مشابه حالت زیر توی Options API هست:

data() {
  return {
    x: 1,
  }
}

 

برای اینکه پراپرتی x سرتاسر برنامه در دسترس باشه، باید توسط setup به خروجی داده بشه. خروجی setup باید یک آبجکت باشه:

import { ref } from 'vue'

export default {
  setup() {
    const x = ref(1);

>>     return { x }; // equivalent to { x: x }
  }
}

هر چیزی که توی این آبجکت باشه، بقیه جاهای برنامه مثل روش‌های قدیمی در دسترس هست:

<template>
>>   <p>{{ x }}</p>
</template>

<script>
export default {
  setup() {
    return {
>>       x: ref(1),
    }
  },
  mounted() {
>>     alert(this.x)
  }
};
</script>

همون طور که می‌بینیم x به صورت this.x توی بقیه بخش‌ها کامپوننت در دسترس هست. اما نکته‌ای که باید در نظر داشته باشیم اینه که اگه بخوایم یک پراپرتی رو داخل خود setup فراخونی کنیم دیگه از this استفاده نمی‌کنیم. چون مقدار this توی این متد کاملاً متفاوت هست با چیزی که انتظار داریم.

پراپرتی‌هایی که داخل خود setup به ref ساخته میشن همگی به صورت آبجکت هستن که برای دسترسی به مقدار اونها باید کلید value رو از پراپرتی بخونیم:

export default {
  setup() {
    const x = ref(1);

>>     console.log(x.value); // 1

>>     console.log(typeof x); // object

    return { x }
  },
  mounted() {
    alert(this.x) // 1
  }
}

 

تعریف کردن متد توی setup

متدهایی که قبلاً توی قسمت methods داشتیم رو می‌تونیم اینطوری تعریف کنیم:

setup() {
  const hello = (name) => `Hello ${name}`;

  // or
  function signup() {
    // ...
  }

  return { hello, signup }
}

و درست مثل روش‌های قبل می‌تونیم از اونها توی قسمت‌های مختلف کامپوننت استفاده کنیم:

<template>
>>   <p>{{ hello("Mario") }}</p>
</template>

<script>
export default {
  // ...
  mounted() {
>>     this.signup()
  },
}
</script>

 

تعریف کردن Watch توی setup

کافیه از تابع watch استفاده کنیم. برای مثال توی کد زیر می‌خوایم برای پراپرتی counter یک watch بنویسیم:

<template>
  <button @click="counter++">{{ counter }}</button>
</template>

<script>
import { ref, watch } from 'vue';

export default {
setup() {
  const counter = ref(0);

>>   watch(counter, (newVal, oldValue) => {
    alert("Counter value was changed!");
  });

  return { counter }
},
</script>

حالا هر جایی از برنامه مقدار counter عوض بشه، تابعی که توی آرگومان دوم watch نوشتیم اجرا میشه. باید دقت کنیم که watch فقط برای پراپرتی‌های reactive کار می‌کنه. پس ما برای پراپرتی‌ها از ref استفاده می‌کنیم.

 

تعریف پراپرتی‌های Computed توی setup

برای این کار از تابع computed به صورت زیر استفاده می‌کنیم:

<template>
  <button @click="counter++">{{ counter }} {{ dupCounter }}</button>
</template>

<script>
>> import { ref, computed } from 'vue'

export default {
setup() {
  const counter = ref(0);

>>   const dupCounter = computed(function() {
    return counter.value * 2
  });

  return { counter, dupCounter }
}
</script>

درست مثل پراپرتی‌های معمولی توی setup، پراپرتی‌های computed هم با .value قابل دسترسی هستن:

setup() {
  const counter = ref(0);

  const dupCounter = computed(function() {
    return counter.value * 2
  });

>>   console.log(dupCounter.value);
}

 

تعریف Lifecycle Hook کامپوننت‌ها توی setup

Lifecycle Hook ها مثل mounted رو می‌تونیم به صورت زیر تعریف کنیم. برای همه Hook های یک کامپوننت، یک معادل به صورت on[HookName] توی setup وجود داره. برای مثال mounted میشه onMounted:

>> import { onMounted } from 'vue'

export default {
setup() {
  onMounted(function() {
    alert('I mounted');
  });
},

هر چیزی که توی onMounted بنویسیم درست همون رفتارهایی داره که mounted داشت.

 

دسترسی به props توی setup

props که اطلاعاتی هستن که از بیرون به کامپوننت پاس داده میشن، به صورت زیر در دسترس هستن:

setup(props) {
  console.log(props)
}

 

خب حالا وقتشه مثالی که داشتیم رو با Composition API بنویسم. با استفاده از این ویژگی می‌خوایم ۳ موجودیتی که توی مثالمون داشتیم یعنی پست، دسته‌بندی و تگ رو منتقل کنیم به بخش‌های مجزا. پس کدهایی که توی روش سنتی داشتیم مثل mounted، پراپرتی‌های computed و یا watch رو باید منتقل کنیم به متد setup.

ابتدا همه ویژگی‌ها و قابلیت‌های مربوط به پست‌ها که توی کامپوننت وجود داره مثل پراپرتی posts توی data و getPosts توی methods رو جدا و منتقل می‌کنیم به بخش setup:

import { ref, onMounted } from 'vue'

export default {
  setup() {
    const posts = ref([]);
    const getPosts = function() {
      posts.value = dataFromServer('/posts');
    }

    const searchPosts = function(query) {
      return posts.value.filter(item => item.title === query)
    }

    onMounted(function() {
      getPosts();
    });

    return { posts, getPosts, searchPosts }
  }

حالا بخش مربوط به دسته‌بندی‌ها و تگ‌ها رو هم اضافه کنیم:

import { ref, watch, onMounted } from 'vue'

export default {
  setup() {
    const posts = ref([]);
    const getPosts = function() {
      posts.value = dataFromServer('/posts');
    }
    const searchPosts = function(query) {
      return posts.value.filter(item => item.title === query)
    }
    onMounted(function() {
      getPosts();
    });

    const categories = ref([]);
    const getCategories = () => ...
    onMounted(function() {
      getCategories();
    });
    watch(posts, function() {
      getCategories();
    })
    
    const tags = ref([]);
    const getTags = () => ...;
    onMounted(function() {
      getTags();
    });
    watch(posts, function() {
      getTags();
    })

    return {
      posts,
      getPosts,
      searchPosts,

      categories,
      getCategories,

      tags,
      getTags,
    }
  },
}

خب الان تقریباً همه بخش‌ها (به استثناء بعضی از مواردی که از اونها صرف نظر کردیم) رو منتقل کردیم و بقیه قسمت‌های کامپوننت مثل data و methods خالی شدن. اما همونطور که می‌بینیم همه کدها جمع شدن توی متد setup که باعث شده این متد طولانی و شلوغ بشه. همچنین کدهای بی‌ربط هنوز کنار هم هستن و مشکلی رو که می‌خواستیم حل کنیم یعنی توسعه‌پذیر بودن کامپوننت هنوز میسر نشده. برای این مسئله هم راه حلی وجود داره.

به لطف قابلیت ماژول‌های ES6 ما می‌تونیم همه این بخش‌ها رو منتقل کنیم به فایل‌های جداگونه و بعد اون رو به setup اضافه کنیم.

ابتدا برای کدهایی که برای پست‌ها داشتیم یک فایل مجزا درست می‌کنیم. توی برنامه یک مسیر درست می‌کنیم به اسم composable تا همه فایل‌های رو توی اون قرار بدیم.

فایل Posts.js رو درست کنیم:

// src/composables/Posts.js
import { ref, onMounted } from 'vue';

export default function setup() {
  const posts = ref([]);
  const getPosts = function() {
    posts.value = dataFromServer('/posts');
  }
  const searchPosts = function(query) {
    return posts.value.filter(item => item.title === query)
  }
  onMounted(function() {
    getPosts();
  });

  return { posts, getPosts, searchPosts }
}

همونطور که می‌بینیم همه کدهای مربوط به پست‌ها رو بردیم توی فایل جداگونه خودش. انگار یک متد setup اختصاصی فقط برای پست‌ها.

حالا بخش دسته‌بندی‌ها رو منتقل کنیم به فایل جداگونه خودش:

// src/composables/Categories.js
import { ref, watch, onMounted } from 'vue'

export default function setup(posts) {
  const categories = ref([]);
  const getCategories = function() {
    categories.value = '/categories';
  }

  onMounted(function() {
    getCategories();
  });

  watch(posts, function() {
    getCategories();
  });

  return { categories, getCategories }
}

برای Tags:

// src/composables/Tags.js
import { ref, watch, onMounted } from 'vue'

export default function setup(posts) {
  const tags = ref([]);
  const getTags = () => '...';

  onMounted(function() {
    getTags();
  });
  watch(posts, function() {
    getTags();
  });

  return { tags, getTags }
}

مرحله بعد اینه که این فایل‌ها رو به کامپوننت وارد (import) کنیم و از اون‌ها توی قسمت setup استفاده کنیم:

// src/components/MyComponent.vue

import Posts from '../composables/Posts';
import Categories from '../composables/Categories';
import Tags from '../composables/Tags';

export default {
  setup() {
    const { posts, getPosts, searchPosts } = Posts();
    const { categories, getCategories } = Categories(posts);
    const { tags, getTags } = Tags(posts);

    return {
      posts,
      getPosts,
      searchPosts,

      categories,
      getCategories,

      tags,
      getTags,
    }
  },

توی خط‌های ۹ تا ۱۱ با استفاده از Object Destructuring آیتم‌هایی که مدنظرمون بود رو خوندیم و اضافه کردیم به setup و همچنین در انتها اونها رو return کردیم تا سرتاسر کامپوننت در دسترس باشن. همونطور که می‌بینیم متد setup خلوت‌تر شد و اینطوری برای توسعه هر قسمت کافیه به فایل مربوط به خودش مراجعه کنیم.

 

نتیجه‌گیری

همونطور که می‌دونیم Composition هم به معنی ترکیب هست و ما تونستیم قسمت‌های مختلف یک کامپوننت رو جدا کنیم و نهایتاً اون‌ها رو با کامپوننت ترکیب کنیم. اگه یک کامپوننت بزرگ داریم، برای کم کردن حجم و افزایش توسعه‌پذیری اون استفاده از Composition API گزینه مناسبی هست.

امیدوارم از این پست استفاده کرده باشین. روزتون خوش 😉

منابع: