JavaScript FileReader

摘要: 本教程将介绍 JavaScript FileReader API 及其在实现文件上传中的应用。

JavaScript FileReader API 简介

当您将文件 拖放 到网页浏览器中或选择文件通过文件输入元素上传时,JavaScript 将每个文件表示为一个 File 对象。

File 对象允许您在 JavaScript 中访问所选文件。JavaScript 使用 FileList 对象来保存 File 对象。

要读取文件的内容,您需要使用 FileReader 对象。请注意,FileReader 只能访问您通过拖放或文件输入选择的那些文件。

要使用 FileReader 对象,您需要执行以下步骤:

首先,创建一个新的 FileObject

const reader = new FileReader();Code language: JavaScript (javascript)

其次,调用其中一个读取方法来读取文件的内容。例如:

reader.readAsDataURL(file);Code language: JavaScript (javascript)

readAsDataURL() 方法读取您从 FileList 对象获得的文件内容。

readAsDataURL() 方法返回一个包含 result 属性的对象,该属性包含 data: URL 格式的数据。data: URL 代表文件数据,以 base64 编码字符串形式表示。

例如,您可以使用 readAsDataURL() 读取图像并在网页上显示其 base64 编码字符串。

除了 readAsDataURL() 方法之外,FileReader 还有其他方法用于读取文件数据,例如 readAsText()readAsBinaryString()readAsArrayBuffer()

由于所有这些方法都以异步方式读取文件数据,因此您不能像这样直接返回结果:

const data = reader.readAsDataURL(file);Code language: JavaScript (javascript)

readAsDataURL() 方法成功完成读取文件时,FileReader 将触发 load 事件。

第三,添加一个事件处理程序来处理 FileReader 对象的 load 事件:

reader.addEventListener('load', (e) => {
    const data = e.target.result;
}Code language: JavaScript (javascript)

使用 JavaScript FileReader 实现图像上传应用程序

我们将使用 FileReader 来实现一个 图像上传应用程序

JavaScript FileReader API Demo

当您将图像拖放到放置区域时,该应用程序将使用 FileReader 读取图像,并在页面上显示它们,以及文件名和文件大小。

JavaScript FileReader API Demo

此外,该应用程序将使用 Fetch API 将文件上传到服务器。

对于服务器端,我们将实现一个简单的 PHP 脚本,将图像上传到服务器上的 'uploads' 文件夹。

设置项目结构

首先,创建以下文件和目录结构:

├── css
|  └── style.css
├── images
|  └── upload.svg
├── js
|  └── app.js
├── index.html
├── upload.php
└── uploadsCode language: JavaScript (javascript)

index.html

以下是 index.html 文件的示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="css/style.css" />
    <title>FileReader API Demo - Image Upload Application</title>
</head>
<body>
    <main>
        <div class="dropzone">
            <img src="images/upload.svg" alt="upload" width="60" />

            <input type="file" class="files" id="images"
                    accept="image/png, image/jpeg"
                    multiple />
             <label for="images">Choose multiple images</label>

            <h3>or drag & drop your PNG or JPEG files here</h3>
        </div>
        <div class="image-list"></div>
    </main>
    <script src="js/app.js"></script>
</body>
</html>Code language: JavaScript (javascript)

index.html 中,我们将 css/style.css 添加到 html 文档的 head 部分,并将 js/app.js 添加到结束 body 标记之前。

具有 dropzone 类别的 div 元素允许您将图像拖放到其中。此外,文件输入元素将使您能够选择要上传的文件。

文件输入元素接受多个文件,并且只允许 jpeg 和 png 图片。

<input type="file" class="files" id="images"
    accept="image/png, image/jpeg"
    multiple />Code language: JavaScript (javascript)

style.css 提供将文件输入元素转换为按钮的样式。此外,它还具有 active 类,当您将文件拖放到放置区域时,它会突出显示放置区域。

具有 image-list 类的 div 元素将显示上传的图像。

app.js

首先,使用 querySelector() 方法选择放置区域、文件输入(文件)和图像列表元素。

const imageList = document.querySelector('.image-list');
const fileInput = document.querySelector('.files');
const dropzone = document.querySelector('.dropzone');Code language: JavaScript (javascript)

其次,定义一个函数,该函数在放置区域添加或移除 active 类。

const setActive = (dropzone, active = true) => {
    const hasActiveClass = dropzone.classList.contains('active');

    if (active && !hasActiveClass) {
        return dropzone.classList.add('active');
    }

    if (!active && hasActiveClass) {
        return dropzone.classList.remove('active');
    }
};Code language: JavaScript (javascript)

如果您调用 setActive(dropzone),它将向 dropzone 添加 active 类。如果您调用 setActive(dropzone, false),它将从 dropzone 中移除 active 类。

第三,当 dragenterdragover 事件发生时,突出显示 dropzone,当 dragleavedrop 事件发生时,移除突出显示。

dropzone.addEventListener('dragenter', (e) => {
    e.preventDefault();
    setActive(dropzone);
});

dropzone.addEventListener('dragover', (e) => {
    e.preventDefault();
    setActive(dropzone);
});

dropzone.addEventListener('dragleave', (e) => {
    e.preventDefault();
    setActive(dropzone, false);
});

dropzone.addEventListener('drop', (e) => {
    e.preventDefault();
    setActive(dropzone, false);
    // ..
});Code language: JavaScript (javascript)

第四,在 dropzonedrop 事件处理程序中,获取 e.target 中的 FileList 对象,即 e.target.files

dropzone.addEventListener('drop', (e) => {
    e.preventDefault();
    setActive(dropzone, false);
    // get the FileList
    const { files } = e.dataTransfer;
    handleImages(files);
});Code language: JavaScript (javascript)

在 drop 事件处理程序中,我们使用对象解构来获取 FileList 对象并调用 handleImages() 函数来处理上传的图像。

第五,定义 handleImages() 函数。

const handleImages = (files) => {
    // get valid images
    let validImages = [...files].filter((file) =>
        ['image/jpeg', 'image/png'].includes(file.type)
    );
    //  show the image
    validImages.forEach(showImage);

    // upload all images
    uploadImages(validImages);
};Code language: JavaScript (javascript)

handleImages() 函数获取有效的图像,使用 showImage() 函数在页面上显示每个有效的图像,并使用 uploadImages() 函数将所有图像上传到服务器。

第六,定义 showImage() 函数,该函数在 validImages 数组中显示每个图像。

const showImage = (image) => {
    const reader = new FileReader();
    reader.readAsDataURL(image);
    reader.addEventListener('load', (e) => {
        const div = document.createElement('div');
        div.classList.add('image');
        div.innerHTML = `
            <img src="${e.target.result}" alt="${image.name}">
            <p>${image.name}</p>
            <p>${formatBytes(image.size)}</p>
        `;
        imageList.appendChild(div);
    });
};Code language: JavaScript (javascript)

showImage() 使用 FileReader 以数据 URL 的形式读取上传的图像。当 FileReader 完成读取文件后,它将创建一个新的 div 元素来保存图像信息。

请注意,formatBytes() 函数将以字节为单位的大小转换为人类可读的格式。

function formatBytes(size, decimals = 2) {
    if (size === 0) return '0 bytes';
    const k = 1024;
    const dm = decimals < 0 ? 0 : decimals;
    const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
    const i = Math.floor(Math.log(size) / Math.log(k));
    return parseFloat((size / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}Code language: JavaScript (javascript)

第七,定义 uploadImages() 函数,该函数将所有图像上传到服务器。

const uploadImages = async (images) => {

    const formData = new FormData();

    [...images].forEach((image) =>
        formData.append('images[]', image, image.name)
    );

    const response = await fetch('upload.php', {
        method: 'POST',
        body: formData,
    });

    return await response.json();
};Code language: JavaScript (javascript)

uploadImages() 函数使用 FormData API 来构建要提交的数据。

const formData = new FormData();Code language: JavaScript (javascript)

对于每个图像,我们将其添加到 FormData 对象中。

[...images].forEach((image) =>
    formData.append('images[]', image, image.name)
);Code language: JavaScript (javascript)

请注意,images 变量是一个 FileList 对象,而不是一个数组。要使用 forEach() 方法,可以使用展开运算符 (...) 将 FileList 对象转换为数组,如下所示:

[...images]Code language: JavaScript (javascript)

表单数据中的所有键值对都具有相同的键 images[];在 PHP 中,您可以将其作为数组访问 ($_FILES['images'])。

uploadImages() 函数使用 Fetch API 将图像(作为 FormData 对象)上传到服务器。

const response = await fetch('upload.php', {
    method: 'POST',
    body: formData,
});

return await response.json();Code language: JavaScript (javascript)

第八,如果用户使用此输入元素选择文件,则向文件输入元素添加 change 事件处理程序。

fileInput.addEventListener('change', (e) => {
    const { files } = e.target;
    handleImages(files);
});Code language: JavaScript (javascript)

在 change 事件处理程序中,您可以将 FileList 对象访问为 e.target.files。显示和上传图像的逻辑与拖放相同。

请注意,如果您将图像拖放到放置区域之外,网页浏览器将默认显示这些图像。

要阻止这种情况,您可以像这样调用文档的 dragoverdrop 事件对象的 preventDefault() 方法:

// prevent the drag & drop on the page
document.addEventListener('dragover', (e) => e.preventDefault());
document.addEventListener('drop', (e) => e.preventDefault());Code language: JavaScript (javascript)

以下是完整的 app.js 文件:

const imageList = document.querySelector('.image-list');
const fileInput = document.querySelector('.files');
const dropzone = document.querySelector('.dropzone');

const setActive = (dropzone, active = true) => {
    // active class
    const hasActiveClass = dropzone.classList.contains('active');

    if (active && !hasActiveClass) {
        return dropzone.classList.add('active');
    }

    if (!active && hasActiveClass) {
        return dropzone.classList.remove('active');
    }
};

dropzone.addEventListener('dragenter', (e) => {
    e.preventDefault();
    setActive(dropzone);
});

dropzone.addEventListener('dragover', (e) => {
    e.preventDefault();
    setActive(dropzone);
});

dropzone.addEventListener('dragleave', (e) => {
    e.preventDefault();
    setActive(dropzone, false);
});

dropzone.addEventListener('drop', (e) => {
    e.preventDefault();
    setActive(dropzone, false);
    // get the valid files
    const { files } = e.dataTransfer;
    // hand images
    handleImages(files);
});

const handleImages = (files) => {
    // get valid images
    let validImages = [...files].filter((file) =>
        ['image/jpeg', 'image/png'].includes(file.type)
    );
    //  show the image
    validImages.forEach(showImage);
    // upload files
    uploadImages(validImages);
};

const showImage = (image) => {
    const reader = new FileReader();
    reader.readAsDataURL(image);
    reader.addEventListener('load', (e) => {
        const div = document.createElement('div');
        div.classList.add('image');
        div.innerHTML = `
            <img src="${e.target.result}" alt="${image.name}">
            <p>${image.name}</p>
            <p>${formatBytes(image.size)}</p>
        `;
        imageList.appendChild(div);
    });
};

const uploadImages = async (images) => {
    const formData = new FormData();

    [...images].forEach((image) =>
        formData.append('images[]', image, image.name)
    );

    const response = await fetch('upload.php', {
        method: 'POST',
        body: formData,
    });

    return await response.json();
};

function formatBytes(size, decimals = 2) {
    if (size === 0) return '0 bytes';
    const k = 1024;
    const dm = decimals < 0 ? 0 : decimals;
    const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];

    const i = Math.floor(Math.log(size) / Math.log(k));

    return parseFloat((size / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}

fileInput.addEventListener('change', (e) => {
    const { files } = e.target;
    handleImages(files);
});

// prevent the drag & drop on the page
document.addEventListener('dragover', (e) => e.preventDefault());
document.addEventListener('drop', (e) => e.preventDefault());Code language: JavaScript (javascript)

最后,创建一个简单的 upload.php 脚本,将上传的图像移动到 uploads 文件夹。

<?php

const APP_ROOT = 'http://localhost:8080/';

const UPLOAD_DIR = __DIR__ . '/uploads';

const MESSAGES = [
    UPLOAD_ERR_OK => 'File uploaded successfully',
    UPLOAD_ERR_INI_SIZE => 'File is too big to upload',
    UPLOAD_ERR_FORM_SIZE => 'File is too big to upload',
    UPLOAD_ERR_PARTIAL => 'File was only partially uploaded',
    UPLOAD_ERR_NO_FILE => 'No file was uploaded',
    UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder on the server',
    UPLOAD_ERR_CANT_WRITE => 'File is failed to save to disk.',
    UPLOAD_ERR_EXTENSION => 'File is not allowed to upload to this server',
];

const ALLOWED_FILES = [
    'image/png' => 'png',
    'image/jpeg' => 'jpg'
];

const MAX_SIZE = 5 * 1024 * 1024; //  5MB

const HTTP_STATUSES = [
    200 => 'OK',
    400 => 'Bad Request',
    404 => 'Not Found',
    405 => 'Method Not Allowed'
];


$is_post_request = strtolower($_SERVER['REQUEST_METHOD']) === 'post';
$has_files = isset($_FILES['images']);

if (!$is_post_request || !$has_files) {
    response(405, [
        'success' => false,
        'message' => ' Method not allowed or files do not exist'
    ]);
}

$files = $_FILES['images'];
$file_count = count($files['name']);

// validation
$errors = [];
for ($i = 0; $i < $file_count; $i++) {
    // get the uploaded file info
    $status = $files['error'][$i];
    $filename = $files['name'][$i];
    $tmp = $files['tmp_name'][$i];

    // an error occurs
    if ($status !== UPLOAD_ERR_OK) {
        $errors[$filename] = MESSAGES[$status];
        continue;
    }
    // validate the file size
    $filesize = filesize($tmp);

    if ($filesize > MAX_SIZE) {
        // construct an error message
        $message = sprintf(
            "The file %s is %s which is greater than the allowed size %s",
            $filename,
            format_filesize($filesize),
            format_filesize(MAX_SIZE)
        );

        $errors[$filesize] = $message;
        continue;
    }

    // validate the file type
    if (!in_array(get_mime_type($tmp), array_keys(ALLOWED_FILES))) {
        $errors[$filename] = "The file $filename is allowed to upload";
    }
}

if ($errors) {
    response(400, [
        'success' => false,
        'message' => $errors
    ]);
}

// move the files
for ($i = 0; $i < $file_count; $i++) {
    $filename = $files['name'][$i];
    $tmp = $files['tmp_name'][$i];
    $mime_type = get_mime_type($tmp);

    // set the filename as the basename + extension
    $uploaded_file = pathinfo($filename, PATHINFO_FILENAME) . '.' . ALLOWED_FILES[$mime_type];
    // new filepath
    $filepath = UPLOAD_DIR . '/' . $uploaded_file;

    // move the file to the upload dir
    $success = move_uploaded_file($tmp, $filepath);
    if (!$success) {
        $errors[$filename] = "The file $filename was failed to move.";
    }
}

if ($errors) {
    response(400, [
        'success' => false,
        'message' => $errors
    ]);
}

response(200, [
    'success' => true,
    'message' => 'The files uploaded successfully'
]);


/**
 * Return a mime type of file or false if an error occurred
 *
 * @param string $filename
 * @return string | bool
 */
function get_mime_type(string $filename)
{
    $info = finfo_open(FILEINFO_MIME_TYPE);
    if (!$info) {
        return false;
    }

    $mime_type = finfo_file($info, $filename);
    finfo_close($info);

    return $mime_type;
}

/**
 * Return a human-readable file size
 *
 * @param int $bytes
 * @param int $decimals
 * @return string
 */
function format_filesize(int $bytes, int $decimals = 2): string
{
    $units = 'BKMGTP';
    $factor = floor((strlen($bytes) - 1) / 3);

    return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . $units[(int)$factor];
}

/**
 * Response JSON to the client
 * @param int $status_code
 * @param array|null $data
 */
function response(int $status_code, array $data = null)
{
    header("HTTP/1.1 " . $status_code . " " . HTTP_STATUSES[$status_code]);
    header("Content-Type: application/json");
    echo json_encode($data);
    exit;
}
Code language: JavaScript (javascript)

有关详细信息,请阅读更多关于 如何在 PHP 中上传多个文件 的内容。

总结

  • 使用 JavaScript FileReader API 读取用户通过拖放或文件输入元素选择的那些文件。
本教程是否有帮助?