SlideShare a Scribd company logo
Организация работы с
API на Vue.js
Копачёв Виталий
В чем проблема?
● Пишут запросы в
компонентах
● Простыня кода
● Сложное тестирование
● Сложно поддерживать
Немного кода
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
})
}
}
Я Сын маминой...
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
})
}
}
Получение и отображение смешаны Логика отделена от реализации
Чего хотим?
● Понятный код
● Управление кодом
● Удобное тестирование
● Параллельная работа с
кодом
● Конфигурация “в одном
месте”
● Три уровня абстракции
● Разделение зон ответственности
● Инкапсуляция логики
● Упрощение взаимодействия
К чему пришли
Запросы к
API
META
информация Data Recieving Отслеживание
Post
Processing
Pre
Processing
Уровень сервисов
Абстракции бизнес-логикиАбстракции бизнес-логики
Взаимосвязи уровней
Сервисы
Получение сырых
данных.
Обращение к внешним
АПИ.
Бизнес-логика.
Сущности.
Валидация уровня
предметной области.Вызов методов
Данные АПИ
Взаимосвязи уровней
/**
* Получение текущего курса.
*/
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)
}
}
Взаимосвязи уровней
- Уровень рендеринга.
- Уровень компонентов.
Уровень отображения
Бизнес-логика.
Взаимодействие.
Поведение.
Алгоритмы.
Отображение.
Внешний вид конкретных
данных.
Логика отображения.Вызов действий.
Данные для
отображения
Взаимосвязи уровней
<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>
Уровень отображения
Сервисы
“Это уже не хамство. Однако все еще не сервис”
Довлатов С.
/**
* Получение списка доступных курсов пользователя
*/
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)
}
}
Единый интерфейс общения с АПИ
/**
* Получение списка доступных курсов пользователя
*/
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)
}
}
Единый интерфейс общения с АПИ
/**
* Получение списка доступных курсов пользователя
*/
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)
}
}
Единый интерфейс общения с АПИ
Инкапсулированный функционал
Инициализация конфигурации.
Установка url ресурсов.
Подготовка текущего запроса.
Добавление хедеров.
Внутренняя обработки ответов на
отдельные запросы.
Общая логика выполнения
запросов.
Сервис АПИ
/**
* Объект-обертка над клиентом.
* Реализация 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
});
}
}
Инициализация конфигурации
/**
* Объект-обертка над клиентом.
* Реализация 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
});
}
}
Инициализация конфигурации
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)
)
Подготовка текущего запроса
подготовка текущего запроса
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)
)
Подготовка текущего запроса
пост-обработка ответа
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(
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
}
)
Реакция на ошибку доступа
- Обработка неудачных запросов.
- Отдельная обработка для разных групп запросов.
- Повторное выполнение запросов после выполнения некоторых
действий
Что еще можно делать
конфигурация интерфейса.
/**
* 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/'}
}
Конфигурация интерфейса
/**
* 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/'}
}
Конфигурация интерфейса
- В теле запроса.
- В параметрах запроса. В url ресурса.
- Как часть URL. Идентификаторы ресурсов.
где передаются параметры запроса.
Параметры запроса
внешний код абстрагирован от этой логики.
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)
}
}
Внешний код абстрагирован
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)
}
}
Внешний код абстрагирован
/**
* Прокси объект для динамического вызова функций апи.
*/
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]
}
}
}
)
Универсальный метод
/**
* Прокси объект для динамического вызова функций апи.
*/
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]
}
}
}
)
Универсальный метод
/**
* Прокси объект для динамического вызова функций апи.
*/
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]
}
}
}
)
Универсальный метод
/**
* Прокси объект для динамического вызова функций апи.
*/
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]
}
}
}
)
Универсальный метод
реализация универсального метода
/**
* Прокси объект для динамического вызова функций апи.
*/
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]
}
}
}
)
Универсальный метод
/**
* Прокси объект для динамического вызова функций апи.
*/
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]
}
}
}
)
Универсальный метод
/**
* Прокси объект для динамического вызова функций апи.
*/
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]
}
}
}
)
Универсальный метод
// Получение токенов при помощи авторизационных данных пользователя.
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)
}
}
Специальные методы
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()
}
}
Специальные методы
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'}))
}
Хелперы
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'}))
}
Хелперы
REST-like JSON-RPC
- Обработки ответов опирается на JSON-RPC коды
- Упрощенная логика формирования запроса
- Упрощенная логика обработки ошибок
- Возможность множественного вызова
JSON-RPC vs REST-like
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 реализация
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 кодам
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 реализация
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]
}
}
}
)
Обработка ошибок
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'}
}
Конфигурация
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'}
}
Конфигурация
/**
* Генератор маршрутизатора транспорта 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()
})
Формирование запроса
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}`)
}
}
}
Формирование запроса
/**
* Получение текущего курса.
*/
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)
}
}
Интерфейс не изменился
/**
* Получение текущего курса.
*/
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)
}
}
Интерфейс не изменился
/**
* Отладка мульти-вызова.
* 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)
}
}
Мультивызовы
JSON-RPC реализация на Proxy
формирование запроса с применением 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
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
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
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
/**
* Получение текущего курса.
*/
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)
}
}
Интерфейс не изменился
Сервер
Как это выглядит на сервере
как это выглядит на сервере
Как это выглядит на сервере
как это выглядит на сервере
Как это выглядит на сервере
Как это выглядит на сервере
Копачёв Виталий
tg: @vetos_88
Спасибо за внимание
внутренняя логика. Выделение токенов

More Related Content

PPTX
Реализация шаблонов корпоративных приложений в Magento
PPTX
course js day 4
PDF
Примеры решения типичных задач за рамками ядра Yii2
PPTX
Индексирование в Magento
PPT
Общая архитектура Yii2
PPTX
Meet Magento Belarus debug Pavel Novitsky (rus)
PDF
YiiConf: Миграции и инсталляции
PPTX
Yii2
Реализация шаблонов корпоративных приложений в Magento
course js day 4
Примеры решения типичных задач за рамками ядра Yii2
Индексирование в Magento
Общая архитектура Yii2
Meet Magento Belarus debug Pavel Novitsky (rus)
YiiConf: Миграции и инсталляции
Yii2

What's hot (19)

PPT
Web осень 2012 лекция 4
PPTX
Все дороги ведут в Checkout
PDF
Продвинутое использование ActiveRecord в Yii2
PPT
Web весна 2013 лекция 4
PPTX
Разработка расширяемых приложений на Django
PPT
Yii development
PPTX
Andrew Borisenko "Magic of Vue.js""
PPTX
DevHub 3 - Pricing
PDF
MySQL replication from setup to advanced features. Hidden MySQL replication o...
PDF
Ф'Yii'лософия
PDF
2014 Jeeconf - Geb Spock
PDF
Crazy owl yii1=> yii2
PPT
Form api в drupal 7
PPT
Импорт данных с фреймворком Migrate. Владислав Богатырев.
PPTX
ZFConf 2011: Разделение труда: Организация многозадачной, распределенной сист...
PDF
Rambler.iOS #8: Как не стать жертвой бэкендеров
PDF
"Жизнь без интернета" Кувалдин Артём, Яндекс
PDF
Pycon Russia 2013 - Разработка через тестирование в Python и Django
PPTX
automation is iOS development
Web осень 2012 лекция 4
Все дороги ведут в Checkout
Продвинутое использование ActiveRecord в Yii2
Web весна 2013 лекция 4
Разработка расширяемых приложений на Django
Yii development
Andrew Borisenko "Magic of Vue.js""
DevHub 3 - Pricing
MySQL replication from setup to advanced features. Hidden MySQL replication o...
Ф'Yii'лософия
2014 Jeeconf - Geb Spock
Crazy owl yii1=> yii2
Form api в drupal 7
Импорт данных с фреймворком Migrate. Владислав Богатырев.
ZFConf 2011: Разделение труда: Организация многозадачной, распределенной сист...
Rambler.iOS #8: Как не стать жертвой бэкендеров
"Жизнь без интернета" Кувалдин Артём, Яндекс
Pycon Russia 2013 - Разработка через тестирование в Python и Django
automation is iOS development
Ad

Similar to Организация работы с API на Vue.js, Виталий Копачёв (14)

PDF
'The best practices' by KONSTANTIN KULAKSYZ at OdessaJS'2020
PDF
Тестирование API дизайна [NoBugs WTF PRO уровень]
PPT
Rich-client, или Как я перестал боятся и полюбил велосипеды / Владимир Дупелев
PPTX
Дизайн REST API для высокопроизводительных систем / Александр Лебедев (Новые ...
PPTX
RESTful API: Best practices, versioning, design documentation
PPTX
ITmozg, Даниил Павлючков
PPTX
API плюс толстый клиент – новая парадигма веб-разработки? / Андрей Лебедев (Г...
PDF
Vuejs composition API
PPTX
Enterprise flex pure mvc, slides, russian
PPT
JavaScript-библиотека
PPTX
Особенности разработки API / Всеволод Шмыров (Яндекс)
PDF
"Рекомендации по проектированию API". Марина Степанова, Яндекс
PDF
"Рекомендации по проектированию API" — Марина Степанова, Яндекс
PDF
Оптимизация react+redux приложений | Odessa Frontend Meetup #7
'The best practices' by KONSTANTIN KULAKSYZ at OdessaJS'2020
Тестирование API дизайна [NoBugs WTF PRO уровень]
Rich-client, или Как я перестал боятся и полюбил велосипеды / Владимир Дупелев
Дизайн REST API для высокопроизводительных систем / Александр Лебедев (Новые ...
RESTful API: Best practices, versioning, design documentation
ITmozg, Даниил Павлючков
API плюс толстый клиент – новая парадигма веб-разработки? / Андрей Лебедев (Г...
Vuejs composition API
Enterprise flex pure mvc, slides, russian
JavaScript-библиотека
Особенности разработки API / Всеволод Шмыров (Яндекс)
"Рекомендации по проектированию API". Марина Степанова, Яндекс
"Рекомендации по проектированию API" — Марина Степанова, Яндекс
Оптимизация react+redux приложений | Odessa Frontend Meetup #7
Ad

More from Mail.ru Group (20)

PDF
Автоматизация без тест-инженеров по автоматизации, Мария Терехина и Владислав...
PDF
BDD для фронтенда. Автоматизация тестирования с Cucumber, Cypress и Jenkins, ...
PDF
Другая сторона баг-баунти-программ: как это выглядит изнутри, Владимир Дубровин
PDF
Использование Fiddler и Charles при тестировании фронтенда проекта pulse.mail...
PDF
Управление инцидентами в Почте Mail.ru, Антон Викторов
PDF
DAST в CI/CD, Ольга Свиридова
PDF
Почему вам стоит использовать свой велосипед и почему не стоит Александр Бел...
PDF
CV в пайплайне распознавания ценников товаров: трюки и хитрости Николай Масл...
PDF
RAPIDS: ускоряем Pandas и scikit-learn на GPU Павел Клеменков, NVidia
PDF
WebAuthn в реальной жизни, Анатолий Остапенко
PDF
AMP для электронной почты, Сергей Пешков
PDF
Как мы захотели TWA и сделали его без мобильных разработчиков, Данила Стрелков
PDF
Кейсы использования PWA для партнерских предложений в Delivery Club, Никита Б...
PDF
Метапрограммирование: строим конечный автомат, Сергей Федоров, Яндекс.Такси
PDF
Как не сделать врагами архитектуру и оптимизацию, Кирилл Березин, Mail.ru Group
PDF
Этика искусственного интеллекта, Александр Кармаев (AI Journey)
PDF
Нейро-машинный перевод в вопросно-ответных системах, Федор Федоренко (AI Jour...
PDF
Конвергенция технологий как тренд развития искусственного интеллекта, Владими...
PDF
Обзор трендов рекомендательных систем от Пульса, Андрей Мурашев (AI Journey)
PDF
Мир глазами нейросетей, Данила Байгушев, Александр Сноркин ()
Автоматизация без тест-инженеров по автоматизации, Мария Терехина и Владислав...
BDD для фронтенда. Автоматизация тестирования с Cucumber, Cypress и Jenkins, ...
Другая сторона баг-баунти-программ: как это выглядит изнутри, Владимир Дубровин
Использование Fiddler и Charles при тестировании фронтенда проекта pulse.mail...
Управление инцидентами в Почте Mail.ru, Антон Викторов
DAST в CI/CD, Ольга Свиридова
Почему вам стоит использовать свой велосипед и почему не стоит Александр Бел...
CV в пайплайне распознавания ценников товаров: трюки и хитрости Николай Масл...
RAPIDS: ускоряем Pandas и scikit-learn на GPU Павел Клеменков, NVidia
WebAuthn в реальной жизни, Анатолий Остапенко
AMP для электронной почты, Сергей Пешков
Как мы захотели TWA и сделали его без мобильных разработчиков, Данила Стрелков
Кейсы использования PWA для партнерских предложений в Delivery Club, Никита Б...
Метапрограммирование: строим конечный автомат, Сергей Федоров, Яндекс.Такси
Как не сделать врагами архитектуру и оптимизацию, Кирилл Березин, Mail.ru Group
Этика искусственного интеллекта, Александр Кармаев (AI Journey)
Нейро-машинный перевод в вопросно-ответных системах, Федор Федоренко (AI Jour...
Конвергенция технологий как тренд развития искусственного интеллекта, Владими...
Обзор трендов рекомендательных систем от Пульса, Андрей Мурашев (AI Journey)
Мир глазами нейросетей, Данила Байгушев, Александр Сноркин ()

Организация работы с API на Vue.js, Виталий Копачёв

  • 1. Организация работы с 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. ● Три уровня абстракции ● Разделение зон ответственности ● Инкапсуляция логики ● Упрощение взаимодействия К чему пришли
  • 8. Запросы к API META информация Data Recieving Отслеживание Post Processing Pre Processing Уровень сервисов
  • 11. Сервисы Получение сырых данных. Обращение к внешним АПИ. Бизнес-логика. Сущности. Валидация уровня предметной области.Вызов методов Данные АПИ Взаимосвязи уровней
  • 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) } } Взаимосвязи уровней
  • 13. - Уровень рендеринга. - Уровень компонентов. Уровень отображения
  • 14. Бизнес-логика. Взаимодействие. Поведение. Алгоритмы. Отображение. Внешний вид конкретных данных. Логика отображения.Вызов действий. Данные для отображения Взаимосвязи уровней
  • 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> Уровень отображения
  • 16. Сервисы “Это уже не хамство. Однако все еще не сервис” Довлатов С.
  • 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) } } Единый интерфейс общения с АПИ
  • 20. Инкапсулированный функционал Инициализация конфигурации. Установка url ресурсов. Подготовка текущего запроса. Добавление хедеров. Внутренняя обработки ответов на отдельные запросы. Общая логика выполнения запросов. Сервис АПИ
  • 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 }); } } Инициализация конфигурации
  • 23. 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) ) Подготовка текущего запроса
  • 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) } } Интерфейс не изменился
  • 64. Как это выглядит на сервере
  • 65. как это выглядит на сервере Как это выглядит на сервере
  • 66. как это выглядит на сервере Как это выглядит на сервере
  • 67. Как это выглядит на сервере