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

蓝咒 提交于 2019-12-03 14:23:52

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

字段 类型 必需 描述
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://私自获取文件,我们还安全么?

        那么针对于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.

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

我们来看看效果:

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!