微信小程序API的Promise化(ts版本)

469 阅读6分钟

微信小程序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'

image.png image.png image.png image.png

  • 方式二:通过在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可能更适合你的需求。