在cordova中使用HTML5的多文件上傳

        咱們先看看linkface給開放的接口:javascript

字段 類型 必需 描述
api_id string API 帳戶
api_secret string API 密鑰
selfie_file file 見下方註釋 需上傳的圖片文件 1,上傳本地圖片進行檢測時選取此參數
selfie_url string 見下方註釋 圖片 1 的網絡地址,採用抓取網絡圖片方式時需選取此參數
selfie_image_id file 見下方註釋 圖片 1 的id,在雲端上傳過圖片可採用
historical_selfie_file file 見下方註釋 需上傳的圖片文件 2,上傳本地圖片進行檢測時選取此參數
historical_selfie_url string 見下方註釋 圖片 2 的網絡地址,採用抓取網絡圖片方式時需選取此參數
historical_selfie_image_id string 見下方註釋 圖片 2 的id,在雲端上傳過圖片可採用
selfie_auto_rotate boolean 值爲 true 時,對圖片 1 進行自動旋轉。默認值爲 false,不旋轉
historical_selfie_auto_rotate boolean 值爲 true 時,對圖片 2 進行自動旋轉。默認值爲 false,不旋轉

        如文件所示,接口須要同時上傳兩個文件和兩個字段,通常咱們的web前端就很簡單了,兩個file類型的input組成的form提交就能夠,若想實現文件的異步上傳通俗的方式就是安裝瀏覽器安全插件,或者就是使用form表單的提交target指向爲iframe,而後將iframe隱藏,使用視窗的父子級調用完成,可是這仍須要咱們使用form組件選擇文件,很顯然這樣會使得咱們的移動APP體驗極差,咱們指望的就是使用相機拍完照而後直接異步上傳執行檢測,固然咱們可使用XMLHTTPReauest2拼接一個formatdata上傳前端

//不徹底代碼
let formData = new FormData();
formData.append('fileName',input.files[0]);
xhr.open("post", encodeURI(url));
xhr.send(formData);

可是,在web端,若是用戶不使用input選擇文件,咱們是沒法私自獲取並上傳文件的,這個瀏覽器的安全機制,想一想若是能夠拼接file://私自獲取文件,咱們還安全麼?java

        那麼針對於cordova plugin 就至關於咱們瀏覽器的插件了,道理是必定的,經過js的方式調用底層接口。咱們首先可以想獲得的就是file-transfer這個插件,可是很遺憾的告訴你,這個插件一次只能上傳一個文件,  https://github.com/apache/cordova-plugin-file-transfergit

Parameters:

fileURL: Filesystem URL representing the file on the device or a data URI. For backwards compatibility, this can also be the full path of the file on the device. (See Backwards Compatibility Notes below)

server: URL of the server to receive the file, as encoded by encodeURI().

successCallback: A callback that is passed a FileUploadResult object. (Function)

errorCallback: A callback that executes if an error occurs retrieving the FileUploadResult. Invoked with a FileTransferError object. (Function)

options: Optional parameters (Object). Valid keys:

fileKey: The name of the form element. Defaults to file. (DOMString)
fileName: The file name to use when saving the file on the server. Defaults to image.jpg. (DOMString)
httpMethod: The HTTP method to use - either PUT or POST. Defaults to POST. (DOMString)
mimeType: The mime type of the data to upload. Defaults to image/jpeg. (DOMString)
params: A set of optional key/value pairs to pass in the HTTP request. (Object, key/value - DOMString)
chunkedMode: Whether to upload the data in chunked streaming mode. Defaults to true. (Boolean)
headers: A map of header name/header values. Use an array to specify more than one value. On iOS, FireOS, and Android, if a header named Content-Type is present, multipart form data will NOT be used. (Object)
trustAllHosts: Optional parameter, defaults to false. If set to true, it accepts all security certificates. This is useful since Android rejects self-signed security certificates. Not recommended for production use. Supported on Android and iOS. (boolean)

        我真搞不懂既然cordova plugin封裝,爲啥不封裝成文件數組接口呢,支持多文件和困難麼?那麼咱們就來看看他的源碼:github

boolean multipartFormUpload = (headers == null) || !headers.has("Content-Type");
                    if (multipartFormUpload) {
                        conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
                    }

                    // Set the cookies on the response
                    String cookie = getCookies(target);

                    if (cookie != null) {
                        conn.setRequestProperty("Cookie", cookie);
                    }

                    // Handle the other headers
                    if (headers != null) {
                        addHeadersToRequest(conn, headers);
                    }

                    /*
                        * Store the non-file portions of the multipart data as a string, so that we can add it
                        * to the contentSize, since it is part of the body of the HTTP request.
                        */
                    StringBuilder beforeData = new StringBuilder();
                    try {
                        for (Iterator<?> iter = params.keys(); iter.hasNext();) {
                            Object key = iter.next();
                            if(!String.valueOf(key).equals("headers"))
                            {
                              beforeData.append(LINE_START).append(BOUNDARY).append(LINE_END);
                              beforeData.append("Content-Disposition: form-data; name=\"").append(key.toString()).append('"');
                              beforeData.append(LINE_END).append(LINE_END);
                              beforeData.append(params.getString(key.toString()));
                              beforeData.append(LINE_END);
                            }
                        }
                    } catch (JSONException e) {
                        Log.e(LOG_TAG, e.getMessage(), e);
                    }

                    beforeData.append(LINE_START).append(BOUNDARY).append(LINE_END);
                    beforeData.append("Content-Disposition: form-data; name=\"").append(fileKey).append("\";");
                    beforeData.append(" filename=\"").append(fileName).append('"').append(LINE_END);
                    beforeData.append("Content-Type: ").append(mimeType).append(LINE_END).append(LINE_END);
                    byte[] beforeDataBytes = beforeData.toString().getBytes("UTF-8");
                    byte[] tailParamsBytes = (LINE_END + LINE_START + BOUNDARY + LINE_START + LINE_END).getBytes("UTF-8");

        看到了嗎,它是拼接了報文,這就是可以解釋它爲啥還須要依賴 cordova-plugin-file這個插件了,它能夠直接獲取文件ArrayBuffer,很聰明啊,真的很聰明,爲何拼報文?豈不是很麻煩,正常我麼使用java的http client是須要依賴 httpclient-4.0.1.jar commons-codec-1.3.jar  apache-mime4j-0.6.jar httpcore-4.0.1.jar httpmime-4.0.1.jar ,這無形之中就增大了app的大小,做爲卡插拔式的插件,大小也是一個硬傷,因此封裝插件的同窗們學習吧,人家可不是蓋的,拼接報文天然使得插件不須要依賴那些包了。web

        咱們開腦補一下http報文協議:apache

        一個HTTP請求報文由請求行(request line)、請求頭部(header)、空行請求數據4個部分組成,下圖給出了請求報文的通常格式。api

        因此按照標準拼寫報文也是能夠的。數組

        可是我是一個H5工程師,我首先會使用H5技術去解決這件事,否則我就只能發揮java技能更改file-transfer這個插件了。XHR拼接formdata,能夠是file也能夠是一個blob,我曾將想過是否是有接口可以模擬封裝input的file或者使用FileReader,然而仍是那句話,瀏覽器爲了安全不會讓咱們本身拼接file:// 的,可是cordova跨平臺能夠訪問文件系統(你能夠看一下 https://github.com/apache/cordova-plugin-file裏http-equiv="Content-Security-Policy"相關的描述),畢竟咱們開發的是移動app,這個功能是不可缺乏的,咱們使用cordova的file plugin仍是能夠獲取文件的咱們來看看ionic2提供的接口(http://ionicframework.com/docs/v2/native/file/  ):瀏覽器

readAsArrayBuffer(path, file)

Read file and return data as an ArrayBuffer.

Param Type Details
path string

Base FileSystem. Please refer to the iOS and Android filesystems above

file string

Name of file, relative to path.

Returns: Promise<ArrayBuffer|FileError> Returns a Promise that resolves with the contents of the file as ArrayBuffer or rejects with an error.

驚喜吧!有個這個咱們就可以本身拼寫blob類型的formdata了,話很少說咱們直接上代碼:

先寫封裝一個文件轉換類,file-convert-util.ts:

import {File, FileError} from "ionic-native";
/***
 * @author 趙俊明
 */

export class FileConvertUtil {

    constructor() {

    }

    //講文件轉換爲Blob
    public static convertFileToBlob(fullFilePath: string): Promise<Blob|FileError> {

        return new Promise((resolve, reject)=> {
            FileConvertUtil.convertFileToArrayBuffer(fullFilePath).then((arrayBuffer)=> {
                resolve(new Blob([arrayBuffer], {type: "image/" + FileConvertUtil.extractFileType(fullFilePath)}));
            }).catch((reason)=> {
                reject(reason);
            });
        });

    }

    //將文件裝換爲ArrayBuffer
    public static convertFileToArrayBuffer(fullFilePath: string): Promise<ArrayBuffer | FileError> {

        return File.readAsArrayBuffer(FileConvertUtil.extractFilePath(fullFilePath), FileConvertUtil.extractFileName(fullFilePath));

    }

    //截取文件路徑
    public static extractFilePath(fullFilePath: string): string {
        return fullFilePath.substr(0, fullFilePath.lastIndexOf('/'));
    }

    //截取文件名稱
    public static extractFileName(fullFilePath: string): string {
        return fullFilePath.substr(fullFilePath.lastIndexOf('/') + 1);
    }

    //截取文件類型
    public static extractFileType(fullFilePath: string): string {
        return fullFilePath.split(".")[1];
    }

}

        基於XHR2的upload,xhr-multipart-upload.ts:

import {BrowserXhr} from "@angular/http";
import {FileConvertUtil} from "./file-convert-util";
import {FileError} from "ionic-native";
import {Injectable, Component} from "@angular/core";
/**
 * @author zhaojunming
 */

export class XHRMultipartFileUpload {

    private static browserXhr = new BrowserXhr();

    constructor() {

    }

    public static upload(url: string, files: {name: string,path: string}[], params: any): Promise<any> {

        const xhr = XHRMultipartFileUpload.browserXhr.build();

        xhr.open("post", encodeURI(url));

        let formData = new FormData();

        return new Promise((resolve, reject)=> {

            if (params) {
                for (let _v in params) {
                    if (params.hasOwnProperty(_v)) {
                        formData.append(_v, params[_v]);
                    }
                }
            }

            let blobPromiseList: Array<Promise<Blob|FileError>> = [];

            files.forEach((file)=> {
                blobPromiseList.push(FileConvertUtil.convertFileToBlob(file.path));
            });

            Promise.all(blobPromiseList).then((result)=> {

                result.forEach((blob, index)=> {
                    formData.append(files[index].name, blob, FileConvertUtil.extractFileName(files[index].path));
                });

                xhr.onreadystatechange = ()=> {
                    if (xhr.readyState == 4) {
                        if (xhr.status == 200) {
                            resolve(JSON.parse(xhr.responseText));
                        } else {
                            reject({code: xhr.status, message: JSON.parse(xhr.responseText)});
                        }
                    }

                }

                xhr.send(formData);


            }).catch((reason)=> {
                reject(reason);
            });

        });

    }


}

調用linkface的provider,linkface-verfication.ts:

/***
 * @author 趙俊明
 */

import {Injectable, Component} from "@angular/core";
import {XHRMultipartFileUpload} from "./xhr-multipart-upload";
import {Storage, LocalStorage} from "ionic-angular";

//400 錯誤碼對應信息
const ERROR_MAPPING = {
    "ENCODING_ERROR": "參數非UTF-8編碼",
    "DOWNLOAD_TIMEOUT": "網絡地址圖片獲取超時",
    "DOWNLOAD_ERROR": "網絡地址圖片獲取失敗",
    "IMAGE_FILE_SIZE_TOO_BIG": "圖片體積過大 ",
    "IMAGE_ID_NOT_EXIST": "圖片不存在",
    "NO_FACE_DETECTED": "圖片未檢測出人臉 ",
    "CORRUPT_IMAGE": "文件不是圖片文件或已經損壞",
    "INVALID_IMAGE_FORMAT_OR_SIZE": "圖片大小或格式不符合要求",
    "INVALID_ARGUMENT": "請求參數錯誤",
    "UNAUTHORIZED": "帳號或密鑰錯誤",
    "KEY_EXPIRED": "帳號過時",
    "RATE_LIMIT_EXCEEDED": "調用頻率超出限額",
    "NO_PERMISSION": "無調用權限",
    "NOT_FOUND": "請求路徑錯誤",
    "INTERNAL_ERROR": "服務器內部錯誤"
};

@Injectable()
export class LinkFaceVerfication {

    //普通照片比對URL
    private historicalSelfieVerificationURL = "https://v1-auth-api.visioncloudapi.com/identity/historical_selfie_verification";

    //公安水印照片與普通照片比對URL
    private selfieWatermarkVerificationURL = "https://v1-auth-api.visioncloudapi.com/identity/selfie_watermark_verification";

    private apiId: string;

    private apiSecret: string;

    //LocalStorage
    private storage = new Storage(LocalStorage);

    constructor() {

        this.getApiId()
            .then(apiId=> {
                this.apiId = apiId || "6b666502c4324026b8604c8001a2cd14";
            })
            .catch(()=> {
                this.apiId = "6b666502c4324026b8604c8001a2cd14";
            });

        this.getApiSecret()
            .then(apiSecret=> {
                this.apiSecret = apiSecret || "28cf8b8693e54d0b930d0a5089831841";
            })
            .catch(()=> {
                this.apiSecret = "28cf8b8693e54d0b930d0a5089831841";
            });

    }

    //普通照片比對
    public historicalSelfieVerification(selfie_file: string, historical_selfie_file: string, selfie_auto_rotate: boolean = true, historical_selfie_auto_rotate: boolean = true): Promise<any> {

        let params = {
            api_id: this.apiId,
            api_secret: this.apiSecret,
            selfie_auto_rotate: selfie_auto_rotate,
            historical_selfie_auto_rotate: historical_selfie_auto_rotate
        };

        let files = []
        files.push({name: "selfie_file", path: selfie_file});
        files.push({name: "historical_selfie_file", path: historical_selfie_file});

        return new Promise((resolve, reject)=> {

            XHRMultipartFileUpload.upload(this.historicalSelfieVerificationURL, files, params)
                .then(result=> {
                    resolve(result);
                })
                .catch(error=> {
                    if (error && error.code == 400) {
                        reject(ERROR_MAPPING[error.message.status]);
                    } else {
                        reject(JSON.stringify(error));
                    }
                });

        });

    }

    //公安水印照片與普通照片比對
    public selfieWatermarkVerification(selfie_file: string, watermark_picture_file: string): Promise<any> {
        let params = {api_id: this.apiId, api_secret: this.apiSecret};
        let files = []
        files.push({name: "selfie_file", path: selfie_file});
        files.push({name: "watermark_picture_file", path: watermark_picture_file});

        return new Promise((resolve, reject)=> {

            XHRMultipartFileUpload.upload(this.selfieWatermarkVerificationURL, files, params)
                .then(result=> {
                    resolve(result);
                })
                .catch(error=> {
                    if (error && error.code == 400) {
                        reject(ERROR_MAPPING[error.message.status]);
                    } else {
                        reject(JSON.stringify(error));
                    }
                });
        });
    }


    setApiId(apiId): boolean {
        if (apiId) {
            this.apiId = apiId;
            this.storage.set("apiId", apiId);
            return true;
        }
        return false;
    }

    setApiSecret(apiSecret): boolean {
        if (apiSecret) {
            this.apiSecret = apiSecret;
            this.storage.set("apiSecret", apiSecret);
            return true;
        }
        return false;
    }


    getApiId(): Promise<string> {
        return this.storage.get("apiId");
    }

    getApiSecret(): Promise<string> {
        return this.storage.get("apiSecret");
    }

}

看看咱們怎麼調用:

this.linkFaceVerfication.historicalSelfieVerification(this.selfie_file, this.historical_selfie_file, true, true)
            .then(result=> {
                this.confidence = (result.confidence * 100).toFixed(2);
                this.uploading = false;
            })
            .catch(reason=> {
                this.toastMessage(reason);
                this.uploading = false;
            });

咱們來看看效果:

相關文章
相關標籤/搜索