β

图片上传进阶 VS 京东户簿实战

JDC | 京东设计中心 10 阅读

由于现在身份比较敏感,所以很多情况下你在网购时都会涉及到需要实名认证,此时就需要上传身份证照片。为了让大家能够在任何情况下都可以进行实名认证,前端的上传工作必不可少,如何进行图片上传的功能呢?下面会一步一步的深入到上传的海洋中,让你畅游其中。

京东推出了整站京东户簿组件,来统一管理用户的实名认证信息。此组件支持不同业务线跨域接入并兼容到 IE7+ 的不同场景。(黑科技)上传的图片支持 OCR 图像识别,可以与输入的证件号码做匹配,防止随便录入信息。

户簿系统组件的界面如下图:

TimLine图片20170814105758

接下来我们来逐步深入的剖析一下此组件的是如何来实现的。上传主要使用了 Ajax 与 XMLHttpRequest(文章中简称为 XHR ) 技术来实现。Ajax 介绍以及背景请参考 Ajax 背景

(如不了解  XHR ,可参考 w3.org 的一些文章基础教程文章即可快速了解,可参考如下连接地址: https://www.w3.org/TR/XMLHttpRequest/ .)

上传图片功能实现

代码实现:

var param = new FormData();
param.append('imgData', file);
xhr = new XMLHttpRequest();
xhr.addEventListener("load", _self.uploadComplete, false);
xhr.addEventListener("error", _self.uploadFailed, false);
xhr.addEventListener("abort", _self.uploadCanceled, false);
xhr.open("POST", url);
xhr.send(param);

以上代码中,文件需要通过 FormData 来承载并通过 xhr 发送给服务端,FormData 会有哪些隐患呢?来看一下 XHR 的发展历程。

XHR 一开始只是微软浏览器提供的一个接口,后来各大浏览器纷纷效仿也提供了这个接口,再后来W3C对它进行了标准化,提出了 XHR 标准 。XHR 标准又分为Level 1 和 Level 2。

XHR Level 1 中,XHR 有以下三个缺点:

XHR Level 2 新版本针对老版本做出了大幅改进:

FormData 参数为 XHR Level 2  中新增接口,所以会存在或多或少的兼容性隐患问题,兼容下如下图:

11

(数据来源: caniuse.com)

根据以上的兼容性,基本上 IE10 以下的浏览器就不用考虑了。

进度条实现

在 XHR 对象中有一个存在这么一个对象:upload。(来个传送门: https://xhr.spec.whatwg.org/#the-upload-attribute

upload是一个XMLHttpRequestUpload,可以将代码优化成以下方法:

var param = new FormData();
param.append('imgData', file);
xhr = new XMLHttpRequest();
/*新增进度条监听函数*/
xhr.upload.addEventListener("progress", _self.uploadProgress, false);
xhr.addEventListener("load", _self.uploadComplete, false);
xhr.addEventListener("error", _self.uploadFailed, false);
xhr.addEventListener("abort", _self.uploadCanceled, false);
xhr.open("POST", url);
xhr.send(param);

跨域功能实现

针对以上需求,我们可以提供2种跨域解决方案: JSONP 与 CORS。

JSONP:实现原理参考: https://en.wikipedia.org/wiki/JSONP 。由于 JSONP 无法使用 post 请求,故 PASS。

CORS :全程“跨域资源共享”(Cross-origin Resource Sharing),也是一个 Level2 中新增加的功能。兼容性可参考 caniuse 中 Level2兼容性 . 以下提供一张截图

22

所以只能采用此种方式来支持跨域 post 请求的图片上传功能。

CORS 的配置需要前端与后端共同来完成,服务器端需要配置可以访问接入的域。发送请求的时候也会在 http 的响应头中添加  Access-Control-Allow-Origin:*。

* 表明,该资源可以被任意外域访问。如果服务端仅允许来自 http://foo.example 的访问,该首部字段的内容如下:Access-Control-Allow-Origin: http://foo.example

低版本浏览器支持

身为前端开发人员,我相信大家此时都在诅咒微软为啥还不倒闭,微软已经严重的阻碍的大前端的发展脚步,但是抱怨归抱怨,用户群体还是很大的,只能想法来解决了。

根据以上的步骤其实 IE10+ 浏览器已经没问题了,但是因为使用了FormData,CORS 等一些 Level2 的新标准,所以无法再低版本浏览器更好的运行。

解决方案:使用 Flash 来支持低版本浏览器。

市面上比较好的插件有 webUploader ,可以根据浏览器类型来切换 flash 跟 XHR ,单因为此插件需要依赖于 jquery1.10+ 才可以,故无法使用。

最后使用了 SWFUploader 来支持,注:Flash 使用也是存在跨域问题的。

Flash文件的使用方式:

  1. swf放在自己项目中,创建一个 XML,将文件放在与接口同域的服务器上,Crossdomain.xml 文件内容如下,(以下例子为允许所有网站访问)

<?xml version=”1.0” encoding=”UTF-8”?>

<cross-domain-policy>

<allow-access-from domain=”*”/>

</cross-domain-policy>

  1. 将 SWF 文件放在与接口同域的服务器上,使用对应 JS 引用 SWF 即可。

以上两种方式均可, SWFUploader 配置调用方式

var setting = {
    debug: false,
    upload_url: url, //服务器接受文件路径
    flash_url: 'flashurl/swfupload.swf?ver=' + Math.random(), //上传的swf文件路径
    //文件配置
    file_post_name: 'imgData', //服务器接受的文件数据key值
    post_params: {
        sessionId: opts.param.sessionId
    }, //上传文件时带的参数
    file_types: '*.bmp;*.jpeg;*.jpg;*.gif;*.png', //文件类型过滤配置
    file_types_description: '浏览器图片格式', //上传文件类型描述
    file_size_limit: opts.fileSizeLimit, //指定要上传的文件的最大体积,
    //可以带单位,合法的单位有:B、KB、MB、GB,如果省略了单位,则默认为KB。该属性为0时,表示不限制文件的大小。
    file_upload_limit: "200",
    file_queue_limit: "1",
    prevent_swf_caching: false,
    preserve_relative_urls: false,
    //按钮配置
    button_placeholder_id: opts.id,
    button_width: (opts.type == 'pop') ? 160 : 175,
    button_height: (opts.type == 'pop') ? 100 : 109,
    button_text: "<span></span>",
    button_text_style: "",
    button_text_left_padding: 0,
    button_text_top_padding: 0,
    button_action: SWFUpload.BUTTON_ACTION.SELECT_FILES,
    button_disabled: false,
    button_cursor: SWFUpload.CURSOR.HAND,
    button_window_mode: "transparent",
    custom_settings: {
        index: index
    },
    // 事件
    file_dialog_start_handler: _self.fileDialogStart,
    file_queued_handler: _self.fileQueued,
    file_queue_error_handler: _self.fileQueueError,
    file_dialog_complete_handler: _self.fileDialogComplete,
    upload_progress_handler: _self.uploadProgress,
    upload_error_handler: _self.uploadError,
    upload_success_handler: _self.uploadSuccess,
    upload_complete_handler: _self.uploadComplete
}
var swfupload = new SWFUpload(setting);

函数配置好就行了,具体使用方式可以看一下文档: http://www.leeon.me/upload/other/swfupload.html

配置好以后,问题出现:发现在高版本浏览器是没问题的,但是在低版本浏览器是获取不到返回值的。

低版本IE;

33

高版本:

44

接受参数是不同的,所以会出现刚刚的问题,服务端需要按照这个类型来做兼容支持。这样既可实现 IE7+ 的图片上传功能。

服务端配置代码如下图:

增加登录验证支持

看似简单而合理的需求,实则坑道无限。问题,无论低版本或者高版本都是无法上传成功,都会被登录拦截器拦截。

原因:

  1. XHR 在 CORS 时,浏览器认为请求是不安全的,所以不会自动携带 cookie 信息。
  2. Flash 的请求不是在页面发送的,所以也无法携带 cookie 信息。

解决方法:
高版本浏览器:

需要前端后端一起修改来支持此需求,前端改动:

需要给 XHR 的 withCredentials 设置为 true ,并且服务器端也需要打开开关配置才可以正常的传递 cookie。文档描述: https://xhr.spec.whatwg.org/#the-withcredentials-attribute

11

代码如下:

var param = new FormData();
param.append('imgData', file);
xhr = new XMLHttpRequest();
/*新增进度条监听函数*/
xhr.upload.addEventListener("progress", _self.uploadProgress, false);
xhr.addEventListener("load", _self.uploadComplete, false);
xhr.addEventListener("error", _self.uploadFailed, false);
xhr.addEventListener("abort", _self.uploadCanceled, false);
xhr.open("POST", url);
xhr.withCredentials = true;
xhr.send(param);

注意:当设置 withCredentials 时,Access-Control-Allow-Origin 不能设置为 * 。必须设置为具体的域名。

IE 浏览器

Flash 无法获取参数,但是可以通过 JS 来获取到 cookie ,并且将 cookie 作为 param 跟随请求一起发送。可以自己手动写,也可以使用插件,swfupload.cookies.js 。此插件是自动添加 cookie 的,只需要引入即可。

解决了以上问题之后,又出现了另外一个问题:cookie 成功传递过去了,依旧无法通过登录过滤器。

经过排查发现,在XHR上传图片的时候都会发送2次请求:如下图

22

来仔细对比一下有什么区别?

第一个请求:

33

第二次请求:

44

仔细看发现多了一次类型为 OPTIONS 的请求,而且重要的是不携带 cookie。造成此类请求的原因:当使用 CORS 方式跨域请求时,如果信息为“复杂请求”,那么浏览器会自动发送一条“预检请求”,以获知服务器是否允许该实际请求。“预检请求”的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响,那么什么样的请求为复杂请求呢?

当请求满足下述任一条件时,则会提前发送预检请求

一、用了下面任一 http 方法:

二、人为设置了 对 CORS 安全的首部字段集合 之外的其他首部字段。该集合为:

三、 Content-Type 的值不属于下列之一:

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain

以下为一个复杂请求的请求过程:

11

由于上传图片属于复杂请求,所以在请求前,浏览器会先发送一次 OPTIONS 请求,服务端需要处理 OPTIONS 类型的请求,以避免被过滤器拦截掉。

到此为止,一个支持高低版本浏览器,以及支持跨域请求的上传图片组件即将开发完成。

封装功能,形成组件

考虑到位了让不同业务线使用,我们需要暴露不同的接口已支持不同的需求:

最后来看一下如何来调用:

seajs.use(['componentURL'], function(auth) {
  auth.page({
      componentKey: '', // 组件key ,
      transactionId: '1', // 上传交易ID ,
      container: '.renzheng',
      title: '',
      id: 'authP', //可以通过此ID来重写样式
      showMsgFn: function(msg) {
      alert(msg);
    },
    onReady: function(el) {},
    onSubmit: function(data) {
    }
  });
  $(".submit-btn").bind("click", function() {
    auth.submit(function(data) {
    console.log(JSON.stringify(data));
    });
  });
})

总结:

通过此组件开发,可以深刻理解跨域的一些常用的解决方案,CORS的用法以及遇到问题时的解决方案。最后希望此篇博客对于正在开发上传功能的同学有一定的帮助。


参考资料列表:

XMLHttpRequest: https://xhr.spec.whatwg.org/ 以及 https://segmentfault.com/a/1190000004322487

XMLHttpRequest Level1与Level2区别介绍: http://www.ruanyifeng.com/blog/2012/09/xmlhttprequest_level_2.html

CORS以及OPTIONS: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS

JSONP文档: https://en.wikipedia.org/wiki/JSONP

作者:JDC | 京东设计中心
京东设计中心
原文地址:图片上传进阶 VS 京东户簿实战, 感谢原作者分享。

发表评论