Vuexは、Vue.jsフレームワーク用に特別に設計された公式のアプリケーション状態管理ライブラリです。
Vuexは、アプリケーションのすべてのコンポーネントの集中データストアとして機能する状態管理パターンを実装します。
アプリケーションが大きくなると、このストレージも大きくなり、アプリケーションデータは1つの大きなオブジェクトに配置されます。
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
, .
() , , , , -. , , .
, . , .