UNPKG

hm-react-cli

Version:

Create a Huimei React project by module

789 lines (718 loc) 31.6 kB
/** * Created by cheesu on 2015/8/17. */ /** * React文件上传组件,兼容IE8+ * 现代浏览器采用AJAX(XHR2+File API)上传。低版本浏览器使用form+iframe上传。 * 使用到ES6,需要经babel转译 */ /*eslint indent: 0 */ const React = require('react'); const emptyFunction = function () {}; /*当前IE上传组的id*/ let currentIEID = 0; /*存放当前IE上传组的可用情况*/ const IEFormGroup = [true]; /*当前xhr的数组(仅有已开始上传之后的xhr)*/ let xhrList = []; let currentXHRID = 0; const PT = React.PropTypes; const FileUpload = React.createClass({ /*类型验证*/ propTypes: { options: PT.shape({ /*basics*/ baseUrl: PT.string.isRequired, param: PT.oneOfType([PT.object, PT.func]), dataType: PT.string, chooseAndUpload: PT.bool, paramAddToField: PT.oneOfType([PT.object, PT.func]), wrapperDisplay: PT.string, timeout: PT.number, accept: PT.string, multiple: PT.bool, numberLimit: PT.oneOfType([PT.number, PT.func]), fileFieldName: PT.oneOfType([PT.string, PT.func]), withCredentials: PT.bool, requestHeaders: PT.object, /*specials*/ tag: PT.string, userAgent: PT.string, disabledIEChoose: PT.oneOfType([PT.bool, PT.func]), _withoutFileUpload: PT.bool, filesToUpload: PT.arrayOf(PT.object), textBeforeFiles: PT.bool, /*funcs*/ beforeChoose: PT.func, chooseFile: PT.func, beforeUpload: PT.func, doUpload: PT.func, uploading: PT.func, uploadSuccess: PT.func, uploadError: PT.func, uploadFail: PT.func, onabort: PT.func }).isRequired, style: PT.object, className: PT.string }, /*根据props更新组件*/ _updateProps(props) { this.isIE = !(this.checkIE() < 0 || this.checkIE() >= 10); const options = props.options; this.baseUrl = options.baseUrl; //域名 this.param = options.param; //get参数 this.chooseAndUpload = options.chooseAndUpload || false; //是否在用户选择了文件之后立刻上传 this.paramAddToField = options.paramAddToField || undefined; //需要添加到FormData的对象。不支持IE /*upload success 返回resp的格式*/ this.dataType = 'json'; options.dataType && options.dataType.toLowerCase() == 'text' && (this.dataType = 'text'); this.wrapperDisplay = options.wrapperDisplay || 'inline-block'; //包裹chooseBtn或uploadBtn的div的display this.timeout = typeof options.timeout == 'number' && options.timeout > 0 ? options.timeout : 0; //超时时间 this.accept = options.accept || ''; //限制文件后缀 this.multiple = options.multiple || false; this.numberLimit = options.numberLimit || false; //允许多文件上传时,选择文件数量的限制 this.fileFieldName = options.fileFieldName || false; //文件附加到formData上时的key,传入string指定一个file的属性名,值为其属性的值。不支持IE this.withCredentials = options.withCredentials || false; //跨域时是否使用认证信息 this.requestHeaders = options.requestHeaders || false; //要设置的请求头键值对 /*生命周期函数*/ /** * beforeChoose() : 用户选择之前执行,返回true继续,false阻止用户选择 * @param null * @return {boolean} 是否允许用户进行选择 */ this.beforeChoose = options.beforeChoose || emptyFunction; /** * chooseFile(file) : 用户选择文件后的触发的回调函数 * @param file {File | string} 现代浏览器返回File对象,IE返回文件名 * @return */ this.chooseFile = options.chooseFile || emptyFunction; /** * beforeUpload(file,mill) : 用户上传之前执行,返回true继续,false阻止用户选择 * @param file {File | string} 现代浏览器返回File对象,IE返回文件名 * @param mill {long} 毫秒数,如果File对象已有毫秒数则返回一样的 * @return {boolean || object} 是否允许用户进行上传 (hack:如果是obj{ * assign:boolean 默认true * param:object * }), 则对本次的param进行处理 */ this.beforeUpload = options.beforeUpload || emptyFunction; /** * doUpload(file,mill) : 上传动作(xhr send | form submit)执行后调用 * @param file {File | string} 现代浏览器返回File对象,IE返回文件名 * @param mill {long} 毫秒数,如果File对象已有毫秒数则返回一样的 * @return */ this.doUpload = options.doUpload || emptyFunction; /** * uploading(progress) : 在文件上传中的时候,浏览器会不断触发此函数。IE中使用每200ms触发的假进度 * @param progress {Progress} progress对象,里面存有例如上传进度loaded和文件大小total等属性 * @return */ this.uploading = options.uploading || emptyFunction; /** * uploadSuccess(resp) : 上传成功后执行的回调(针对AJAX而言) * @param resp {json | string} 根据options.dataType指定返回数据的格式 * @return */ this.uploadSuccess = options.uploadSuccess || emptyFunction; /** * uploadError(err) : 上传错误后执行的回调(针对AJAX而言) * @param err {Error | object} 如果返回catch到的error,其具有type和message属性 * @return */ this.uploadError = options.uploadError || emptyFunction; /** * uploadFail(resp) : 上传失败后执行的回调(针对AJAX而言) * @param resp {string} 失败信息 */ this.uploadFail = options.uploadFail || emptyFunction; /** * onabort(mill, xhrID) : 主动取消xhr进程的响应 * @param mill {long} 毫秒数,本次上传时刻的时间 * @param xhrID {int} 在doUpload时会返回的当次xhr代表ID */ this.onabort = options.onabort || emptyFunction; this.files = options.files || this.files || false; //保存需要上传的文件 /*特殊内容*/ /*IE情况下,由于上传按钮被隐藏的input覆盖,不能进行disabled按钮处理。 * 所以当disabledIEChoose为true(或者func返回值为true)时,禁止IE上传。 */ this.disabledIEChoose = options.disabledIEChoose || false; this._withoutFileUpload = options._withoutFileUpload || false; //不带文件上传,为了给秒传功能使用,不影响IE this.filesToUpload = options.filesToUpload || []; //使用filesToUpload()方法代替 this.textBeforeFiles = options.textBeforeFiles || false; //make this true to add text fields before file data /*使用filesToUpload()方法代替*/ if (this.filesToUpload.length && !this.isIE) { this.filesToUpload.forEach((file) => { this.files = [file]; this.commonUpload(); }); } /*放置虚拟DOM*/ let chooseBtn, uploadBtn, flag = 0; const before = [], middle = [], after = []; if (this.chooseAndUpload) { React.Children.forEach(props.children, (child) => { if (child && child.ref == 'chooseAndUpload') { chooseBtn = child; flag++; } else { flag == 0 ? before.push(child) : flag == 1 ? middle.push(child) : ''; } }); } else { React.Children.forEach(props.children, (child) => { if (child && child.ref == 'chooseBtn') { chooseBtn = child; flag++; } else if (child && child.ref == 'uploadBtn') { uploadBtn = child; flag++; } else { flag == 0 ? before.push(child) : flag == 1 ? middle.push(child) : after.push(child); } }); } this.setState({ chooseBtn, uploadBtn, before, middle, after }); }, /*触发隐藏的input框选择*/ /*触发beforeChoose*/ commonChooseFile() { const jud = this.beforeChoose(); if (jud != true && jud != undefined) return; this.refs['ajax_upload_file_input'].click(); }, /*现代浏览器input change事件。File API保存文件*/ /*触发chooseFile*/ commonChange(e) { let files; e.dataTransfer ? (files = e.dataTransfer.files) : e.target ? (files = e.target.files) : ''; /*如果限制了多文件上传时的数量*/ const numberLimit = typeof this.numberLimit === 'function' ? this.numberLimit() : this.numberLimit; if (this.multiple && numberLimit && files.length > numberLimit) { const newFiles = {}; for (let i = 0; i < numberLimit; i++) newFiles[i] = files[i]; newFiles.length = numberLimit; files = newFiles; } this.files = files; this.chooseFile(files); this.chooseAndUpload && this.commonUpload(); }, /*执行上传*/ commonUpload() { /*mill参数是当前时刻毫秒数,file第一次进行上传时会添加为file的属性,也可在beforeUpload为其添加,之后同一文件的mill不会更改,作为文件的识别id*/ const mill = (this.files.length && this.files[0].mill) || new Date().getTime(); const jud = this.beforeUpload(this.files, mill); if (jud != true && jud != undefined && typeof jud != 'object') { /*清除input的值*/ this.refs['ajax_upload_file_input'].value = ''; return; } if (!this.files) return; if (!this.baseUrl) throw new Error('baseUrl missing in options'); /*用于存放当前作用域的东西*/ const scope = {}; /*组装FormData*/ let formData = new FormData(); /*If we need to add fields before file data append here*/ if (this.textBeforeFiles) { formData = this.appendFieldsToFormData(formData); } if (!this._withoutFileUpload) { const fieldNameType = typeof this.fileFieldName; /*判断是用什么方式作为formdata item 的 name*/ Object.keys(this.files).forEach((key) => { if (key == 'length') return; if (fieldNameType == 'function') { const file = this.files[key]; const fileFieldName = this.fileFieldName(file); formData.append(fileFieldName, file); } else if (fieldNameType == 'string') { const file = this.files[key]; formData.append(this.fileFieldName, file); } else { const file = this.files[key]; formData.append(file.name, file); } }); } /*If we need to add fields after file data append here*/ if (!this.textBeforeFiles) { formData = this.appendFieldsToFormData(formData); } const baseUrl = this.baseUrl; /*url参数*/ /*如果param是一个函数*/ const param = typeof this.param === 'function' ? this.param(this.files) : this.param; let paramStr = ''; if (param) { const paramArr = []; param['_'] = mill; Object.keys(param).forEach((key) => paramArr.push(`${key}=${param[key]}`)); paramStr = '?' + paramArr.join('&'); } const targeturl = baseUrl + paramStr; /*AJAX上传部分*/ const xhr = new XMLHttpRequest(); xhr.open('POST', targeturl, true); /*跨域是否开启验证信息*/ xhr.withCredentials = this.withCredentials; /*是否需要设置请求头*/ const rh = this.requestHeaders; rh && Object.keys(rh).forEach((key) => xhr.setRequestHeader(key, rh[key])); /*处理超时。用定时器判断超时,不然xhr state=4 catch的错误无法判断是超时*/ if (this.timeout) { xhr.timeout = this.timeout; xhr.ontimeout = () => { this.uploadError({ type: 'TIMEOUTERROR', message: 'timeout' }); scope.isTimeout = false; }; scope.isTimeout = false; setTimeout(() => { scope.isTimeout = true; }, this.timeout); } xhr.onreadystatechange = () => { /*xhr finish*/ try { if (xhr.readyState == 4 && xhr.status >= 200 && xhr.status < 400) { const resp = this.dataType == 'json' ? JSON.parse(xhr.responseText) : xhr.responseText; this.uploadSuccess(resp); } else if (xhr.readyState == 4) { /*xhr fail*/ const resp = this.dataType == 'json' ? JSON.parse(xhr.responseText) : xhr.responseText; this.uploadFail(resp); } } catch (e) { /*超时抛出不一样的错误,不在这里处理*/ !scope.isTimeout && this.uploadError({ type: 'FINISHERROR', message: e.message }); } }; /*xhr error*/ xhr.onerror = () => { try { const resp = this.dataType == 'json' ? JSON.parse(xhr.responseText) : xhr.responseText; this.uploadError({ type: 'XHRERROR', message: resp }); } catch (e) { this.uploadError({ type: 'XHRERROR', message: e.message }); } }; /*这里部分浏览器实现不一致,而且IE没有这个方法*/ xhr.onprogress = xhr.upload.onprogress = (progress) => { this.uploading(progress, mill); }; /*不带文件上传,给秒传使用*/ this._withoutFileUpload ? xhr.send(null) : xhr.send(formData); /*保存xhr id*/ xhrList.push(xhr); const cID = xhrList.length - 1; currentXHRID = cID; /*有响应abort的情况*/ xhr.onabort = () => this.onabort(mill, cID); /*trigger执行上传的用户回调*/ this.doUpload(this.files, mill, currentXHRID); /*清除input的值*/ this.refs['ajax_upload_file_input'].value = ''; }, /*组装自定义添加到FormData的对象*/ appendFieldsToFormData(formData) { const field = typeof this.paramAddToField == 'function' ? this.paramAddToField() : this.paramAddToField; field && Object.keys(field).map((index) => formData.append(index, field[index])); return formData; }, /*iE选择前验证*/ /*触发beforeChoose*/ IEBeforeChoose(e) { const jud = this.beforeChoose(); jud != true && jud != undefined && e.preventDefault(); }, /*IE需要用户真实点击上传按钮,所以使用透明按钮*/ /*触发chooseFile*/ IEChooseFile(e) { this.fileName = e.target.value.substring(e.target.value.lastIndexOf('\\') + 1); this.chooseFile(this.fileName); /*先执行IEUpload,配置好action等参数,然后submit*/ this.chooseAndUpload && this.IEUpload() !== false && document.getElementById(`ajax_upload_file_form_${this.IETag}${currentIEID}`).submit(); e.target.blur(); }, /*IE处理上传函数*/ /*触发beforeUpload doUpload*/ IEUpload(e) { const mill = new Date().getTime(); const jud = this.beforeUpload(this.fileName, mill); if (!this.fileName || (jud != true && jud != undefined)) { e && e.preventDefault(); return false; } const that = this; /*url参数*/ const baseUrl = this.baseUrl; const param = typeof this.param === 'function' ? this.param(this.fileName) : this.param; let paramStr = ''; if (param) { const paramArr = []; param['_'] = mill; param['ie'] === undefined && (param['ie'] = 'true'); for (const key in param) { if (param[key] != undefined) paramArr.push(`${key}=${param[key]}`); } paramStr = '?' + paramArr.join('&'); } const targeturl = baseUrl + paramStr; document.getElementById(`ajax_upload_file_form_${this.IETag}${currentIEID}`).setAttribute('action', targeturl); /*IE假的上传进度*/ const getFakeProgress = this.fakeProgress(); let loaded = 0, count = 0; const progressInterval = setInterval(() => { loaded = getFakeProgress(loaded); this.uploading( { loaded, total: 100 }, mill ); /*防止永久执行,设定最大的次数。暂时为30秒(200*150)*/ ++count >= 150 && clearInterval(progressInterval); }, 200); /*当前上传id*/ const partIEID = currentIEID; /*回调函数*/ window.attachEvent ? document .getElementById(`ajax_upload_file_frame_${this.IETag}${partIEID}`) .attachEvent('onload', handleOnLoad) : document .getElementById(`ajax_upload_file_frame_${this.IETag}${partIEID}`) .addEventListener('load', handleOnLoad); function handleOnLoad() { /*clear progress interval*/ clearInterval(progressInterval); try { that.uploadSuccess(that.IECallback(that.dataType, partIEID)); } catch (e) { that.uploadError(e); } finally { /*清除输入框的值*/ const oInput = document.getElementById(`ajax_upload_hidden_input_${that.IETag}${partIEID}`); oInput.outerHTML = oInput.outerHTML; } } this.doUpload(this.fileName, mill); /*置为非空闲*/ IEFormGroup[currentIEID] = false; }, /*IE回调函数*/ //TODO 处理Timeout IECallback(dataType, frameId) { /*回复空闲状态*/ IEFormGroup[frameId] = true; const frame = document.getElementById(`ajax_upload_file_frame_${this.IETag}${frameId}`); const resp = {}; const content = frame.contentWindow ? frame.contentWindow.document.body : frame.contentDocument.document.body; if (!content) throw new Error('Your browser does not support async upload'); try { resp.responseText = content.innerHTML || 'null innerHTML'; resp.json = JSON ? JSON.parse(resp.responseText) : eval(`(${resp.responseText})`); } catch (e) { /*如果是包含了<pre>*/ if (e.message && e.message.indexOf('Unexpected token') >= 0) { /*包含返回的json*/ if (resp.responseText.indexOf('{') >= 0) { const msg = resp.responseText.substring( resp.responseText.indexOf('{'), resp.responseText.lastIndexOf('}') + 1 ); return JSON ? JSON.parse(msg) : eval(`(${msg})`); } return { type: 'FINISHERROR', message: e.message }; } throw e; } return dataType == 'json' ? resp.json : resp.responseText; }, /*外部调用方法,主动触发选择文件(等同于调用btn.click()), 仅支持现代浏览器*/ forwardChoose() { if (this.isIE) return false; this.commonChooseFile(); }, /** * 外部调用方法,当多文件上传时,用这个方法主动删除列表中某个文件 * TODO: 此方法应为可以任意操作文件数组 * @param func 用户调用时传入的函数,函数接收参数files(filesAPI 对象) * @return Obj File API 对象 * File API Obj: * { * 0 : file, * 1 : file, * length : 2 * } */ fowardRemoveFile(func) { this.files = func(this.files); }, /*外部调用方法,传入files(File API)对象可以立刻执行上传动作,IE不支持。调用随后会触发beforeUpload*/ filesToUpload(files) { if (this.isIE) return; this.files = files; this.commonUpload(); }, /*外部调用方法,取消一个正在进行的xhr,传入id指定xhr(doupload时返回)或者默认取消最近一个。*/ abort(id) { id === undefined ? xhrList[currentXHRID].abort() : xhrList[id].abort(); }, /*判断ie版本*/ checkIE() { const userAgent = this.userAgent; const version = userAgent.indexOf('MSIE'); if (version < 0) return -1; return parseFloat(userAgent.substring(version + 5, userAgent.indexOf(';', version))); }, /*生成假的IE上传进度*/ fakeProgress() { let add = 6; const decrease = 0.3, end = 98, min = 0.2; return (lastTime) => { let start = lastTime; if (start >= end) return start; start += add; add = add - decrease; add < min && (add = min); return start; }; }, getUserAgent() { const userAgentString = this.props.options && this.props.options.userAgent; const navigatorIsAvailable = typeof navigator !== 'undefined'; if (!navigatorIsAvailable && !userAgentString) { throw new Error( '`options.userAgent` must be set rendering react-fileuploader in situations when `navigator` is not defined in the global namespace. (on the server, for example)' ); } return navigatorIsAvailable ? navigator.userAgent : userAgentString; }, getInitialState() { return { chooseBtn: {}, //选择按钮。如果chooseAndUpload=true代表选择并上传。 uploadBtn: {}, //上传按钮。如果chooseAndUpload=true则无效。 before: [], //存放props.children中位于chooseBtn前的元素 middle: [], //存放props.children中位于chooseBtn后,uploadBtn前的元素 after: [] //存放props.children中位于uploadBtn后的元素, }; }, componentWillMount() { this.userAgent = this.getUserAgent(); this.isIE = !(this.checkIE() < 0 || this.checkIE() >= 10); /*因为IE每次要用到很多form组,如果在同一页面需要用到多个<FileUpload>可以在options传入tag作为区分。并且不随后续props改变而改变*/ const tag = this.props.options && this.props.options.tag; this.IETag = tag ? tag + '_' : ''; this._updateProps(this.props); }, componentDidMount() { for (let i = 0; i < IEFormGroup.length; i++) { if (IEFormGroup[i] && hasFree) return; i = `${this.IETag}${i}`; <iframe id={`ajax_upload_file_frame_${i}`} name={`ajax_upload_file_frame_${i}`} key={`ajax_upload_file_frame_${i}`} className="ajax_upload_file_frame" style={{ display: 'none', width: 0, height: 0, margin: 0, border: 0 }} ></iframe>; } }, componentDidUpdate() { <iframe id={`ajax_upload_file_frame_${i}`} name={`ajax_upload_file_frame_${i}`} key={`ajax_upload_file_frame_${i}`} className="ajax_upload_file_frame" style={{ display: 'none', width: 0, height: 0, margin: 0, border: 0 }} ></iframe>; }, componentWillReceiveProps(newProps) { this._updateProps(newProps); }, render() { return this._packRender(); }, /*打包render函数*/ _packRender() { /*IE用iframe表单上传,其他用ajax Formdata*/ let render = ''; if (this.isIE) { render = this._multiIEForm(); } else { const restAttrs = { accept: this.accept, multiple: this.multiple }; render = ( <div className={this.props.className} style={this.props.style}> {this.state.before} <div onClick={this.commonChooseFile} style={{ overflow: 'hidden', postion: 'relative', display: this.wrapperDisplay }} > {this.state.chooseBtn} </div> {this.state.middle} <div onClick={this.commonUpload} style={{ overflow: 'hidden', postion: 'relative', display: this.chooseAndUpload ? 'none' : this.wrapperDisplay }} > {this.state.uploadBtn} </div> {this.state.after} <input type="file" name="ajax_upload_file_input" ref="ajax_upload_file_input" style={{ display: 'none' }} onChange={this.commonChange} {...restAttrs} /> </div> ); } return render; }, /*IE多文件同时上传,需要多个表单+多个form组合。根据currentIEID代表有多少个form。*/ /*所有不在空闲(正在上传)的上传组都以display:none的形式插入,第一个空闲的上传组会display:block捕捉。*/ _multiIEForm() { const formArr = []; let hasFree = false; /* IE情况下,由于上传按钮被隐藏的input覆盖,不能进行disabled按钮处理。 * 所以当disabledIEChoose为true(或者func返回值为true)时,禁止IE上传。 */ const isDisabled = typeof this.disabledIEChoose === 'function' ? this.disabledIEChoose() : this.disabledIEChoose; /*这里IEFormGroup的长度会变,所以不能存len*/ for (let i = 0; i < IEFormGroup.length; i++) { _insertIEForm.call(this, formArr, i); /*如果当前上传组是空闲,hasFree=true,并且指定当前上传组ID*/ if (IEFormGroup[i] && !hasFree) { hasFree = true; currentIEID = i; } /*如果所有上传组都不是空闲状态,push一个新增组*/ i == IEFormGroup.length - 1 && !hasFree && IEFormGroup.push(true); } return ( <div className={this.props.className} style={this.props.style} id="react-file-uploader"> {formArr} </div> ); function _insertIEForm(formArr, i) { /*如果已经push了空闲组而当前也是空闲组*/ if (IEFormGroup[i] && hasFree) return; /*是否display*/ const isShow = IEFormGroup[i]; /*Input内联样式*/ const style = { position: 'absolute', left: '-30px', top: 0, zIndex: '50', fontSize: '80px', width: '200px', opacity: 0, filter: 'alpha(opacity=0)' }; /*是否限制了文件后缀,以及是否disabled*/ const restAttrs = { accept: this.accept, disabled: isDisabled }; const input = ( <input type="file" name={`ajax_upload_hidden_input_${i}`} id={`ajax_upload_hidden_input_${i}`} ref={`ajax_upload_hidden_input_${i}`} onChange={this.IEChooseFile} onClick={this.IEBeforeChoose} style={style} {...restAttrs} /> ); i = `${this.IETag}${i}`; formArr.push( <form id={`ajax_upload_file_form_${i}`} method="post" target={`ajax_upload_file_frame_${i}`} key={`ajax_upload_file_form_${i}`} encType="multipart/form-data" ref={`form_${i}`} onSubmit={this.IEUpload} style={{ display: isShow ? 'block' : 'none' }} > {this.state.before} <div style={{ overflow: 'hidden', position: 'relative', display: 'inline-block' }}> {this.state.chooseBtn} {/*input file 的name不能省略*/} {input} </div> {this.state.middle} <div style={{ overflow: 'hidden', position: 'relative', display: this.chooseAndUpload ? 'none' : this.wrapperDisplay }} > {this.state.uploadBtn} <input type="submit" style={{ position: 'absolute', left: 0, top: 0, fontSize: '50px', width: '200px', opacity: 0 }} /> </div> {this.state.after} </form> ); } } }); module.exports = FileUpload;