阅读量:0
思路
1、自定义Upload
重点:<input ref={inputRef} type="file" accept={accept} onClick={e => e.stopPropagation()} onChange={uploadFile} multiple={multiple}/>
使用input标签设置type是file,将input元素通过forwardRef暴露给父组件,使父组件可以通过useImperativeHandle透传的resetValue方法在外部控制input的value值,
2、自定义AudioUpload
通过区分iOS和Android设置不同的accept来解决格式的兼容性问题
上传流程:文件格式校验——音频时长校验——获取upload token——上传文件——获取到ossUrl
3、使用 AudioUpload组件
.mp3, .wav, .m4a 和 audio/*
.mp3, .wav, 和 .m4a 是具体的音频文件格式
audio/* 是一个 MIME 类型,它表示所有音频文件类型
- MP3 比较流行,有损压缩的音频格式
- wav 无损未压缩的,文件较大
- m4a 通常用于Apple设备
- audio/*支持大多数音频
总结:需要处理特定格式的音频文件用前者;希望支持多种音频格式用后者;
1、自定义Upload
Upload/index.module.css
.tongyi-upload { outline: 0; }
Upload/index.tsx
import React, { ReactNode, forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'; import classnames from 'classnames'; import styles from './index.module.css'; interface UploadChangeParam { file: File; fileList: File[]; event?: { percent: number }; } interface UploadProps { className?: string; style?: React.CSSProperties; accept?: string; multiple?: boolean; withCredentials?: boolean; children: ReactNode; // 限制大小 maxSize?: number; // onChange?: (info: UploadChangeParam) => void; customRequest: (info: { file: File }) => Promise<void>; onError?: (e: any) => void; } export default forwardRef((props: UploadProps, ref: any) => { const { className, accept, maxSize, multiple = false, customRequest, onError, children } = props; const inputRef = useRef<HTMLInputElement>(null); const uploadFile = (e: React.ChangeEvent<HTMLInputElement>) => { const { files } = e.target; if (!files) { return; } const originFiles = [...files] as File[]; originFiles.forEach((file: File) => { if (maxSize && file.size > maxSize) { onError && onError({ code: 'FILE_EXCEEDS_SIZE' }); return; } return customRequest && customRequest({file}); }); }; useImperativeHandle(ref, () => { return { resetValue: () => { if (inputRef.current) { inputRef.current.value = ''; } } } }); return ( <div className={classnames(styles['tongyi-upload'], className)} onClick={() => inputRef.current?.click()} style={props.style} > <input type="file" ref={inputRef} style={{ display: 'none' }} accept={accept} onClick={e => e.stopPropagation()} onChange={uploadFile} multiple={multiple} /> {children} </div> ) })
2、自定义AudioUpload
AudioUpload.tsx
import { getUploadToken, uploadFile } from '@/services/file'; import Upload from './Upload'; import { getDeviceType } from '@/utils'; import { getAudioDuration } from '@/utils/audioFile'; import React, { forwardRef, ReactNode, useImperativeHandle, useRef, } from 'react'; interface UploadTokenData { accessId: string; policy: string; signature: string; dir: string; host: string; expire: number; bucketName: string; key: string; } interface UploadProps { className?: string; accept?: string; multiple?: boolean; withCredentials?: boolean; style?: React.CSSProperties; children: ReactNode; beforeUpload?: () => void; onChange?: (ossUrl: string) => void; onError?: (err: any) => void; } // 文件大小限制 const maxSize = 10 * 1024 * 1024; const audioFileType = ['mp3', 'aac', 'wav', 'flac', 'ogg', 'm4a']; export default forwardRef((props: UploadProps, ref: any) => { const uploadStatusRef = useRef<any>({}); const uploadRef = useRef<any>({}); const proxyFn = (fn: () => Promise<any>) => { if (uploadStatusRef.current.status !== 'cancel') { return fn(); } return Promise.reject('cancel'); }; const customRequest = async (info: { file: File }) => { uploadStatusRef.current.status = 'ready'; if (!info.file?.type.startsWith('audio/')) { props.onError && props.onError({ code: 'FILE_TYPE_ERROR', message: '', }); return Promise.reject('FILE_TYPE_ERROR'); } const duration: number = await getAudioDuration(info.file); console.log('获取到的时长', duration); if (duration < 10 || duration > 30) { props.onError && props.onError({ code: 'FILE_DURATION_ERROR', message: '', }); return Promise.reject('FILE_DURATION_ERROR'); } props.beforeUpload && props.beforeUpload(); console.log('文件信息=========', info.file); return ( // proxyFn(() => getAudioUploadToken(info.file.name)) proxyFn(() => getUploadToken()) .then((data: UploadTokenData) => { console.log('getAudioUploadToken的结果', data); return proxyFn(() => uploadFile(data, info.file)) .then((uploadRes) => { console.log('uploadFile成功了', uploadRes); props.onChange && props.onChange(uploadRes?.ossUrl); }) .catch((e) => { console.log('uploadFile报错了===========', e); props.onError && props.onError({ code: 'UNKNOW', message: e.errorMsg, }); }); }) .catch((e) => { console.log('e=========', e); const { errorMsg } = e; if (e !== 'cancel') { props.onError && props.onError({ code: 'UPLOAD_ERROR', }); } }) ); }; useImperativeHandle( ref, () => ({ cancel: () => { uploadStatusRef.current.status = 'cancel'; uploadRef.current.resetValue(); clearTimeout(uploadStatusRef.current.clock); }, }), [], ); const accept = getDeviceType() ? '.mp3, .wav, .m4a' : 'audio/*'; return ( <Upload maxSize={maxSize} accept={accept} {...props} ref={uploadRef} customRequest={customRequest} > {props.children} </Upload> ); });
3、使用 AudioUpload组件
// 上传组件 const uploadRef = useRef<any>(); // 上传状态 const[uploadStatus, setUploadStatus] = useState<string>('default'); // 上传定时器 const uploadTimer = useRef<any>(); // 合成进度 const [percent, setPercent] = useState<number>(0); /** * 开始上传 */ const startUpload = () => { console.log('上传中'); setUploadStatus('processing'); setPercent(0); const fn = () => { const i = Math.ceil(Math.random() * 3); uploadTimer.current = setTimeout(() => { fn(); }, 1 * 1000); setPercent((pre) => { let current = pre + i; if (current >= 100) { current = 100; clearTimeout(uploadTimer.current); } return current; }); }; clearTimeout(uploadTimer.current); uploadTimer.current = setTimeout(() => { fn(); }, 1 * 1000); }; /** * 上传失败 * @param e */ const onError = (e: any) => { console.log('上传失败了'); onChange(''); setUploadStatus('error'); clearTimeout(uploadTimer.current); uploadRef.current.cancel(); setPercent(0); let msg = ''; switch (e.code) { case 'FILE_DURATION_ERROR': msg = '请上传10-30秒音频文件'; break; case 'FILE_EXCEEDS_SIZE': msg = '请上传10M以下的文件'; break; case 'FILE_TYPE_ERROR': msg = '抱歉,请上传音频类型的文件'; break; case 'SEC_RESULT': msg = e.message || `${e.code}抱歉,出错了,请换一个文件试试!`; break; case 'UPLOAD_ERROR': msg = '抱歉,上传失败,请重新上传'; break; default: msg = `${e.code}抱歉,出错了,请换一个文件试试!`; break; } Toast.show({ type: 'error', content: msg, }); }; /** * 上传成功 */ const onUploaded = (ossUrl: string) => { console.log('上传成功获取到ossUrl', ossUrl); clearTimeout(uploadTimer.current); setUploadStatus('success'); // 上传成功之后调用接口合成数字声音。。。 onMergeSound(ossUrl); }; return ( <AudioUpload onError={onError} onChange={onUploaded} beforeUpload={startUpload} ref={uploadRef} > <div>上传</div> </AudioUpload> )
4、音频时长校验
/** * 异步获取音频文件的时长 * @param file 音频文件 * @returns 返回音频的时长(秒) */ export const getAudioDuration = async (file) => { try { const audio = new Audio(URL.createObjectURL(file)); await new Promise((resolve) => (audio.onloadedmetadata = resolve)); const { duration } = audio; return duration; } catch (error) { console.error('获取音频时长时发生错误:', error); return 0; } };
5、上传文件
export const uploadFile = (data: UploadTokenData, file: File) => { console.log('uploadFile开始了', data, '====', file); const bodyFormData = new FormData(); const url = `${data.host}/${data.dir}${file.name}`; bodyFormData.append('OSSAccessKeyId', data.accessId); bodyFormData.append('policy', data.policy); bodyFormData.append('signature', data.signature); bodyFormData.append('key', `${data.dir}${file.name}`); bodyFormData.append('dir', data.dir); bodyFormData.append('success_action_status', '200'); bodyFormData.append('file', file); console.log('uploadFile上传的url: ', url); return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.onerror = function error(e) { console.log('upload error', e); reject(e); }; xhr.onload = async () => { // allow success when 2xx status see https://github.com/react-component/upload/issues/34 if (xhr.status < 200 || xhr.status >= 300) { reject('上传异常'); } console.log('upload success'); resolve({ ...data, ossUrl: url, }); }; xhr.open('post', data.host, true); xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); xhr.send(bodyFormData); }); };