Вы когда-нибудь хотели обмениваться файлами с вашего 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