微信小程序API的Promise化(ts版本)
在微信小程序中很多API的调用不支持以Promise风格调用,都需要在success中触发回调,这对于多个API链式调用不是很方便,所以提供一个将微信小程序的异步API转换为返回Promise对象的函数,以支持async/await语法,同时保持接口参数及返回结果的语法智能提示
方法介绍
wxPromise
-
作用:针对单个API进行Promise化。
-
优点:
-
灵活性:允许开发者针对单个API进行转换,根据需要选择性地将某些API转换为Promise形式。
-
明确性:需要明确指定要转换的API,有助于代码的可读性和维护性。
-
缺点:
-
重复性:如果多个API需要转换,可能导致代码中出现大量重复模式。
-
管理难度:随着项目规模扩大,管理大量转换的API可能变得复杂。
wxPromiseAll
-
作用:自动将所有(或大部分)微信小程序API转换为返回Promise的形式。
-
优点:
-
自动化:减少手动转换的需要,简化代码。
-
一致性:确保项目中所有API的调用方式都是统一的Promise形式。
-
缺点:
-
灵活性降低:可能包括一些实际上不需要转换的API,导致不必要的性能开销。
-
黑名单管理:需要维护一个不应该被转换的API名称列表,可能增加维护成本。
类型定义
为了保持接口参数及返回结果的语法智能提示,我们定义了一系列辅助类型:
// interfasce.d.ts
// 用于完整展开类型,浮动的时候显示所有类型细节,方便查看
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// 定义一个辅助类型,用于提取函数的第一个参数类型
type FirstParam<T> = T extends (arg1: infer P, ...args: any[]) => any ? P : never;
// 定义一个辅助类型,用于提取函数第一个参数的success回调的参数类型
type SuccessParam<T> = T extends { success?: (result: infer R) => void } ? R : never;
// 用于检测API参数类型中是否只包含 success, fail, complete
type IsEmptyParam<T> = keyof Omit<T, 'success' | 'fail' | 'complete'> extends never ? true : false;
// 提取函数的参数类型,如果函数不接受参数则返回never
type ExtractWxApiParams<T> = T extends (args: infer P) => any ? P : never;
// 提取函数参数对象中的success回调的参数类型
type ExtractSuccessCallbackArg<T> = T extends { success?: (res: infer S) => any } ? S : never;
// 判断函数是否包含success回调参数
type HasSuccessCallback<T> = undefined extends ExtractSuccessCallbackArg<T> ? false : true;
// 定义Promise化的API函数类型
type PromisifiedWxApi<T> = T extends (...args: any[]) => any ? (
args: Expand<Omit<ExtractWxApiParams<T>, 'success' | 'fail' | 'complete'>>
) => Promise<ExtractSuccessCallbackArg<ExtractWxApiParams<T>>> : never;
// 映射所有微信API,只对包含success回调的API进行Promise化
type WxPromisifiedApis = {
[K in keyof WechatMiniprogram.Wx]: HasSuccessCallback<ExtractWxApiParams<WechatMiniprogram.Wx[K]>> extends true ? PromisifiedWxApi<WechatMiniprogram.Wx[K]> : WechatMiniprogram.Wx[K];
};
不需要转换的API白名单
某些API已经支持Promise或不适合转换,我们将它们列入白名单:
// api-white-list.ts
export const asyncMethods = [
'canvasGetImageData',
'canvasPutImageData',
'canvasToTempFilePath',
'setEnableDebug',
'startAccelerometer',
'stopAccelerometer',
'getBatteryInfo',
'getClipboardData',
'setClipboardData',
'startCompass',
'stopCompass',
'addPhoneContact',
'startGyroscope',
'stopGyroscope',
'startBeaconDiscovery',
'stopBeaconDiscovery',
'getBeacons',
'startLocalServiceDiscovery',
'stopLocalServiceDiscovery',
'startDeviceMotionListening',
'stopDeviceMotionListening',
'getNetworkType',
'makePhoneCall',
'scanCode',
'getSystemInfo',
'vibrateShort',
'vibrateLong',
'getExtConfig',
'chooseLocation',
'getLocation',
'openLocation',
'chooseMessageFile',
'loadFontFace',
'chooseImage',
'previewImage',
'getImageInfo',
'saveImageToPhotosAlbum',
'compressImage',
'chooseVideo',
'saveVideoToPhotosAlbum',
'downloadFile',
'request',
'connectSocket',
'closeSocket',
'sendSocketMessage',
'uploadFile',
'login',
'checkSession',
'chooseAddress',
'authorize',
'addCard',
'openCard',
'chooseInvoice',
'chooseInvoiceTitle',
'getUserInfo',
'requestPayment',
'getWeRunData',
'showModal',
'showToast',
'hideToast',
'showLoading',
'hideLoading',
'showActionSheet',
'pageScrollTo',
'startPullDownRefresh',
'stopPullDownRefresh',
'setBackgroundColor',
'setBackgroundTextStyle',
'setTabBarBadge',
'removeTabBarBadge',
'showTabBarRedDot',
'hideTabBarRedDot',
'showTabBar',
'hideTabBar',
'setTabBarStyle',
'setTabBarItem',
'setTopBarText',
'saveFile',
'openDocument',
'getSavedFileList',
'getSavedFileInfo',
'removeSavedFile',
'getFileInfo',
'getStorage',
'setStorage',
'removeStorage',
'clearStorage',
'getStorageInfo',
'closeBLEConnection',
'closeBluetoothAdapter',
'createBLEConnection',
'getBLEDeviceCharacteristics',
'getBLEDeviceServices',
'getBluetoothAdapterState',
'getBluetoothDevices',
'getConnectedBluetoothDevices',
'notifyBLECharacteristicValueChange',
'openBluetoothAdapter',
'readBLECharacteristicValue',
'startBluetoothDevicesDiscovery',
'stopBluetoothDevicesDiscovery',
'writeBLECharacteristicValue',
'getHCEState',
'sendHCEMessage',
'startHCE',
'stopHCE',
'getScreenBrightness',
'setKeepScreenOn',
'setScreenBrightness',
'connectWifi',
'getConnectedWifi',
'getWifiList',
'setWifiList',
'startWifi',
'stopWifi',
'getBackgroundAudioPlayerState',
'playBackgroundAudio',
'pauseBackgroundAudio',
'seekBackgroundAudio',
'stopBackgroundAudio',
'getAvailableAudioSources',
'startRecord',
'stopRecord',
'setInnerAudioOption',
'playVoice',
'pauseVoice',
'stopVoice',
'getSetting',
'openSetting',
'getShareInfo',
'hideShareMenu',
'showShareMenu',
'updateShareMenu',
'checkIsSoterEnrolledInDevice',
'checkIsSupportSoterAuthentication',
'startSoterAuthentication',
'navigateBackMiniProgram',
'navigateToMiniProgram',
'setNavigationBarTitle',
'showNavigationBarLoading',
'hideNavigationBarLoading',
'setNavigationBarColor',
'redirectTo',
'reLaunch',
'navigateTo',
'switchTab',
'navigateBack'
]
方法定义
// api-promise.ts
import { asyncMethods } from './api-white-list'
/**
* 将微信小程序的异步API转换为返回Promise对象的函数,以支持async/await语法。
*
* @template T - 目标函数的类型,该函数接受一个参数对象,并在对象中提供success, fail, complete回调。
* @param {T} apiFunction - 微信小程序的异步API函数,如wx.login, wx.request等。
* @returns {FunctionType} - 返回一个新的函数。这个函数返回一个Promise对象,该Promise在原API的success回调中被resolve,在fail回调中被reject。
* 如果原API不需要参数,则返回的函数不接受参数;如果原API需要参数,则返回的函数接受一个参数对象,该对象应包含原API所需的所有参数,除了success, fail, complete。
* @description
* 此函数利用TypeScript的高级类型推断,根据传入的apiFunction参数自动推断出返回函数的参数类型和返回的Promise对象的类型。
* 使用示例:
* ```typescript
* const login = wxPromise(wx.login)();
* const downloadFile = wxPromise(wx.downloadFile)({ url: 'https://example.com/image.png' });
* ```
*/
export function wxPromise<T extends (...args: any) => any>(apiFunction: T) {
type ParamsType = FirstParam<T>;
type SuccessType = SuccessParam<ParamsType>;
type FunctionType = IsEmptyParam<ParamsType> extends true ?
() => Promise<Expand<SuccessType>> :
(params: Expand<Omit<ParamsType, 'success' | 'fail' | 'complete'>>) => Promise<Expand<SuccessType>>;
return ((params?: Omit<ParamsType, 'success' | 'fail' | 'complete'>) => {
return new Promise<SuccessType>((resolve, reject) => {
const options: any = { ...params, success: resolve, fail: reject };
apiFunction(options);
});
}) as unknown as FunctionType;
}
/**
* 检查给定对象是否包含特定的回调键。
* @param {Record<string, any>} obj - 要检查回调键的对象
* @return {boolean} 如果对象包含回调键则返回true,否则返回false
*/
const hasCallback = (obj: Record<string, any>) => {
let cbs = ['success', 'fail', 'complete']
return Object.keys(obj).some(k => cbs.includes(k))
}
/**
* 使用Proxy对象封装微信小程序所有API,将回调风格的API转换为返回Promise的API。
* 这样可以让我们在使用微信小程序API时,能够使用async/await语法糖,使异步代码更加简洁易读。
*
* @export
* @const wxPromiseAll
* @type {WxPromisifiedApis} - 返回一个包含所有微信小程序API的对象,这些API都被转换为返回Promise的形式。
*
* @example
* 使用前需要确保`wx`对象已经存在,且`asyncMethods`数组中包含了所有不需要转换的API名称。
* wxPromiseAll.login().then(res => {
* console.log(res);
* });
*
* @remarks
* - `wx`是微信小程序的全局API对象,包含了所有微信小程序提供的API。
* - `asyncMethods`是一个数组,包含了所有不需要转换为Promise风格的API名称,例如已经返回Promise的API。
* - 使用Proxy的`get`陷阱函数拦截对wx对象属性的访问。
* - 如果访问的属性是函数,并且该函数名称不在`asyncMethods`数组中,则将该函数转换为返回Promise的函数。
* - 转换后的函数内部会调用原始的微信小程序API,并将成功和失败的回调转换为Promise的resolve和reject。
* - 如果转换的函数被调用时传入的参数对象中包含`success`、`fail`或`complete`回调,则直接返回原始函数,不进行转换。
* - 这样做是为了兼容那些即使在转换为Promise风格后,仍然需要使用回调的特殊情况。
*/
export const wxPromiseAll: WxPromisifiedApis = new Proxy(wx, {
get(target, apiName, receiver) {
const origMethod = Reflect.get(target, apiName, receiver);
if (typeof origMethod === 'function' && !asyncMethods.includes(apiName as string)) {
return function (params: any = {}) {
// 虽然类型定义已经限制了不能传入['success', 'fail', 'complete'],但还是给返回原方法的调用
if (hasCallback(params)) {
return origMethod;
}
return new Promise((resolve, reject) => {
origMethod(Object.assign({}, params, {
success: (res: any) => resolve(res),
fail: (err: any) => reject(err),
}));
});
};
}
return origMethod;
},
}) as unknown as WxPromisifiedApis;
使用示例
●方式一:通过import导入,
import { wxPromise, wxPromiseAll } from '../../utils/api-promise'
- 方式二:通过在app.ts注册挂载在app上的全局属性
// app.ts
import { wxPromise, wxPromiseAll } from './utils/api-promise'
App<IAppOption>({
onLaunch(option) {},
wxPromise,
wxPromiseAll
})
// 子页面中使用
const APP: WxGetApp = getApp()
Page({
async handlerSave() {
const { filePath, tempFilePath } = await APP.wxPromise(wx.downloadFile)({url: ''})
const { filePath, tempFilePath } = await APP.wxPromiseAll.downloadFile({url: ''})
}
})
总结:
- wxPromise和wxPromiseAll两个函数都旨在将微信小程序的异步API转换为返回Promise对象的函数,以便支持async/await语法,使异步代码更加简洁易读。尽管它们的目标相同,但它们在实现方式和使用场景上有所不同。
- 如果你的项目中只需要对少数几个微信小程序API进行Promise转换,或者你需要对转换过程有更细粒度的控制,wxPromise可能是更好的选择。
- 如果你希望简化代码,统一项目中所有微信小程序API的调用方式,并且不介意额外维护一个不需要转换的API列表,wxPromiseAll可能更适合你的需求。