معرفی و کار با Vuex

گردآوری و تالیف : ارسطو عباسی
تاریخ انتشار : 19 شهریور 1398
دسته بندی ها : vuejs

در یک اپلیکیشن تک صفحه‌ای مفهوم state به هر قسمتی از داده که قابلیت تغییر داشته باشد مربوط می‌شود. یک مثال از state می‌تواند جزئیاتی باشد که کاربر برای ورود به وبسایت از آن‌ها استفاده می‌کند. 

اما مدیریت کردن stateها در یک اپلیکیشن تک صفحه‌ای می‌تواند کار سخت و زمان‌بری باشد. زمانی که یک اپلیکیشن بزرگ‌تر و پیچیده‌تر می‌شود شما برخی از stateها را در کامپوننت‌های مختلف بکار می‌برید. این کامپوننت‌ها برای تغییر وضعیت نیاز به تعامل با همدیگر را دارند و وقتی میزان این کامپوننت‌ها زیاد باشد منجر به حجیم شدن کدها و بی نظمی می‌شود. حالتی که تحت عنوان prop drilling معرفی شده است.

Vuex یک راه‌حل برای این موضوع است. Vuex ابزار رسمی برای مدیریت Stateها در Vue.JS است. این ابزار با ایجاد یک مکان مرکزی که تمام stateها را به اشتراک می‌گذارد قابلیت مرتب‌سازی اپلیکیشن را به ما می‌دهد.  Vuex همچنین متدهایی را ایجاد می‌کند که به هر کامپوننت اجازه می‌دهد تا به Stateها دسترسی داشته باشند. Vuex برای آن ساخته شده تا به شما کمک کند تا داده‌های اپلیکیشن‌تان با ظاهر آن سازگاری داشته باشد.

در این مطلب از وبسایت راکت قصد داریم نگاهی به Vuex بیاندازیم و یک مثال عملی با آن را نیز پیاده‌سازی کنیم.

پیش‌نیازها

این آموزش برای توسعه‌دهندگانی نوشته شده که با Vue.js آشنایی دارند. بنابراین نیاز است که ویوجی‌اس را بدانید. اگر که این پیش‌نیاز را داشته باشید می‌توانید بقیه آموزش را مطالعه کنید در غیر اینصورت بهتر است توقف کرده و به نقشه راه ویوجی‌اس مراجعه کنید.

مثال – سبد خرید

ابتدا بیایید با یک مثال واقعی شروع کنیم تا مشکلی که Vuex حل می‌کند را متوجه شویم. 

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

حال به این فکر کنید که چگونه می‌توانید چنین سناریویی را پیاده‌سازی کنید؟ ممکن است در نگاه اول ساده به نظر برسد اما چند نکته مهم در این ماجرا وجود دارد. شما یک تابع دیگر برای بروزرسانی تعداد کالا‌ها در زمانی که به آن اضافه می‌شود نیز نیاز دارید، در این حالت باز هم برچسب موجود بودن و دکمه افزودن به سبد خرید باید تغییر بکند. بنابراین باید state مربوط به قسمت‌های مختلف را با همدیگر سازگار نگه‌دارید. حال تصور کنید که مدیر پروژه به شما می‌گوید باید یک API برای اینکارها نیز توسعه دهید! هر چقدر پروژه جلوتر می‌رود به نظر می‌رسد که غیر قابل حل‌تر و دردسر آفرین‌تر می‌شود.

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

برای ادامه این آموزش نیاز است که از npm و node.js نیز استفاده کنید. به همین دلیل مطمئن باشید که این موارد را روی سیستم عامل‌تان به صورت نصب شده در اختیار دارید.

ایجاد یک شمارنده با استفاده از State محلی

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

اولین کاری که باید انجام دهید نصب vue-cli است:

npm install -g @vue/cli

حال با استفاده از رابط متنی یک پروژه جدید را ایجاد کنید:

vue create vuex-counter

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

بعد از این کار وارد دایرکتوری src/components شده و نام HelloWorld.vue را به Counter.vue تغییر دهید:

cd vuex-counter
mv src/components/HelloWorld.vue src/components/Counter.vue

در نهایت src/App.vue را باز کرده و کدهای آن را با موارد زیر جایگزین نمایید:

<template>
  <div id="app">
    <h1>Vuex Counter</h1>
    <Counter/>
  </div>
</template>

<script>
import Counter from './components/Counter.vue'

export default {
  name: 'app',
  components: {
    Counter
  }
}
</script>

ایجاد Counter

حال بیایید با مقداردهی اولیه و خروجی صفحه شروع کنیم. ما قصد داریم در کنار عدد شمارنده به کاربر بگوییم که این عدد زوج و یا فرد است. برای این کار فایل src/components/Counter.vue را باز کرده و با کدهای زیر جایگزین کنید:

<template>
  <div>
    <p>Clicked {{ count }} times! Count is {{ parity }}.</p>
  </div>
</template>

<script>
export default {
  name: 'Counter',
  data: function() {
    return {
      count: 0
    };
  },
  computed: {
    parity: function() {
      return this.count % 2 === 0 ? 'even' : 'odd';
    }
  }
}
</script>

همانطور که مشاهده می‌کنید ما در این جا یک متغیر state به نام count داشته و یک تابع با نام parity نیز فراخوانی کرده‌ایم که رشته even و odd را برمی‌گرداند. این رشته براساس محاسبه باقی مانده عدد بر ۲ برگشت داده می‌شود.

حال برای اجرای پروژه به مسیر روت پروژه برگشته و دستور npm run serve را اجرا کنید. در مرورگر نیز مسیر localhost:8080 را برای مشاهده خروجی باز کنید.

برای آنکه از درستی کارکرد اپلیکیشن مطمئن شوید می‌توانید مقدار count را تغییر دهید.

افزایش و کاهش

بعد از خاصیت computed در همان قسمت <script> مربوط به فایل Counter.vue کدهای زیر را اضافه نمایید:

methods: {
  increment: function () {
    this.count++;
  },
  decrement: function () {
    this.count--;
  },
  incrementIfOdd: function () {
    if (this.parity === 'odd') {
      this.increment();
    }
  },
  incrementAsync: function () {
    setTimeout(() => {
      this.increment()
    }, 1000)
  }
}

توابع increment و decrement از نام‌شان معلوم است که چکاری را انجام می‌دهند. تابع incrementIfOdd نیز تنها زمانی اجرا می‌شود که مقدار count برابر با یک عدد فرد باشد. incrementAsync نیز تابع دیگری است که به صورت غیرهمزمان ایجاد شده و بعد از  یک ثانیه به مقدار count یک عدد را اضافه می‌کند. 

برای آنکه بتوانیم به این متدهای جدید در قالب اصلی دسترسی داشته باشیم نیاز است که یکسری دکمه جدید را تعریف کنیم. برای اینکار کدهای زیر را بعد از کد template اضافه نمایید:

<button @click="increment" variant="success">Increment</button>
<button @click="decrement" variant="danger">Decrement</button>
<button @click="incrementIfOdd" variant="info">Increment if Odd</button>
<button @click="incrementAsync" variant="warning">Increment Async</button>

حال مرورگر را یک بار دیگر باز کرده و روی تمام دکمه‌ها کلیک کنید. می‌توانید خروجی این شمارنده را در این لینک مشاهده کنید.

مثال Counter ما تا به این جا به صورت کامل نوشته شد، حال بیایید با Vuex برای نوشتن مجدد این پروژه آشنا شویم.

Vuex چگونه کار می‌کند؟

قبل از پیاده‌سازی پروژه با استفاده از Vuex ابتدا نیاز است که با کارکرد کلی Vuex آشنا شویم. اگر قبلا با فریمورک‌هایی مانند Redux کار کرده باشید مطمئنا Vuex برای‌تان موضوع شگفت انگیزی نخواهد بود چرا که تقریبا کارکرد مشابهی را ارائه می‌کنند. 

Vuex Store

Store یک مخزن متمرکز را ایجاد کرده و تمام stateهای به اشتراک گذاشته شده بین اپلیکیشن‌های Vue را در خود نگه‌داری می‌کند. قالب پایه‌ی این حالت به صورت زیر است:

// src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    // put variables and collections here
  },
  mutations: {
    // put sychronous functions for changing state e.g. add, edit, delete
  },
  actions: {
    // put asynchronous functions that can call one or more mutation functions
  }
})

بعد از ایجاد یک  Store حال نیاز است تا آن را به صورت زیر وارد فایل اصلی پروژه کنید:

// src/main.js
import store from './store'

new Vue({
  store,
  render: h => h(App)
}).$mount('#app')

این کار باعث می‌شود تا تمام کامپوننت‌های داخل اپلیکیشن به this.$store دسترسی داشته باشند.

کار با State

State را می‌توان یک شئ دانست که در آن تمام داده‌های مربوط به اپلیکیشن فرانت-اند قرار دارد. Vuex نیز مانند Redux تنها از یک Store استفاده می‌کند. داده‌های اپلیکیشن نیز در یک ساختار درختی قرار خواهند گرفت. یک مثال از این حالت را می‌توانید در زیر مشاهده کنید:

state: {
  products: [],
  count: 5,
  loggedInUser: {
    name: 'John',
    role: 'Admin'
  }
}

در این قسمت ما یک مقدار products را در اختیار داریم که با یک آرایه خالی مقداردهی شده و همچنین یک count که برابر با مقدار ۵ قرار گرفته است. یک شئ جاوااسکریپتی را نیز با نام loggedInUser تعریف کرده‌ایم که خود دو مقدار name و role را نگه‌داری می‌کند. خصوصیات مربوط به state می‌توانند هر نوع داده‌ای که برای جاوااسکریپت معتبر است را استفاده کنند. 

راه‌های مختلفی برای نمایش stateها در قسمت view وجود دارد. یکی از راه های ساده ارجاع مستقیم به store از طریق template است:

<template>
  <p>{{ $store.state.count }}</p>
</template>

همچنین می‌توانیم از طریق خاصیت computed اینکار را انجام دهیم:

<template>
  <p>{{ count }}</p>
</template>

<script>
export default {
  computed: {
    count() {
      return this.$store.state.count;
    }
  }
}
</script>

از آنجایی که Store مربوط به Vuex تعاملی است هر بار که مقدار $store.state.count تغییر کند مقدار داخل view نیز تغییر خواهد کرد. تمام این موارد در پشت صحنه انجام می‌شود به همین دلیل است که راه‌حلی تمیز و ساده به نظر می‌رسد.

mapState

حال تصور کنید که با چند state روبرو هستید که قصد دارید آن‌ها را در views نمایش بدهید. تعریف کردن یک لیست طولانی از خاصیت‌های computed به نظر کار منطقی نمی‌رسد، به همین دلیل Vuex به ما قابلیت استفاده از mapState را داده است. با استفاده از این قابلیت ما می‌توانیم چندین خاصیت computed را ایجاد کنیم:

<template>
  <div>
    <p>Welcome, {{ loggedInUser.name }}.</p>
    <p>Count is {{ count }}.</p>
  </div>
</template>

<script>
import { mapState } from 'vuex';

export default {
  computed: mapState({
    count: state => state.count,
    loggedInUser: state => state.loggedInUser
  })
}
</script>

یک راه‌حل ساده‌تر نیز برای این مورد وجود دارد که برای آن می‌توانیم از یک آرایه استفاده کنیم:

export default {
  computed: mapState([
    'count', 'loggedInUser'
  ])
}

دو حالت گفته شده دقیقا یک کار را انجام خواهند داد. شما همچنین باید در نظر بگیرید که mapState یک شئ را برگشت می‌دهد. اگر قصد استفاده از این مورد همراه با خاصیت‌های computed دیگر را دارید می‌توانید از یک عملگر spread استفاده کنید. مثال این حالت را در زیر می‌توانید مشاهده کنید:

computed: {
  ...mapState([
    'count', 'loggedInUser'
  ]),
  parity: function() {
    return this.count % 2  === 0 ? 'even' : 'odd'
  }
}

Getters

در Vuex، قابلیت Getters دقیقا مانند خاصیت computed عمل می‌کنند. Getterها به ما این قابلیت را می‌دهند تا بتوانیم stateهایی را ایجاد کنیم که می‌توانند بین کامپوننت‌های متفاوت به اشتراک گذاشته شوند. یک مثال از این حالت را در زیر می‌توانید مشاهده کنید:

getters: {
  depletedProducts: state => {
    return state.products.filter(product => product.stock <= 0)
  }
}

mapGetters

 می‌توانید برای نوشتن getterها از mapGetters استفاده کنید:

import { mapGetters } from 'vuex'

export default {
  //..
  computed: {
    ...mapGetters([
      'depletedProducts',
      'anotherGetter'
    ])
  }
}

اگر قصد اجرای یک کوئری در getter را داشته باشید می‌توانید به صورت زیر عمل کنید:

getters: {
  getProductById: state => id => {
    return state.products.find(product => product.id === id);
  }
}

store.getters.getProductById(5)

تغییر State با Mutation

یکی از جنبه‌های مهم معماری Vuex آن است که کامپوننت‌ها هیچگاه نمی‌توانند به صورت مستقیم state را تغییر دهند چرا که انجام چنین کاری باعث بوجود آمدن باگ و ناسازگاری در اپلیکیشن می‌شود.

بجای آن می‌توانیم از طریق استفاده از mutation مقدار state داخل یک store را تغییر دهیم. برای آنهایی که با Redux آشنایی دارند باید بگویم که این قابلیت شبیه به reducers است. 

یک مثال از Mutation برای افزایش مقدار count به صورت زیر است:

export default new Vuex.Store({
  state:{
    count: 1
  },
  mutations: {
    increment(state) {
      state.count++
    }
  }
})

لازم به ذکر است که بگویم، شما نمی‌توانید یک mutation handler را به صورت مستقیم فراخوانی کنید. بجای آن باید یکی از آن‌ها را commit می‌کنید:

methods: {
  updateCount() {
    this.$store.commit('increment');
  }
}

همچنین می‌توانید یک پارامتر را در mutation نیز قرار دهید:

// store.js
mutations: {
  incrementBy(state, n) {
    state.count += n;
  }
}

// component
updateCount() {
  this.$store.commit('incrementBy', 25);
}

در مثال بالا، ما یک ورودی عددی را در mutation قرار داده‌ایم که مقدار افزایش را تعیین می‌کند. البته می‌توانید این پارامتر ورودی را به صورت یک شئ نیز وارد کنید. برای مثال:

// store.js
mutations: {
  incrementBy(state, payload) {
    state.count += payload.amount;
  }
}

// component
updateCount() {
  this.$store.commit('incrementBy', { amount: 25 });
}

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

store.commit({
  type: 'incrementBy',
  amount: 25
})

mapMutations

درست مانند mapState و mapGetters می‌توانید از mapMutations نیز برای کم کردن کدها و واضح نگه‌داشتن آن‌ها استفاده کنید:

import { mapMutations } from  'vuex'

export default{
  methods: {
    ...mapMutations([
      'increment', // maps to this.increment()
      'incrementBy' // maps to this.incrementBy(amount)
    ])
  }
}

به عنوان یک نکته: mutation باید به صورت همزمان تعریف شود. البته می‌توانید تابع mutation را به صورت asynchronous تعریف نمایید اما بعدها متوجه خواهید شد که انجام این کار منجر به باگ و خطاهای غیر عادی می‌شود.

Actions

Actions توابعی هستند که خودشان stateها را تغییر نمی‌دهند. بجای این کار آن‌ها mutation را بعد از اجرای یکسری logic کامیت می‌کنند. یک مثال ساده از این حالت:

//..
actions: {
  increment(context) {
    context.commit('increment');
  }
}

در این حالت Action شئ context را به عنوان آرگومان اول دریافت می‌کند. این کار باعث می‌شود که ما بتوانیم به خصوصیات و متدهای store دسترسی پیدا کنیم. برای مثال شما می‌توانید context.commit یا context.state را اجرا کنید.

می‌توانید با استفاده از argument destructing نیز خصوصیات store را استخراج کنید:

actions: {
  increment({ commit }) {
    commit('increment');
  }
}

Actionها می‌توانند به صورت asynchronous تعریف شوند:

actions: {
  incrementAsync: async({ commit }) => {
    return await setTimeout(() => { commit('increment') }, 1000);
  }
}

درست مانند mutationها، Actionها نیز به صورت مستقیم فراخوانی نمی‌شوند. برای اینکار از متد dispatch باید استفاده شود. برای مثال:

store.dispatch('incrementAsync')

// dispatch with payload
store.dispatch('incrementBy', { amount: 25})

// dispatch with object
store.dispatch({
  type: 'incrementBy',
  amount: 25
})

برای انتساب action handler به متدهای محلی می‌توانید از mapActions نیز استفاده کنید:

import { mapActions } from  'vuex'

export default {
  //..
  methods: {
    ...mapActions([
      'incrementBy', // maps this.increment(amount) to this.$store.dispatch(increment)
      'incrementAsync', // maps this.incrementAsync() to this.$store.dispatch(incrementAsync)
      add: 'increment' // maps this.add() to this.$store.dispatch(increment)
    ])
  }
}

ایجاد اپلیکیشن Counter با استفاده از Vuex

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

ابتدا فایل src/store.js را ایجاد کرده و براساس کدهای زیر آن را ویرایش کنید:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    count: 0
  },
  getters: {
    parity: state => state.count % 2 === 0 ? 'even' : 'odd'
  },
  mutations: {
    increment(state) {
      state.count++;
    },
    decrement(state) {
      state.count--;
    }
  },
  actions: {
    increment: ({ commit }) => commit('increment'),
    decrement: ({ commit }) => commit('decrement'),
    incrementIfOdd: ({ commit, getters }) => getters.parity === 'odd' ? commit('increment') : false,
    incrementAsync: ({ commit }) => {
      setTimeout(() => { commit('increment') }, 1000);
    }
  }
});

بعد از آن وارد فایل src/components/Counter.vue شده و قسمت <script> را با کدهای زیر بروزرسانی کنید:

import { mapState mapGetters, mapActions } from 'vuex'

export default {
  name: 'Counter',
  computed: {
    ...mapState([
      'count'
    ]),
    ...mapGetters([
      'parity'
    ])
  },
  methods: mapActions([
    'increment',
    'decrement',
    'incrementIfOdd',
    'incrementAsync'
  ])
}

کدهای مربوط به template نیازی به تغییر ندارند اما اگر قصد دارید بجای استفاده از getter map استفاده کنید می‌توانید روش ساد‌ه‌تر را به کار ببرید که دسترسی مستقیم را به ما می‌داد.

<p>
  Clicked {{ $store.state.count }} times! Count is {{ $store.getters.parity }}.
</p>

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

در پایان

در این مطلب از وبسایت راکت ما به شما کلیات استفاده از Vuex را همراه با ویژگی‌های اصلی آن گفتیم. همانطور که متوجه شدید Vuex می‌تواند مشکلات اساسی را برای ما حل کند و اپلیکیشن ما را تمیز‌تر و مدیریت‌پذیر‌تر نگه دارد. 

برای آشنایی کاملتر با Vuex به شما پیشنهاد می‌کنم که به دوره آموزشی «آموزش پروژه محور Vuex» مراجعه کنید.

منبع

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

  • حس اتوماتیک سازی کارهای front-end با gulp

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

    حسام موسوی