Vuex est la bibliothèque officielle de gestion de l'état des applications conçue spécifiquement pour le framework Vue.js.
Vuex implémente un modèle de gestion d'état qui sert de magasin de données centralisé pour tous les composants d'une application.
À mesure que l'application se développe, ce stockage augmente et les données de l'application sont placées dans un seul grand objet.
CloudBlue Connect , , , :
- , ;
- Vuex, ;
- - .
, . , .
. , , .
Vuex, ().
Vuex
1.
BaseRepository
REST API. CRUD-, , .
, , API.
, (: /v1/users
).
:
— query
— .
class BaseRepository {
constructor(entity, version = 'v1') {
this.entity = entity;
this.version = version;
}
get endpoint() {
return `/${this.version}/${this.entity}`;
}
async query({
method = 'GET',
nestedEndpoint = '',
urlParameters = {},
queryParameters = {},
data = undefined,
headers = {},
}) {
const url = parameterize(`${this.endpoint}${nestedEndpoint}`, urlParameters);
const result = await axios({
method,
url,
headers,
data,
params: queryParameters,
});
return result;
}
...
}
— getTotal
— .
Content-Range, : Content-Range: <unit> <range-start>-<range-end>/<size>
.
// getContentRangeSize :: String -> Integer
// getContentRangeSize :: "Content-Range: items 0-137/138" -> 138
const getContentRangeSize = header => +/(\w+) (\d+)-(\d+)\/(\d+)/g.exec(header)[4];
...
async getTotal(urlParameters, queryParameters = {}) {
const { headers } = await this.query({
queryParameters: { ...queryParameters, limit: 1 },
urlParameters,
});
if (!headers['Content-Range']) {
throw new Error('Content-Range header is missing');
}
return getContentRangeSize(headers['Content-Range']);
}
:
listAll
— ;list
— ( );get
— ;create
— ;update
— ;delete
— .
: .
listAll
, . getTotal
, , . chunkSize
.
, .
import axios from 'axios';
// parameterize :: replace substring in string by template
// parameterize :: Object -> String -> String
// parameterize :: {userId: '123'} -> '/users/:userId/activate' -> '/users/123/activate'
const parameterize = (url, urlParameters) => Object.entries(urlParameters)
.reduce(
(a, [key, value]) => a.replace(`:${key}`, value),
url,
);
// responsesToCollection :: Array -> Array
// responsesToCollection :: [{data: [1, 2]}, {data: [3, 4]}] -> [1, 2, 3, 4]
const responsesToCollection = responses => responses.reduce((a, v) => a.concat(v.data), []);
// getContentRangeSize :: String -> Integer
// getContentRangeSize :: "Content-Range: items 0-137/138" -> 138
const getContentRangeSize = header => +/(\w+) (\d+)-(\d+)\/(\d+)/g.exec(header)[4];
// getCollectionAndTotal :: Object -> Object
// getCollectionAndTotal :: { data, headers } -> { collection, total }
const getCollectionAndTotal = ({ data, headers }) => ({
collection: data,
total: headers['Content-Range'] && getContentRangeSize(headers['Content-Range']),
})
export default class BaseRepository {
constructor(entity, version = 'v1') {
this.entity = entity;
this.version = version;
}
get endpoint() {
return `/${this.version}/${this.entity}`;
}
async query({
method = 'GET',
nestedEndpoint = '',
urlParameters = {},
queryParameters = {},
data = undefined,
headers = {},
}) {
const url = parameterize(`${this.endpoint}${nestedEndpoint}`, urlParameters);
const result = await axios({
method,
url,
headers,
data,
params: queryParameters,
});
return result;
}
async getTotal(urlParameters, queryParameters = {}) {
const { headers } = await this.query({
queryParameters: { ...queryParameters, limit: 1 },
urlParameters,
});
if (!headers['Content-Range']) {
throw new Error('Content-Range header is missing');
}
return getContentRangeSize(headers['Content-Range']);
}
async list(queryParameters, urlParameters) {
const result = await this.query({ urlParameters, queryParameters });
return {
...getCollectionAndTotal(result),
params: queryParameters,
};
}
async listAll(queryParameters = {}, urlParameters, chunkSize = 100) {
const params = {
...queryParameters,
offset: 0,
limit: chunkSize,
};
const requests = [];
const total = await this.getTotal(urlParameters, queryParameters);
while (params.offset < total) {
requests.push(
this.query({
urlParameters,
queryParameters: params,
}),
);
params.offset += chunkSize;
}
const result = await Promise.all(requests);
return {
total,
params: {
...queryParameters,
offset: 0,
limit: total,
},
collection: responsesToCollection(result),
};
}
async create(requestBody, urlParameters) {
const { data } = await this.query({
method: 'POST',
urlParameters,
data: requestBody,
});
return data;
}
async get(id = '', urlParameters, queryParameters = {}) {
const { data } = await this.query({
method: 'GET',
nestedEndpoint: `/${id}`,
urlParameters,
queryParameters,
});
return data;
}
async update(id = '', requestBody, urlParameters) {
const { data } = await this.query({
method: 'PUT',
nestedEndpoint: `/${id}`,
urlParameters,
data: requestBody,
});
return data;
}
async delete(id = '', requestBody, urlParameters) {
const { data } = await this.query({
method: 'DELETE',
nestedEndpoint: `/${id}`,
urlParameters,
data: requestBody,
});
return data;
}
}
API, .
, users
:
const usersRepository = new BaseRepository('users');
const win0err = await usersRepository.get('USER-007');
, ?
, , POST- /v1/users/:id/activate
.
, :
class UsersRepository extends BaseRepository {
constructor() {
super('users');
}
activate(id) {
// POST /v1/users/:id/activate
return this.query({
nestedEndpoint: '/:id/activate',
method: 'POST',
urlParameters: { id },
});
}
}
API :
const usersRepository = new UsersRepository();
await usersRepository.activate('USER-007');
await usersRepository.listAll();
2.
, , .
. , , .
, .
value
, :
import {
is,
clone,
} from 'ramda';
const mutations = {
replace: (state, { obj, value }) => {
const data = clone(state[obj]);
state[obj] = is(Function, value) ? value(data) : value;
},
}
, - , .
, .
, :
collection
— ;current
— ;total
— .
, , : get
, list
, listAll
, create
, update
delete
. , .
, , .
, registerModule
: store.registerModule(name, module);
.
, , . , , .
import {
clone,
is,
mergeDeepRight,
} from 'ramda';
const keyBy = (pk, collection) => {
const keyedCollection = {};
collection.forEach(
item => keyedCollection[item[pk]] = item,
);
return keyedCollection;
}
const replaceState = (state, { obj, value }) => {
const data = clone(state[obj]);
state[obj] = is(Function, value) ? value(data) : value;
};
const updateItemInCollection = (id, item) => collection => {
collection[id] = item;
return collection
};
const removeItemFromCollection = id => collection => {
delete collection[id];
return collection
};
const inc = v => ++v;
const dec = v => --v;
export const createStore = (repository, primaryKey = 'id') => ({
namespaced: true,
state: {
collection: {},
currentId: '',
total: 0,
},
getters: {
collection: ({ collection }) => Object.values(collection),
total: ({ total }) => total,
current: ({ collection, currentId }) => collection[currentId],
},
mutations: {
replace: replaceState,
},
actions: {
async list({ commit }, attrs = {}) {
const { queryParameters = {}, urlParameters = {} } = attrs;
const result = await repository.list(queryParameters, urlParameters);
commit({
obj: 'collection',
type: 'replace',
value: keyBy(primaryKey, result.collection),
});
commit({
obj: 'total',
type: 'replace',
value: result.total,
});
return result;
},
async listAll({ commit }, attrs = {}) {
const {
queryParameters = {},
urlParameters = {},
chunkSize = 100,
} = attrs;
const result = await repository.listAll(queryParameters, urlParameters, chunkSize)
commit({
obj: 'collection',
type: 'replace',
value: keyBy(primaryKey, result.collection),
});
commit({
obj: 'total',
type: 'replace',
value: result.total,
});
return result;
},
async get({ commit, getters }, attrs = {}) {
const { urlParameters = {}, queryParameters = {} } = attrs;
const id = urlParameters[primaryKey];
try {
const item = await repository.get(
id,
urlParameters,
queryParameters,
);
commit({
obj: 'collection',
type: 'replace',
value: updateItemInCollection(id, item),
});
commit({
obj: 'currentId',
type: 'replace',
value: id,
});
} catch (e) {
commit({
obj: 'currentId',
type: 'replace',
value: '',
});
throw e;
}
return getters.current;
},
async create({ commit, getters }, attrs = {}) {
const { data, urlParameters = {} } = attrs;
const createdItem = await repository.create(data, urlParameters);
const id = createdItem[primaryKey];
commit({
obj: 'collection',
type: 'replace',
value: updateItemInCollection(id, createdItem),
});
commit({
obj: 'total',
type: 'replace',
value: inc,
});
commit({
obj: 'current',
type: 'replace',
value: id,
});
return getters.current;
},
async update({ commit, getters }, attrs = {}) {
const { data, urlParameters = {} } = attrs;
const id = urlParameters[primaryKey];
const item = await repository.update(id, data, urlParameters);
commit({
obj: 'collection',
type: 'replace',
value: updateItemInCollection(id, item),
});
commit({
obj: 'current',
type: 'replace',
value: id,
});
return getters.current;
},
async delete({ commit }, attrs = {}) {
const { urlParameters = {}, data } = attrs;
const id = urlParameters[primaryKey];
await repository.delete(id, urlParameters, data);
commit({
obj: 'collection',
type: 'replace',
value: removeItemFromCollection(id),
});
commit({
obj: 'total',
type: 'replace',
value: dec,
});
},
},
});
const StoreFactory = (repository, extension = {}) => {
const genericStore = createStore(
repository,
extension.primaryKey || 'id',
);
['state', 'getters', 'actions', 'mutations'].forEach(
part => {
genericStore[part] = mergeDeepRight(
genericStore[part],
extension[part] || {},
);
}
)
return genericStore;
};
export default StoreFactory;
:
const usersRepository = new UsersRepository();
const usersModule = StoreFactory(usersRepository);
, , .
:
import { assoc } from 'ramda';
const usersRepository = new UsersRepository();
const usersModule = StoreFactory(
usersRepository,
{
actions: {
async activate({ commit }, { urlParameters }) {
const { id } = urlParameters;
const item = await usersRepository.activate(id);
commit({
obj: 'collection',
type: 'replace',
value: assoc(id, item),
});
}
}
},
);
3.
, , , , :
import BaseRepository from './BaseRepository';
import StoreFactory from './StoreFactory';
const createRepository = (endpoint, repositoryExtension = {}) => {
const repository = new BaseRepository(endpoint, 'v1');
return Object.assign(repository, repositoryExtension);
}
const ResourceFactory = (
store,
{
name,
endpoint,
repositoryExtension = {},
storeExtension = () => ({}),
},
) => {
const repository = createRepository(endpoint, repositoryExtension);
const module = StoreFactory(repository, storeExtension(repository));
store.registerModule(name, module);
}
export default ResourceFactory;
. , ( ) :
const store = Vuex.Store();
ResourceFactory(
store,
{
name: 'users',
endpoint: 'users',
repositoryExtension: {
activate(id) {
return this.query({
nestedEndpoint: '/:id/activate',
method: 'POST',
urlParameters: { id },
});
},
},
storeExtension: (repository) => ({
actions: {
async activate({ commit }, { urlParameters }) {
const { id } = urlParameters;
const item = await repository.activate(id);
commit({
obj: 'collection',
type: 'replace',
value: assoc(id, item),
});
}
}
}),
},
);
, : , :
{
computed: {
...mapGetters('users', {
users: 'collection',
totalUsers: 'total',
currentUser: 'current',
}),
...mapGetters('groups', {
users: 'collection',
}),
...
},
methods: {
...mapActions('users', {
getUsers: 'list',
deleteUser: 'delete',
updateUser: 'update',
activateUser: 'activate',
}),
...mapActions('groups', {
getAllUsers: 'listAll',
}),
...
async someMethod() {
await this.activateUser({ urlParameters: { id: 'USER-007' } });
...
}
},
}
- , .
, , .
:
ResourceFactory(
store,
{
name: 'userOrders',
endpoint: 'users/:userId/orders',
},
);
:
{
...
methods: {
...mapActions('userOrders', {
getOrder: 'get',
}),
async someMethod() {
const order = await this.getOrder({
urlParameters: {
userId: 'USER-007',
id: 'ORDER-001',
}
});
console.log(order);
}
}
}
. , — . — , . — (mocks), , .
, — , .
, DRY, . , , API . , Content-Range
, .
() , , , , -. , , .
, . , .