我需要使用 React Dropzone 验证上传视频的持续时间。问题是我发现的每个方法都是异步的,而库的验证器函数应该是同步的。如何在同步函数中运行异步代码?
获取视频时长功能:
async function getVideoDuration(videoFile: File | Blob) {
return new Promise<number>((res, _) => {
const video = document.createElement("video")
video.preload = "metadata"
video.onloadeddata = function () {
res(video.duration)
}
video.src = URL.createObjectURL(videoFile)
})
}
我的 React Zone 验证器功能:
validator(f) {
let file: (File) | null
if (f instanceof DataTransferItem) {
file = f.getAsFile()
} else file = f
if (!file) return { message: "File Expected", code: "IvalidDragObject" }
console.log(file)
if (file.size > 5e8) return { message: "File too large (max 500 MB)", code: "FileTooLarge" }
if (!file) return { message: "File Expected", code: "IvalidDragObject" }
const duration = await getVideoDuration(file) // 'await' expressions are only allowed within async functions and at the top levels of modules.
if (duration > 15 * 60) {
return { message: "File Duration should be less than 15 minutes", code: "LargeFileDuration" }
}
return null
},
其他解决方法是使用
getFilesFromEvent
函数并将自定义道具传递到那里的文件,但它给出的事件非常不同,并且实现起来很乏味。
我已经在here分叉了这个项目,并进行了支持异步验证所需的更改。请注意,目前不支持
noDragEventsBubbling
参数。只需传入一个异步(或同步)验证回调函数,如下所示,并按最初的预期使用。
import { useDropzone } from 'react-dropzone'
function MyDropzone() {
const onDrop = useCallback(acceptedFiles => {
// Do something with the files
}, [])
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
validator: async (file) => {
// do validation here
}
})
return (
<div {...getRootProps()}>
<input {...getInputProps()} />
{
isDragActive ?
<p>Drop the files here ...</p> :
<p>Drag 'n' drop some files here, or click to select files</p>
}
</div>
)
}
发现这种方法是最模块化的并且接近react-dropzone的原始设计。没有任何额外的依赖。在我们的例子中,我们检查图像的大小,但您可以看到添加其他 MIME 类型的验证不会有问题。
import {DropEvent} from 'react-dropzone'
interface ExtendedFile extends File {
width?: number
height?: number
}
const extractFiles = async (e: DropEvent) => {
const files: File[] = []
if (e instanceof Array) {
const fileHandles = e.filter((it) => it instanceof FileSystemFileHandle)
const selectedFiles = await Promise.all(fileHandles.map((it) => it.getFile()))
files.push(...selectedFiles)
} else if (e instanceof DragEvent) {
files.push(...(e.dataTransfer?.files || []))
} else if (e.target instanceof HTMLInputElement) {
files.push(...Array.from(e.target.files || []))
}
return files
}
type ResolveType = (value: ExtendedFile | PromiseLike<ExtendedFile>) => void
const extractDimensionForImage = (file: File, resolve: ResolveType): void => {
const image = new Image()
image.onload = () => {
Object.defineProperties(file, {
width: {value: image.width},
height: {value: image.height},
})
resolve(file satisfies ExtendedFile)
}
image.src = URL.createObjectURL(file)
}
const convertToExtended = async (files: File[]): Promise<ExtendedFile[]> => {
const promises = files.map(
(file) =>
new Promise<ExtendedFile>((resolve) => {
if (file.type.includes('image/')) return extractDimensionForImage(file, resolve)
resolve(file)
}),
)
return Promise.all(promises)
}
export const fileGetter = async (e: DropEvent) => {
const files = await extractFiles(e)
return convertToExtended(files)
}
useDropzone({
getFilesFromEvent: fileGetter,
...dropzoneOptions,
})
const validateImage = (file: ExtendedFile): ReturnType<Required<DropzoneOptions>['validator']> => {
const {width, height} = file
if (!width || !height) return null
if (width !== height) {
return {
code: 'invalid-size',
message: 'Uploaded image must be squared',
}
}
return null
}
const dropzoneOptions = {
accept: {
'image/svg+xml': ['.svg'],
},
validator: validateImage
}