评论

小程序批量文件上传

小程序批量文件上传

小程序没有提供 FormData 等,uploadFile 一次只能上传一个文件,只能手工撸一个了。先贴代码:

const regeneratorRuntime = require('regenerator-runtime'), _ = require('lodash.min'), {TextEncoder} = require('fast-text-encoding'), Mime = require('mime-lite');
require('wxPromise');
 
class Request {
    _filePromises = [];
    _methods = 'OPTIONS HEAD CONNECT TRACE GET POST PUT DELETE'.split(/\s+/);
 
    constructor({url, header = {}, method = 'GET', dataType = 'json', responseType = 'text', data = {}, files}) {
        Object.assign(this, {url, header, method, dataType, responseType, data, files});
 
        _.each(this._methods, method => {
            const exec = (...args) => {
                this.method = method;
                return this.exec.apply(this, args);
            };
            this[method] = exec.bind(this);
            this[method.toLowerCase()] = exec.bind(this);
        });
 
        let privateProperties = _.filter(Object.keys(this), key => key[0] === '_');
        _.each(privateProperties, key => Object.defineProperty(this, key, {configurable : false, enumerable : false, writable : true}));
    }
 
    _data = {};
 
    get data() {
        return this._data;
    }
 
    set data(data) {
        if (_.isObjectLike(this._data))
            Object.assign(this._data, data);
        else if (_.isString(this._data))
            this._data += data;
        else if (_.isArray(this._data))
            this._data = this._data.concat(data);
        else
            this._data = data;
    }
 
    get files() {
        return Promise.all(this._filePromises);
    }
 
    set files(files) {
        const addFile = file => new Promise((resolve, reject) => {
            file = _.isString(file) ? {name : 'files', path : file} : file;
            let {name = 'files', path} = file, type = 'file';
            wx.Promise.FileSystemManager.readFile({filePath : path}).then(fileData => resolve({name, type, path, value : fileData.data}), reject);
        });
 
        _.each(files, file => this._filePromises.push(addFile(file)));
    }
 
    async exec() {
        let countCRLF = 0;
        const CRLF = () => ++countCRLF && '\r\n';
 
        // fixme: not correct, many different TypedArray
        const toTypedArray = buf => _.isString(buf) ? new TextEncoder().encode(buf) : _.isTypedArray(buf) ? buf : _.isArrayBuffer(buf) ? new Uint8Array(buf) : undefined;
        const joinBuffers = (buffers) => {
            if (_.every(buffers, _.isString)) {
                return buffers.join('');
            }
 
            buffers = _.map(buffers, toTypedArray);
            let len = _.sumBy(buffers, buf => buf.length || buf.byteLength), result = new Uint8Array(len), start = 0;
 
            _.each(buffers, buf => {
                result.set(buf, start);
                start += buf.length;
            });
            return result;
        };
        const toDataBuf = (data, boundary) => {
            const {getType} = require('mime-lite');
            let {name, type, value, path} = data;
            let head = ['Content-Disposition: form-data'];
            name && head.push(`name="${name}"`);
 
            if (type === 'file') {
                let contentType = Mime.getType(path) || 'application/octet-stream';
                head.push(`filename="${path}"`);
                head = head.join('; ') + CRLF() + 'Content-Type: ' + contentType + CRLF() + CRLF();
            }
            else {
                head = head.join('; ') + CRLF() + CRLF();
                value = JSON.stringify(value);
            }
 
            return ['--' + boundary + CRLF(), head, value, CRLF()];
        };
 
        let {url, header, method = 'GET', dataType = 'json', responseType = 'text', data} = this, files = await this.files;
 
        if (_.isEmpty(files)) {
            return wx.Promise.request({url, data, header, method, dataType, responseType});
        }
 
        let boundary = 'WeApp-----------------' + Date.now();
        header['Content-Type'] = 'multipart/form-data; boundary=' + boundary;
 
        let dataPart = _.flatMap(data, (val, key) => toDataBuf({name : key, value : val}, boundary));
        let filePart = _.flatMap(files, data => toDataBuf(data, boundary));
        let length = _.sumBy(dataPart, "length") + _.sumBy(filePart, "length") + boundary.length - countCRLF;
 
        let payload = joinBuffers([
            'Content-Type: multipart/form-data; boundary=' + boundary + CRLF(),
            'Content-Length: ' + length + CRLF() + CRLF(),
            ...dataPart, ...filePart, '--' + boundary + '--'
        ]);
 
        data = payload.buffer || payload;
 
        return wx.Promise.request({url, data, header, method, dataType, responseType});
    }
 
    append(name, value) {
        this._data = _.isObjectLike(this._data) ? this._data : {};
        Object.assign(this._data, {[name] : value});
    }
 
    async appendFile(name, path) {
        this.files = [{name, path}];
    }
}
 
module.exports = Request;


最后一次编辑于  09-01  (未经腾讯允许,不得转载)
点赞 1
收藏
评论

6 个评论

  • Nickmit
    Nickmit
    09-01

    最后 wxPromise.js

    const _ = require('lodash.min');
     
    const wxMethods = "drawCanvas,createContext,createCanvasContext,canvasToTempFilePath,canvasGetImageData,canvasPutImageData,createOffscreenCanvas,getAccountInfoSync,getShareInfo,pageScrollTo,chooseInvoiceTitle,chooseInvoice,arrayBufferToBase64,base64ToArrayBuffer,openSetting,getExtConfig,chooseMedia,chooseMultiMedia,chooseMessageFile,chooseWeChatContact,uploadEncryptedFileToCDN,onUploadEncryptedFileToCDNProgress,getExtConfigSync,showShareMenu,hideShareMenu,updateShareMenu,shareAppMessageForFakeNative,openUrl,setNavigationBarColor,setNavigationBarAlpha,vibrateShort,vibrateLong,getSetting,checkIsSupportFacialRecognition,startFacialRecognitionVerify,startFacialRecognitionVerifyAndUploadVideo,startCustomFacialRecognitionVerify,startCustomFacialRecognitionVerifyAndUploadVideo,sendBizRedPacket,sendGoldenRedPacket,openGoldenRedPacketDetail,addPhoneContact,setScreenBrightness,getScreenBrightness,getWeRunData,uploadWeRunData,addWeRunData,canIUse,setPageStyle,triggerGettingWidgetData,navigateToMiniProgram,navigateToMiniProgramDirectly,navigateToDevMiniProgram,navigateBackMiniProgram,launchMiniProgram,launchApplicationDirectly,launchApplicationForNative,setNavigationBarRightButton,onTapNavigationBarRightButton,setTopBarText,setTabBarBadge,removeTabBarBadge,showTabBarRedDot,hideTabBarRedDot,showTabBar,hideTabBar,setTabBarStyle,setTabBarItem,setBackgroundColor,setBackgroundTextStyle,setEnableDebug,captureScreen,onUserCaptureScreen,setKeepScreenOn,checkIsSupportSoterAuthentication,startSoterAuthentication,checkIsSoterEnrolledInDevice,openDeliveryList,navigateBackH5,openBusinessView,navigateBackApplication,navigateBackNative,reportIDKey,reportKeyValue,setNavigationBarTitle,showNavigationBarLoading,hideNavigationBarLoading,startPullDownRefresh,stopPullDownRefresh,operateWXData,getOpenDeviceId,getMenuButtonBoundingClientRect,getSelectedTextRange,openBluetoothAdapter,closeBluetoothAdapter,getBluetoothAdapterState,onBluetoothAdapterStateChange,startBluetoothDevicesDiscovery,stopBluetoothDevicesDiscovery,getBluetoothDevices,getConnectedBluetoothDevices,createBLEConnection,closeBLEConnection,getBLEDeviceServices,getBLEDeviceCharacteristics,notifyBLECharacteristicValueChanged,notifyBLECharacteristicValueChange,readBLECharacteristicValue,writeBLECharacteristicValue,onBluetoothDeviceFound,onBLEConnectionStateChanged,onBLEConnectionStateChange,onBLECharacteristicValueChange,startBeaconDiscovery,stopBeaconDiscovery,getBeacons,onBeaconUpdate,onBeaconServiceChange,startWifi,stopWifi,getWifiList,getConnectedWifi,connectWifi,presetWifiList,setWifiList,onGetWifiList,onWifiConnected,onEvaluateWifi,getHCEState,startHCE,stopHCE,sendHCEMessage,onHCEMessage,startLocalServiceDiscovery,stopLocalServiceDiscovery,onLocalServiceFound,offLocalServiceFound,onLocalServiceLost,offLocalServiceLost,onLocalServiceDiscoveryStop,offLocalServiceDiscoveryStop,onLocalServiceResolveFail,offLocalServiceResolveFail,redirectTo,reLaunch,navigateTo,switchTab,navigateBack,onAppShow,offAppShow,onAppHide,offAppHide,onError,offError,getLaunchOptionsSync,onWindowResize,offWindowResize,getStorage,getStorageSync,setStorage,setStorageSync,removeStorage,removeStorageSync,clearStorage,clearStorageSync,getStorageInfo,getStorageInfoSync,getBackgroundFetchData,onBackgroundFetchData,setBackgroundFetchToken,getBackgroundFetchToken,request,connectSocket,closeSocket,sendSocketMessage,onSocketOpen,onSocketClose,onSocketMessage,onSocketError,uploadFile,downloadFile,addNativeDownloadTask,downloadApp,installDownloadApp,getAppInstallState,queryDownloadAppTask,cancelDownloadAppTask,resumeDownloadAppTask,pauseDownloadAppTask,onDownloadAppStateChange,downloadAppForIOS,calRqt,secureTunnel,chooseImage,previewImage,getImageInfo,saveImageToPhotosAlbum,compressImage,startRecord,stopRecord,playVoice,pauseVoice,stopVoice,onVoicePlayEnd,chooseVideo,saveVideoToPhotosAlbum,loadFontFace,getLocation,openLocation,chooseLocation,onLocationChange,startLocationUpdateBackground,startLocationUpdate,stopLocationUpdate,getNetworkType,onNetworkStatusChange,getSystemInfo,getSystemInfoSync,getBatteryInfo,getBatteryInfoSync,startAccelerometer,stopAccelerometer,onAccelerometerChange,startCompass,stopCompass,onCompassChange,startDeviceMotionListening,stopDeviceMotionListening,onDeviceMotionChange,startGyroscope,stopGyroscope,onGyroscopeChange,reportAction,getBackgroundAudioManager,getRecorderManager,getBackgroundAudioPlayerState,playBackgroundAudio,pauseBackgroundAudio,seekBackgroundAudio,stopBackgroundAudio,onBackgroundAudioPlay,onBackgroundAudioPause,onBackgroundAudioStop,joinVoIPChat,exitVoIPChat,updateVoIPChatMuteConfig,onVoIPChatMembersChanged,onVoIPChatSpeakersChanged,onVoIPChatInterrupted,login,checkSession,authorize,getUserInfo,requestPayment,verifyPaymentPassword,bindPaymentCard,requestPaymentToBank,requestVirtualPayment,openOfflinePayView,openWCPayCardList,requestMallPayment,setCurrentPaySpeech,loadPaySpeechDialectConfig,faceVerifyForPay,openOfficialAccountProfile,openUserProfile,openMiniProgramProfile,openMiniProgramSearch,openMiniProgramHistoryList,openMiniProgramStarList,batchGetContactDirectly,preventApplePayUI,getWxSecData,addCard,openCard,scanCode,openQRCode,chooseAddress,saveFile,openDocument,getSavedFileList,getSavedFileInfo,getFileInfo,removeSavedFile,getFileSystemManager,getABTestConfig,chooseContact,removeUserCloudStorage,setUserCloudStorage,makePhoneCall,makeVoIPCall,onAppRoute,onAppRouteDone,onAppEnterBackground,onAppEnterForeground,onAppUnhang,onPageReload,onPageNotFound,offPageNotFound,createAnimation,createInnerAudioContext,getAvailableAudioSources,onAudioInterruptionBegin,offAudioInterruptionBegin,onAudioInterruptionEnd,offAudioInterruptionEnd,setInnerAudioOption,createAudioContext,createVideoContext,createMapContext,createCameraContext,createLivePlayerContext,createLivePusherContext,onWebviewEvent,onNativeEvent,hideKeyboard,onKeyboardHeightChange,getPublicLibVersion,showModal,showToast,hideToast,showLoading,hideLoading,showActionSheet,showShareActionSheet,reportAnalytics,reportMonitor,getClipboardData,setClipboardData,createSelectorQuery,createIntersectionObserver,nextTick,updatePerfData,traceEvent,onMemoryWarning,getUpdateManager,createWorker,voiceSplitJoint,uploadSilkVoice,downloadSilkVoice,getResPath,setResPath,setCookies,getCookies,getLabInfo,setLabInfo,createUDPSocket,isSystemError,isSDKError,isThirdError,createRewardedVideoAd,createInterstitialAd,getLogManager,getRealtimeLogManager,chooseShareGroup,enterContact".split(',');
     
    const wxPromiseTimeout = 2000, notWatching = ['chooseImage'];
     
    wx.Promise = {FileSystemManager : {}};
     
    wxMethods.forEach(name => {
        wx.Promise[name] = (...args) => new Promise((resolve, reject) => {
            let object = args[0], watching = notWatching.indexOf(name) === -1;
            const __resolve = watching ? (...args) => {
                watching = false;
                resolve.apply(this, args);
            } : resolve;
            const __reject = watching ? (...args) => {
                watching = false;
                reject.apply(this, args);
            } : resolve;
     
            if (typeof object === 'object') {
                let {success, fail} = object;
                object.success = res => {
                    if (typeof success === 'function')
                        res = success(res);
                    __resolve(res);
                };
     
                object.fail = err => {
                    if (typeof fail === 'function')
                        err = fail(err);
                    __reject(err);
                };
            }
            else {
                args = [{success : __resolve, fail : __reject}];
            }
     
            wx[name].apply(wx, args);
            watching && setTimeout(() => {
                if (watching)
                    console.log(`%c wx.${name} did not complete after ${wxPromiseTimeout}ms`, 'color:red;font-size:16px;');
            }, wxPromiseTimeout);
        });
    });
     
    const fsMethods = "access,appendFile,copyFile,getFileInfo,getSavedFileInfo,getSavedFileList,mkdir,readFile,readdir,removeSavedFile,rename,rmdir,saveFile,stat,unlink,unzip,writeFile".split(',');
     
    let FileSystemManager = wx.getFileSystemManager();
    fsMethods.forEach(name => {
        wx.Promise.FileSystemManager[name] = (...args) => new Promise((resolve, reject) => {
            let object = args[0];
            if (typeof object === 'object') {
                object.success = resolve;
                object.fail = reject;
            }
            FileSystemManager[name].apply(FileSystemManager, args);
        });
    });


    09-01
    赞同 1
    回复
  • Nickmit
    Nickmit
    09-01

    $ npm i -s mime

    $ browserify -s Mime -r mime/lite -o miniprogram_npm/mime-lite.js

    09-01
    赞同
    回复
  • Nickmit
    Nickmit
    09-01

    $npm i -s fast-text-encoder

    $ cp node_modules/fast-text-encoder/text.min.js miniprogram_npm/fast-text-encoder.js

    把尾部这部分

    ("undefined"!==typeof window?window:"undefined"!==typeof global?global:this)

    替换成

    (module.exports);
    09-01
    赞同
    回复
  • Nickmit
    Nickmit
    09-01

    $ npm i -s lodash

    $ cp node_modules/lodash/lodash.min.js miniprogram_npm


    在文件首部增加一行:

    Object.assign(global, {Array, Date, Error, Function, Math, Object, RegExp, String, TypeError, setTimeout, clearTimeout, setInterval, clearInterval});


    09-01
    赞同
    回复
  • Nickmit
    Nickmit
    09-01

    用到几个第三方库,分别说明一下。

    $ npm i -s regenerator-runtime

    $ cp node_modules/regenerator-runtime/runtime.js miniprogram_npm/regenerator-runtime.js

    注释掉最后一段 try ... catch

    09-01
    赞同
    回复
  • Nickmit
    Nickmit
    09-01

    使用方法,参数与 wx.request 一致,增加 files:

    const Request = require('request');

    let request = new Request({
    url : 'https://myserver.com/api/uploadPhotos',
    files : _.map(photos, photo => photo.path),
    data : {photos}
    });

    let result = await request.post();

    也可以这么写:

    request.files = _.map(photos, photo => photo.path);
    request.data = {photos};
    request.post(console.log)




    09-01
    赞同
    回复