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

        我们先看看linkface给开放的接口:

字段类型必需描述
api_idstringAPI 账户
api_secretstringAPI 密钥
selfie_filefile见下方注释需上传的图片文件 1,上传本地图片进行检测时选取此参数
selfie_urlstring见下方注释图片 1 的网络地址,采用抓取网络图片方式时需选取此参数
selfie_image_idfile见下方注释图片 1 的id,在云端上传过图片可采用
historical_selfie_filefile见下方注释需上传的图片文件 2,上传本地图片进行检测时选取此参数
historical_selfie_urlstring见下方注释图片 2 的网络地址,采用抓取网络图片方式时需选取此参数
historical_selfie_image_idstring见下方注释图片 2 的id,在云端上传过图片可采用
selfie_auto_rotateboolean值为 true 时,对图片 1 进行自动旋转。默认值为 false,不旋转
historical_selfie_auto_rotateboolean值为 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://私自获取文件,我们还安全么?

        那么针对于cordova plugin 就相当于我们浏览器的插件了,道理是一定的,通过js的方式调用底层接口。我们首先能够想得到的就是file-transfer这个插件,但是很遗憾的告诉你,这个插件一次只能上传一个文件,  https://github.com/apache/cordova-plugin-file-transfer

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封装,为啥不封装成文件数组接口呢,支持多文件和困难么?那么我们就来看看他的源码:

                    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的大小,作为卡插拔式的插件,大小也是一个硬伤,所以封装插件的同学们学习吧,人家可不是盖的,拼接报文自然使得插件不需要依赖那些包了。

        我们开脑补一下http报文协议:

        一个HTTP请求报文由请求行(request line)、请求头部(header)、空行请求数据4个部分组成,下图给出了请求报文的一般格式。

        所以按照标准拼写报文也是可以的。

        但是我是一个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.

ParamTypeDetails
pathstring

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

filestring

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;
            });

我们来看看效果:

转载于:https://my.oschina.net/u/259422/blog/747399

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值