предисловие
Во фронтенд-разработке, если странице необходимо взаимодействовать с фоновым интерфейсом, а страница не обновляется, вам нужно использовать http-библиотеку Ajax для завершения работы по стыковке с фоновым интерфейсом данных. существуетjQuery
Когда это будет очень популярно, мы будем использовать$.ajax()
, а теперь есть и другие варианты, например:SuperAgent
,Axios
,Fetch
…так далее. Благодаря этим http-библиотекам нам больше не нужно обращать внимание на слишком много деталей, связанных с базовым ajax. Во многих случаях и сценариях нам нужно сосредоточиться только на том, как составить запрос и как обработать ответ, но даже если эти http-библиотеки в определенной степени упростили нашу работу по разработке, нам все равно нужно сосредоточиться на реальных потребностях проект, внутренняя технология команды Спецификация инкапсулирует эти http-библиотеки для оптимизации эффективности нашей разработки.
В этой статье будет объединена http-библиотека, используемая нашей командой.Axios
Поделитесь некоторыми сценариями проекта разработки нашей команды и поделитесь опытом нашей команды внешнего интерфейса по инкапсуляции http-библиотеки.
Базовая инкапсуляция библиотеки http
Определение интерфейса URL на стороне сервера
Возьмем, к примеру, модуль управления пользователями. Для модуля управления пользователями сервер обычно определяет следующие интерфейсы:
-
GET /users?page=0&size=20
- Получить разбитый на страницы список информации о пользователе -
GET /users/all
- Получить список всей информации о пользователе -
GET /users/:id
- уточнитьid
информация о пользователе -
POST /users application/x-www-form-urlencoded
- Создать пользователя -
PUT /users/:id application/x-www-form-urlencoded
- Обновить информацию о пользователе указанного идентификатора -
DELETE /users/:id
удалить обозначениеid
информация о пользователе
С помощью приведенных выше определений нетрудно обнаружить, что это интерфейсы, определенные на основе стандарта RESTful.
Модульная инкапсуляция интерфейса
Для такого модуля управления пользователями первое, что нам нужно сделать, это определить класс модуля управления пользователями.
// UserManager.js
import axios from 'axios'
class UserManager {
constructor() {
this.$http = axios.create({
baseUrl: 'https://api.forcs.com' // 当然,这个地址是虚拟的
})
// 修改POST和PUT请求默认的Content-Type,根据自己项目后端的定义而定,不一定需要
this.dataMethodDefaults = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
transformRequest: [function (data) {
return qs.stringify(data)
}]
}
}
}
export default new UserManager() // 单例模块
существуетUserManager
В конструкторе мы устанавливаем некоторые общедоступные параметры запроса, такие как интерфейсbaseUrl
, чтобы при последующем запросе в URL-адресе нужно было использовать только относительный путь. В то же время мы также скорректировали запрос POST и запрос PUT по умолчанию.Content-Type
.Axios
По умолчаниюapplication/json
, мы настраиваем его на тип формы в соответствии с определением внутреннего интерфейсаapplication/x-www-form-urlencoded
. Наконец, благодаря модульности ES6 мыUserManager
Синглтон.
В реальных сценариях набор спецификаций внутреннего интерфейса, соответствующих отраслевым стандартам, намного сложнее. Поскольку они не являются предметом обсуждения, они упрощены.
Далее дайтеUserManager
Добавьте метод для вызова интерфейса.
import axios from 'axios'
import qs from 'query-string'
class UserManager {
constructor() {
this.$http = axios.create({
baseUrl: 'https://api.forcs.com'
})
this.dataMethodDefaults = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
transformRequest: [function (data) {
return qs.stringify(data)
}]
}
}
getUsersPageableList (page = 0, size = 20) {
return this.$http.get(`/users?page=${page}&size=${size}`)
}
getUsersFullList () {
return this.$http.get('/users/all')
}
getUser (id) {
if (!id) {
return Promise.reject(new Error(`getUser:id(${id})无效`))
}
return this.$http.get(`/users/${id}`)
}
createUser (data = {}) {
if (!data || !Object.keys(data).length) {
return Promise.reject(new Error('createUser:提交的数据无效'))
}
return this.$http.post('/users', data, { ...this.dataMethodDefaults })
}
updateUser (id, update = {}) {
if (!update || !Object.keys(update).length) {
return Promise.reject(new Error('updateUser:提交的数据无效'))
}
return this.$http.put(`/users/${id}`, update, { ...this.dataMethodDefaults })
}
deleteUser (id) {
if (!id) {
return Promise.reject(new Error(`deleteUser:id(${id})无效`))
}
return this.$http.delete(`/users/${id}`)
}
}
export default new UserManager()
В новом методе нет ничего особенного, он понятен с первого взгляда, т.Axios
Выполните http-запрос для вызова интерфейса сервера. Стоит отметить, что вgetUser()
,createUser()
,updateUser()
,deleteUser()
В этих четырех методах мы просто проверили параметры.Конечно, реальная сцена будет сложнее, чем пример кода.На самом деле проверка параметров не имеет значения, ключ находится в проверке.if
в блоке операторов,return
этоPromise
объект, это для иAxios
API остается стабильным.
Внешний интерфейс вызывает инкапсулированный метод
После такой инкапсуляции взаимодействие между фронтенд-страницей и сервером становится намного проще. Ниже приведен пример внешнего кода версии Vue.
<!-- src/components/UserManager.vue -->
<template>
<!-- 模板代码可以忽略 -->
</template>
<script>
import userManager from '../services/UserManager'
export default {
data () {
return {
userList: [],
currentPage: 0,
currentPageSize: 20,
formData: {
account: '',
nickname: '',
email: ''
}
}
},
_getUserList () {
userManager.getUser(this.currentPage, this.currentPageSize)
.then(response => {
this.userList = response.data
}).catch(err => {
console.error(err.message)
})
},
mounted () {
// 加载页面的时候,获取用户列表
this._getUserList()
},
handleCreateUser () {
// 提交创建用户的表单
userManager.createUser({ ...this.formData })
.then(response => {
// 刷新列表
this._getUserList()
}).catch(err => {
console.error(err.message)
})
}
}
</script>
Конечно, аналогичный код js также применим на странице интерфейса версии React.
// src/components/UserList.js
import React from 'react'
import userManager from '../servers/UserManager'
class UserManager extends React.Compnent {
constructor (props) {
super(props)
this.state.userList = []
this.handleCreateUser = this.handleCreateUser.bind(this)
}
_getUserList () {
userManager.getUser(this.currentPage, this.currentPageSize)
.then(response => {
this.setState({ userList: userList = response.data })
}).catch(err => {
console.error(err.message)
})
}
componentDidMount () {
this._getUserList()
}
handleCreateUser (data) {
userManager.createUser({ ...data })
.then(response => {
this._getUserList()
}).catch(err => {
console.error(err.message)
})
}
render () {
// 模板代码就可以忽略了
return (/* ...... */)
}
}
export default UserManager
В целях экономии места код, вызывающий инкапсулированный модуль на странице внешнего интерфейса, позже не будет отображаться.
хорошо, интерфейс очень удобен в использовании, и кажется, что ничего плохого в инкапсуляции на этом шаге нет. Однако как приложение может иметь так много интерфейсов, оно будет включать несколько интерфейсов, и разные интерфейсы могут быть классифицированы в разные модули. Возьмем, к примеру, наш закулисный проект, модуль управления контентом разделен на управление отдельными частями и управление эпизодами. Интерфейс модуля управления определяется следующим образом:
Монолитное управление:
GET /videos?page=0&size=20
GET /videos/all
GET /videos/:id
POST /videos application/x-www-form-urlencoded
PUT /videos/:id application/x-www-form-urlencoded
DELETE /videos/:id
Управление эпизодом:
GET /episodes?page=0&size=20
GET /episodes/all
GET /episodes/:id
POST /episodes application/x-www-form-urlencoded
PUT /episodes/:id application/x-www-form-urlencoded
DELETE /episodes/:id
Из-за нехватки места перечислены не все интерфейсы. Вы можете видеть, что интерфейс по-прежнему определен в соответствии со стандартом RESTful. В соответствии с упомянутой ранее практикой мы можем сразу же инкапсулировать эти интерфейсы.
Определить класс модуля для управления элементамиVideoManager
// VideoManager.js
import axios from 'axios'
import qs from 'query-string'
class VideoManager {
constructor () {
this.$http = axios.create({
baseUrl: 'https://api.forcs.com'
})
this.dataMethodDefaults = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
transformRequest: [function (data) {
return qs.stringify(data)
}]
}
}
getVideosPageableList (page = 0, size = 20) {
return this.$http.get(`/videos?page=${page}&size=${size}`)
}
getVideosFullList () {
return this.$http.get('/videos/all')
}
getVideo (id) {
if (!id) {
return Promise.reject(new Error(`getVideo:id(${id})无效`))
}
return this.$http.get(`/videos/${id}`)
}
// ... 篇幅原因,后面的接口省略
}
export default new VideoManager()
и класс модуля для управления эпизодамиEpisodeManager.js
//EpisodeManager.js
import axios from 'axios'
import qs from 'query-string'
class EpisodeManager {
constructor () {
this.$http = axios.create({
baseUrl: 'https://api.forcs.com'
})
this.dataMethodDefaults = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
transformRequest: [function (data) {
return qs.stringify(data)
}]
}
}
getEpisodesPageableList (page = 0, size = 20) {
return this.$http.get(`/episodes?page=${page}&size=${size}`)
}
getEpisodesFullList () {
return this.$http.get('/episodes/all')
}
getEpisode (id) {
if (!id) {
return Promise.reject(new Error(`getEpisode:id(${id})无效`))
}
return this.$http.get(`/episodes/${id}`)
}
// ... 篇幅原因,后面的接口省略
}
export default new EpisodeManager()
Вы нашли проблему? Есть дублирующий код, который выдаст скрытую опасность. Среди принципов программирования есть очень известный принцип:DRY, перевод должен максимально избегать повторения кода. В гибкой фронтенд-разработке больше внимания уделяйте этому принципу: чем больше дублируется код, тем выше стоимость обслуживания и тем ниже гибкость и надежность. Думайте об этом как о крупномасштабном приложении, включающем десятки модулей, и каждый модуль должен написать такой код.Если позже будет какая-либо корректировка общедоступных свойств, такое изменение просто катастрофа!
Что нужно сделать, чтобы улучшить возможность повторного использования кода, гибкость и уменьшить повторяющийся код? Если вы знакомы с ООП, вы сможете довольно быстро во всем разобраться — определить родительский класс и абстрагироваться от общих частей.
Сделайте инкапсулированные модули более пригодными для повторного использования
Рефакторинг с использованием наследования
определить родительский классBaseModule
, и поместите все общие части кода в этот родительский класс.
// BaseModule.js
import axios from 'axios'
import qs from 'query-string'
class BaseModule {
constructor () {
this.$http = axios.create({
baseUrl: 'https://api.forcs.com'
})
this.dataMethodDefaults = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
transformRequest: [function (data) {
return qs.stringify(data)
}]
}
}
get (url, config = {}) {
return this.$http.get(url, config)
}
post (url, data = undefined, config = {}) {
return this.$http.post(url, data, { ...this.dataMethodDefaults, ...config })
}
put (url, data = undefined, config = {}) {
return this.$http.put(url, data, { ...this.dataMethodDefaults, ...config })
}
delete (url, config = {}) {
return this.$http.delete(url, config)
}
}
export default BaseModule
тогда пустьUserManager
,VideoManager
,EpisodeManager
унаследовал от этогоBaseModule
, чтобы удалить повторяющийся код.
UserManager.js
+ import BaseModule from './BaseModule'
- import axios from 'axios'
- import qs from 'query-string'
+ class UserManager extends BaseModule {
- class UserManager {
constructor() {
+ super()
- this.$http = axios.create({
- baseUrl: 'https://api.forcs.com'
- })
- this.dataMethodDefaults = {
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded'
- },
- transformRequest: [function (data) {
- return qs.stringify(data)
- }]
- }
}
getUsersPageableList (page = 0, size = 20) {
+ return this.get(`/users?page=${page}&size=${size}`)
- return this.$http.get(`/users?page=${page}&size=${size}`)
}
getUsersFullList () {
+ return this.get('/users/all')
- return this.$http.get('/users/all')
}
getUser (id) {
if (!id) {
return Promise.reject(new Error(`getUser:id(${id})无效`))
}
+ return this.get(`/users/${id}`)
- return this.$http.get(`/users/${id}`)
}
// ......
}
export default new UserManager()
VideoManager.js
+ import BaseModule from './BaseModule'
- import axios from 'axios'
- import qs from 'query-string'
+ class VideoManager extends BaseModule {
- class VideoManager {
constructor () {
+ super()
- this.$http = axios.create({
- baseUrl: 'https://api.forcs.com'
- })
- this.dataMethodDefaults = {
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded'
- },
- transformRequest: [function (data) {
- return qs.stringify(data)
- }]
- }
}
getVideosPageableList (page = 0, size = 20) {
+ return this.get(`/videos?page=${page}&size=${size}`)
- return this.$http.get(`/videos?page=${page}&size=${size}`)
}
getVideosFullList () {
+ return this.get('/videos/all')
- return this.$http.get('/videos/all')
}
getVideo (id) {
if (!id) {
return Promise.reject(new Error(`getVideo:id(${id})无效`))
}
+ return this.get(`/videos/${id}`)
- return this.$http.get(`/videos/${id}`)
}
// ......
}
export default new VideoManager()
EpisodeManager.js
+ import BaseModule from './BaseModule'
- import axios from 'axios'
- import qs from 'query-string'
+ class EpisodeManager extends BaseModule {
- class EpisodeManager {
constructor () {
+ super()
- this.$http = axios.create({
- baseUrl: 'https://api.forcs.com'
- })
- this.dataMethodDefaults = {
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded'
- },
- transformRequest: [function (data) {
- return qs.stringify(data)
- }]
- }
}
getEpisodesPageableList (page = 0, size = 20) {
+ return this.get(`/episodes?page=${page}&size=${size}`)
- return this.$http.get(`/episodes?page=${page}&size=${size}`)
}
getEpisodesFullList () {
+ return this.get('/episodes/all')
- return this.$http.get('/episodes/all')
}
getEpisode (id) {
if (!id) {
return Promise.reject(new Error(`getEpisode:id(${id})无效`))
}
+ return this.get(`/episodes/${id}`)
- return this.$http.get(`/episodes/${id}`)
}
// ... 篇幅原因,后面的接口省略
}
export default new EpisodeManager()
Используя функцию наследования ООП, общий код извлекается в родительский класс, так что код, который инкапсулирует интерфейс модуля, в определенной степени упрощается.Если в будущем свойства по умолчанию публичной части интерфейса будут изменены, требуется только техническое обслуживание.BaseModule
Вот и все. если ты правBaseModule
Если вы обратите внимание, вы должны заметить,BaseModule
Он также не полностью скрывает в себе общественные части. в то же время,BaseModule
даAxios
прокси-метод объекта (axios.get()
,axios.post()
,axios.put()
,axios.delete()
) Был упакован, такAxios
Сплоченность внутри себя, снижение иерархии зависимостей подклассов. Для подклассов больше не нужно заботитьсяAxios
Object, вам нужно заботиться только о методах и некоторых свойствах, предоставляемых родительским классом. Таким образом, с одной стороны, улучшается возможность повторного использования родительского класса, а с другой стороны, подкласс может лучше расширять родительский класс, не затрагивая другие подклассы.
Для общей сцены посылка здесь, бой можно считать успешным, и, наконец, можно пойти сварить чашечку кофе и немного передохнуть. Однако компания еще не перешла черту, как же все может кончиться...
Проблемы с базовым модулем
Через неделю был запущен новый проект, и этот проект был подключен к интерфейсу другой back-end команды. Вообще говоря, стиль именования интерфейса по-прежнему в основном соответствует стандарту RESTful, однако доменное имя адреса запроса было изменено, а заголовок запросаContent-Type
Это также отличается от определения предыдущей команды.application/json
.
Конечно, на самом деле интерфейсы, определенные разными back-end командами, могут не так уж и отличаться :(
Столкнувшись с такой сценой, нашей первой реакцией может быть: хорошо, поставьте предыдущий проектBaseModule
Скопируйте его в текущий проект и настройте.
import axios from 'axios'
import qs from 'query-string'
class BaseModule {
constructor () {
this.$http = axios.create({
- baseUrl: 'https://api.forcs.com'
+ baseUrl: 'https://api2.forcs.com'
})
- this.dataMethodDefaults = {
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded'
- },
- transformRequest: [function (data) {
- return qs.stringify(data)
- }]
- }
}
get (url, config = {}) {
return this.$http.get(url, config)
}
post (url, data = undefined, config = {}) {
- return this.$http.post(url, data, { ...this.dataMethodDefaults, ...config })
+ return this.$http.post(url, data, config)
}
put (url, data = undefined, config = {}) {
- return this.$http.post(url, data, { ...this.dataMethodDefaults, ...config })
+ return this.$http.put(url, data, config)
}
delete (url, config = {}) {
return this.$http.delete(url, config)
}
}
export default BaseModule
из-заAxios
Заголовки запросов POST и PUT по умолчаниюContent-Type
даapplication/json
, так что просто установите предыдущийContent-Type
Код можно удалить. Затем вы можете пить кофе, слушать песни и счастливо инкапсулировать интерфейс для подключения данных!
Если внимательно оглянуться назад, это на самом деле поднимает проблему, о которой мы упоминали ранее:повторяющийся код. Вы можете подумать, что это все равно не проект, а код поддерживается самостоятельно, так что это не имеет значения. Я думаю, с объективной точки зрения, нет ничего плохого в том, чтобы делать это для каких-то небольших проектов или небольших команд, но если, я имею в виду, если проектов становится все больше и больше, действительно ли хорошо копировать набор кода для каждого проект? Если back-end команда в один прекрасный день сделает единую спецификацию, и заголовки запросов всех интерфейсов будут установлены по набору спецификаций, по факту, предыдущие коды придется корректировать одну за другой? Боже, сколько труда. Вкратце,Дублирующийся код — это яма!
Как справиться с этой ситуацией?
Сделайте инкапсулированные модули более универсальными
Среди принципов объектно-ориентированного программирования есть такой:принцип открыто-закрыто. То есть расширение разрабатывается, а модификация закрывается. В соответствии с этим принципом решение, о котором я думал, состоит в том, чтобы инкапсулироватьBaseModule
Предоставляйте параметры для внешних настроек, как и большинство плагинов jQuery, фабричный метод предоставитoptions
Параметры объекта, которые удобны для внешнего слоя, чтобы настроить некоторые свойства плагина. мы также можемBaseModule
Внесите некоторые изменения, чтобы сделать его более гибким и легким для расширения.
Рефакторинг BaseModule
Далее необходимоBaseModule
Рефакторинг, чтобы сделать его более общим.
import axios from 'axios'
import qs from 'query-string'
function isEmptyObject (obj) {
return !obj || !Object.keys(obj).length
}
// 清理headers中不需要的属性
function clearUpHeaders (headers) {
[
'common',
'get',
'post',
'put',
'delete',
'patch',
'options',
'head'
].forEach(prop => headers[prop] && delete headers[prop])
return headers
}
// 组合请求方法的headers
// headers = default <= common <= method <= extra
function resolveHeaders (method, defaults = {}, extras = {}) {
method = method && method.toLowerCase()
// check method参数的合法性
if (!/^(get|post|put|delete|patch|options|head)$/.test(method)) {
throw new Error(`method:${method}不是合法的请求方法`)
}
const headers = { ...defaults }
const commonHeaders = headers.common || {}
const headersForMethod = headers[method] || {}
return _clearUpHeaders({
...headers,
...commonHeaders,
...headersForMethod,
...extras
})
}
// 组合请求方法的config
// config = default <= extra
function resolveConfig (method, defaults = {}, extras = {}) {
if (isEmptyObject(defaults) && isEmptyObject(extras)) {
return {}
}
return {
...defaults,
...extras,
resolveHeaders(method, defaults.headers, extras.headers)
}
}
class HttpClientModule {
constructor (options = {}) {
const defaultHeaders = options.headers || {}
if (options.headers) {
delete options.headers
}
const defaultOptions = {
baseUrl: 'https://api.forcs.com',
transformRequest: [function (data, headers) {
if (headers['Content-Type'] === 'application/x-www-form-urlencoded') {
// 针对application/x-www-form-urlencoded对data进行序列化
return qs.stringify(data)
} else {
return data
}
}]
}
this.defaultConfig = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
...defaultHeaders
}
}
this.$http = axios.create({ ...defaultOptions, ...options })
}
get (url, config = {}) {
return new Promise((resolve) => {
resolve(this.$http.get(url, resolveConfig(
'get', this.defaultConfig, config)))
})
}
post (url, data = undefined, config = {}) {
return new Promise((resolve) => {
resolve(this.$http.post(url, data, resolveConfig(
'post', this.defaultConfig, config)))
})
}
put (url, data = undefined, config = {}) {
return new Promise((resolve) => {
resolve(this.$http.put(url, data, resolveConfig(
'put', this.defaultConfig, config)))
})
}
delete (url, config = {}) {
return new Promise((resolve) => {
resolve(this.$http.delete(url, resolveConfig(
'delete', this.defaultConfig, config)))
})
}
}
// 导出工厂方法
export function createHttpClient (options, defaults) {
return new HttpClientModule(options, defaults)
}
// 默认导出模块对象
export default HttpClientModule // import
рефакторингBaseModule
Оно изменилось до неузнаваемости, а название модуля изменено на более общее:HttpClientModule
.HttpClientModule
Конструктор предоставляетoptions
параметры, чтобы снизить стоимость обучения модуля,options
в основном используетсяAxios
изRequest ConfigОпределенная структура. Единственная разница в том, чтоoptions
изheaders
обращение с имуществом.
Здесь нужно сказать больше, это кажется идеальнымAxios
Есть относительно серьезная ошибка, которая до сих пор не исправлена, т. е. черезdefaults
Атрибутыустановить заголовкине работает, вы должны выполнять операцию запроса (вызовrequest()
,get()
,post()
... и т. д. метод запроса), через методconfig
Заголовок настройки параметра вступит в силу. Чтобы обойти ошибку этой функции, яHttpClientModule
В этом модуле, согласноAxios
Дизайн API и реализация аналогичных функций вручную. либо черезcommon
Атрибут устанавливает общедоступный заголовок, а имя метода запроса (get, post, put и т. д.) также можно использовать в качестве имени атрибута для установки заголовка по умолчанию для запроса определенного метода запроса. Вероятно, что-то вроде этого:
const options = {
// ...
headers: {
// 设置公共的header
common: {
Authorization: AUTH_TOKEN
},
// 为post和put请求设置请求时的Content-Type
post: {
'Content-Type': 'application/x-www-form-urlencoded'
},
put: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
}
const httpClient = new HttpClientModule(options)
Независимая публикация рефакторинговых инкапсулированных модулей
мы можемHttpClientModule
Создайте отдельный проект npm, дайте ему имя, напримерhttpclient-module
. Прежде чем присваивать имя, лучше всего зайти в npmjs, чтобы проверить, не использовалось ли это имя другими модулями, и постараться сохранить уникальность имени. затем пройтиwebpack,rollup,parcelПодождите, пока инструмент сборки упакует,Опубликовать в npmjs. Конечно, если код включает конфиденциальную информацию о конфигурации, вы также можетеСоздайте свой собственный репозиторий частного сервера npm, а затем распространить его на частный сервер. Таким образом, черезnpm install
Команда устанавливает модуль непосредственно в наш проект для использования. Модули можно установить с помощью следующей команды:
npm install httpclient-module --save
# or
npm i httpclient-module -S
Настройка модулей слоя бизнес-интерфейса
Помните предыдущее определение бизнес-уровняUserManager
,VideoManager
так же какEpisodeManager
, все они наследуют отBaseModule
, но для того, чтобы родительский классBaseModule
Более универсальный, мы восстановили его и выпустили его самостоятельно с другим именем, то как должен использовать модули менеджеров этих слоев бизнеса, используют этот рефакторенный модульHttpClientModule
Шерстяная ткань?
Поскольку эти модули менеджера наследуются от родительского классаBaseModule
, нам просто нужноBaseModule
Просто внесите коррективы.
- import axios from 'axios'
- import qs from 'query-string'
+ import { createHttpClient } from 'httpclient-module'
+ const P_CONTENT_TYPE = 'application/x-www-form-urlencoded'
class BaseModule {
constructor () {
- this.$http = axios.create({
- baseUrl: 'https://api.forcs.com'
- })
- this.dataMethodDefaults = {
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded'
- },
- transformRequest: [function (data) {
- return qs.stringify(data)
- }]
- }
+ this.$http = createHttpClient({
+ headers: {
+ post: { 'Content-Type': P_CONTENT_TYPE },
+ put: { 'Content-Type': P_CONTENT_TYPE }
+ }
+ })
}
get (url, config = {}) {
return this.$http.get(url, config)
}
post (url, data = undefined, config = {}) {
- return this.$http.post(url, data, { ...this.dataMethodDefaults, ...config })
+ return this.$http.post(url, data, config)
}
put (url, data = undefined, config = {}) {
- return this.$http.put(url, data, { ...this.dataMethodDefaults, ...config })
+ return this.$http.put(url, data, config)
}
delete (url, config = {}) {
return this.$http.delete(url, config)
}
}
export default BaseModule
По сути, он упакован сам с собойhttpclient-module
заменил оригиналAxios
. Какая от этого польза?
httpclient-module
можно считатьAxios
Адаптер со слоем бизнес-интерфейса. БудуAxios
упаковано вhttpclient-module
, что снижает зависимость интерфейсных проектов от сторонних библиотек. упомянутый ранееAxios
Есть некоторые очевидные ошибки.После этого уровня инкапсуляции мы можем уменьшить влияние ошибок на проект, и нам нужно только поддерживатьhttpclient-module
, вы можете избежать влияния сторонних ошибок. Если в будущем вы найдете лучшую библиотеку http, вам необходимо заменить ееAxios
Просто обновитьhttpclient-module
Вот и все. Для бизнес-уровня никаких серьезных корректировок не требуется.
имеютhttpclient-module
Этот слой адаптеров также упрощает работу группы по созданию технических спецификаций унификации. Если спецификация интерфейса команды в будущем будет скорректирована, например, доменное имя интерфейса переключено на https, аутентификация заголовка запроса настроена единообразно, или в заголовке запроса необходимо увеличить или уменьшить другие параметры, его нужно только обновить.httpclient-module
Просто хорошо. Если единые корректировки делает не команда, а отдельные проекты, то корректировать нужно толькоBaseModule
, измените его, чтобы перейти кhttpclient-module
изoptions
параметры.
Позвольте упакованным модулям повысить эффективность нашей разработки
использоватьhttpclient-module
После счастливой работы некоторое время мы столкнулись с новыми проблемами.
С итерацией проекта к фронтенду добавляется все больше бизнес-функций, также постепенно увеличивается количество бизнес-интерфейсов, которые необходимо подключить к бэкенду. Например, чтобы добавить модуль управления контент-провайдером, нам нужно создатьCPManager
, а затем добавьте метод вызова запроса интерфейса и добавьте модуль управления метками содержимого, вам необходимо определитьTagManager
, а затем добавьте метод, вызывающий запрос интерфейса. код, как показано ниже.
Добавлен модуль управления контент-провайдером:
// CPManager.js
// ...
class CPManager extends BaseModule {
constructor () { /* ... */ }
createCp (data) { /* ... */ }
getCpPageableList (page = 0, size = 20) { /* ... */ }
getCpFullList () { /* ... */ }
getCp (id) { /* ... */ }
updateCp (id, update) { /* ... */ }
deleteCp (id) { /* ... */ }
// ...
}
Модуль управления тегами контента:
// TagManager.js
// ...
class TagManager extends BaseModule {
constructor () { /* ... */ }
createTag (data) { /* ... */ }
getTagPageableList (page = 0, size = 20) { /* ... */ }
getTagFullList () { /* ... */ }
getTag (id) { /* ... */ }
updateTag (id, update) { /* ... */ }
deleteTag (id) { /* ... */ }
// ...
}
Новые модули намного больше, чем они. Мы обнаружили, что в коде много повторений, таких какcreateXXX()
,getXXX()
,updateXXX()
,deleteXXX()
, которые соответствуют интерфейсу CRUD в модуле, и если бизнес-интерфейс не слишком особенный, определите интерфейс только для инкапсуляции вызова.
// ...
class TagManager extends BaseModule {
// ...
createTag (data) {
// 定义createTag()方法,就是为了简化/tags的POST请求
return this.$http.post('/tags', data)
}
// ...
}
Мы считаем, что эти повторяющиеся задачи можно упростить. По привычке семантически именовать методы, метод создания ресурсов будет называться какcreate
В качестве префикса, соответствующее исполнениеPOST
просить. Обновление использования ресурсовupdate
В качестве префикса имени метода указывается соответствующее выполнениеPUT
просить. Получить ресурс или список ресурсов, имя метода начинается сget
начало, соответствующееGET
просить. Чтобы удалить ресурс, используйтеdelete
начало, соответствующееDELETE
просить. Как показано в таблице ниже:
префикс имени метода | Функция | метод запроса | интерфейс |
---|---|---|---|
create | Создать ресурс | POST | /resources |
get | Доступ к ресурсам | GET | /ресурсы/: идентификатор, /ресурсы, /ресурсы/все |
update | Обновление ресурсов | PUT | /resources/:id |
delete | удалить ресурс | DELETE | /resources/:id |
В соответствии с этим соглашением наша команда подумала, что, поскольку префикс метода, метод запроса и интерфейс URL могут иметь однозначное соответствие, можем ли мы передатьKey -> Value
Как автоматизировать генерацию и привязку URL-запросов?
НапримерTagManager
, который мы хотим создать с помощью следующего кода.
// TagManager.js
const urls = {
createTag: '/tags',
updateTag: '/tags/:id',
getTag: '/tags/:id',
getTagPageableList: '/tags',
getTagFullList: '/tags/all',
deleteTag: '/tags/:id'
}
export default moduleCreator(urls)
Затем на уровне пользовательского интерфейса вы можете напрямую вызвать метод созданного модуля.
// TagManager.vue
<script>
import tagManager from './service/TagManager.js'
// ...
export default {
data () {
return {
tagList: [],
page: 0,
size: 20,
// ...
}
},
// ...
_refresh () {
const { page, size } = this
// GET /tags?page=[page]&size=[size]
tagManager.getTagPageableList({ page, size })
.then(resolved => this.tagList = resolved.data)
},
mounted () {
this._refresh()
},
handleCreate (data) {
// POST /tags
tagManager.createTag({ ...data })
.then(_ => this._refresh())
.catch(err => console.error(err.message))
},
handleUpdate (id, update) {
// PUT /tags/:id
tagManager.updateTag({ id }, { ...update })
.then(_ => this._refresh())
.catch(err => console.error(err.message))
},
handleDelete (id) {
// DELETE /tags/:id
tagManager.deleteTag({ id })
.then(_ => this._refresh())
.catch(err => console.error(err.message))
},
// ...
}
</script>
Гораздо удобнее определить модуль бизнес-интерфейса на фронтенде :) Также, вы заметили, что мы также скорректировали параметры интерфейса. Будь то переменная пути URL-адреса или параметр запроса, мы можем передать ее объективным образом. Настройка унифицированного типа параметра упрощает изучение интерфейса, а автоматически генерируемые методы привязывают параметры к интерфейсу объектно-ориентированным способом.
В стандартном интерфейсе RESTful URL-адрес интерфейса может иметь два параметра:переменная пути(переменные пути) ипараметры запроса(Аргумент запроса).
- Переменная пути: это переменная, участвующая в URL-адресе, сопоставленном с указанным ресурсом, например /resources/:id, где :id относится к идентификатору ресурса.При работе с разными ресурсами путь :id в URL-адресе также будет будь другим. /ресурсы/1, /ресурсы/2...и т.д.
- Параметр запроса: относится к параметру запроса в URL-адресе, обычно это абзац после вопросительного знака в URL-адресе запроса GET или запроса DELETE, например /resources?page=0&size=20, страница и размер являются параметрами запроса.
Сначала приходит волна идей
Во-первых, разработан метод модуля, который автоматически генерируется и привязывается к URL-адресу.
// GET, DELETE
methodName ([params|querys:PlainObject, [querys|config:PlainObject, [config:PlainObject]]]) => :Promise
// POST, PUT
methodName ([params|data:PlainObject, [data|config:PlainObject, [config:PlainObject]]]) => :Promise
Это кусок псевдокода.params
представляет объект параметра пути,querys
выражатьGET
илиDELETE
объект параметра запроса для запроса,data
выражатьPOST
илиPUT
Объект данных, отправленный запросом, вероятно, означает:
- Автоматически сгенерированный метод будет принимать 3 типа в качестве
Plain Object
Все необязательные параметры возвращаютPromise
объект. - При передаче в метод трех объектов параметров параметрами являются объект переменной пути, объект параметра запроса или объект данных, в свою очередь, совместимый с
Axios
Объект конфигурации API.
Используйте один нижеGET
запрос иPUT
Попросите схематическую иллюстрацию, посмотрите сначалаGET请求
:
НижеPUT
просить:
- При передаче двух параметров, если интерфейс URL не принимает переменную пути, то первым параметром является объект параметра запроса (
GET
метод илиDELETE
методы) или объекты данных (POST
метод илиPUT
способ), второйconfig
объект. Если интерфейс URL имеет переменную пути, первый параметр представляет объект переменной пути, а второй параметр — объект параметра запроса или объект данных.
Например, следующие дваGET
URL-интерфейс метода, тот, что слева, не имеет переменной пути, а тот, что справа, имеет переменную пути.:id
. Слева предположим, что имя метода, привязанного к интерфейсу URL, равноgetTagPageableList
, когда мы вызываем метод только с двумя параметрами, то первый параметр будет преобразован в формат параметра запросаkey1=value1&key2=value2&...&keyn=valuen
, второй параметр эквивалентенAxios
изconfig
объект. Справа, потому что в интерфейсе URL есть переменная пути:id
, затем вызовите метод, который связывает интерфейс URLgetTagById
Когда передаются два параметра, первый объект параметраkey
Вместо переменной пути в интерфейсе URL второй параметр будет использоваться как параметр запроса.
POST
Методы иPUT
Запрос метода аналогичен, за исключением того, что параметры запроса заменяются отправленными данными.
- Когда передается только один параметр, если URL-адрес интерфейса не имеет переменной пути, то параметр является объектом параметра запроса или объектом данных. Если URL-адрес интерфейса имеет переменную пути, то объект параметра будет сопоставлен с переменной пути .
дваGET
просить:
ОдинPOST
запрос иPUT
просить:
Превратите идеи в реализованный код
существуетhttpclient-module
функция в.
// ...
/* 请求方法与模块方法名的映射关系对象
* key -> 请求方法
* value -> pattern:方法名的正则表达式,sendData:表示是否是POST,PUT或者PATCH方法
*/
const methodPatternMapper = {
get: { pattern: '^(get)\\w+$' },
post: { pattern: '^(create)\\w+$', sendData: true },
put: { pattern: '^(update)\\w+$', sendData: true },
delete: { pattern: '^(delete)\\w+$' }
}
// 辅助方法,判断是否是函数
const isFunc = function (o) {
return typeof o === 'function'
}
// 辅助方法,判断是否是plain object
// 这个方法相对简单,如果想看更加严谨的实现,可以参考lodash的源码
const isObject = function (o) {
return Object.prototype.toString.call(o) === '[object Object]'
}
/*
* 将http请求绑定到模块方法中
*
* @param method 请求方法
* @param moduleInstance 模块实例对象或者模块类的原型对象
* @param shouldSendData 表示是否是POST,或者PUT这类请求方法
*
* @return Axios请求api返回的Promise对象
*/
function bindModuleMethod(method, moduleInstance, shouldSendData) {
return function (url, args, config = {}) {
return new Promise(function (resolve, reject) {
let p = undefined
config = { ...config, url, method }
if (args) {
shouldSendData ?
config.data = args :
config.url = `${config.url}?${qs.stringify(args)}`
}
moduleInstance.$http.request(config)
.then(response => resolve(response))
.catch((error) => reject(error))
})
}
}
/*
* 根据定义的模块方法名称,通过methodPatternMapper转换成绑定URL的模块方法
*
* @param moduleInstance 模块实例对象或者模块类的原型对象
* @param name 模块方法名称
*
* @return Function 绑定的模块方法
* @throw 方法名称和请求方法必须一一匹配
* 如果发现匹配到的方法不止1个或者没有,则会抛出异常
*/
function resolveMethodByName(moduleInstance, name) {
let requestMethod = Object.keys(metherPatternMapper).filter(key => {
const { pattern } = methodPatternMapper[key]
if (!(pattern instanceof RegExp)) {
// methodPatternMapper每个属性的value的pattern
// 既可以是正则表达式字符串,也可是是正则类型的对象
pattern = new RegExp(pattern)
}
return pattern.test(name)
})
if (requestMethod.length !== 1) {
throw `
解析${name}异常,解析得到的方法有且只能有1个,
但实际解析到的方法个数是:${requestMethod.length}
`
}
requestMethod = requestMethod[0]
return bindModuleMethod(requestMethod, moduleInstance,
methodPatternMapper[requestMethod].sendData)
}
/*
* 将参数映射到路径变量
*
* @param url
* @param params 被映射到路径变量的参数
*
* @return 将路径变量替换好的URL
*/
function mapParamsToPathVariables(url, params) {
if (!url || typeof url !== 'string') {
throw new Error(`url ${url} 应该是URL字符串`)
}
return url.replace(/:(\w+)/ig, (_, key) => params[key])
}
export function bindUrls (urls = {}) {
// 为什么返回一个函数对象?后面会给大家解释
return module => {
const keys = Object.keys(urls)
if (!keys.length) {
console.warn('urls对象为空,无法完成URL的映射')
return
}
const instance = module.prototype || module
keys.forEach(name => {
const url = urls[name]
if (!url) {
throw new Error(`${name}()的地址无效`)
}
// 根据urls对象动态定义模块方法
Object.defineProperty(instance, name, {
configurable: true,
writable: true,
enumerable: true,
value: ((url, func, thisArg) => () => {
let args = Array.prototype.slice.call(arguments)
if (args.length > 0 && url.indexOf('/:') >= 0) {
if (isObject(args[0])) {
const params = args[0]
args = args.slice(1)
url = mapParamsToPathVariables(url, params)
}
}
return func && func.apply(thisArg, [ url ].concat(args))
})(url, resolveMethodByName(instance, name), instance)
})
})
}
}
Для удобства чтения я собрал несколько ключевых мест, но в реальных проектах рекомендуется соответствующим образом разделить код для обслуживания и тестирования.
Мы реализовали функцию, которая связывает запрос URL к методу экземпляра модуляbindUrls()
, и пройтиhttpclient-module
экспорт.bundUrls()
Реализация не сложная.urls
это метод, имя которогоkey
, URL какvalue
Объект. правильноurls
Объект обход, процесс обход, сначала с объектомkey
Выполните регулярное сопоставление, чтобы получить соответствующий метод запроса (см.methodPatternMapper
) и привязать запрос к функции (см.resolveMethodByName()
а такжеbindModuleMethod()
). затем пройтиObject.defineProperty()
Метод добавляет метод к экземпляру (или прототипу) объекта модуля, имя методаurls
изkey
. Когда вызывается метод, который динамически добавляется к объекту экземпляра модуля, он сначала определяет, имеет ли URL-адрес, привязанный к методу, переменную пути.mapParamsToPathVariables()
сделать преобразование, то перед его выполнением черезresolveMethodByName()
Полученная функция была привязана к запросу.
мы используемbindUrls()
к предыдущемуTagManager
Сделайте макияж.
// TagManager.js
// ...
+ import { bindUrls } from 'httpclient-module'
class TagManager extends BaseModule {
constructor () {
/* ... */
+ bindUrls({
+ createTag: '/tags',
+ getTagPageableList: '/tags',
+ getTagFullList: '/tags/all',
+ getTag: '/tags/:id',
+ updateTag: '/tags/:id',
+ deleteTag: '/tags/:id'
+ })(this)
}
- createTag (data) { /* ... */ }
- getTagPageableList (page = 0, size = 20) { /* ... */ }
- getTagFullList () { /* ... */ }
- getTag (id) { /* ... */ }
- updateTag (id, update) { /* ... */ }
- deleteTag (id) { /* ... */ }
// ...
}
ЗачемbindUrls()
Чтобы вернуть функцию, обработайте возвращенную функциюmodule
этот параметр вместоmodule
так какbindUrls
А как насчет второго параметра, который нужно обработать?
Целью этого является рассмотрение совместимости с декораторами ES7.@decoratorписьма. В среде ES7 мы также можем использовать декораторы для привязки URL-адресов к методам модуля.
import { bindUrls } from 'httpclient-module'
@bindUrls({
createTag: '/tags',
getTagPageableList: '/tags',
getTagFullList: '/tags/all',
getTag: '/tags/:id',
updateTag: '/tags/:id',
deleteTag: '/tags/:id'
})
class TagManager extends BaseModule {
/* ... */
}
Таким образом, мы можем пройтиbindUrls()
, который удобно добавить в модуль ряд методов экземпляра, которые могут выполнять запросы URL.
Улучшить гибкость bindUrls()
bindUrls()
Есть возможности для улучшения гибкости. Текущая версияurls
Этот параметр может поддерживать только строковый типvalue
,мы думаемurls
изvalue
Помимо строки, это могут быть и другие типы, напримерplain object
. в то же время,key
Префикс может быть толькоcreate
,update
,get
,delete
В-четвертых, это кажется немного жестким, мы хотим поддерживать больше префиксов, или имя метода не должно быть ограничено определенным форматом, и метод может называться свободно.
Мы внесли небольшие изменения в текущую версию, чтобы улучшитьbindUrls()
гибкость.
// ...
// 支持更多的前缀
const methodPatternMapper = {
- get: { pattern: '^(get)\\w+$' },
+ get: { pattern: '^(get|load|query|fetch)\\w+$' },
- post: { pattern: '^(create)\\w+$', sendData: true },
+ post: { pattern: '^(create|new|post)\\w+$', sendData: true },
- put: { pattern: '^(update)\\w+$', sendData: true },
+ put: { pattern: '^(update|edit|modify|put)\\w+$', sendData: true },
- delete: { pattern: '^(delete)\\w+$' }
+ delete: { pattern: '^(delete|remove)\\w+$' }
}
/* ... */
+ function resolveMethodByRequestMethod(moduleInstance, requestMethod) {
+ if (/^(post|put)$/.test(requestMethod)) {
+ return bindModuleMethod(requestMethod, moduleInstance, true)
+ } else if (/^(delete|get)$/.test(requestMethod)) {
+ return bindModuleMethod(requestMethod, moduleInstance)
+ } else {
+ throw new Error(`未知的请求方法: ${requestMethod}`)
+ }
+ }
export function mapUrls (urls = {}) {
return module => {
const keys = Object.keys(urls)
if (!keys.length) {
console.warn('urls对象为空,无法完成URL的映射')
return
}
const instance = module.prototype || module
keys.forEach(name => {
let url = urls[name]
+ let requestMethod = undefined
+ if (isObject(url)) {
+ requestMethod = url['method']
+ url = url['url']
+ }
if (!url) {
throw new Error(`${name}()的地址无效`)
}
+ let func = undefined
+ if (!requestMethod) {
+ func = resolveMethodByName(instance, name)
+ } else {
+ func = resolveMethodByRequestMethod(instance, requestMethod)
+ }
Object.defineProperty(instance, name, {
configurable: true,
writable: true,
enumerable: true,
value: ((url, func, thisArg) => () => {
let args = Array.prototype.slice.call(arguments)
if (args.length > 0 && url.indexOf('/:') >= 0) {
if (isObject(args[0])) {
const params = args[0]
args = args.slice(1)
url = mapParamsToUrlPattern(url, params)
}
}
return func && func.apply(thisArg, [ url ].concat(args))
- })(url, resolveMethodByName(instance, name), instance)
+ })(url, func, instance)
})
})
}
}
СкорректированоbindUrls()
правильноurls
служба поддержкиplain object
Типvalue
.plain object
Типvalue
может быть дваkey
,одинurl
, — это URL-адрес интерфейса, а другой —method
, вы можете указать метод запроса. если установленоmethod
, то и не нужноurls
изkey
Выводится префикс метода запроса, так что конфигурация может бытьurls
более гибкий.
const urls = {
loadUsers: '/users',
}
// or
const urls = {
users: { url: '/users', method: 'get' }
}
bindUrls(urls)(module)
module.users({ page: 1, size: 20 }) // => GET /users?page=1&size=20
Теперь нам просто нужно пройтиbindUrls()
, Простое определение объекта, вы можете добавить метод запрошенного интерфейсного модуля.
Суммировать
Просмотрите некоторые из нашихAxios
Несколько этапов инкапсуляции этой библиотеки http
- Определите модуль, например
UserManager
, а затем добавить в модуль несколько методов для вызова интерфейса URL, указать параметры, а затем на уровне интерфейса можно использовать метод модуля для вызова интерфейса URL для связи с фоном, что упрощает процесс вызов API библиотеки http. - Если в проекте будет все больше и больше интерфейсов, это приведет к появлению все большего количества соответствующих модулей, таких как
VideoManager
,EpisodeManager
,CPManager
Ждать. С увеличением количества модулей мы обнаруживаем, что повторяющийся код также увеличивается, и необходимо улучшить возможность повторного использования кода.Затем мы можем определить базовый класс для этих модулей менеджера.BaseModule
, а затем переместите код, связанный с библиотекой http, вBaseModule
, чтобы в подклассе вызывался метод интерфейса URL. - Позднее было обнаружено, что даже при
BaseModule
Устранен повторяющийся код, но по-прежнему остается повторяющаяся работа, такая как написание этих методов CRUD от руки, поэтому мыBaseModule
стоять в одиночестве как отдельный проектhttpclient-module
, от предыдущего отношения наследования к отношению композиции, и разработал APIbindUrls()
. С помощью этого API мы можемkey -> value
Этот метод элемента конфигурации динамически добавляет метод для выполнения запросов интерфейса URL к модулю, тем самым еще больше упрощая наш код и повышая эффективность нашей разработки. - Наконец, отдай
bindUrls()
Улучшенная гибкость.
Во время всего процесса инкапсуляции HTTP мы сделали некоторые соображения, такие как повторное использование, универсальность и гибкость. Его конечная цель - повысить эффективность нашего процесса развития и уменьшить дублирование работы. Но оглядываясь назад, инкапсуляция библиотеки HTTP не обязательно должна быть последним шагом. Мы также итализировали шаг за шагом в соответствии с реальной ситуацией. Поэтому нет точного ответа на конкретную степень инкапсуляции. Мы должны начать с фактической сцены и выбрать наиболее подходящий метод после комплексного рассмотрения.
Кроме того, на самом деле продумывание всего процесса (а не кода) подходит не только дляAxios
библиотека, также может использоваться для других http-библиотек, таких какSuperAgent
илиfetch
, и он применим не только к инкапсуляции библиотеки http, но и к инкапсуляции других типов модулей, но его нужно обойти по аналогии.
Вышеизложенный опыт разработки нашей команды инкапсулирует Axios, и мы надеемся быть полезными и вдохновляющими для всех. В тексте есть неуместные места, критика и обсуждение приветствуются.