ایجاد یک اپلیکیشن Vue.js  پیچیده و بزرگ با استفاده از Vuex

گردآوری و تالیف : ارسطو عباسی
تاریخ انتشار : 31 مرداد 1397
دسته بندی ها : جاوا اسکریپت

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

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

دوره آنلاین آموزش پروژه محور Vuex به شما کمک میکند که کامل کار با این ابزار را فرابگیرید

Vuex چیست؟

Vuex یک کتابخانه مدیریت وضعیت است که مخصوص ایجاد اپلیکیشن‌های پیچیده و بزرگ در ویوجی‌اس است. در یک اپلیکیشن، Vuex از یک مکان ذخیره‌سازی همگانی و مرکزی برای تمام کامپوننت‌ها استفاده می‌کند. همچنین قابلیت بروزرسانی‌های سریع را نیز به شما می‌دهد.

مکان ذخیره‌سازی Vuex به شیوه‌ای طراحی شده است که هر کامپوننتی نمی‌تواند وضعیت آن را تغییر دهد. این موضوع به شما اطمینان می‌دهد که وضعیت‌ها تنها به دلایل منطقی تغییر خواهند کرد. بنابراین موارد ذخیره شده یک منبع قابل اطمینان و درست هستند: داده مربوط به هر المان تنها یکبار ذخیره شده و در حالت read-only قرار می‌گیرد، بنابراین کامپوننت‌های دیگر نمی‌تواند روی آن تاثیر بگذارند. 

چرا به Vuex نیاز دارید؟

شاید این سوال برای شما نیز پیش بیاید که چرا باید از Vuex استفاده کنید؟ چرا وضعیت‌های به اشتراک گذاری شده را در یک فایل معمولی جاوااسکریپت قرار نمی‌دهیم و بعد آن را در اپلیکیشن ویوجی‌اس import نمی‌کنیم؟

مطمئنا چنین کاری را نیز می‌توانید انجام دهید، اما استفاده کردن از حالت ذخیره‌سازی Vuex مطمئنا مزیت‌های بسیار بیشتری را خواهد داشت:

  • مکان ذخیره‌سازی Vuex واکنشگرا است. وقتی که یک کامپوننت وضعیت خود را تغییر دهد، Vuex این تغییرات را به صورت سریع و واکنش‌پذیرانه در قسمت View اعمال می‌کند.  
  • کامپوننت‌ها نمی‌توانند به صورت مستقیم در روند ذخیره‌سازی وضعیت‌ها دخالت داشته باشند. این موضوع باعث می‌شود که اپلیکیشن یک پیشینه از خود داشته باشد. چنین حالتی باعث می‌شود تست و دیباگ کردن اپلیکیشن بسیار ساده‌تر شود.
  • به لطف ادغام آسان Vuex با ابزار توسعه‌دهندگان Vue دیباگ کردن چنین پروژه‌هایی آسان‌تر است.
  • Vuex شما را با چشم یک عقاب که همه چیز را می‌بیند و تمام ارتباطات و تاثیرات بر اپلیکیشن را مشاهده می‌کند، مسلح می‌نماید.
  • پایداری و همزمان‌سازی بین وضعیت‌ها در چند کامپونت جداگانه بسیار آسان‌تر است. حتی در صورتی که تغییرات به صورت سلسله‌ای باشد این وضعیت باز هم بدین صورت است.
  • Vuex می‌تواند ارتباط میان چندین کامپوننت را بوجود بیاورد.
  • اگر یک کامپوننت در View آسیب ببیند، وضعیت آن در محل ذخیره‌سازی Vuex بدون مشکل باقی می‌ماند.

شروع کار با Vuex

قبل از اینکه شروع به استفاده از Vuex کنیم، باید یکسری از موضوعات را به صورت شفاف بیان کرد.

ابتدا، برای دنبال کردن این آموزش باید دانش خوبی از Vue.js و سیستم کامپوننت‌ها داشته باشید، داشتن یک تجربه کم هم برای این مرحله خوب است.

همچنین، هدف این مقاله دقیقا ساخت یک اپلیکیشن بسیار پیچیده نیست، بلکه قصد داریم روی اینکه Vuex می‌تواند چکاری انجام دهد و چگونه با آن می‌شود اپلیکیشن پیچیده‌ای را ساخت تمرکز می‌کنیم. 

در نهایت، ما از سینتکس ES2015 استفاده می‌کنیم. اگر آن را نمی‌دانید پس باید ابتدا به این لینک مراجعه کنید. 

بیایید شروع کنیم!

پیاده‌سازی پروژه Vuex

اولین قدم که باید به آن توجه کنید داشتن ویوجی‌اس و Vuex به صورت نصب شده روی کامپیوترتان است. راه‌های مختلفی برای این کار وجود دارد. راه آسان استفاده کردن از یک CDN است. فایل HTML زیر را مشاهده کنید:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<!-- Put the CSS code here -->
</head>
<body>
 
<!-- Put the HTML template code here -->
 
<script src="https://unpkg.com/vue"></script>
<script src="https://unpkg.com/vuex"></script>
 
<script>
// Put the Vue code here
</script>
</body>
</html>

برای اینکه کامپوننت جلوه زیبا‌تری داشته باشد به آن‌ها مقداری CSS نیز اعمال می‌کنم. البته این کار در روند آموزشی اصلا مهم نیست. اما خب پروژه را به صورت بصری زیبا جلوه می‌دهد:

<style>
  #app {
    background-color: yellow;
    padding: 10px;
  }
  #parent {
    background-color: green;
    width: 400px;
    height: 300px;
    position: relative;
    padding-left: 5px;
  }
  h1 {
    margin-top: 0;
  }
  .child {
    width: 150px;
    height: 150px;
    position:absolute;
    top: 60px;
    padding: 0 5px 5px;
  }
  .childA {
    background-color: red;
    left: 20px;
  }
  .childB {
    background-color: blue;
    left: 190px;
  }
</style>

حال بیایید کامپوننت‌هایی را در پروژه تعریف کنیم. در داخل تگ script درست قبل از </body> کدهای ویو زیر را بنویسید:

Vue.component('ChildB',{
  template:`
    <div class="child childB">
      <h1> Score: </h1>
    </div>`
})
 
Vue.component('ChildA',{
  template:`
    <div class="child childA">
      <h1> Score: </h1>
    </div>`
})
 
Vue.component('Parent',{
  template:`
    <div id="parent">
      <childA/>
      <childB/>
      <h1> Score: </h1>
    </div>`
})
 
new Vue ({
  el: '#app'
})

در این قسمت ما یک نمونه از Vue، یک کامپوننت والد و دو کامپوننت فرزند را در اختیار داریم. هر کدام از کامپوننت‌ها یک سربرگ با عنوان Score: دارند که وضعیت کامپوننت‌ها در آنجا نمایش داده می‌شود. آخرین چیزی که نیاز به انجام آن است قرار دادن یک <div> همرهاه با id="app" بعد از تگ body است. بعد از آن کامپوننت Parent را در آن قرار دهید:

<div id="app">
  <parent/>
</div>

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

کار با Vuex

مدیریت وضعیت

در زندگی واقعی ما از استراتژی‌هایی برای دسته‌بندی و ساختاردهی به محتوایی که استفاده می‌کنیم، بهره می‌بریم. اینگونه پیچیدگی‌های زندگی واقعی را کمتر می‌کنیم. برای مثال، چیزهایی که به همدیگر مرتبط هستند را در دسته‌های مختلف قرار می‌دهیم. کتابخانه‌ای را در نظر بگیرید: در یک کتابخانه کتاب‌ها براساس موارد مختلفی دسته‌بندی شده و در قسمت‌های مختلفی قرار می‌گیرند. به این صورت ما سریع‌تر می‌توانیم آن‌ها را پیدا نماییم. Vuex داده‌ها و منطق مربوط به وضیعت اپلیکیشن را در چهار دسته‌بندی، مرتب می‌کند: 

state, getters, mutations, actions

State و Mutations اساس و پایه ذخیره‌سازی Vuex است:

  • State یک شئ است که وضعیت داده‌های اپلیکیشن را ذخیره می‌کند. 
  • Mutations نیز یک شئ است که در آن متدهایی قرار گرفته و روی State تاثیر می‌گذارد.

Getters و Actions پیشبینی‌های منطقی از State و Mutations به حساب می‌آیند:

  • getters شامل متدهایی می‌شود که برای خلاصه کردن دسترسی به وضعیت و انجام یکسری از وظایف پیش‌پردازنده (در صورت نیاز) استفاده می‌شود.
  • actions شامل متدهایی برای اجرا کدهای غیرهمزمان و Mutations می‌شود.

بیایید این موارد را در دیاگرام زیر بهتر مشاهده کنیم:

در قسمت سمت چپ ما مثالی از ذخیره‌سازی Vuex را مشاهده می‌کنیم که بعدا در همین آموزش آن را نیز ایجاد می‌کنیم. در قسمت سمت راست، دیاگرام روندکاری Vuex را مشاهده می‌کنیم که شیوه ارتباط اجزای مختلف Vuex را نمایش می‌دهد.

ایجاد Vuex Store

حال که با شیوه کاری Vuex آشنایی پیدا کردیم، در این مرحله قصد داریم ساختار اصلی پروژه Vuex Store را بنویسیم:

const store = new Vuex.Store({
  state: {
 
  },
  getters: {
 
  },
  mutations: {
 
  },
  actions: {
 
  }
})

برای اینکه بتوانیم به صورت Global در پروژه به store دسترسی داشته باشیم، نیاز است که در نمونه Vue خاصیت store را نیز قرار دهیم:

new Vue ({
  el: '#app',
  store // register the Vuex store globally
})

حال می‌توانیم با استفاده از this.$store در هر کامپوننتی به قسمت store دسترسی داشته باشیم. 

تا به اینجای کار اگر کدها را مشاهده کرده باشید (CodePen)، اپلیکیشن ما شبیه به زیر خواهد بود.

خصوصیات State

شئ state شامل تمام داده‌هایی است که در اپلیکیشن به اشتراک گذاشته شده است. البته هر کدام از کامپوننت‌ها می‌تواند سطح دسترسی private نیز داشته باشند. 

تصور کنید که شما قصد ایجاد یک اپلیکیشن بازی را دارید. در این حالت به یک متغیر نیاز خواهید داشت که امتیازات داخل بازی را در خود ذخیره کند. بنابراین یک متغیر را در شئ state قرار دهید:

state: {
  score: 0
}

حال می‌توانید به score به صورت مستقیم دسترسی پیدا کنید. بیایید به قسمت کامپوننت‌ها برگردیم و از داده مورد نظر استفاده کنیم. برای اینکه به داده score دسترسی داشته باشیم، یک خاصیت را در شئ computed استفاده می‌کنیم:

computed: {
  score () {
    return this.$store.state.score
  }
}

حال در قالب اصلی عبارت زیر را در بین h1 قرار دهید:

<h1> Score: {{ score }} </h1>

برای کامپوننت‌های دیگر نیز به همین صورت عمل کنید.

ایجاد Getter

دوباره استفاده کردن از داده‌های مربوط به وضعیت حالت قبل بسیار خوب است، اما وضعیت‌های زیر را تصور کنید:

  1. در یک اپلیکیشن بزرگ تصور کنید چندین کامپوننت را در اختیار دارید که تمامی آن‌ها از طریق this.$store.state.score به وضعیت موجود دسترسی دارند و این مقدار در تمامی آن‌ها نیز قرار دارد. حالتی را تصور کنید که قصد دارید نام score را تغییر دهید. در این صورت باید نام متغیر را در هر کدام از کامپوننت‌ها تغییر دهید!
  2. تصور کنید که قصد استفاده از مقدار محاسبه شده یک وضعیت را دارید. برای مثال، می‌خواهید با رسیدن امتیازات یک بازیکن به ۱۰۰، به آن ۱۰ امتیاز را جایزه بدهید. برای چنین کاری باید یک تابع تعریف کرده و شرایط بالا را در آن پیاده‌سازی کنید. اما برای استفاده از این تابع شما مجبور هستید که آن را در هر کامپوننت به صورت تکراری به کار ببرید، این حالت خوبی نیست!

خوشبختانه، Vuex به شما راه‌حل‌های خوبی را برای مدیریت چنین وضعیتی می‌دهد. یک آیتم getter مرکزی را تصور کنید که به وضعیت store دسترسی دارد و یک تابع را می‌تواند به تمام آیتم‌های وضعیت اعمال کند. در چنین حالتی وقتی شما نیاز به تغییر چیزی داشته باشید آن را تنها در یک مکان تغییر می‌دهید.

بیایید یک getter را برای score() ایجاد کنیم:

getters: {
  score (state){
    return state.score
  }
}

یک getter به عنوان اولین آرگومان state را دریافت کرده و سپس از آن برای دسترسی داشتن به خصوصیات state استفاده می‌کند.

نکته: در داخل getterها می‌توانید به عنوان آرگومان دوم از یک getter دیگر استفاده کنید. 

در تمام کامپوننت‌ها، خاصیت score() را تغییر دهید. بجای اینکه به صورت مستقیم score() را قرار دهید، از طریق getter این کار را انجام دهید.

computed: {
  score () {
    return this.$store.getters.score
  }
}

 حال اگر تصمیم داشته باشید که score را به result تغییر دهید، تنها نیاز است این کار را در یک جا انجام دهید: در getter مربوط به score().

ایجاد Mutations

Mutations تنها راه مجاز برای تغییر یک state است. Mutation بیشتر شبیه به یک تابع برای مدیریت رویدادهاست که توسط یک نام تعریف می‌شود. این تابع در اولین آرگومان خود مقدار state را دریافت می‌کند. می‌توانید آرگومان دومی را نیز که به آن payload می‌گویند به تابع اضافه کنید.

ایجاد increment() با استفاده از Mutation:

mutations: {

  increment (state, step) {

    state.score += step

  }

}

Mutationها را نمی‌توان به صورت مستقیم فراخوانی کرد. برای اجرای یک Mutation باید متد commit() را همراه با نام Mutation فراخوانی کنید. 

methods: {

  changeScore (){

    this.$store.commit('increment', 3);

  }

}

حال می‌توانید از متد changeScore() استفاده کنید. برای اینکار در قالب اصلی پروژه به صورت زیر عمل کنید:

<button @click="changeScore">Change Score</button>

ایجاد Actions

یک action تنها تابعی ساده است که یک mutation را اجرا می کند. با استفاده از این متد می‌توان وضعیت‌ را به صورت غیر مستقیم تغییر دهید. این کار باعث می‌شود که بتوان عملیات‌های غیرهمزمان را اجرا کرد.

بیایید یک action با نام incrementScore را ایجاد کنیم:

actions: {

  incrementScore: ({ commit }, delay) => {

    setTimeout(() => {

      commit('increment', 3)

    }, delay)

  }

}

Actions به عنوان اولین آرگومان context را می‌پذیرند. این مورد شامل تمام متدها و خاصیت‌های store می شود. Action مانند Mutation می‌تواند یک آرگومان payload را نیز دریافت کند. 

در کامپوننت ChildB می‌توانید متد changeScore() را به صورت زیر تغییر دهید:

methods: {
  changeScore (){
    this.$store.dispatch('incrementScore', 3000);
  }
}

برای فراخوانی یک action ما از متد dispatch استفاده می‌کنیم. می‌توانید تغییرات تا به اینجای کار را در این لینک مشاهده کنید. 

Vuex Mapping Helpers

Vuex چندین کمک کننده بسیار مفید را پیشنهاد می‌دهد که می‌تواند فرایند توسعه را ساده‌تر کند. بجای اینکه تمام توابع را به صورت دستی بنویسید، می‌تواند به Vuex بگویید که این کارها را برای شما انجام دهد. بیایید شیوه کاری این موارد را مشاهده کنیم. 

بجای نوشتن خاصیت score() به صورت زیر:

computed: {
  score () {
    return this.$store.state.score
  }
}

می‌توانیم از mapState() به صورت زیر استفاده کنیم:

computed: {
  ...Vuex.mapState(['score'])
}

می‌توانید یک مثال کامل از این موضوع را در این لینک مشاهده کنید. 

ماژولار کردن Store

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

استفاده از ماژول‌های Vuex

Vuex به شما اجازه می‌دهد که شئ store را در چندین ماژول مختلف قرار دهید. هر کدام از ماژول‌ها شامل state, mutation, action و getter مربوط به خودشان هستند. بعد از آنکه ماژول‌های لازم را ایجاد کردیم، حال باید آن‌ها را در Store ذخیره کنیم. 

const childB = {

  state: {

    result: 3

  },

  getters: {

    result (state) {

      return state.result

    }

  },

  mutations: {

    increase (state, step) {

      state.result += step

    }

  },

  actions: {

    increaseResult: ({ commit }, delay) => {

      setTimeout(() => {

        commit('increase', 6)

      }, delay)

    }

  }

}

const childA = {

  state: {

    score: 0

  },

  getters: {

    score (state) {

      return state.score

    }

  },

  mutations: {

    increment (state, step) {

      state.score += step

    }

  },

  actions: {

    incrementScore: ({ commit }, delay) => {

      setTimeout(() => {

        commit('increment', 3)

      }, delay)

    }

  }

}

const store = new Vuex.Store({

  modules: {

    scoreBoard: childA, 

    resultBoard: childB

  }

})

در کدهای بالا ما دو ماژول را ایجاد کردیم. ماژول‌ها شامل شئ‌های ساده‌ای است که به عنوان scoreBoard و resultBoard در شئ modules پیاده‌سازی شده‌اند. 

حالت بیایید کامپوننت ChildB را برای اعمال تغییرات در ماژول resultBoard آماده کنیم:

Vue.component('ChildB',{

  template:`

    <div class="child childB">

      <h1> Result: {{ result }} </h1>

      <button @click="changeResult()">Change Result</button>

    </div>`,

  computed: {

    result () {

      return this.$store.getters.result

    }

  },

  methods: {

    changeResult () {

      this.$store.dispatch('increaseResult', 3000);

    }

  }

})

در کامپوننت ChildA تنها چیزی که نیاز به تغییر دارد متد changeScore() است:

Vue.component('ChildA',{

  template:`

    <div class="child childA">

      <h1> Score: {{ score }} </h1>

      <button @click="changeScore()">Change Score</button>

    </div>`,

  computed: {

    score () {

      return this.$store.getters.score

    }

  },

  methods: {

    changeScore () {

      this.$store.dispatch('incrementScore', 3000);

    }

  }

})

همانطور که مشاهده می‌کنید، تقسیم کردن store به ماژول‌ها باعث می‌شود که کدها قابلیت نگه‌داری بیشتری داشته باشند. پروژه را تا به اینجای کار می‌توانید در این لینک مشاهده کنید. 

جداسازی Vuex Store به فایل‌های جداگانه

در قسمت قبلی ما ماژولار کردن اپلیکیشن به آن ساختار خیلی خوبی دادیم. با این حال هنوز هم فایل Store به نظر می‌رسد که بسیار بزرگ است و حجم بالایی دارد. 

بنابراین قدم منطقی بعدی جدا کردن Vuex store در فایل‌های جداگانه است. این ایده بدین صورت است که یک فایل منحصر به فرد برای خود store در اختیار داشته باشیم و برای هر شئ یک فایل جداگانه را ایجاد کنیم. 

استفاده از کامپوننت‌های تک فایلی Vue

ما Vuex را تا جایی که امکان‌ش بود به صورت ماژولار در آوردیم. کار بعدی که می‌توانیم انجام دهیم این است که چنین استراتژی را به کامپوننت‌های ویوجی‌اس نیز اعمال کنیم. می‌توانیم هر کامپوننت را در یک فایل با فرمت .vue قرار دهیم. برای اینکه این موضوع را بهتر درک نمایید می‌توانید این صفحه از مستندات را مطالعه کنید. 

در مثال ما، ما سه فایل Parent.vue, ChildA.vue, ChildB.vue را در اختیار داریم.

در نهایت وقتی که این تکنیک‌ها را در کنار همدیگر قرار دادیم، ساختار پروژه به صورت زیر خواهد بود:

├── index.html
└── src
    ├── main.js
    ├── App.vue
    ├── components
    │   ├── Parent.vue
    │   ├── ChildA.vue
    │   ├── ChildB.vue
    └── store
        ├── store.js     
        ├── state.js     
        ├── getters.js        
        ├── mutations.js 
        ├── actions.js     
        └── modules
            ├── childA.js       
            └── childB.js

در این مخزن گیت‌هاب می‌توانید پروژه را به صورت کامل مشاهده کنید. 

خلاصه

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

وضعیت ذخیره‌سازی در این کتابخانه به صورت تعاملی است و کامپوننت‌ها نمی‌توانند به صورت مستقیم روی این روند تاثیر بگدارند. تنها راه برای تغییر دادن استفاده کردن از mutationها است که عملیات همزمان را انجام می‌دهند. 

فرایندهای منطقی همزمانی باید توسط actionها بسته‌بندی شوند. هر action می‌تواند یک یا چند mutation را commit کند. 

منبع

مقالات پیشنهادی

ساخت اپلیکیشن مقیاس بزرگ با Jooby

Jooby یک میکرو فریمورک ماژولار برای ساخت اپلیکیشن ها در جاوا و Kotlin هست. اگر این اولین باره که درمورد Jooby می شونید, میتونید این لینک رو مطالعه کنی...

چه زمانی یک تجارت کوچک به اپلیکیشن موبایل نیاز دارد؟

در یک دهه گذشته اپلیکیشن‌های موبایل توانسته‌اند که سیطره بسیار خوبی روی دنیای ما به عنوان کاربر داشته باشند. اما این بدان معنا نیست، هر کسی که از اپلی...

ایجاد اپلیکیشنی مدرن با استفاده از Django و Vue.js - بخش دوم

JWT یک URL-safe کوچک است که برای نمایش درستی یک انتقال بین دو موجودیت استفاده می‌شود. درستی در JWT در یک شئ اینکود شده داخل JSON قرار دارد و به عنوان...

ایجاد اپلیکیشنی مدرن با استفاده از Django و Vue.js - بخش اول

در این مقاله ما قصد داریم از چهارچوب Django و Django REST برای بک‌اند و Vue.js برای فرانت‌اند است. APIها با کمک Axois (یک کتابخانه HTTP Client) از طری...