思路
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: File): Promise<number> => {
try {
// 读取音频文件为 ArrayBuffer
const arrayBuffer = await new Promise<ArrayBuffer>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result as ArrayBuffer);
};
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
// 使用 Web Audio API 解析音频文件
const audioContext = new (window.AudioContext ||
window.webkitAudioContext)();
const buffer = await audioContext.decodeAudioData(arrayBuffer);
// 获取音频文件的时长
const { duration } = buffer;
// 关闭 AudioContext
audioContext.close().catch(() => {});
return duration;
} catch (error) {
console.log('获取音频时长时发生错误:', error);
return 0;
}
};
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);
});
};