Typescript?封裝?Axios攔截器方法實(shí)例
引言
對(duì) axios 二次封裝,更加的可配置化、擴(kuò)展性更加強(qiáng)大靈活
通過 class 類實(shí)現(xiàn),class 具備更強(qiáng)封裝性(封裝、繼承、多態(tài)),通過實(shí)例化類傳入自定義的配置
創(chuàng)建 class
嚴(yán)格要求實(shí)例化時(shí)傳入的配置,擁有更好的代碼提示
/**
* @param {AxiosInstance} axios實(shí)例類型
* @param {AxiosRequestConfig} axios配置項(xiàng)類型
*/
import type { AxiosInstance, AxiosRequestConfig } from 'axios'
class Http {
axios: AxiosInstance
constructor(config: AxiosRequestConfig) {
// 創(chuàng)建一個(gè)實(shí)例 axios.create([config])
this.axios = axios.create(config)
}
}
// 每實(shí)例化一個(gè) axios 時(shí),都是不同的 axios 示例,互不干擾
new Http({
baseURL:'qq.com';
timeout:60 * 1
});
new Http({
baseURL:'web.com'
});
axios.create([config])
const instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
});
AxiosRequestConfig 的類型注解
export interface AxiosRequestConfig<D = any> {
url?: string;
method?: Method | string;
baseURL?: string;
transformRequest?: AxiosRequestTransformer | AxiosRequestTransformer[];
transformResponse?: AxiosResponseTransformer | AxiosResponseTransformer[];
headers?: AxiosRequestHeaders;
params?: any;
paramsSerializer?: (params: any) => string;
data?: D;
timeout?: number;
timeoutErrorMessage?: string;
withCredentials?: boolean;
adapter?: AxiosAdapter;
auth?: AxiosBasicCredentials;
responseType?: ResponseType;
responseEncoding?: responseEncoding | string;
xsrfCookieName?: string;
xsrfHeaderName?: string;
onUploadProgress?: (progressEvent: any) => void;
onDownloadProgress?: (progressEvent: any) => void;
maxContentLength?: number;
validateStatus?: ((status: number) => boolean) | null;
maxBodyLength?: number;
maxRedirects?: number;
beforeRedirect?: (options: Record<string, any>, responseDetails: {headers: Record<string, string>}) => void;
socketPath?: string | null;
httpAgent?: any;
httpsAgent?: any;
proxy?: AxiosProxyConfig | false;
cancelToken?: CancelToken;
decompress?: boolean;
transitional?: TransitionalOptions;
signal?: AbortSignal;
insecureHTTPParser?: boolean;
env?: {
FormData?: new (...args: any[]) => object;
};
}
封裝 request(config)通用方法
/**
* axios#request(config)
* @param {*} config
* @returns {*}
*/
request(config: AxiosRequestConfig) {
return this.axios.request(config)
}
?? 在 axios 中,request中的 config 參數(shù)與實(shí)例化時(shí),axios.create(config)傳入的參數(shù)是相同的,以下是 axios 方法具體參數(shù)
axios.request(config) axios.get(url[, config]) axios.delete(url[, config]) axios.head(url[, config]) axios.options(url[, config]) axios.post(url[, data[, config]]) axios.put(url[, data[, config]]) axios.patch(url[, data[, config]]) axios.getUri([config])
封裝-攔截器(單個(gè)實(shí)例獨(dú)享)
攔截器的hooks,在請(qǐng)求或響應(yīng)被 then 或 catch 處理前攔截處理
??在請(qǐng)求中,如攜帶token、loading動(dòng)畫、header配置...,都是一些公有的邏輯,所以可以寫到全局的攔截器里面
??注意的是,可能存在某些項(xiàng)目請(qǐng)求,需要的公有的邏輯配置方式不一樣,如 A項(xiàng)目:攜帶token、loading動(dòng)畫,B項(xiàng)目:攜帶token、header配置
??考慮到攔截方式不一樣,固不能將 class 里的攔截器寫si,所以,需要通過不同的 axios 實(shí)例化傳入自定義的 hooks(攔截器),實(shí)現(xiàn)單個(gè)實(shí)例獨(dú)享,擴(kuò)展性更強(qiáng)
在上面實(shí)例化 Http(Axios) 時(shí),僅僅傳入 axios 約定好的 config(AxiosRequestConfig)
new Http({
baseURL:'qq.com';
timeout:60 * 1
});
經(jīng)過分析考慮,需要在實(shí)例化 Http 時(shí),可以傳入更多自定義的 hooks,擴(kuò)展 axios。但是,在實(shí)例化時(shí),直接傳入定義好的攔截器是不可行的 Why?
new Http({
baseURL: 'qq.com',
timeout: 60 * 1,
hooks: {}, // ERROR ????
interceptor: () => {} // ERROR ????
})
“hooks”不在類型“AxiosRequestConfig<any>”中,在上面 Http class 的 constructor 構(gòu)造函數(shù)中,嚴(yán)格約束傳入的參數(shù)應(yīng)為AxiosRequestConfig類型(TS)
AxiosRequestConfig 中并不存在 hooks and interceptor類型的屬性(AxiosRequestConfig 是由 axios 提供的一個(gè)類型注解)
??擴(kuò)展 Http 自定義攔截器
對(duì) AxiosRequestConfig 類型注解進(jìn)行繼承,使得在實(shí)例化時(shí),可以傳入擴(kuò)展后的類型,定義:
???IinterceptorHooks接口存儲(chǔ)自定義的攔截器函數(shù)、
???IHttpRequestConfig接口繼承 AxiosRequestConfig,并擴(kuò)展自定義攔截器的屬性,屬性類型為:IinterceptorHooks
???`IinterceptorHooks`攔截器Hook接口類型
import type { AxiosResponse, AxiosRequestConfig } from 'axios'
/**
* 攔截器的hooks,在請(qǐng)求或響應(yīng)被 then 或 catch 處理前攔截
* @param {beforeRequestInterceptor(?)} 發(fā)送請(qǐng)求之前攔截器
* @param {requestErrorInterceptor(?)} 請(qǐng)求錯(cuò)誤攔截器
* @param {responseSuccessInterceptor(?)} 響應(yīng)成功攔截器
* @param {responseFailInterceptor(?)} 響應(yīng)失敗攔截器
*/
interface interceptorHooks {
beforeRequestInterceptor: (config: AxiosRequestConfig) => AxiosRequestConfig
requestErrorInterceptor: (error: any) => any
responseSuccessInterceptor: (result: AxiosResponse) => AxiosResponse
responseFailInterceptor: (error: any) => any
}
export type IinterceptorHooks = Partial<interceptorHooks>
??Partial:Typescript 內(nèi)置類型,將定義的類型注解全部變?yōu)榭蛇x的屬性
??調(diào)用說明:axios.interceptors.request.use( beforeRequestInterceptor , requestErrorInterceptor );
請(qǐng)求之前攔截器中(beforeRequestInterceptor),config 參數(shù)同樣是與axios.create中的參數(shù)類型相同,為AxiosRequestConfig
注意:并不是IHttpRequestConfig
???`IHttpRequestConfig` 類構(gòu)造函數(shù) config 接口類型
/**
* 實(shí)例化Http類的配置項(xiàng)參數(shù),繼承于AxiosRequestConfig
* @param {interceptors(?)} 攔截器Hooks
* @param {loading} 請(qǐng)求loading
* @param {...} 其它的配置項(xiàng)
* @param {AxiosRequestConfig} axios原生的配置選
*/
export interface IHttpRequestConfig extends AxiosRequestConfig {
interceptors?: IinterceptorHooks
}
??使用說明:
import { IHttpRequestConfig, IinterceptorHooks } from './types'
class Http {
axios: AxiosInstance
interceptors?: IinterceptorHooks
constructor(config: IHttpRequestConfig) {
// 解構(gòu)自定義的屬性
const { interceptors, ...AxiosRequestConfig } = config
this.axios = axios.create(AxiosRequestConfig)
// 存儲(chǔ)自定義攔截Hooks or 直接 use() 使用
this.interceptors = interceptors
// 傳入自定義請(qǐng)求攔截器Hooks
this.axios.interceptors.request.use(
this.interceptors?.beforeRequestInterceptor,
this.interceptors?.requestErrorInterceptor
)
// 傳入自定義響應(yīng)攔截器Hooks
this.axios.interceptors.response.use(
this.interceptors?.responseSuccessInterceptor,
this.interceptors?.responseFailInterceptor
)
}
}
// 擴(kuò)展后的實(shí)例化...?
interceptors: {
// ... 自定義的攔截Hooks
beforeRequestInterceptor: (config: IHttpRequestConfig) => {
const token = localStorage.getItem('token')
if (token && config.headers) {
config.headers.Authorization = token
}
return config
}
}
封裝-攔截器(所有實(shí)例共享)
無論存在多少個(gè)實(shí)例的 Http,所有實(shí)例都會(huì)共享的同一套攔截器,在 class 中固定的
class Http {
axios: AxiosInstance
// ...
constructor(config: IHttpRequestConfig) {
// ...單個(gè)實(shí)例獨(dú)享攔截器處理??
// 所有實(shí)例共享的攔截-請(qǐng)求攔截器??
this.axios.interceptors.request.use(
function (config) {
// 在發(fā)送請(qǐng)求之前做些什么
return config
},
function (error) {
// 對(duì)請(qǐng)求錯(cuò)誤做些什么
return Promise.reject(error)
}
)
// 所有實(shí)例共享的攔截-響應(yīng)攔截器
this.axios.interceptors.response.use(
function (response) {
// 2xx 范圍內(nèi)的狀態(tài)碼都會(huì)觸發(fā)該函數(shù)。
// 對(duì)響應(yīng)數(shù)據(jù)做點(diǎn)什么
return response
},
function (error) {
// 超出 2xx 范圍的狀態(tài)碼都會(huì)觸發(fā)該函數(shù)。
// 對(duì)響應(yīng)錯(cuò)誤做點(diǎn)什么
return Promise.reject(error)
}
)
}
}
封裝-攔截器(單個(gè)請(qǐng)求獨(dú)享)
在 axios 原生的 config(AxiosRequestConfig) 中,存在兩個(gè)允許對(duì)請(qǐng)求or響應(yīng)的數(shù)據(jù)進(jìn)行轉(zhuǎn)化處理:
// `transformRequest` 允許在向服務(wù)器發(fā)送前,修改請(qǐng)求數(shù)據(jù)
// 它只能用于 'PUT', 'POST' 和 'PATCH' 這幾個(gè)請(qǐng)求方法
// 數(shù)組中最后一個(gè)函數(shù)必須返回一個(gè)字符串, 一個(gè)Buffer實(shí)例,ArrayBuffer,F(xiàn)ormData,或 Stream
// 你可以修改請(qǐng)求頭。
transformRequest: [function (data, headers) {
// 對(duì)發(fā)送的 data 進(jìn)行任意轉(zhuǎn)換處理
return data;
}],
// `transformResponse` 在傳遞給 then/catch 前,允許修改響應(yīng)數(shù)據(jù)
transformResponse: [function (data) {
// 對(duì)接收的 data 進(jìn)行任意轉(zhuǎn)換處理
return data;
}],
由于transformRequest、transformResponse為 Axios 原生的API,所以盡量不去改變?cè)械?config API
??在需要不修改原生 Axios config 情況下,擴(kuò)展單個(gè)請(qǐng)求獨(dú)享的攔截器時(shí)
??class 封裝的方法:request(config: AxiosRequestConfig){}參數(shù)中不能再使用原生AxiosRequestConfig作為參數(shù)的注解
??前面提到過,request 中的 config 參數(shù)與實(shí)例化時(shí)傳入的參數(shù)是相同的,所以在這里同樣可以使用IHttpRequestConfig,作為 request 的參數(shù)注解
/**
* axios#request(config)
* @param {*} config
* @returns {*}
*/
request(config: AxiosRequestConfig) {
return this.axios.request(config)
}
// ...改為 AxiosRequestConfig -> IHttpRequestConfig
request(config: IHttpRequestConfig) {
// ... 在執(zhí)行請(qǐng)求之前,執(zhí)行單個(gè)請(qǐng)求獨(dú)享的攔截器(?)
return this.axios.request(config).then(
// ...
)
}
具體代碼實(shí)現(xiàn):
// Http 封裝的request??
request(config: IHttpRequestConfig) {
// 存在單個(gè)方法獨(dú)享自定義攔截器Hooks(請(qǐng)求之前)??
if (config.interceptors?.beforeRequestInterceptor) {
// 立即執(zhí)行beforeRequestInterceptor方法,傳入config處理返回新的處理后的config
config = config.interceptors.beforeRequestInterceptor(config)
}
return this.axios
.request(config)
.then((result) => {
// 存在單個(gè)方法獨(dú)享自定義攔截器Hooks(響應(yīng)成功)??
if (config.interceptors?.responseSuccessInterceptor) {
// 立即執(zhí)行beforeRequestInterceptor方法,傳入config處理返回新的處理后的config
result = config.interceptors.responseSuccessInterceptor(result)
}
return result
})
.catch((error) => {
// 存在單個(gè)方法獨(dú)享自定義攔截器Hooks(響應(yīng)失敗)??
if (config.interceptors?.responseFailInterceptor) {
// 立即執(zhí)行beforeRequestInterceptor方法,傳入config處理返回新的處理后的config
error = config.interceptors.responseFailInterceptor(error)
}
return error
})
}
// 執(zhí)行 request 方法,傳入攔截器處理??,如:
axios.request({
url: '/api/addUser',
methed: 'POST',
interceptors: {
beforeRequestInterceptor:(config) => {
// ...處理config數(shù)據(jù)
return config
},
// ...請(qǐng)求錯(cuò)誤處理不攔截
responseSuccessInterceptor:(result) => {
return result
}
}
})
注意:當(dāng)存在單個(gè)方法獨(dú)享自定義請(qǐng)求攔截器時(shí),應(yīng)當(dāng)在發(fā)送請(qǐng)求之前立即執(zhí)行 beforeRequestInterceptor 方法,而不是通過傳入到 use() 回調(diào),執(zhí)行請(qǐng)求攔截方法處理完之后返回一個(gè)經(jīng)過處理的 config,在傳入到請(qǐng)求方法中,發(fā)送請(qǐng)求
其它的單個(gè)方法獨(dú)享自定義響應(yīng)攔截器一樣
裝修 Http class
返回經(jīng)過
在響應(yīng)數(shù)據(jù)時(shí)候(響應(yīng)攔截器),Axios 為數(shù)據(jù)在外層包裝了一層對(duì)象,后臺(tái)返回的數(shù)據(jù)就存在于 result 的 data 中,所以需要對(duì)數(shù)據(jù)再一步的處理,扒開最外層的皮
注意:Axios 在外層包裝的對(duì)象數(shù)據(jù),其實(shí)在某些情況下是需要 result 中的一些屬性數(shù)據(jù)的,并不僅僅需要 data,比如返回格式為文件數(shù)據(jù)中,需要 headers 中的一些數(shù)據(jù)對(duì)文件進(jìn)行處理,命名等...
這里的扒皮處理,邏輯應(yīng)當(dāng)屬于所有實(shí)例共享的一個(gè)攔截器,具體工具需要進(jìn)行處理
// 所有實(shí)例共享的攔截-響應(yīng)攔截器
this.axios.interceptors.response.use(
function (response) {
// 返回為文件流時(shí),直接返回(文件需要單獨(dú)處理)??
if (['blob', 'arraybuffer'].includes(response.request.responseType)) {
// if (response.headers['content-disposition']) {
// let fileName = response.headers['content-disposition'].split('filename=')[1]
// fileName = decodeURI(fileName)
// return {
// data: response.data,
// fileName
// }
// }
return response
}
// 根據(jù)業(yè)務(wù)碼 or 狀態(tài)碼...進(jìn)行判斷
// 扒皮??
return response.data
},
function (error) {
// 超出 2xx 范圍的狀態(tài)碼都會(huì)觸發(fā)該函數(shù)。
// 對(duì)響應(yīng)錯(cuò)誤做點(diǎn)什么
return Promise.reject(error)
}
)
request 返回?cái)?shù)據(jù)結(jié)構(gòu)(DTO)
定義返回?cái)?shù)據(jù)類型注解
// 最終數(shù)據(jù)類型注解
interface IDateType {
[key: string]: any
}
// axios 返回的數(shù)據(jù)類型注解,IDateType === T
interface IDTO<T = any> {
code: number
data: T
message: string
state: number
[prop: string]: any
}
// 請(qǐng)求方法,傳入注解
axios.request<IDTO<IDateType>>({
url: '/api/addUser',
methed: 'POST',
})
由于在所有實(shí)例共享的響應(yīng)攔截器中,修改了返回的數(shù)據(jù)結(jié)構(gòu)return response.data,到達(dá)方法響應(yīng)攔截器中的數(shù)據(jù)類型已經(jīng)不再是AxiosResponse,導(dǎo)致在響應(yīng)成功的攔截器中類型錯(cuò)誤無法賦值,以及.then返回的類型不正確無法返回,正確應(yīng)該為DTO類型,解決方案:
// 修改 AxiosRequestConfig 類型注解,默認(rèn)類型為原來的 AxiosResponse,傳遞到響應(yīng)成功的攔截中進(jìn)行泛型注解,用于在方法中重新修改返回的數(shù)據(jù)類型
// 最終的泛型類型注解到達(dá) responseSuccessInterceptor 中
interface interceptorHooks<T> {
beforeRequestInterceptor: (config: AxiosRequestConfig) => AxiosRequestConfig
requestErrorInterceptor: (error: any) => any
responseSuccessInterceptor: (result: T) => T
responseFailInterceptor: (error: any) => any
}
export type IinterceptorHooks<T = AxiosResponse> = Partial<interceptorHooks<T>>
export interface IHttpRequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
interceptors?: IinterceptorHooks<T>
}
// 修改 Http class request方法,通過 泛型 T 接受方法傳達(dá)過來的 DTO 類型注解,傳遞到 IHttpRequestConfig 中修改 responseSuccessInterceptor 的參數(shù)類型注解以及返回值注解,在單個(gè)方法獨(dú)享自定義攔截器中就可以接受參數(shù),并且返回正確的 DTO 類型數(shù)據(jù)
// 由于在下面的 this.axios.request() 方法中,返回的數(shù)據(jù)已經(jīng)被上一個(gè)攔截器扒皮修改了,返回的 result 類型注解中存在類型不正確情況(正確返回應(yīng)返回response.data的類型注解),實(shí)際返回為 AxiosResponse<any,any>類型注解,導(dǎo)致數(shù)據(jù)返回到最外層方法時(shí)編輯器報(bào)錯(cuò)(最外層接受 DTO 類型),所以需要手動(dòng)修改this.axios.request() 方法中返回的類型注解 this.axios.request<any, T>()
request<T>(config: IHttpRequestConfig<T>): Promise<T> {
return this.axios
.request<any, T>(config)
.then((result) => {
// 存在單個(gè)方法獨(dú)享自定義攔截器Hooks(響應(yīng)成功)
if (config.interceptors?.responseSuccessInterceptor) {
// 立即執(zhí)行beforeRequestInterceptor方法,傳入config處理返回新的處理后的config
result = config.interceptors.responseSuccessInterceptor(result)
}
return result
})
.catch((error) => {
// ...
})
}
攔截器執(zhí)行順序
由于在 constructor(構(gòu)造函數(shù)) 代碼順序:單個(gè)實(shí)例獨(dú)享攔截器 -> 所有實(shí)例共享攔截器 ->...
??單個(gè)實(shí)例獨(dú)享攔截器位于所有實(shí)例共享攔截器之前,執(zhí)行順序?yàn)椋?/p>
所有實(shí)例共享請(qǐng)求攔截器 -> 單個(gè)實(shí)例獨(dú)享請(qǐng)求攔截器 -> 單個(gè)實(shí)例獨(dú)享響應(yīng)攔截器 -> 所有實(shí)例共享響應(yīng)攔截器
反之,則:
??所有實(shí)例共享攔截器位于之前單個(gè)實(shí)例獨(dú)享攔截器,執(zhí)行順序?yàn)椋?/p>
單個(gè)實(shí)例獨(dú)享請(qǐng)求攔截器 -> 所有實(shí)例共享請(qǐng)求攔截器 -> 所有實(shí)例共享響應(yīng)攔截器 -> 單個(gè)實(shí)例獨(dú)享響應(yīng)攔截器
??單個(gè)方法獨(dú)享攔截器(單個(gè)實(shí)例獨(dú)享攔截器位于所有實(shí)例共享攔截器之前) ,執(zhí)行順序?yàn)椋?/p>
單個(gè)方法請(qǐng)求攔截器 -> 實(shí)例共享請(qǐng)求攔截器 -> 單個(gè)獨(dú)享請(qǐng)求攔截器 -> 單個(gè)獨(dú)享響應(yīng)攔截器 -> 實(shí)例共享響應(yīng)攔截器 -> 單個(gè)方法響應(yīng)攔截器
請(qǐng)求攔截:constructor先執(zhí)行的代碼(use()),攔截器里面的回調(diào)hook后被執(zhí)行,反之,先被執(zhí)行
響應(yīng)攔截:constructor先執(zhí)行的代碼(use()),攔截器里面的回調(diào)hook先被執(zhí)行,反之,后被執(zhí)行
需要修改執(zhí)行順序可調(diào)整代碼的執(zhí)行順序
操作場景控制
由于存在三種攔截器,存在一些復(fù)雜的操作場景時(shí),比如,通過給所有實(shí)例或者單獨(dú)實(shí)例提前添加了操作loading、錯(cuò)誤捕獲...,但是現(xiàn)在需要在某個(gè)請(qǐng)求方法中不進(jìn)行此操作時(shí),如何解決?
解決方案:通過繼續(xù)擴(kuò)展特定的 IHttpRequestConfig 類型屬性,因?yàn)閱蝹€(gè)方法請(qǐng)求攔截器是最先執(zhí)行的,IHttpRequestConfig 配置項(xiàng),在所有的攔截器中是共享的,層級(jí)傳遞的,在攔截器中判斷特定的屬性值關(guān)閉不需要的操作
以上就是Typescript 封裝 Axios攔截器方法實(shí)例的詳細(xì)內(nèi)容,更多關(guān)于Typescript 封裝 Axios的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
微信小程序 本地存儲(chǔ)及登錄頁面處理實(shí)例詳解
這篇文章主要介紹了微信小程序 本地存儲(chǔ)實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下2017-01-01
微信小程序上滑加載下拉刷新(onscrollLower)分批加載數(shù)據(jù)(二)
這篇文章主要介紹了微信小程序上滑加載下拉刷新(onscrollLower)分批加載數(shù)據(jù)的相關(guān)資料,需要的朋友可以參考下2017-05-05
原生js實(shí)現(xiàn)鼠標(biāo)滑過播放音符方法詳解
本文使用原生js的AudioContext接口實(shí)現(xiàn)一個(gè)劃過菜單播放天空之城的鼠標(biāo)特效,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08
利用js實(shí)現(xiàn)簡單開關(guān)燈代碼
這篇文章主要分享的是如何利用js實(shí)現(xiàn)簡單開關(guān)燈代碼,下面文字圍繞js實(shí)現(xiàn)簡單開關(guān)燈的相關(guān)資料展開具體內(nèi)容,需要的朋友可以參考以下,希望對(duì)大家又所幫助2021-11-11
document 和 document.all 分別什么時(shí)候用
document 和 document.all 分別什么時(shí)候用...2006-06-06
JS前端實(shí)現(xiàn)fsm有限狀態(tài)機(jī)實(shí)例詳解
這篇文章主要為大家介紹了JS前端實(shí)現(xiàn)fsm有限狀態(tài)機(jī)實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
gulp-font-spider實(shí)現(xiàn)中文字體包壓縮實(shí)踐
這篇文章主要為大家介紹了gulp-font-spider實(shí)現(xiàn)中文字體包壓縮實(shí)踐詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03
微信小程序滾動(dòng)Tab實(shí)現(xiàn)左右可滑動(dòng)切換
這篇文章主要介紹了微信小程序滾動(dòng)Tab實(shí)現(xiàn)左右可滑動(dòng)切換的相關(guān)資料,這里提供實(shí)現(xiàn)實(shí)例幫助大家實(shí)現(xiàn)這樣的功能,需要的朋友可以參考下2017-08-08

