記一次封裝Axios的經歷 - 掘金
https://juejin.im/post/5a52c9a4f265da3e2a0d6b74
前言
前端開發(fā)中,如果頁面需要與后臺接口交互,并且無刷新頁面,那么需要借助一下Ajax的http庫來完成與后臺數據接口的對接工作。在jQuery很盛行的時候,我們會使用$.ajax(),現在,可選擇的就更多,例如:SuperAgent、Axios、Fetch…等等。有了這些http庫,我們不在需要關注太多與ajax底層相關的細節(jié)的問題。很多時候和場景下,只需要關注如何構建一個request以及如何處理一個response即可,但即便這些http庫已經在一定程度上簡化了我們的開發(fā)工作,我們仍然需要針對項目的實際需要,團隊內部技術規(guī)范對這些http庫進行封裝,進而優(yōu)化我們的開發(fā)效率。
本文將結合我們團隊使用的一個http庫Axios和我們團隊開發(fā)工程的一些場景,分享我們前端團隊對http庫進行封裝的經歷。
對http庫進行基本的封裝
服務端URL接口的定義
以用戶管理模塊為例。對于用戶管理模塊,服務端通常會定義如下接口:
-
GET /users?page=0&size=20- 獲取用戶信息的分頁列表 -
GET /users/all- 獲取所有的用戶信息列表 -
GET /users/:id- 獲取指定id的用戶信息 -
POST /users application/x-www-form-urlencoded- 創(chuàng)建用戶 -
PUT /users/:id application/x-www-form-urlencoded- 更新指定id的用戶信息 -
DELETE /users/:id刪除指定id的用戶信息
通過以上定義,不難發(fā)現這些都是基于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,這樣后面在發(fā)起請求的時候,URL只需要使用相對路徑即可。與此同時,我們還調整了POST請求和PUT請求默認的Content-Type。Axios默認是application/json,我們根據后端接口的定義,將其調整成了表單類型application/x-www-form-urlencoded。最后,借助ES6模塊化的特性,我們將UserManager單例化。
實際的場景中,一套符合行業(yè)標準的后端接口規(guī)范要比這復雜得多。由于這些內容不是本文討論的重點,所以簡化了。
接著,給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執(zhí)行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 () { // 提交創(chuàng)建用戶的表單 userManager.createUser({ ...this.formData }) .then(response => { // 刷新列表 this._getUserList() }).catch(err => { console.error(err.message) }) } }</script>
當然,類似的js代碼在React版本的前端頁面上也是適用的。
// src/components/UserList.jsimport 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
為了節(jié)省篇幅,后面就不再展示前端頁面上調用封裝模塊的代碼了。
ok,接口用起來很方便,封裝到這一步感覺似乎沒啥毛病??墒?,一個APP怎么可能就這么些接口呢,它會涉及到若干個接口,而不同的接口可能歸類在不同的模塊。就拿我們的后臺項目來說,內容管理模塊就分為單片管理和劇集管理,劇集管理即包括劇集實體自身的管理,也包括對單片進行打包的管理,所以,后臺對內容管理模塊的接口定義如下:
單片管理:
GET /videos?page=0&size=20GET /videos/allGET /videos/:idPOST /videos application/x-www-form-urlencodedPUT /videos/:id application/x-www-form-urlencodedDELETE /videos/:id
劇集管理:
GET /episodes?page=0&size=20GET /episodes/allGET /episodes/:idPOST /episodes application/x-www-form-urlencodedPUT /episodes/:id application/x-www-form-urlencodedDELETE /episodes/:id
篇幅關系,就不列出所有的接口了??梢钥吹浇涌谝廊皇前凑誖ESTful標準來定義的。按照之前說的做法,我們可以立即對這些接口進行封裝。
定義一個單品管理的模塊類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()
發(fā)現問題了嗎?存在重復的代碼,會給后期的維護埋下隱患。編程原則中,有一個很著名的原則:DRY,翻譯過來就是要盡可能的避免重復的代碼。在靈活的前端開發(fā)中,要更加留意這條原則,重復的代碼越多,維護的成本越大,靈活度和健壯性也隨之降低。想想要是大型的APP涉及到的模塊有數十個以上,每個模塊都擼一遍這樣的代碼,如果后期公共屬性有啥調整的話,這樣的改動簡直就是個災難!
為了提升代碼的復用性,靈活度,減少重復的代碼,應該怎么做呢?如果了解OOP的話,你應該可以很快想出對——定義一個父類,抽離公共部分。
讓封裝的模塊更具備復用性
使用繼承的方式進行重構
<figure style="user-select: text !important; display: block; margin: 22px auto; text-align: center; color: rgb(51, 51, 51); font-family: -apple-system, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">[圖片上傳中...(image-71456b-1528791284991-7)]
<figcaption style="user-select: text !important; display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>
</figure>
定義一個父類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()
利用OOP的繼承特性,將公共代碼抽離到父類中,使得封裝模塊接口的代碼得到一定程度的簡化,以后如果接口的公共部分的默認屬性有何變動,只需要維護BaseModule即可。如果你對BaseModule有留意的話,應該會注意到,BaseModule也不完全將公共部分隱藏在自身當中。同時,BaseModule還對Axios對象的代理方法(axios.get()、axios.post()、axios.put()、axios.delete())進行了包裝,從而將Axios內聚在自身內部,減少子類的依賴層級。對于子類,不再需要關心Axios對象,只需要關心父類提供的方法和部分屬性即可。這樣做,一方面提升了父類的復用性,另一方面也使得子類可以更加好對父類進行擴展,同時又不影響到其他子類。
對于一般場景,封裝到這里,此役也算是可以告捷,終于可以去沖杯咖啡小歇一會咯。不過,公司還沒跨,事情怎么可能完呢……
BaseModule的問題
過了一周后,新項目啟動,這個項目對接的是另一個后端團隊的接口。大體上還好,接口命名風格依然基本跟著RESTful的標準走,可是,請求地址的域名換了,請求頭的Content-Type也和之前團隊定義的不一樣,這個后端團隊用的是application/json。
當然,實際上不同的后端團隊定義的接口,差異未必會這么小:(
面對這種場景,我們的第一反應可能是:好擼,把之前項目的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請求Header的Content-Type是application/json,所以只需要將之前設置Content-Type的代碼移除即可。接著,就可以喝著咖啡,聽著歌,愉快的封裝接口對接數據了!
認真回想一下,這樣做其實又了我們之前提到一個問題:重復的代碼。你可能認為,反正不是一個項目的,代碼獨立維護,所以這樣也不打緊。我從客觀的角度認為,對于一些小項目或者小團隊,這樣做的確沒啥毛病,但如果,我是說如果,項目越來越多了,這樣每個項目復制一套代碼真的好嗎?假如哪天后端團隊做了統(tǒng)一規(guī)范,所有接口的請求頭都按照一套規(guī)范來設置,其實之前的代碼都得逐一調整?我的天,這得多大工作量。總之,重復的代碼就是個坑!
應對這種情況,怎么破?
讓封裝的模塊更具備通用性
在面向對象編程的原則中,有這么一條:開閉原則。即對擴展開發(fā),對修改關閉。根據這條原則,我想到的一個方案,就是給封裝的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存在一個比較嚴重,但至今還沒修復的bug,就是通過defaults屬性設置headers是不起作用的,必須在執(zhí)行請求操作(調用request()、get()、post()…等請求方法)時,通過方法的config參數設置header才會生效。為了規(guī)避這個特性的bug,我在HttpClientModule這個模塊中,按照Axios的API設計,自己手動實現了類似的features。既可以通過common屬性設置公共的header,也可以以請求方法名(get、post、put…等)作為屬性名來給特定請求方法的請求設置默認的header。大概像下面這樣:
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)
獨立發(fā)布重構的封裝模塊
我們可以為HttpClientModule單獨創(chuàng)建一個npm項目,給它取一個名詞,例如httpclient-module。取名前最好先上npmjs上查一下名稱是否已經被其它模塊使用了,盡量保持名稱的唯一性。然后通過webpack、rollup、parcel等構建工具進行打包,發(fā)布到npmjs上。當然,如果代碼中涉及到私有的配置信息,也可以自己搭建一個npm私服倉庫,然后布到私服上。這樣,就可以通過npm install命令直接將模塊安裝到我們的項目中來使用了。安裝模塊可以通過如下命令:
npm install httpclient-module --save
# or
npm i httpclient-module -S
對業(yè)務接口層的模塊進行調整
還記得前面針對業(yè)務層定義的UserManager、VideoManager以及EpisodeManager嗎,他們都繼承自BaseModule,但為了讓父類BaseModule更具通用性,我們以及將它進行了重構,并且換了個名稱進行了獨立發(fā)布,那么這幾個業(yè)務層的manager模塊應該如何使用這個經過重構的模塊HttpClientModule呢?
因為那些manager模塊都繼承自父類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。這樣有什么好處呢?
<figure style="user-select: text !important; display: block; margin: 22px auto; text-align: center; color: rgb(51, 51, 51); font-family: -apple-system, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">[圖片上傳中...(image-db2000-1528791284989-6)]
<figcaption style="user-select: text !important; display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>
</figure>
httpclient-module可以認為是Axios與業(yè)務接口層之間的適配器。將Axios封裝到httpclient-module,降低了前端項目對第三方庫的依賴。前面有提到Axios是存在一些比較明顯的bug的,經過這層封裝,我們可以降低bug對項目的影響,只需要維護httpclient-module,就可以規(guī)避掉第三方bug帶來的影響。如果以后發(fā)現有更好的http庫,需要替換掉Axios,只需要升級httpclient-module就可以了。對于業(yè)務層,不需要做太大的調整。
有了httpclient-module這層適配器,也給團隊做技術統(tǒng)一化規(guī)范帶來方便。假如以后團隊的接口規(guī)范做了調整,比如接口域名切換到https,請求頭認證做統(tǒng)一調整,或者請求頭需要增減其他參數,也只需要更新httpclient-module就好。如果不是團隊做統(tǒng)一調整,而是個別項目,也只需要調整BaseModule,修改一下傳遞給httpclient-module的options參數即可。
讓封裝的模塊提高我們開發(fā)效率
用httpclient-module愉快的工作了一段時間后,我們又遇到了新的問題。
隨著項目迭代,前端加入的業(yè)務功能越來越多,需要對接后臺的業(yè)務接口也逐漸增多。比如新增一個內容供應商管理模塊,我們就需要為此創(chuàng)建一個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) { /* ... */ }
// ...
}
新增的模塊遠不止這些,我們發(fā)現,代碼中存在很多重復的地方,比如createXXX()、getXXX()、updateXXX()、deleteXXX(),分別對應的都是模塊下的CRUD接口,而且如果業(yè)務接口沒有太特殊的場景時,定義一個接口,僅僅就是為了封裝一個調用。
// ...
class TagManager extends BaseModule {
// ...
createTag (data) {
// 定義createTag()方法,就是為了簡化/tags的POST請求
return this.$http.post('/tags', data)
}
// ...
}
我們覺得這些重復的工作是可以簡化掉的。根據方法語義化命名的習慣,創(chuàng)建資源的方法我們會以create作為前綴,對應執(zhí)行POST請求。更新資源使用update作為方法名的前綴,對應執(zhí)行PUT請求。獲取資源或者資源列表,方法名以get開頭,對應GET請求。刪除資源,則用delete開頭,對應DELETE請求。如下表所示:
| 方法名前綴 | 功能 | 請求方法 | 接口 |
|---|---|---|---|
| create | 創(chuàng)建資源 | POST | /resources |
| get | 獲取資源 | GET | /resources/:id、/resources、/resources/all |
| update | 更新資源 | PUT | /resources/:id |
| delete | 刪除資源 | DELETE | /resources/:id |
按照這個約定,我們團隊想,既然方法的前綴、請求方法和URL接口三者可以存在一一對應的關系,那么能不能通過Key -> Value的方式自動化的生成與URL請求綁定好了的方法呢?
例如TagManager,我們希望通過類似下面的代碼進行創(chuàng)建。
// TagManager.js
const urls = {
createTag: '/tags',
updateTag: '/tags/:id',
getTag: '/tags/:id',
getTagPageableList: '/tags',
getTagFullList: '/tags/all',
deleteTag: '/tags/:id'
}
export default moduleCreator(urls)
然后在UI層可以直接調用創(chuàng)建好的模塊方法。
// 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>
這樣在前端定義一個業(yè)務接口的模塊是不是方便多了:)而且,有沒有注意到,我們對接口的傳參也做了調整。無論是URL的路徑變量還是查詢參數,我們都可以通過對象化的方式進行傳遞。這種統(tǒng)一參數類型的調整,簡化了接口的學習成本,自動生成的方法都是通過對象化的方式將參數綁定到接口當中。
在RESTful標準的接口中,接口的URL可能會存在兩種參數,路徑變量(Path Variables)和查詢參數(Query Argument)。
- 路徑變量:就是URL中映射到指定資源所涉及的變量,比如/resources/:id,這里的:id,指的就是資源id,操作不同的資源時,URL中:id這段路徑也會不同。/resources/1,/resources/2…等
- 查詢參數:指的是URL中的query參數,通常就是GET請求或者DELETE請求的URL中問號后面那段,比如/resources?page=0&size=20,page和size就是查詢參數
先來一波實現的思路
首先對自動生成的與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對象。 - 當給方法傳遞三個參數對象的時候,參數依次是路徑變量對象,查詢參數對象或者數據對象,兼容
AxiosAPI的config對象。
下面用一個GET請求和一個PUT請求進行圖解示意,先看看GET請求:
<figure style="user-select: text !important; display: block; margin: 22px auto; text-align: center; color: rgb(51, 51, 51); font-family: -apple-system, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">[圖片上傳中...(image-fb509-1528791284988-5)]
<figcaption style="user-select: text !important; display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>
</figure>
下面是PUT請求:
<figure style="user-select: text !important; display: block; margin: 22px auto; text-align: center; color: rgb(51, 51, 51); font-family: -apple-system, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">[圖片上傳中...(image-bf3ff0-1528791284988-4)]
<figcaption style="user-select: text !important; display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>
</figure>
- 當傳遞兩個參數時,如果URL接口不帶路徑變量,那么第一個參數是查詢參數對象(
GET方法或者DELETE方法)或者數據對象(POST方法或者PUT方法),第二個是config對象。如果URL接口帶有路徑變量,那么第一個參數就表示路徑變量對象,第二個參數是查詢參數對象或者數據對象。
比如下面兩個GET方法的URL接口,左邊這個不帶路徑變量,右邊的帶有路徑變量:id。左邊的,假設與URL接口綁定的方法名是getTagPageableList,當我們調用方式只穿兩個參數,那么第一個參數會轉換成查詢參數的格式key1=value1&key2=value2&...&keyn=valuen,第二個參數則相當于Axios的config對象。右邊的,因為URL接口中帶有路徑變量:id,那么調用綁定URL接口的方法getTagById并傳了兩個參數時,第一個參數對象被根據key替換掉URL接口中的路徑變量,第二個參數則會被作為查詢參數使用。
<figure style="user-select: text !important; display: block; margin: 22px auto; text-align: center; color: rgb(51, 51, 51); font-family: -apple-system, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">[圖片上傳中...(image-837de1-1528791284988-3)]
<figcaption style="user-select: text !important; display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>
</figure>
POST方法和PUT方法的請求也是類似,只是將查詢參數替換成了提交的數據。
<figure style="user-select: text !important; display: block; margin: 22px auto; text-align: center; color: rgb(51, 51, 51); font-family: -apple-system, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">[圖片上傳中...(image-913b91-1528791284988-2)]
<figcaption style="user-select: text !important; display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>
</figure>
- 當只傳遞一個參數時,如果接口URL不帶路徑變量,那么這個參數就是查詢參數對象或者數據對象,如果接口URL帶有路徑變量,那么這個參數對象就會映射到路徑變量中。
兩個GET請求:
<figure style="user-select: text !important; display: block; margin: 22px auto; text-align: center; color: rgb(51, 51, 51); font-family: -apple-system, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">[圖片上傳中...(image-ba9bf-1528791284988-1)]
<figcaption style="user-select: text !important; display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>
</figure>
一個POST請求和一個PUT請求:
<figure style="user-select: text !important; display: block; margin: 22px auto; text-align: center; color: rgb(51, 51, 51); font-family: -apple-system, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">[圖片上傳中...(image-2bed7-1528791284988-0)]
<figcaption style="user-select: text !important; display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>
</figure>
將思路轉換成實現的代碼
在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 方法名稱和請求方法必須一一匹配
* 如果發(fā)現匹配到的方法不止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對象動態(tài)定義模塊方法
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。被動態(tài)添加到模塊實例對象的方法在被調用時,先判斷與方法綁定的URL是否有路徑變量,如果有,則通過mapParamsToPathVariables()進行轉換,然后在執(zhí)行之前通過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的環(huán)境中,我們還可以用裝飾器來將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(),方便的給模塊添加一系列可以執(zhí)行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接口的方法,規(guī)定好參數,然后在界面層可以通過模塊的方法來調用URL接口與后臺進行數據通信,簡化了調用http庫API的流程。 - 假如項目中,接口越來越多,那么會導致相應的模塊也越來越多,比如
VideoManager、EpisodeManager、CPManager等。隨著模塊模塊逐漸增多,我們發(fā)現重復的代碼也在增多,需要提升代碼的復用性,那么,可以給這些Manager模塊定義一個基類BaseModule,然后將http庫相關的代碼轉移到BaseModule中,從而子類中調用URL接口的方法。 - 后來發(fā)現,即使有了
BaseModule消除了重復的代碼,但還是存在重復的工作,比如手寫那些CRUD方法,于是,我們將BaseModule獨立成一個單獨的項目httpclient-module,從之前的繼承關系轉為組合關系,并設計了一個APIbindUrls()。通過這個API,我們可以以key -> value這種配置項的方式,動態(tài)的給一個模塊添加執(zhí)行URL接口請求的方法,從而進一步的簡化我們的代碼,提升我們開發(fā)的效率。 - 最后,還給
bindUrls()做了靈活性的提升工作。
在整個http封裝過程中,我們進行了一些思考,比如復用性,通用性,靈活性。其最終的目的是為了提升我們開發(fā)過程的效率,減少重復工作。但回過頭來看,對于http庫的封裝其實并非一定要做到最后這一步的樣子。我們也是根據實際情況一步一步迭代過來的,所以,具體需要封裝到哪一程度,并沒有確切的答案,得從實際的場景出發(fā),綜合考慮后,選擇最合適的方式。
另外的,其實整個過程的思考(不是代碼),不僅僅適用于Axios庫,也可以用于其他的http庫,比如SuperAgent或者fetch,也不僅僅適用于http庫的封裝,對于其他類型的模塊的封裝也同樣適用,不過需要觸類旁通。
以上是我們團隊封裝Axios的開發(fā)經歷,希望對大家有幫助和啟發(fā)。文中有不當的地方,歡迎批評和討論。