时光仿若白驹过隙,转瞬即逝,距离我上次发布的有关封装网络请求库的文章 如何让 Android 网络请求像诗一样优雅 已经有一年多的时间了,随着华为纯血鸿蒙的正式使用,鸿蒙 App 的开发也提上了日程。在 Harmony 应用开发中,网络请求是必不可少的,如何封装才能使自己的网络请求代码更加简洁优雅,更具扩展性,方便以后的开发呢?本文是基于 Axios 网络请求库来做的二次封装,好了,废话不多说,开整 ~
依赖
首先,使用命令下载安装 Axios 库。
ohpm install @ohos/axios
安装完成之后,我们就可以在 oh-package.json5 中看到该依赖。
"dependencies": {
"@ohos/axios": "^2.2.4"
}
创建实例
private axiosInstance: AxiosInstance = axios.create({
timeout: 10000
})
定义拦截器
定义请求和响应拦截器,这里只做了请求和响应信息的日志打印,方便以后查看和调试相关问题。
this.axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
console.info('Axios Request:' + JSON.stringify(config))
return config
})
this.axiosInstance.interceptors.response.use((response: AxiosResponse) => {
console.info('Axios Response:' + JSON.stringify(response))
return response
})
数据模型
export type NetParamType = string | number | boolean
export class BaseResult<T> {
readonly code: number = 0
readonly message?: string = ''
readonly warnMessage?: string = ''
readonly data?: T
}
GET 请求
先定义一个私有方法执行原始 get 请求。
private axiosGet<T>(url: string, axiosConfig: AxiosRequestConfig): Promise<T> {
return this.axiosInstance.get<T, AxiosResponse<T>, null>(url, axiosConfig).then((response: AxiosResponse<T>) => {
const data = response.data
if (!data) {
Promise.reject(new Error('response data is null'))
}
return data
}).catch((error: Error) => {
return Promise.reject(error)
})
}
然后提供一个方法,用于发起 get 请求,这里统一配置 url,请求参数,请求头 token 等。
netGet<T>(url: string, params?: Map<string, NetParamType>,
headers?: Map<string, string>): Promise<BaseResult<T>> {
//请求参数
const axiosParams = new Map<string, NetParamType>()
if (params && params.size > 0) {
params.forEach((value, key, _) => {
axiosParams[key] = value
})
}
//请求头
const axiosHeaders = new AxiosHeaders()
if (globalToken.length > 0) {
axiosHeaders.set('token', globalToken)
}
if (headers && headers.size > 0) {
headers.forEach((value, key, _) => {
axiosHeaders.set(key, value)
})
}
const axiosRequestConfig: AxiosRequestConfig = {
headers: axiosHeaders,
params: axiosParams
}
const axiosUrl = BASE_URL + url
return this.axiosGet<BaseResult<T>>(axiosUrl, axiosRequestConfig)
}
POST 请求
先定义一个私有方法执行原始 post 请求。
private axiosPost<T>(url: string, params: string, axiosConfig: AxiosRequestConfig): Promise<T> {
return this.axiosInstance.post<T, AxiosResponse<T>, string>(url, params,
axiosConfig).then((response: AxiosResponse<T>) => {
const data = response.data
if (!data) {
Promise.reject(new Error('response data is null'))
}
return data
}).catch((error: Error) => {
return Promise.reject(error)
})
}
然后提供一个方法,用于发起 post 请求,这里统一配置 url,请求参数,请求头 token 等。
netPost<T>(url: string, params?: Map<string, NetParamType>, headers?: Map<string, string>): Promise<BaseResult<T>> {
let formParams = ''
if (params && params.size > 0) {
const formArray: string[] = []
params.forEach((value, key, _) => {
const encodedKey = encodeURIComponent(key)
const encodeValue = encodeURIComponent(value)
formArray.push(`${encodedKey}=${encodeValue}`)
})
formParams = formArray.join('&')
}
const axiosHeaders = new AxiosHeaders().set('Content-Type', 'application/x-www-form-urlencoded')
if (globalToken.length > 0) {
axiosHeaders.set('token', globalToken)
}
if (headers && headers.size > 0) {
headers.forEach((value, key, _) => {
axiosHeaders.set(key, value)
})
}
const axiosRequestConfig: AxiosRequestConfig = {
headers: axiosHeaders
}
const axiosUrl = BASE_URL + url
return this.axiosPost<BaseResult<T>>(axiosUrl, formParams, axiosRequestConfig)
}
文件上传
upLoadFile(url: string, filePath: string, params?: Map<string, NetParamType>,
onUploadProgress: (progress: number) => void = () => {
}): Promise<BaseResult<object>> {
const formData = new FormData()
if (params && params.size > 0) {
params.forEach((value, key, _) => {
formData.append(key, value)
})
}
const axiosHeaders = new AxiosHeaders({ 'Content-Type': 'multipart/form-data' })
if (globalToken.length > 0) {
axiosHeaders.set('token', globalToken)
}
try {
let file2 = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)
let stat = fs.lstatSync(filePath)
let buf2 = new ArrayBuffer(stat.size)
fs.readSync(file2.fd, buf2)
fs.fsyncSync(file2.fd)
fs.closeSync(file2.fd)
formData.append('video', buf2)
} catch (e) {
console.error('read file error: ' + JSON.stringify(e))
}
return this.axiosInstance.post<BaseResult<object>, AxiosResponse<BaseResult<object>>, FormData>(BASE_URL + url,
formData, {
headers: axiosHeaders,
context: getContext(this),
onUploadProgress: (event: AxiosProgressEvent): void => {
let progress = (event && event.loaded && event.total) ? Math.ceil(event.loaded / event.total * 100) : 0
onUploadProgress(progress)
},
}).then((response: AxiosResponse<BaseResult<object>>) => {
const data = response.data
if (!data) {
Promise.reject(new Error('response data is null'))
}
return data
}).catch((error: Error) => {
return Promise.reject(error)
})
}
文件下载
downLoadFile(context: Context, url: string, method: string, filePath: string,
onDownloadProgress: (progress: number) => void = () => {
}, onDownloadResult: (result: AxiosResponse) => void = () => {
}, onDownloadError: (errorMsg: string) => void = () => {
}) {
this.axiosInstance<AxiosResponse>({
url: url,
method: method,
context: context,
filePath: filePath,
onDownloadProgress: (event: AxiosProgressEvent): void => {
let progress = (event && event.loaded && event.total) ? Math.ceil(event.loaded / event.total * 100) : 0
onDownloadProgress(progress)
}
}).then((result: AxiosResponse) => {
onDownloadResult(result)
}).catch((e: AxiosError) => {
onDownloadError(e.message)
})
}
带身份验证的请求
HTTP 异常的状态码有很多,需要统一处理的状态码主要是 401 ,表示 token 失效,需要重新刷新 token,毕竟这会直接影响我们几乎所有的网络请求。
//获取身份信息,比如 token
refreshIdentity(): Promise<BaseResult<IdentityInfo[]>> {
return GeneralRequest.getIdentityInfo(this)
}
这里定义一个方法,用于 token 失效的时候重新请求获取 token,获取到新的 token 之后,再一次发起我们的请求。
private requestByCheckIdentity<T>(block: () => Promise<BaseResult<T>>): Promise<BaseResult<T>> {
if (globalToken.length == 0) {
return this.refreshIdentity().then(() => {
return block()
})
}
return block().then((result) => {
if (result.code == 401) {
return this.refreshIdentity().then(() => {
return block()
})
} else {
return result
}
})
}
执行请求都统一调用这个方法,这样我们就可以一致避免 token 失效的情况。
getByCheckIdentity<T>(url: string, params?: Map<string, NetParamType>,
headers?: Map<string, string>): Promise<BaseResult<T>> {
return this.requestByCheckIdentity<T>(() => this.netGet<T>(url, params, headers))
}
postByCheckIdentity<T>(url: string, params?: Map<string, NetParamType>,
headers?: Map<string, string>): Promise<BaseResult<T>> {
return this.requestByCheckIdentity<T>(() => this.netPost<T>(url, params, headers))
}
upLoadFileByCheckIdentity(url: string, filePath: string, params?: Map<string, NetParamType>,
onUploadProgress: (progress: number) => void = () => {
}): Promise<BaseResult<object>> {
return this.requestByCheckIdentity<object>(() => this.upLoadFile(url, filePath, params, onUploadProgress))
}
最后贴上请求基类的完整代码
export abstract class BaseNetRequest {
private axiosInstance: AxiosInstance = axios.create({
timeout: 10000
})
constructor() {
this.axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
console.info('Axios Request:' + JSON.stringify(config))
return config
})
this.axiosInstance.interceptors.response.use((response: AxiosResponse) => {
console.info('Axios Response:' + JSON.stringify(response))
return response
})
}
private axiosGet<T>(url: string, axiosConfig: AxiosRequestConfig): Promise<T> {
return this.axiosInstance.get<T, AxiosResponse<T>, null>(url, axiosConfig).then((response: AxiosResponse<T>) => {
const data = response.data
if (!data) {
Promise.reject(new Error('response data is null'))
}
return data
}).catch((error: Error) => {
return Promise.reject(error)
})
}
//get 请求
netGet<T>(url: string, params?: Map<string, NetParamType>,
headers?: Map<string, string>): Promise<BaseResult<T>> {
//请求参数
const axiosParams = new Map<string, NetParamType>()
if (params && params.size > 0) {
params.forEach((value, key, _) => {
axiosParams[key] = value
})
}
//请求头
const axiosHeaders = new AxiosHeaders()
if (globalToken.length > 0) {
axiosHeaders.set('token', globalToken)
}
if (headers && headers.size > 0) {
headers.forEach((value, key, _) => {
axiosHeaders.set(key, value)
})
}
const axiosRequestConfig: AxiosRequestConfig = {
headers: axiosHeaders,
params: axiosParams
}
const axiosUrl = BASE_URL + url
return this.axiosGet<BaseResult<T>>(axiosUrl, axiosRequestConfig)
}
private axiosPost<T>(url: string, params: string, axiosConfig: AxiosRequestConfig): Promise<T> {
return this.axiosInstance.post<T, AxiosResponse<T>, string>(url, params,
axiosConfig).then((response: AxiosResponse<T>) => {
const data = response.data
if (!data) {
Promise.reject(new Error('response data is null'))
}
return data
}).catch((error: Error) => {
return Promise.reject(error)
})
}
//post 请求
netPost<T>(url: string, params?: Map<string, NetParamType>, headers?: Map<string, string>): Promise<BaseResult<T>> {
let formParams = ''
if (params && params.size > 0) {
const formArray: string[] = []
params.forEach((value, key, _) => {
const encodedKey = encodeURIComponent(key)
const encodeValue = encodeURIComponent(value)
formArray.push(`${encodedKey}=${encodeValue}`)
})
formParams = formArray.join('&')
}
const axiosHeaders = new AxiosHeaders().set('Content-Type', 'application/x-www-form-urlencoded')
if (globalToken.length > 0) {
axiosHeaders.set('token', globalToken)
}
if (headers && headers.size > 0) {
headers.forEach((value, key, _) => {
axiosHeaders.set(key, value)
})
}
const axiosRequestConfig: AxiosRequestConfig = {
headers: axiosHeaders
}
const axiosUrl = BASE_URL + url
return this.axiosPost<BaseResult<T>>(axiosUrl, formParams, axiosRequestConfig)
}
//文件下载
downLoadFile(context: Context, url: string, method: string, filePath: string,
onDownloadProgress: (progress: number) => void = () => {
}, onDownloadResult: (result: AxiosResponse) => void = () => {
}, onDownloadError: (errorMsg: string) => void = () => {
}) {
this.axiosInstance<AxiosResponse>({
url: url,
method: method,
context: context,
filePath: filePath,
onDownloadProgress: (event: AxiosProgressEvent): void => {
let progress = (event && event.loaded && event.total) ? Math.ceil(event.loaded / event.total * 100) : 0
onDownloadProgress(progress)
}
}).then((result: AxiosResponse) => {
onDownloadResult(result)
}).catch((e: AxiosError) => {
onDownloadError(e.message)
})
}
//文件上传
upLoadFile(url: string, filePath: string, params?: Map<string, NetParamType>,
onUploadProgress: (progress: number) => void = () => {
}): Promise<BaseResult<object>> {
const formData = new FormData()
if (params && params.size > 0) {
params.forEach((value, key, _) => {
formData.append(key, value)
})
}
const axiosHeaders = new AxiosHeaders({ 'Content-Type': 'multipart/form-data' })
if (globalToken.length > 0) {
axiosHeaders.set('token', globalToken)
}
try {
let file2 = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)
let stat = fs.lstatSync(filePath)
let buf2 = new ArrayBuffer(stat.size)
fs.readSync(file2.fd, buf2)
fs.fsyncSync(file2.fd)
fs.closeSync(file2.fd)
formData.append('video', buf2)
} catch (e) {
console.error('read file error: ' + JSON.stringify(e))
}
return this.axiosInstance.post<BaseResult<object>, AxiosResponse<BaseResult<object>>, FormData>(BASE_URL + url,
formData, {
headers: axiosHeaders,
context: getContext(this),
onUploadProgress: (event: AxiosProgressEvent): void => {
let progress = (event && event.loaded && event.total) ? Math.ceil(event.loaded / event.total * 100) : 0
onUploadProgress(progress)
},
}).then((response: AxiosResponse<BaseResult<object>>) => {
const data = response.data
if (!data) {
Promise.reject(new Error('response data is null'))
}
return data
}).catch((error: Error) => {
return Promise.reject(error)
})
}
//获取身份信息,比如 token
refreshIdentity(): Promise<BaseResult<IdentityInfo[]>> {
return GeneralRequest.getIdentityInfo(this)
}
//带身份验证检查的请求方法
private requestByCheckIdentity<T>(block: () => Promise<BaseResult<T>>): Promise<BaseResult<T>> {
if (globalToken.length == 0) {
return this.refreshIdentity().then(() => {
return block()
})
}
return block().then((result) => {
if (result.code == 401) { //token失效,重新刷新 token。
return this.refreshIdentity().then(() => {
return block()
})
} else {
return result
}
})
}
// 带 token 验证的 get 请求
getByCheckIdentity<T>(url: string, params?: Map<string, NetParamType>,
headers?: Map<string, string>): Promise<BaseResult<T>> {
return this.requestByCheckIdentity<T>(() => this.netGet<T>(url, params, headers))
}
// 带 token 验证的 post 请求
postByCheckIdentity<T>(url: string, params?: Map<string, NetParamType>,
headers?: Map<string, string>): Promise<BaseResult<T>> {
return this.requestByCheckIdentity<T>(() => this.netPost<T>(url, params, headers))
}
// 带 token 验证的文件上传
upLoadFileByCheckIdentity(url: string, filePath: string, params?: Map<string, NetParamType>,
onUploadProgress: (progress: number) => void = () => {
}): Promise<BaseResult<object>> {
return this.requestByCheckIdentity<object>(() => this.upLoadFile(url, filePath, params, onUploadProgress))
}
}
使用案例
使用时我们只需继承这个 BaseNetRequest 即可实现各种请求。
export class NetRequest extends BaseNetRequest {
getHttpData(params: Map<string, string>): Promise<BaseResult<GetBean[]>> {
return this.getByCheckIdentity<GetBean[]>('/huawei/harmony/net/get/test', params)
}
postHttpData(params: Map<string, string>): Promise<BaseResult<PostBean[]>> {
return this.postByCheckIdentity<PostBean[]>('/huawei/harmony/net/post/test', params)
}
uploadLocalFile(filePath: string, params?: Map<string, NetParamType>,
onUploadProgress: (progress: number) => void = () => {
}): Promise<BaseResult<object>> {
return this.upLoadFileByCheckIdentity('/huawei/harmony/net/uploadLocalFile/test', filePath, params,
onUploadProgress)
}
}
export const netRequest = new NetRequest()
然后在实际界面中调用即可,如下所示:
@Entry
@Component
struct Index {
async getHttpData() {
try {
const response = await netRequest.getHttpData(new Map([
['manufacturer', 'huawei'],
['system', 'harmony'],
]))
const data = response.data
if (data) {
console.info('getHttpData: ' + JSON.stringify(data))
} else {
console.error('getHttpData fail: ' + JSON.stringify(response))
}
} catch (e) {
console.error('getHttpData error: ' + e)
}
}
async postHttpData() {
try {
const response = await netRequest.postHttpData(new Map([
['system', 'harmony']
]))
if (response.code == 200) {
console.info('postHttpData success')
} else {
console.error('postHttpData fail: ' + JSON.stringify(response))
}
} catch (e) {
console.error('postHttpData error: ' + e)
}
}
async uploadLocalFile() {
try {
const filePath = getContext(this).cacheDir + '/video/video.mp4'
const response = await netRequest.uploadLocalFile(filePath, new Map([
['manufacturer', 'huawei'],
['system', 'harmony'],
]), (progress: number) => {
console.info('uploadLocalFile progress: ' + progress)
})
if (response.code == 200) {
console.info('uploadLocalFile success')
} else {
console.error('uploadLocalFile fail: ' + JSON.stringify(response))
}
} catch (e) {
console.error('uploadLocalFile error: ' + e)
}
}
async downLoadFile() {
try {
const dir = getContext(this).cacheDir + '/video'
if (!fs.accessSync(dir)) {
fs.mkdirSync(dir)
}
//具体的文件路径是:/data/app/el2/100/base/<Package_Name>/haps/entry/cache/video/video.mp4
const downloadPath = dir + '/video.mp4'
if (fs.accessSync(downloadPath)) {
fs.unlinkSync(downloadPath)
}
netRequest.downLoadFile(getContext(this),
'https://developer.huawei.com/consumer/cn/test.mp4', 'GET',
downloadPath, (progress) => {
console.info('downLoadFile progress:' + progress)
}, (result: AxiosResponse) => {
if (result.status == 200) {
console.info('downLoadFile success')
} else {
console.error('downLoadFile result:' + result)
}
}, (errorMsg: string) => {
console.error('downLoadFile errorMsg: ' + errorMsg)
})
} catch (e) {
console.error('downLoadFile error: ' + e)
}
}
build() {
Column({ space: 10 }) {
Button('get request').width(100).height(50).onClick(() => {
this.getHttpData()
})
Button('post request').width(100).height(50).onClick(() => {
this.postHttpData()
})
Button('downLoadFile').width(100).height(50).onClick(() => {
this.downLoadFile()
})
Button('uploadFile').width(100).height(50).onClick(() => {
this.uploadLocalFile()
})
}
.height('100%')
.width('100%')
}
}
至此,我们就已完成了鸿蒙 Axios 的二次封装。Axios 为开发者提供了高效且便捷的工具,相信在未来的鸿蒙生态建设中,Axios 将继续发挥关键作用,进一步拓展鸿蒙开发的无限潜力。