Trong bài này chúng ta sẽ build 2 màn hình, màn hình Customer và màn hình shipping address
Codebase sẽ được tổ chức như sau
src
├── App.vue
├── ...
├── store
│ ├── action-types.js
│ ├── index.js
│ ├── modules
│ │ ├── customer.js
│ │ ├── forms
│ │ │ ├── address.js
│ │ │ ├── contact.js
│ │ │ └── name.js
│ │ └── shipping-address.js
│ └── mutation-types.js
└── ...
src/store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
// bật strict mode trên môi trường dev
strict: process.env.NODE_ENV !== 'production',
});
File này sẽ ko có gì nhiều, mọi logic xử lý được tách thành từng modules
src/store/modules/forms
Trên màn hình Customer: chúng ta có 3 dữ liệu: name, contact, address.
Trên màn hình Shipping Address: chúng ta có 2 dữ liệu: contact, address.
Chúng ta có thể tái sử dụng data của 2 màn hình này. Đó là lý do bên trong forms chúng ta có 3 file: contact.js
, address.js
, name.js
File contact.js
// src/store/modules/forms/contact.js
import { getField, updateField } from 'vuex-map-fields';
import { ADD_ROW } from '../../mutation-types';
import { Contact } from '../../../models/Contact';
const mutations = {
updateField,
[ADD_ROW](state) {
// cho phép thêm nhiều row mới.
state.rows.push(new Contact());
},
};
const getters = {
getField,
};
// state phải return 1 function
// để có thể reuse module.
// Xem: https://vuex.vuejs.org/en/modules.html#module-reuse
const state = () => ({
// tạo 1 row rỗng
rows: [new Contact()],
});
export default {
// chúng ta sử dụng namespacing
// trên tất cả modules.
namespaced: true,
mutations,
getters,
state,
};
src/store/modules/customer.js
Module này khá là nhiều, nên chúng ta sẽ giải thích một cách từ từ.
import { createHelpers } from 'vuex-map-fields';
// API được dùng để gửi
// dữ liệu user nhập vào lên server.
import api from '../../utils/api';
// Models của data để gửi lên API
import { createCustomer } from '../../models/Customer';
import { createRequest } from '../../models/Request';
import { SUBMIT } from '../action-types';
import { ERROR, SUCCESS } from '../mutation-types';
import address from './forms/address';
import contact from './forms/contact';
import name from './forms/name';
// ...
Chúng ta sử dụng model để map dữ liệu từ store thành một structure chúng ta sẽ gửi lên API. Nếu muốn xem chi tiết phần này bạn xem trên Github
Phần action của module customer
// src/store/modules/customer.js
// ...
const actions = {
async [SUBMIT]({ commit, state }) {
try {
const customerData = createCustomer({
// Chúng ta chỉ cho user nhập
// một địa chỉ
// (hoặc tên).
address: state.address.rows[0],
// Cho phép user nhập vào nhiều contact
contacts: state.contact.rows,
name: state.name.rows[0],
});
const requestData = createRequest(customerData);
await api(requestData);
commit(SUCCESS);
} catch (error) {
commit(ERROR, error.message);
}
},
};
// ...
SUBMIT
action ở trên chịu trách nhiệm gửi data lên API, xử lý status trả về. createCustomer
là function sẽ giúp transform data mà API yêu cầu.
Chúng ta cần 2 mutation cho màn customer form:
1. ERROR
để set state error khi request bị fail
2. SUCCESS
cho trường hợp thành công
// src/store/modules/customer.js
// ...
const mutations = {
[ERROR](state, error) {
state.error = error;
state.success = false;
},
[SUCCESS](state) {
state.error = false;
state.success = true;
},
};
// ...
Tới đây chúng ta có thể define object state cần có
// src/store/modules/customer.js
// ...
const state = () => ({
error: false,
success: false,
});
const modules = {
address,
contact,
name,
};
// ...
Để thuận tiện hơn trong lúc code, chúng ta đang sử dụng một số hàm helper của vuex-map-fields
// src/store/modules/customer.js
// ...
// Để mapping form fields qua Vuex.
// Xem: https://github.com/maoberlehner/vuex-map-fields#custom-getters-and-mutations
export const { mapFields: mapAddressFields } = createHelpers({
getterType: 'customer/address/getField',
mutationType: 'customer/address/updateField',
});
export const { mapMultiRowFields: mapContactMultiRowFields } = createHelpers({
getterType: 'customer/contact/getField',
mutationType: 'customer/contact/updateField',
});
export const { mapFields: mapNameFields } = createHelpers({
getterType: 'customer/name/getField',
mutationType: 'customer/name/updateField',
});
export const customer = {
namespaced: true,
actions,
mutations,
state,
modules,
};
PageCustomer.vue
<template>
<div :class="$options.name">
<h1>New Customer</h1>
<p class="success" v-if="success">
SUCCESS!
</p>
<p class="error" v-if="error">
ERROR: {{ error }}
</p>
<template v-if="!success">
<div class="form-sections">
<section class="form-section">
<div class="form-element">
<label for="firstName" class="form-label">First name:</label>
<input id="firstName" v-model="firstName">
</div>
<div class="form-element">
<label for="lastName" class="form-label">Last name:</label>
<input id="lastName" v-model="lastName">
</div>
</section>
<section class="form-section">
<div class="form-repeatable" v-for="(contact, index) in contacts" :key="index">
<div class="form-element">
<label for="email" class="form-label">E-Mail:</label>
<input id="email" type="email" v-model="contact.email">
</div>
<div class="form-element">
<label for="phone" class="form-label">Phone:</label>
<input id="phone" v-model="contact.phone">
</div>
</div>
<button class="form-button" @click="addContact">Add contact</button>
</section>
<section class="form-section">
<div class="form-element">
<label for="zip" class="form-label">ZIP:</label>
<input id="zip" v-model="zip">
</div>
<div class="form-element">
<label for="town" class="form-label">Town:</label>
<input id="town" v-model="town">
</div>
<div class="form-element">
<label for="street" class="form-label">Street:</label>
<input id="street" v-model="street">
</div>
</section>
</div>
<button class="form-button" @click="submit">
Submit
</button>
</template>
</div>
</template>
<script>
import { createNamespacedHelpers } from 'vuex';
import { SUBMIT } from '../../store/action-types';
import { ADD_ROW } from '../../store/mutation-types';
import store from '../../store';
import {
customer,
mapAddressFields,
mapContactMultiRowFields,
mapNameFields,
} from '../../store/modules/customer';
// chủ động đăng ký `customer` module
// chỉ load module này khi cần thiết
// trước khi đăng ký module
// kiểm tra xem nó được đăng ký chưa
if (!store.state.customer) {
store.registerModule('customer', customer);
}
const {
mapActions: mapCustomerActions,
mapState: mapCustomerState,
} = createNamespacedHelpers('customer');
const {
mapMutations: mapContactMutations,
} = createNamespacedHelpers('customer/contact');
export default {
name: 'PageCustomer',
// Here we're wiring everything up.
computed: {
...mapCustomerState(['error', 'success']),
// Đọc thêm về mapping field value
// https://markus.oberlehner.net/blog/form-fields-two-way-data-binding-and-vuex/
// https://markus.oberlehner.net/blog/how-to-handle-multi-row-forms-with-vue-vuex-and-vuex-map-fields/
...mapNameFields(['rows[0].firstName', 'rows[0].lastName']),
...mapContactMultiRowFields({ contacts: 'rows' }),
...mapAddressFields(['rows[0].zip', 'rows[0].town', 'rows[0].street']),
},
methods: {
...mapContactMutations({
addContact: ADD_ROW,
}),
...mapCustomerActions({
submit: SUBMIT,
}),
},
};
</script>
Bạn có thể thấy là không có xử lý logic bên trong component. Tất cả những gì làm ở component là map các action, field, mutation, field từ store module vào component.
Tổng kết
Để tổng kết lại chúng ta cũng nhìn lại chúng ta đạt được gì và cách tiếp cận structure như thế này trong Vuex store khác gì so với cách truyền thống
Chủ động load các module
Bởi vì chúng ta không đăng ký tất cả các module một cách globally, no cho phép sử dụng tính năng webpack splitting code hoạt động với vue-router.
Tối đa việc tái sử dụng
Với cách thiết kế store như thế này, chúng ta có thể sử dụng lại một số module. Chìa khóa để đạt được chính là đặt tên theo rule cố định và cách structure module, luôn tuân thủ các quy định giúp chúng ta có thể đoán được.
Initializing...