Contenu connexe Similaire à Организация работы с API на Vue.js, Виталий Копачёв (20) Plus de Mail.ru Group (20) Организация работы с API на Vue.js, Виталий Копачёв2. В чем проблема?
● Пишут запросы в
компонентах
● Простыня кода
● Сложное тестирование
● Сложно поддерживать
4. export default {
name: 'AllCoursesPanel',
data() { return { loadingState: false}},
computed: {
...mapState('dashboard', ['availableCourses'])
},
methods: {
...mapActions('dashboard', [
'getAvailableCourses'
])
},
created() {
this.loadingState = true
axios.get('api/v1/user_courses/')
.then((resp) => {
this.$store
.commit('setAvailableCourses', resp.data)
})
.catch((error) => {
if (error.response.status === httpBadRequest) {
this.$store
.dispatch(
'common/errorMessage',
'errors.fetchCoursesError'
)
}
this.$store
.dispatch('common/errorMessage',
'errors.CommonError')
})
.finally(() => {
this.loadingState = false
})
}
}
export default {
name: 'AllCoursesPanel',
data() { return { loadingState: false}},
computed: {
...mapState('dashboard', ['availableCourses'])
},
methods: {
...mapActions('dashboard', [
'getAvailableCourses'
])
},
created() {
this.loadingState = true
this.getAvailableCourses().finally(() => {
this.loadingState = false
})
}
}
Я Сын маминой...
5. export default {
name: 'AllCoursesPanel',
data() { return { loadingState: false}},
computed: {
...mapState('dashboard', ['availableCourses'])
},
methods: {
...mapActions('dashboard', [
'getAvailableCourses'
])
},
created() {
this.loadingState = true
axios.get('api/v1/user_courses/')
.then((resp) => {
this.$store
.commit('setAvailableCourses', resp.data)
})
.catch((error) => {
if (error.response.status === httpBadRequest) {
this.$store
.dispatch(
'common/errorMessage',
'errors.fetchCoursesError'
)
}
this.$store
.dispatch('common/errorMessage',
'errors.CommonError')
})
.finally(() => {
this.loadingState = false
})
}
}
export default {
name: 'AllCoursesPanel',
data() { return { loadingState: false}},
computed: {
...mapState('dashboard', ['availableCourses'])
},
methods: {
...mapActions('dashboard', [
'getAvailableCourses'
])
},
created() {
this.loadingState = true
this.getAvailableCourses().finally(() => {
this.loadingState = false
})
}
}
Получение и отображение смешаны Логика отделена от реализации
6. Чего хотим?
● Понятный код
● Управление кодом
● Удобное тестирование
● Параллельная работа с
кодом
● Конфигурация “в одном
месте”
7. ● Три уровня абстракции
● Разделение зон ответственности
● Инкапсуляция логики
● Упрощение взаимодействия
К чему пришли
12. /**
* Получение текущего курса.
*/
export async function getCurrentCourse({rootGetters, commit,
dispatch}, courseId) {
try {
const data = rootGetters.api.getCourse(courseId)
commit('setCurrentCourse', data)
} catch (err) {
dispatch('common/errorMessage', 'errors.fetchCoursesError',
{root: true})
throw Error(err)
}
}
/**
* Установка текущего курса
*/
export function setCurrentCourse(state, rawCourse) {
state.currentCourse = Course.produceCourse(rawCourse)
}
/**
* Модели дашборда
*/
export class Course {
constructor ({
uid,
name,
description = '',
image_1 = '',
destinationUrl
}) {
this.uid = uid
this.name = name
this.description = description
this.image_1 = image_1
this.destinationUrl = destinationUrl
}
static produceCourse (rawData) {
if (rawData.uid === undefined || rawData.uid === null) {
throw new Error('Bad course data: no data')
}
if (rawData.name === undefined || rawData.name === null) {
throw new Error('Bad course data: no name')
}
if (rawData.destinationUrl === undefined
&& rawData.destinationUrl === null) {
throw new Error('Bad course data: no destinationUrl')
}
return new this(rawData)
}
}
Взаимосвязи уровней
15. <template>
<div class="UserCoursesPanel">
<q-card v-for="course in availableCourses">
<q-card-section>
<div class="text-h6">{{ course.name }}</div>
</q-card-section>`
<q-card-section>{{ course.description }}</q-card-section>
<q-card-actions>
<q-btn color="info" @click="getCurrentCourse(course.uid)">Получить этот курс</q-btn>
</q-card-actions>
</q-card>
<q-btn color="info" @click="getCourses">Посмотреть курсы</q-btn>
</div>
</template>
<script>
import {mapActions, mapState} from 'vuex'
export default {
name: 'UserCoursesPanel',
computed: { ...mapState('dashboard', 'availableCourses')},
methods: {
...mapActions({
getCourses: 'dashboard/getUserCourses',
getCurrentCourse: 'dashboard/getCurrentCourse'
})
}
}
</script>
Уровень отображения
17. /**
* Получение списка доступных курсов пользователя
*/
export async function getAvailableCourses({rootGetters, commit, dispatch})
{
try {
const data = rootGetters.api.getCourses()
commit('setAvailableCourses', data)
} catch (err) {
dispatch('common/errorMessage', 'errors.fetchCoursesError', {root:
true})
throw Error(err)
}
}
Единый интерфейс общения с АПИ
18. /**
* Получение списка доступных курсов пользователя
*/
export async function getAvailableCourses({rootGetters, commit, dispatch})
{
try {
const data = rootGetters.api.getCourses()
commit('setAvailableCourses', data)
} catch (err) {
dispatch('common/errorMessage', 'errors.fetchCoursesError', {root:
true})
throw Error(err)
}
}
Единый интерфейс общения с АПИ
19. /**
* Получение списка доступных курсов пользователя
*/
export async function getAvailableCourses({rootGetters, commit, dispatch})
{
try {
const data = rootGetters.api.getCourses()
commit('setAvailableCourses', data)
} catch (err) {
dispatch('common/errorMessage', 'errors.fetchCoursesError', {root:
true})
throw Error(err)
}
}
Единый интерфейс общения с АПИ
21. /**
* Объект-обертка над клиентом.
* Реализация REST
*/
class ApiClientClass {
constructor(options = {}) {
this.defaultHeaders = options.headers || {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
// Создание экземпляра клиента.
this.client = options.client ||
axios.create({
baseURL: process.env.API_URL ? process.env.API_URL : '',
headers: this.defaultHeaders
});
}
}
Инициализация конфигурации
22. /**
* Объект-обертка над клиентом.
* Реализация REST
*/
class ApiClientClass {
constructor(options = {}) {
this.defaultHeaders = options.headers || {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
// Создание экземпляра клиента.
this.client = options.client ||
axios.create({
baseURL: process.env.API_URL ? process.env.API_URL : '',
headers: this.defaultHeaders
});
}
}
Инициализация конфигурации
24. подготовка текущего запроса
this.client.interceptors.request.use(
/**
* Подготовка запроса
*/
config => {
if (!localStorage.getItem('tAccess')) {
return config
}
const newHeaders = {
...this.defaultHeaders,
Authorization: `Bearer ${localStorage.getItem('tAccess')}`
}
return {
...config,
headers: newHeaders
}
},
e => Promise.reject(e)
)
Подготовка текущего запроса
25. пост-обработка ответа
this.client.interceptors.response.use(
r => r,
async error => {
if (error.response && error.response.status === httpForbidden) {
this.removeTokens()
throw USER_UNAUTHORIZED
}
if (error.response && error.response.status === httpUnauthorized && !error.config.retry) {
try {
const {data} = await this.createRefreshRequest()
this.setTokens({
tAccess: data.access,
tRefresh: data.token
})
const newRequest = {
...error.config,
retry: true
}
return this.client(newRequest)
} catch (err) {
console.warn('')
throw err
} finally {
this.refreshRequest = null
}
}
throw error
}
)
Пост-обработка ответа
26. this.client.interceptors.response.use(
r => r,
async error => {
if (error.response && error.response.status === httpForbidden) {
this.removeTokens()
throw USER_UNAUTHORIZED
}
if (error.response && error.response.status === httpUnauthorized && !error.config.retry) {
try {
const {data} = await this.createRefreshRequest()
this.setTokens({
tAccess: data.access,
tRefresh: data.token
})
const newRequest = {
...error.config,
retry: true
}
return this.client(newRequest)
} catch (err) {
console.warn('')
throw err
} finally {
this.refreshRequest = null
}
}
throw error
}
)
Реакция на ошибку доступа
27. - Обработка неудачных запросов.
- Отдельная обработка для разных групп запросов.
- Повторное выполнение запросов после выполнения некоторых
действий
Что еще можно делать
28. конфигурация интерфейса.
/**
* URL'ы ресурсов бекенда
*/
export const BACKEND_ENDPOINTS = {
createToken: {method: 'post', url: 'api/v1/create_token/'},
updateToken: {method: 'put', url: 'api/v1/update_token/{token}/'},
refreshToken: {method: 'post', url: 'api/v1/refresh_token/'},
userCourses: {method: 'get', url: 'api/v1/user_courses/'},
verifyToken: {method: 'post', url: 'api/v1/verify_token/'},
getCourses: {method: 'get', url: 'api/v1/courses/'},
getCourse: {method: 'get', url: 'api/v1/courses/{courseId}'},
addCourse: {method: 'post', url: 'api/v1/courses'},
modifyCourse: {method: 'patch', url: 'api/v1/courses/{courseId}'},
fetchPracticePages: {method: 'get', url: 'api/v1/practice_pages/'},
attemptsLeft: {method: 'get', url: 'api/v1/quest_stat/attempts_left/'},
currentGitQuestAttempt: {method: 'get', url: 'api/v1/quest_stat/current/'},
startGitNewAttemptQuest: {method: 'post', url: 'api/v1/quest_stat/new_attempt/'},
finishGitNewAttemptQuest: {method: 'put', url: 'api/v1/quest_stat/finish/'}
}
Конфигурация интерфейса
29. /**
* URL'ы ресурсов бекенда
*/
export const BACKEND_ENDPOINTS = {
createToken: {method: 'post', url: 'api/v1/create_token/'},
updateToken: {method: 'put', url: 'api/v1/update_token/{token}/'},
refreshToken: {method: 'post', url: 'api/v1/refresh_token/'},
userCourses: {method: 'get', url: 'api/v1/user_courses/'},
verifyToken: {method: 'post', url: 'api/v1/verify_token/'},
getCourses: {method: 'get', url: 'api/v1/courses/'},
getCourse: {method: 'get', url: 'api/v1/courses/{courseId}'},
addCourse: {method: 'post', url: 'api/v1/courses'},
modifyCourse: {method: 'patch', url: 'api/v1/courses/{courseId}'},
fetchPracticePages: {method: 'get', url: 'api/v1/practice_pages/'},
attemptsLeft: {method: 'get', url: 'api/v1/quest_stat/attempts_left/'},
currentGitQuestAttempt: {method: 'get', url: 'api/v1/quest_stat/current/'},
startGitNewAttemptQuest: {method: 'post', url: 'api/v1/quest_stat/new_attempt/'},
finishGitNewAttemptQuest: {method: 'put', url: 'api/v1/quest_stat/finish/'}
}
Конфигурация интерфейса
30. - В теле запроса.
- В параметрах запроса. В url ресурса.
- Как часть URL. Идентификаторы ресурсов.
где передаются параметры запроса.
Параметры запроса
31. внешний код абстрагирован от этой логики.
getCourse: {method: 'get', url: 'api/v1/courses/{courseId}'},
/**
* Получение текущего курса.
*/
export async function getCurrentCourse({rootGetters, commit, dispatch}, courseId) {
try {
const data = rootGetters.api.getCourse({params: {courseId}})
commit('setCurrentCourse', data)
} catch (err) {
dispatch('common/errorMessage', 'errors.fetchCoursesError', {root: true})
throw Error(err)
}
}
Внешний код абстрагирован
32. addCourse: {method: 'post', url: 'api/v1/courses'},
/**
* Добавление нового курса.
*/
export async function createNewCourse({rootGetters, commit, dispatch}, courseData) {
try {
const data = rootGetters.api.addCourse({data: courseData})
} catch (err) {
dispatch('common/errorMessage', 'errors.addCourseError', {root: true})
throw Error(err)
}
}
Внешний код абстрагирован
33. /**
* Прокси объект для динамического вызова функций апи.
*/
export default new Proxy(
new ApiClientClass(),
{
get: function (target, name) {
if (BACKEND_ENDPOINTS[name] !== undefined) {
return ({params = {}, data = {}, args = {}} = {}) => {
return target.client({
method: BACKEND_ENDPOINTS[name].method,
url: target.urlFormat(BACKEND_ENDPOINTS[name].url, args),
data: data,
params: params
})
.then((serverResponse) => {data: serverResponse.data})
.catch((error) => {
if (error.response.status === httpBadRequest)
new BadDataError('Bad request error')
throw new Error('Server response error')
})
}
} else {
// Если вызов не относиться к вызову стандартного API вызываем его напрямую из объекта
return target[name]
}
}
}
)
Универсальный метод
34. /**
* Прокси объект для динамического вызова функций апи.
*/
export default new Proxy(
new ApiClientClass(),
{
get: function (target, name) {
if (BACKEND_ENDPOINTS[name] !== undefined) {
return ({params = {}, data = {}, args = {}} = {}) => {
return target.client({
method: BACKEND_ENDPOINTS[name].method,
url: target.urlFormat(BACKEND_ENDPOINTS[name].url, args),
data: data,
params: params
})
.then((serverResponse) => {data: serverResponse.data})
.catch((error) => {
if (error.response.status === httpBadRequest)
new BadDataError('Bad request error')
throw new Error('Server response error')
})
}
} else {
// Если вызов не относиться к вызову стандартного API вызываем его напрямую из объекта
return target[name]
}
}
}
)
Универсальный метод
35. /**
* Прокси объект для динамического вызова функций апи.
*/
export default new Proxy(
new ApiClientClass(),
{
get: function (target, name) {
if (BACKEND_ENDPOINTS[name] !== undefined) {
return ({params = {}, data = {}, args = {}} = {}) => {
return target.client({
method: BACKEND_ENDPOINTS[name].method,
url: target.urlFormat(BACKEND_ENDPOINTS[name].url, args),
data: data,
params: params
})
.then((serverResponse) => {data: serverResponse.data})
.catch((error) => {
if (error.response.status === httpBadRequest)
new BadDataError('Bad request error')
throw new Error('Server response error')
})
}
} else {
// Если вызов не относиться к вызову стандартного API вызываем его напрямую из объекта
return target[name]
}
}
}
)
Универсальный метод
36. /**
* Прокси объект для динамического вызова функций апи.
*/
export default new Proxy(
new ApiClientClass(),
{
get: function (target, name) {
if (BACKEND_ENDPOINTS[name] !== undefined) {
return ({params = {}, data = {}, args = {}} = {}) => {
return target.client({
method: BACKEND_ENDPOINTS[name].method,
url: target.urlFormat(BACKEND_ENDPOINTS[name].url, args),
data: data,
params: params
})
.then((serverResponse) => {data: serverResponse.data})
.catch((error) => {
if (error.response.status === httpBadRequest)
new BadDataError('Bad request error')
throw new Error('Server response error')
})
}
} else {
// Если вызов не относиться к вызову стандартного API вызываем его напрямую из объекта
return target[name]
}
}
}
)
Универсальный метод
37. реализация универсального метода
/**
* Прокси объект для динамического вызова функций апи.
*/
export default new Proxy(
new ApiClientClass(),
{
get: function (target, name) {
if (BACKEND_ENDPOINTS[name] !== undefined) {
return ({params = {}, data = {}, args = {}} = {}) => {
return target.client({
method: BACKEND_ENDPOINTS[name].method,
url: target.urlFormat(BACKEND_ENDPOINTS[name].url, args),
data: data,
params: params
})
.then((serverResponse) => {data: serverResponse.data})
.catch((error) => {
if (error.response.status === httpBadRequest)
new BadDataError('Bad request error')
throw new Error('Server response error')
})
}
} else {
// Если вызов не относиться к вызову стандартного API вызываем его напрямую из объекта
return target[name]
}
}
}
)
Универсальный метод
38. /**
* Прокси объект для динамического вызова функций апи.
*/
export default new Proxy(
new ApiClientClass(),
{
get: function (target, name) {
if (BACKEND_ENDPOINTS[name] !== undefined) {
return ({params = {}, data = {}, args = {}} = {}) => {
return target.client({
method: BACKEND_ENDPOINTS[name].method,
url: target.urlFormat(BACKEND_ENDPOINTS[name].url, args),
data: data,
params: params
})
.then((serverResponse) => {data: serverResponse.data})
.catch((error) => {
if (error.response.status === httpBadRequest)
new BadDataError('Bad request error')
throw new Error('Server response error')
})
}
} else {
// Если вызов не относиться к вызову стандартного API вызываем его напрямую из объекта
return target[name]
}
}
}
)
Универсальный метод
39. /**
* Прокси объект для динамического вызова функций апи.
*/
export default new Proxy(
new ApiClientClass(),
{
get: function (target, name) {
if (BACKEND_ENDPOINTS[name] !== undefined) {
return ({params = {}, data = {}, args = {}} = {}) => {
return target.client({
method: BACKEND_ENDPOINTS[name].method,
url: target.urlFormat(BACKEND_ENDPOINTS[name].url, args),
data: data,
params: params
})
.then((serverResponse) => {data: serverResponse.data})
.catch((error) => {
if (error.response.status === httpBadRequest)
new BadDataError('Bad request error')
throw new Error('Server response error')
})
}
} else {
// Если вызов не относиться к вызову стандартного API вызываем его напрямую из объекта
return target[name]
}
}
}
)
Универсальный метод
40. // Получение токенов при помощи авторизационных данных пользователя.
async loginByToken ({ urlArguments }) {
try {
const result = await this.client({
method: BACKEND_ENDPOINTS.updateToken.method,
url: this.urlFormat(BACKEND_ENDPOINTS.updateToken.url, urlArguments)
})
const { data } = result
this.setTokens({
tAccess: data.jwt_token.access,
tRefresh: data.jwt_token.refresh
})
return true
} catch (err) {
if (err.response.status === httpNotFound) {
// Пробуем определить является ли ответ бизнес ответом или это просто не валидный урл
if (err.response.data.result) {
console.warn('Token on server not found err.response = ', err.response)
return false
}
throw new Error(err)
}
throw new Error(err)
}
}
Специальные методы
41. async logout () {
try {
const result = await this.client({
...{
method: BACKEND_ENDPOINTS.updateToken.method,
url: `${BACKEND_ENDPOINTS.updateToken.url}${token}/`
}
})
} catch (error) {
if (error !== USER_UNAUTHORIZED) {
const respStatus = error.response.status
if (![httpForbidden, httpUnauthorized].includes(respStatus)) {
throw new Error(error.response)
}
}
} finally {
this.removeTokens()
}
}
Специальные методы
42. setTokens ({tAccess = '', tRefresh = ''}) {
localStorage.setItem('tAccess', tAccess)
localStorage.setItem('tRefresh', tRefresh)
window.dispatchEvent(new StorageEvent('storage', {key: 'tAccess'}))
window.dispatchEvent(new StorageEvent('storage', {key: 'tRefresh'}))
}
removeTokens() {
localStorage.removeItem('tAccess')
localStorage.removeItem('tRefresh')
window.dispatchEvent(new StorageEvent('storage', {key: 'tAccess'}))
window.dispatchEvent(new StorageEvent('storage', {key: 'tRefresh'}))
}
Хелперы
43. setTokens ({tAccess = '', tRefresh = ''}) {
localStorage.setItem('tAccess', tAccess)
localStorage.setItem('tRefresh', tRefresh)
window.dispatchEvent(new StorageEvent('storage', {key: 'tAccess'}))
window.dispatchEvent(new StorageEvent('storage', {key: 'tRefresh'}))
}
removeTokens() {
localStorage.removeItem('tAccess')
localStorage.removeItem('tRefresh')
window.dispatchEvent(new StorageEvent('storage', {key: 'tAccess'}))
window.dispatchEvent(new StorageEvent('storage', {key: 'tRefresh'}))
}
Хелперы
45. - Обработки ответов опирается на JSON-RPC коды
- Упрощенная логика формирования запроса
- Упрощенная логика обработки ошибок
- Возможность множественного вызова
JSON-RPC vs REST-like
46. JSON-RPC реализация
/**
* Объект-обертка над клиентом.
* Реализация REST
*/
class ApiClientClass {
constructor(options = {}) {
this.defaultHeaders = options.headers || {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
// Создание экземпляра клиента.
this.client = options.client ||
axios.create({
baseURL: process.env.API_URL ?
process.env.API_URL : '',
headers: this.defaultHeaders
})
}
}
/**
* Объект-обертка над клиентом.
* Реализация RPC интерфейса.
*/
class ApiClientClass {
constructor(options = {}) {
this.defaultHeaders = options.headers || {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
const customRequestsUri = process.env.API_URL
+ process.env.API_URN
// Создание экземпляра клиента.
this.client = options.client ||
axios.create({
baseURL: customRequestsUri || 'rpc/',
headers: this.defaultHeaders
})
}
}
JSON - rpc реализация
47. this.client.interceptors.response.use(
r => r,
async error => {
if (error.response && error.response.status ===
httpForbidden) {
this.removeTokens()
throw USER_UNAUTHORIZED
}
if (error.response && error.response.status ===
httpUnauthorized && !error.config.retry) {
try {
const {data} = await this.createRefreshRequest()
this.setTokens({
tAccess: data.access,
tRefresh: data.token
})
const newRequest = {
...error.config,
retry: true
}
return this.client(newRequest)
} catch (err) {
console.warn('')
throw err
} finally {
this.refreshRequest = null
}
}
throw error
}
)
this.client.interceptors.response.use(
async response => {
/**
* Перехватчик неавторизованных запросов при вызове
* одиночного метода Если при попытке доступа к ресурсу
* мы получили код 32001 это означает что нужно
* предпринять попытку повторного запроса токена
*/
const {data} = response
if (data.error
&& data.error.code === RPCUnauthorizedException
&& !response.config.retry
) {
try {
await this.createRefreshRequest()
const newRequest = {
...response.config,
retry: true
}
return this.client(newRequest)
} catch (err) {
console.warn(
'Error when requesting refresh err', err
)
}
}
return response
},
error => error
)
Обработка по JSON - rpc кодам
48. this.client.interceptors.response.use(
r => r,
async error => {
if (error.response && error.response.status ===
httpForbidden) {
this.removeTokens()
throw USER_UNAUTHORIZED
}
if (error.response && error.response.status ===
httpUnauthorized && !error.config.retry) {
try {
const {data} = await this.createRefreshRequest()
this.setTokens({
tAccess: data.access,
tRefresh: data.token
})
const newRequest = {
...error.config,
retry: true
}
return this.client(newRequest)
} catch (err) {
console.warn('')
throw err
} finally {
this.refreshRequest = null
}
}
throw error
}
)
this.client.interceptors.response.use(
async response => {
/**
* Перехватчик неавторизованных запросов при вызове
* одиночного метода Если при попытке доступа к ресурсу
* мы получили код 32001 это означает что нужно
* предпринять попытку повторного запроса токена
*/
const {data} = response
if (data.error
&& data.error.code === RPCUnauthorizedException
&& !response.config.retry
) {
try {
await this.createRefreshRequest()
const newRequest = {
...response.config,
retry: true
}
return this.client(newRequest)
} catch (err) {
console.warn(
'Error when requesting refresh err', err
)
}
}
return response
},
error => error
)
JSON - rpc реализация
49. async callFunction (payload) {
let response = null
try {
response = await this.client({
method: 'post',
data: payload
})
} catch (err) {
console.warn(err)
throw new Error('Not found end point or Server error')
}
const { data } = response
if (Array.isArray(data)) {
return data.map(
rawResultData => rawResultData.error
? { error: rawResultData.error.message, id: rawResultData.id }
: { result: rawResultData.result, id: rawResultData.id }
)
}
if (data.error) {
throw new Error(data.error.message)
}
return data.result
}
/**
* Прокси объект для динамического вызова функций апи.
*/
export default new Proxy(
new ApiClientClass(),
{
get: function (target, name) {
if (BACKEND_ENDPOINTS[name] !== undefined) {
return ({params = {}, data = {}, args = {}} = {}) => {
return target.client({
method: BACKEND_ENDPOINTS[name].method,
url: target.urlFormat(BACKEND_ENDPOINTS[name].url, args),
data: data,
params: params
})
.then((serverResponse) => {data: serverResponse.data})
.catch((error) => {
if (error.response.status === httpBadRequest)
new BadDataError('Bad request error')
throw new Error('Server response error')
})
}
} else {
// Если вызов не относиться к вызову стандартного API
вызываем его напрямую из объекта
return target[name]
}
}
}
)
Обработка ошибок
50. const API_METHODS = {
getCourses: {transport: 'http', url: 'course.getCourses'},
getCourse: {transport: 'http', url: 'course.getCourses'},
addCourse: {transport: 'http', url: 'course.addCourse'},
modifyCourse: {transport: 'http', url: 'course.modifyCourse'},
refreshToken: {transport: 'http', methodName: REFRESH_TOKEN_METHOD_NAME},
verifyCodeByPhoneAndGetToken: {transport: 'http', methodName: VERIFY_SMS_CODE},
createToken: {transport: 'http', methodName: CREATE_TOKEN_METHOD_NAME},
logout: {transport: 'http', methodName: 'logout'},
registerPushNotification: {transport: 'http', methodName: REGISTER_PUSH_NOTIFICATION},
updateCustomerProfile: {transport: 'http', methodName: 'user.updateCustomerProfile'},
getCustomerProfile: {transport: 'http', methodName: 'user.getCustomerProfile'},
getTokenForDownloadFile: {transport: 'http', methodName: 'common.getTokenForDownloadFile'},
personalUserStatistic: {transport: 'http', methodName: 'statistic.personalUserStatistic'}
}
Конфигурация
51. const API_METHODS = {
getCourses: {transport: 'http', url: 'course.getCourses'},
getCourse: {transport: 'http', url: 'course.getCourses'},
addCourse: {transport: 'http', url: 'course.addCourse'},
modifyCourse: {transport: 'http', url: 'course.modifyCourse'},
refreshToken: {transport: 'http', methodName: REFRESH_TOKEN_METHOD_NAME},
verifyCodeByPhoneAndGetToken: {transport: 'http', methodName: VERIFY_SMS_CODE},
createToken: {transport: 'http', methodName: CREATE_TOKEN_METHOD_NAME},
logout: {transport: 'http', methodName: 'logout'},
registerPushNotification: {transport: 'http', methodName: REGISTER_PUSH_NOTIFICATION},
updateCustomerProfile: {transport: 'http', methodName: 'user.updateCustomerProfile'},
getCustomerProfile: {transport: 'http', methodName: 'user.getCustomerProfile'},
getTokenForDownloadFile: {transport: 'http', methodName: 'common.getTokenForDownloadFile'},
personalUserStatistic: {transport: 'http', methodName: 'statistic.personalUserStatistic'}
}
Конфигурация
52. /**
* Генератор маршрутизатора транспорта RPC методов бекенда
*/
Object.keys(API_METHODS)
.forEach(methodName => {
BACKEND_METHODS[methodName] = {
transport: API_METHODS[methodName].transport,
requestPayloadConfig: (params = []) => {
return {
...RPC_20_DATA_STRUCTURE(),
...{
params,
method: API_METHODS[methodName].methodName
}
}
}
}
})
/**
* Регистрация метода API
*/
registerMethod (methodName) {
this[`${methodName}Constructor`] =
BACKEND_METHODS[methodName]
this[methodName] = async (...params) => {
try {
const result = await this
.transports[this[`${methodName}Constructor`].transport]
.callFunction(
this[`${methodName}Constructor`].requestPayloadConfig(pa
rams)
)
return result
} catch (Err) {
throw new Error(`errors.${Err.message}`)
}
}
}
export const RPC_20_DATA_STRUCTURE = () => ({
jsonrpc: '2.0',
method: 'test_example_echo_method',
params: [],
id: uuidv4()
})
Формирование запроса
53. async callFunction (payload) {
let response = null
try {
response = await this.client({
method: 'post',
data: payload
})
} catch (err) {
console.warn(err)
throw new Error('Not found end point or Server error')
}
const { data } = response
if (Array.isArray(data)) {
return data.map(
rawResultData => rawResultData.error
? { error: rawResultData.error.message, id:
rawResultData.id }
: { result: rawResultData.result, id:
rawResultData.id }
)
}
if (data.error) {
throw new Error(data.error.message)
}
return data.result
}
/**
* Регистрация метода API
*/
registerMethod (methodName) {
this[`${methodName}Constructor`] =
BACKEND_METHODS[methodName]
this[methodName] = async (...params) => {
try {
const result = await this
.transports[this[`${methodName}Constructor`].transport]
.callFunction(
this[`${methodName}Constructor`].requestPayloadConfig(pa
rams)
)
return result
} catch (Err) {
throw new Error(`errors.${Err.message}`)
}
}
}
Формирование запроса
54. /**
* Получение текущего курса.
*/
export async function getCurrentCourse({rootGetters, commit, dispatch}, courseId) {
try {
const data = rootGetters.api.getCourse({args: {courseId}})
commit('setCurrentCourse', data)
} catch (err) {
dispatch('common/errorMessage', 'errors.fetchCoursesError', {root: true})
throw Error(err)
}
}
/**
* Получение текущего курса.
*/
export async function getCurrentCourse({rootGetters, commit, dispatch}, courseId) {
try {
const data = rootGetters.api.getCourse(courseId)
commit('setCurrentCourse', data)
} catch (err) {
dispatch('common/errorMessage', 'errors.fetchCoursesError', {root: true})
throw Error(err)
}
}
Интерфейс не изменился
55. /**
* Получение текущего курса.
*/
export async function getCurrentCourse({rootGetters, commit, dispatch}, courseId) {
try {
const data = rootGetters.api.getCourse({args: {courseId}})
commit('setCurrentCourse', data)
} catch (err) {
dispatch('common/errorMessage', 'errors.fetchCoursesError', {root: true})
throw Error(err)
}
}
/**
* Получение текущего курса.
*/
export async function getCurrentCourse({rootGetters, commit, dispatch}, courseId) {
try {
const data = rootGetters.api.getCourse(courseId)
commit('setCurrentCourse', data)
} catch (err) {
dispatch('common/errorMessage', 'errors.fetchCoursesError', {root: true})
throw Error(err)
}
}
Интерфейс не изменился
56. /**
* Отладка мульти-вызова.
* TODO Действие для отладки сервиса АПИ.
*/
export async function sendMulticallMethods ({ rootGetters }) {
try {
const result = await rootGetters.api.callMultiple([
{
methodName: 'testExampleProtectedEchoMethod',
params: [1, 2]
},
{
methodName: 'testExampleEchoMethod',
params: [2, 3]
},
{
methodName: 'testExampleProtectedEchoMethod',
params: [3, 4]
},
{
methodName: 'testExampleEchoMethod',
params: [4, 5]
},
])
return result
} catch (err) {
console.warn('sendMulticallMethods error err =', err)
}
}
Мультивызовы
58. формирование запроса с применением Proxy
export const apiClientFactory = (options = {}) => new Proxy(
new ApiClientClass(options),
{
get: function (target, name) {
return (...args) => {
const preparedData = {
...RPC_20_DATA_STRUCTURE({
inputParams: args,
method: name })
}
// Обращение к Апи.
return target.client({
data: preparedData
}).then(({ data }) => {
if (data.error) {
logger('data.error', data)
throw new Error(data.error)
}
return data.result
}).catch((error) => {
throw new Error(error)
})
}
}
}
)
Применение Proxy
59. export const apiClientFactory = (options = {}) => new Proxy(
new ApiClientClass(options),
{
get: function (target, name) {
return (...args) => {
const preparedData = {
...RPC_20_DATA_STRUCTURE({
inputParams: args,
method: name })
}
// Обращение к Апи.
return target.client({
data: preparedData
}).then(({ data }) => {
if (data.error) {
logger('data.error', data)
throw new Error(data.error)
}
return data.result
}).catch((error) => {
throw new Error(error)
})
}
}
}
)
Применение Proxy
60. export const apiClientFactory = (options = {}) => new Proxy(
new ApiClientClass(options),
{
get: function (target, name) {
return (...args) => {
const preparedData = {
...RPC_20_DATA_STRUCTURE({
inputParams: args,
method: name })
}
// Обращение к Апи.
return target.client({
data: preparedData
}).then(({ data }) => {
if (data.error) {
logger('data.error', data)
throw new Error(data.error)
}
return data.result
}).catch((error) => {
throw new Error(error)
})
}
}
}
)
Применение Proxy
61. export const apiClientFactory = (options = {}) => new Proxy(
new ApiClientClass(options),
{
get: function (target, name) {
return (...args) => {
const preparedData = {
...RPC_20_DATA_STRUCTURE({
inputParams: args,
method: name })
}
// Обращение к Апи.
return target.client({
data: preparedData
}).then(({ data }) => {
if (data.error) {
logger('data.error', data)
throw new Error(data.error)
}
return data.result
}).catch((error) => {
throw new Error(error)
})
}
}
}
)
Применение Proxy
62. /**
* Получение текущего курса.
*/
export async function getCurrentCourse({rootGetters, commit, dispatch}, courseId) {
try {
const data = rootGetters.api.getCourse(courseId)
commit('setCurrentCourse', data)
} catch (err) {
dispatch('common/errorMessage', 'errors.fetchCoursesError', {root: true})
throw Error(err)
}
}
Интерфейс не изменился