Создание веб-приложения для обмена файлами 📂

Вы когда-нибудь хотели обмениваться файлами с вашего iPhone на ваш Windows PC или с вашего ноутбука на другой ноутбук, не проходя сложный путь? В этой статье я расскажу вам, как я создал веб-приложение для обмена файлами с помощью vanilla javascript, Nodejs, express и библиотеки socket io.

Итак, сначала откройте терминал. Мы создадим папку проекта. Эта папка будет содержать клиентский и серверный код.

Давайте сначала создадим папку проекта.

mkdir file-share-app
cd file-share-app
mkdir public

Вход в полноэкранный режим Выход из полноэкранного режима

Далее инициализируем наш проект nodejs, установив необходимые модули, запустив проект:

npm init -y
npm install express socket.io

Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь мы готовы приступить к коду. В папке public создайте файлы index.html & client.js, затем добавьте этот код в index.html:

<!DOCTYPE html>
<html>
<head>
    <title>Socket io File sharing</title>
    <style>
        * {
            padding: 0;
            margin: 0;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif
        }

        #file-input {
            border: 2px solid #00000026;
            padding: 10px;
            border-radius: 10px;
        }

        #file-input:hover {
            background-color: #f1f1f1;
            cursor: pointer;
        }

        .wrapper {
            display: flex;
            align-items: center;
            justify-content: center;
            flex-direction: column;
            height: 100vh;
            /* background: #00e5ff; */
        }

        button {
            padding: 13px;
            background: black;
            border: none;
            width: 140px;
            border-radius: 10px;
            margin-top: 30px;
            color: #fff;
            font-weight: bold;
            cursor: pointer;
            transition: .3s linear;
        }

        button:hover {
            opacity: 0.5;
        }
    </style>
</head>

<body>
    <div class="wrapper">
        <h1>File share 🦄</h1><br><br>
        <input type="file" id="file-input">
        <button id="share-btn">Share this file 🚀</button>
        <div class="dynamic-content"></div>
    </div>

    <script src="/socket.io/socket.io.js"></script>
    <script src="client.js"></script>
</body>
</html>

Войти в полноэкранный режим Выйти из полноэкранного режима

Вы должны увидеть нечто подобное, когда запустите node index из терминала.

В файле client.js мы получим все необходимые html-элементы, а также инициализируем наше сокетное io-соединение, добавив const socket = io().

const fileInputElement = document.getElementById('file-input')
const shareButton = document.getElementById('share-btn')
const dynamicContent = document.querySelector('.dynamic-content')
const socket = io()

window.addEventListener('load', () => {
     // run on page load
})

function downloadFile(blob, name = 'shared.txt') {
     // force download received file
}

shareButton.addEventListener('click', async () => {
      // handle share button press
})

Вход в полноэкранный режим Выйти из полноэкранного режима

Откройте index.js из корневого каталога и добавьте этот код для создания нашего веб-сервера:

const path = require("path")
const http = require("http")
const express = require('express')

const app = express()
const server = http.createServer(app)

const port = process.env.PORT || 3000
const publicDirPath = path.join(__dirname, "/public")

app.use(express.static(publicDirPath))

server.listen(port, () => {
    console.log(`server running on port ${port}! 🦄`)
})

Вход в полноэкранный режим Выйти из полноэкранного режима

Это отобразит файл index.html при переходе на localhost://3000 из браузера.

Давайте инициализируем socket io с сервера. Поэтому в файле index.js добавьте эти строки:

const socketio = require('socket.io')
const io = socketio(server)


Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь давайте обработаем события сокета, когда пользователь подключается или отключается от сервера. По-прежнему в index.js:

io.on("connection", (socket) => {
    console.log('client connected 🎉', socket.id)

    socket.on('disconnect', () => {
        // execute callback when client disconnects from server
        console.log('client left the socket 😢', socket.id)
    })
})

Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь наш index.js должен выглядеть следующим образом:


const path = require("path");
const http = require("http");
const express = require('express');
const socketio = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = socketio(server);

const port = process.env.PORT || 3000;
const publicDirPath = path.join(__dirname, "/public");

app.use(express.static(publicDirPath));

io.on("connection", (socket) => {
    console.log('client connected 🎉', socket.id);

    socket.on('disconnect', () => {
        console.log('client left the socket 😢', socket.id);
    })
})

server.listen(port, () => {
    console.log(`Server is up on port ${port}!`);
})


Вход в полноэкранный режим Выход из полноэкранного режима

Теперь давайте начнем обмениваться файлами, обрабатывая ввод файла.

shareButton.addEventListener('click', async () => {

    if (fileInputElement.files.length === 0) {
        alert('Choose the file you want to send 📁')
        return;
    }

    let file = fileInputElement.files[0]
    let reader = new FileReader()

    reader.onload = () => {
       // reader is loaded and ready
    }

    reader.readAsArrayBuffer(file)
})

Вход в полноэкранный режим Выход из полноэкранного режима

Объект new FileReader() позволяет нашему приложению асинхронно считывать содержимое файла, выбранного из элемента ввода HTML. Функция `reader.readArrayAsArrayBuffer(file) возвращает частичные данные Blob, представляющие количество байт, загруженных в данный момент, в виде доли от общего количества.

Чтобы приложение работало, нам нужно дождаться загрузки объекта FileReader. Поэтому мы добавили функцию reader.onload. В reader.onload мы вызываем функцию, чтобы начать обмен файлом с сокетом.

`
reader.onload = () => {
let buffer = new Uint8Array(reader.result)
initFileShare({ filename: file.name, bufferSize: buffer.length }, buffer)10
}

`

Метод initFileShare принимает два аргумента: метаданные и буфер текущего чанка. Объект metadata принимает filename и bufferSize. Мы передаем bufferSize, чтобы мы могли проверить полный размер файла, а также отследить, был ли файл полностью получен. Вы можете отслеживать ход процесса обмена файлами, но это выходит за рамки данной статьи.

В функции initFileShare мы делаем то, что я обычно называю «разбивкой», когда мы разбиваем файл на более мелкие фрагменты необработанных двоичных данных размером 1 Мб. Почему мы выполняем разбивку? Socket io и nodejs по умолчанию зависят от памяти для запуска асинхронных процессов. И если вся память израсходована, то приложение аварийно завершает работу. Поэтому если мы отправим весь файл в необработанном большом виде, сервер будет перегружен и упадет.

Вы можете не разбивать файл на более мелкие буферы, если только вы передаете файлы размером менее 1 Мб.

В функции initFileShare:

`

function initFileShare(metadata, buffer) {
socket.emit(‘file-metadata’, metadata)

let chunkSize = 1024
let initialChunk = 0

while (initialChunk < metadata.bufferSize) {

    let filePiece = buffer.slice(0, chunkSize)
    console.log(metadata.bufferSize, filePiece.length)

    socket.emit('file-chunk', filePiece)

    initialChunk++;
}
Вход в полноэкранный режим Выйти из полноэкранного режима

}

`

Строка socket.emit('file-metadata', metadata) передает метаданные файла в WebSocket. Мы используем цикл for, чтобы выдать событие file-chunk для каждого полученного чанка. После получения фрагменты будут скомпилированы и преобразованы обратно в полный файл.

Откройте файл index.js:

`

io.on(«connection», (socket) => {
console.log(‘client connected 🎉’, socket.id);

socket.on('file-metadata', metadata => {
    socket.broadcast.emit('file-metadata', metadata)
})

socket.on('file-chunk', chunk => {
    socket.broadcast.emit('file-chunk', chunk)
})

socket.on('disconnect', () => {
    console.log('client left the socket 😢', socket.id);
})
Вход в полноэкранный режим Выход из полноэкранного режима

})

`

Здесь мы слушаем события file-metadata & file-chunk от клиента. Когда сервер получает такие события, мы используем метод socket.broadcast.emit для передачи данных всем подключенным клиентам, кроме отправителя. На этом сервер закончил работу. Поэтому давайте вернемся к client.js.

Мы слушаем события на стороне сервера, когда окно загружается window.addEventListener('load', () => {}), потому что socket io должен подключиться к серверу только один раз. Добавьте этот код для прослушивания событий сокета сервера:

`

window.addEventListener(‘load’, () => {
let newFile = {
buffer: [],
метаданные: null
}

socket.on('file-metadata', metadata => {
    // received metadata ⚡️
})

socket.on('file-chunk', chunk => {
    // received chunk ⚡️
})
Вход в полноэкранный режим Выйти из полноэкранного режима

})

`

socket.on() принимает пользовательское имя события и функцию обратного вызова, которая иногда содержит данные с сервера. В нашем случае событие file-metadata содержит метаданные (имя файла, bufferSize), а событие file-chunk содержит чанк. Теперь давайте прослушаем метаданные файла и вставим их в объект newFile.

`
socket.on(‘file-metadata’, metadata => {
// полученные метаданные ⚡️
newFile.metadata = metadata
newFile.buffer = []

 console.log('received metadata ⚡️')
Вход в полноэкранный режим Выход из полноэкранного режима

})

`

Когда мы получаем чанк:

`

socket.on(‘file-chunk’, chunk => {
/** Используйте dynamicContent.innerHTML для показа HTML-элемента пользователю при получении чанка.
Вы можете отслеживать, вычислять и отображать прогресс
dynamicContent.innerHTML = <b></b>
**/

    newFile.buffer.push(chunk)

    if (newFile.buffer.length === newFile.metadata.bufferSize) {
        // complete file has been received
        let receivedFile = new Blob(newFile.buffer)
        downloadFile(receivedFile, newFile.metadata.filename);

        newFile = {}
        alert('Yayy! File received 🎉')
    }
})
Вход в полноэкранный режим Выход из полноэкранного режима

`

Когда чанк получен, newFile.buffer.push(chunk) добавляет новый чанк в массив буфера newFile. Это делается для того, чтобы мы могли восстановить полный файл со всем необходимым.

Функция downloadFile() принимает блоб и имя файла. На данный момент полный файл получен и готов к загрузке. Поэтому давайте добавим код, который загрузит файл:

`

function downloadFile(blob, name = ‘shared.txt’) {

const blobUrl = URL.createObjectURL(blob);

const link = document.createElement("a");

link.href = blobUrl;
link.download = name;
document.body.appendChild(link);

link.dispatchEvent(
    new MouseEvent('click', {
        bubbles: true,
        cancelable: true,
        view: window
    })
);

document.body.removeChild(link);
Вход в полноэкранный режим Выйти из полноэкранного режима

}

`

Приведенная выше функция создает DOMString, содержащий URL, представляющий объект Blob файла, заданный в параметре. Создается невидимый якорный тег, содержащий Blob полученного файла. Затем мы принудительно щелкаем по тегу якоря с помощью события click MouseEvent. После этого тег якоря удаляется. Таким образом, когда весь файл получен, он автоматически загружается вместе с именем файла.

Заключительные слова

Вы можете разместить этот проект на Heroku или использовать инструмент localtunnel для получения временного веб-адреса для проекта. Вы можете добавить несколько интересных функций, например, объединение комнат или отображение индикатора прогресса при отправке или получении файлов.

Откройте localhost://3000 в двух вкладках и попробуйте отправить файл с одной 🦄🎉.

Резюме

Приятного обмена файлами. Надеюсь, вам понравилась эта статья 🔥🔥🔥.

Следуйте за мной на

Twitter 👉🏼 @langford_dev

YouTube канал 👉🏼 LangfordDev

Оцените статью
devanswers.ru
Добавить комментарий