公司项目的架构是 umi —> nodejs —> api
在最近一次的需求中,有一个批量上传的功能,大体的交互是:在 excel 里先填写好数据,web 通过上传文件来在页面表格里预览,然后再将 excel 文件绑定一些表单的数据一起打包发送给后台,有点类似于表单中嵌套了文件上传,最后再提交表单。
一些想法和对交互的调整 最初的页面设计是放在步骤条里一共分为三步。上传在步骤条的第一步,表单在第二步,ant-design 的步骤条在切换上下步骤之后,上一个组件的 dom 会销毁,导致第二步无法获取到第一步在上传的时候生成的文件对象,也就无法上传文件。于是后来调整页面,将表单和上传文件放在一个页面,这样在解析文件成功之后,在当前这一步里就能一直获取到文件的对象,拿到文件对象就可以向接口发起请求。
未使用 ant-design 上传组件的原因 ant-design 的 Upload 组件上传之后的文件对象会立马返回,但是前端无法将这个对象一直拿着在提交的时候再给接口,因为文件对象的一些 key 不能拷贝过去【也是这次才发现只有 uid 一个字段可以遍历】,而且在通过 document.getElementById('file').files 获取上传的文件对,其 FileList 是 {length: 0},所以后来选择利用原生 input 来解决问题,通过创建 ref 将 input 的 dom 属性存起来,然后将 ref 获取的属性返回到父组件,在父组件里提交的时候,获取 ref 中的文件对象,传递给接口。
具体思路
创建 ref 对象来存储 input dom 属性
初始化利用 addEventListener 来监听原生 input 的 change 事件
利用 button 覆盖默认的上传样式,点击 button 的时候模拟触发点击 input 上传
捕获到事件之后,成功获取到文件对象,依次将文件对象传递给 xlsx 来解析为 json 数据,再将 json 数据传递给 and-table 来显示预览、将 input 的 ref 属性值回传到父组件(handleFileInputRefs 方法是父组件传递的 props 来获取自组件的 input ref)
父组件中也已经接受了表单的数据,并且接受了 input 的属性,通过 FormData 将数据和文件混传给 node 的 controller
controller 获取到文件对象和额外的表单参数,再向真正的接口发起请求
伪代码 前端 子组件 paresExcel.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 const ONE_M_TO_BYTES: number = 1024 * 1024 ;const MAX_FILE_SIZE: number = 10 ;const uploadInput = useRef(null );const formatTitleOrFileld = () => { const entozh = tableColumnKey.map((item, index ) => { return { index, key : item.key, }; }); return entozh; };const handleImpotedJson = () => { const [header, ...tableBody] = jsonArr; const keysArr = formatTitleOrFileld(); const len = header.length; tableBody.forEach((item: any ) => { for (let i = 0 ; i <= len - 1 ; i++) { item[i] = item[i] || '' ; } }); const parsedExcelData = tableBody.map((ele: any ) => { const newitem = {}; ele.forEach((im: any, i: number ) => { const newKey = keysArr[i].key; newitem[newKey] = im; }); return newitem; }); }const beforeUpload = (file: any ) => { if (file.size / ONE_M_TO_BYTES > MAX_FILE_SIZE) { message.warning('请上传小于10M的文件!' ); return false } else { const f = file; const reader = new FileReader(); reader.onload = function (e ) { const datas = e?.target?.result; const workbook = XLSX.read(datas, { type : 'binary' , }); const first_worksheet = workbook.Sheets[workbook.SheetNames[0 ]]; const jsonArr = XLSX.utils.sheet_to_json(first_worksheet, { header : 1 }); handleImpotedJson(jsonArr); }; reader.readAsBinaryString(f); } };const handleUpload = () => { beforeUpload(uploadInput?.current?.files[0 ]); };const handleFakeUpload = () => { uploadInput?.current?.click(); }; useEffect(() => { if (document ) { document .querySelector(`[name=uploadExcel]` )!.addEventListener('change' , function (event ) { if (event?.target && event?.target?.files) { handleFileInputRefs && handleFileInputRefs(uploadInput); handleUpload(); } }); } }, []);return ( <div className="fix-input-button"> <input type="file" name="uploadExcel" ref={uploadInput} /> <Button icon={<Iconfont name="iconshangchuan" />} onClick={handleFakeUpload}> 上传 </Button> </div> )
父组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 const formData = new FormData(); formData.append('fileStream' , uploadRefs?.current?.files[0 ]); formData.append('connectBusiness' , JSON .stringify(connectBusiness)); setIsLoading(true ) fetch('/api/appName/parseExcelUpload' , { method : 'post' , body : formData, }) .then(response => response.json()) .then(data => { console .log(data); }) .catch(err => { console .log('err' , err); }) .finally(() => { setIsLoading(false ); });
node 层node 用的是 thinkjs,controller 其实很简单,包装一下然后请求真正的接口避免直接调接口跨域
伪代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 const Base = require ('../base' );const fs = require ('fs' );const request = require ('request' );const UPLOAD_SERVICE = 'http://xxx' ;module .exports = class extends Base { constructor (props) { super (props) } async parseExcelUploadAction() { const files = this .file('fileStream' ); var req = request.post(UPLOAD_SERVICE, function (err, resp, body ) { if (err) { this .json({ status : 'failed' , msg : `上传失败,url:${UPLOAD_SERVICE} ` }) } else { this .json({ status : 'success' , data : body }) console .log('返回请求' + body); } }); var form = req.form(); form.append('file' , fs.createReadStream(files.path), { filename : files.name, contentType : 'application/vnd.ms-excel' }); form.append('connectBusiness' , JSON .stringify(this .post('connectBusiness' ))); } };
至此通过 node 中间层来上传的一个功能实现了。