Обработка изображений ReactJS — NodeJS

Доброго времени суток.

Разбор полетов провожу на Reactjs (сторона клиента) и Nodejs (сторона сервера).

Недавно в моем маленьком проекте встал вопрос, как легко и просто можно обмениваться изображениями по типу клиент — сервер.

Сегодня мы научимся отправлять бинарные данные (конкретно изображения ) со стороны клиента и обрабатывать их на сервере. Добро пожаловать в под кат.

Если ваше web-приложение — это соц.сеть или мессенджер или что-то подобное, то вам непременно придется работать с бинарными данными, в большинстве случаев — это обработка изображений. Об этом и пойдет речь.

Чтобы иметь общее представление, я буду толковать последовательно.

Ознакомимся со следующим


  1. Двоичная система счисления и десятичная система счисления
  2. Кодовая страница: ASCII, Unicode
  3. Бит и байт
  4. Формат hex
  5. Двоичный файл
  6. Буффер и потоки nodejs
  7. ArrayBuffer и Uint8Array
  8. FileReader

Двоичная система счисления и десятичная система счисления.


Это число, которое записывается с помощью двух символов: единиц и нулей.
Простым языком — это 2 (основание) в степени n (количество разрядов), например число 10001 имеет 5 разрядов:

4 разряд = 1; 2^4 = 16;
3 разряд = 0;
2 разряд = 0;
1 разряд = 0;
0 разряд = 1; 2^0 = 1;

1-true — производим расчет;
0-false — не производим расчет;

В итоге получаем 16 + 1 = 17;
Обратной операцией компьютер представляет данные в двоичном виде.

const num = 17; // десятичное число
const binNum = num.toString(2); // преобразование в двоичный вид
const initNum = parseInt(binNum, 2); // обратная операция

Десятичная система счисления — это число, которое записывается с помощью 10 символов, от 0 — 9. Мы используем их повседневно.

Дело в том, что компьютер живет своим представлением информации. Если мы пишем на родном языке или считаем в десятичной системе счисления, то для компьютера все это двоичное представление. Вы спросите: “Что значит двоичное представление?”. Как говорила моя учительница английского, “как слышится, так и пишется”. Компьютер представляет файлы, содержащие текст или иное, или какие-либо вычисления в двоичную систему счисления — это и есть двоичное представление. Языком машины число «17» является — 10001.

Кодовая страница: ASCII, Unicode


Возникает вопрос. Понятно с числами, но как же текст? Как машина будет преобразовывать текст в двоичный вид? Все просто. Для этого существуют кодировки типа ASCII или Unicode. Это простая таблица ( кодовая страница ), где имеется сопоставление нашему символу число, например символ “S” — это число 53 по таблице ASCII, далее компьютер представит число 53 в двоичном виде — 110101.
Что касаемо Unicode — это мощный стандарт, включающий в себя кодировку UTF-8, которая имеет большой спрос в web-пространстве и unix подобных системах. Принцип работы общий.

Бит и байт


Бит — это единица измерения информации компьютером. А мы уже знаем как компьютер представляет информацию. Да! Вы правы. Один бит равен 1 или 0. Для компьютера 1 или 0 — это что-то вроде транзистора, true или false. Теперь понять что такое байт совсем не трудно — это 8 битов, его еще называют октет ( типа состоит из 8 ).

Формат hex


Переходим к “hex” формату. Мы говорили о двоичной системе счисления, десятичной системе, помимо прочего существует и шестнадцатиричная система счисления, верстальщики поймут о чем я. Это число, которое записывается с помощью 16 символов, от 0 — 9 и от a-f. Навивается вопрос: “Зачем так все усложнять?”. Так как единицей памяти является 8-битный байт, все же значение его удобнее записывать двумя шестнадцатиричными цифрами. Также в стандарте Юникода номер символа принято записывать в шестнадцатиричном виде, используя не менее 4 цифр.
Данный формат работает следующим образом: в случае с 8 битным байтом, он делит 8 битов по полам, то есть 10101011 — 1010 | 1011, после преобразует каждую часть соответственно. 1010 — a, 1011 — b;

const num = 196; // десятичное число
const hexNum = num.toString(16); // преобразование в hex вид
const initNum = parseInt(hexNum, 16); // преобразование в десятичный вид - обратная операция

Двоичный файл


Переходим к следующему пункту, его еще называют бинарным файлом — это массив байтов. Почему двоичный? Байты состоят из битов, то есть символов двоичной системы счисления, отсюда и название двоичный. Под категорию двоичный файл подходят абсолютно все файлы, то есть файлы содержащие текст, изображения — все это двоичные файлы, отсюда двоичные данные, отсюда бинарные данные.

Нередко бинарные данные выводятся символами кодовой страницы ASCII, а также в формате hex, в частности изображения. Приведу пример. У нас есть следующее изображение:

Смотреть вложение


Его частичная визуализация представляется в виде hex-agent-smith

Смотреть вложение

Правая часть символов является символами таблицы ASCII, левая же часть как раз и есть hex формат, преобразованный от двоичного кода.

ArrayBuffer и Uint8Array


ArrayBuffer — объект для работы с бинарными данными на javascript. Фиксирует длину памяти для работы с бинарником. Например: new ArrayBuffer(256) — создаст массивоподобный объект размером в 256 байт, не больше — не меньше. На каждый байт приходится конкретная информация. ArrayBuffer не является массивом и методы массива к нему не применимы. Вы зададите вопрос, как же его использовать? Объект Uint8Array дает представление ArrayBuffer в беззнаковое 8-битное число, то есть от 0 — 255.

FileReader


FileReader? Это конструктор по созданию следующего:

<input type="file">

Что дальше?


Теперь рассмотрим прекрасный пример, демонстрирующий обработку изображения:

  1. Модуль ReactJS
    import React, {useState, useRef, useCallback} from 'react';
    import axios from 'axios';
    
    export const UPLOAD_AVATAR = 'http://localhost:8080/api/upload_avatar';
    
    function App() {
      // определим изменяемый ref для объекта FileReader
      const fileRef = useRef(null);
      const [ loading, setLoading ] = useState(false);
    
      const handleSubmit = useCallback( event => {
        event.preventDefault();
    
        const fetchData = async (uint8Array) => {
          try {
            const response = await axios({
              method: 'post',
              url: UPLOAD_AVATAR,
              data: [...uint8Array] // не отправляем в JSON, размер изображения увеличится
            });
    
            setLoading(false);
            console.log(response);
          } catch (e) {
            console.error(e.message, 'function handleSubmit')
          }
        };
    
        if(!fileRef.current) return void null;
    
        const reader = new FileReader();
        reader.onloadend = () => {
          const uint8Array = new Uint8Array(reader.result);
          setLoading(true);
          fetchData(uint8Array);
        };
    
    
        // рекомендованный метод
        reader.readAsArrayBuffer(fileRef.current[0]);
    
        // метод reader.readAsBinaryString(fileRef.current[0]) 
        // согласно MDN,
        // уже был однажды удален из File API specification, 
        // но после его вернули
        // в использование, но все же рекомендуют 
        // использовать readAsArrayBuffer
        // https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsBinaryString
      }, []);
    
      const nodeDom = (
        <form onSubmit={handleSubmit}>
          <div>
            <input
              onChange={e => fileRef.current = e.target.files}
              accept="image/*"
              type="file"
              id="button-file"
            />
          </div>
          <button type="submit">{loading ? 'Сохраняю...' : 'Сохранить'}</button>
        </form>
      );
    
      return nodeDom
    }
    
    export default App;
    
                   

  2. Модуль ExpressJS
    const express = require("express");
    const cors = require("cors");
    
    const crypto = require('crypto');
    const fs = require('fs');
    
    const PORT = 8080;
    
    const app = express();
    
    app.use(express.static("public"));
    app.use(express.json({ limit: "50mb" }));
    app.use(cors());
    
    app.post("/api/upload_avatar", (req, res) => {
      console.log(req.body);
      const randomString = crypto.randomBytes(5).toString('hex');
      const stream = fs.createWriteStream(`./public/images/${randomString}.png`);
    
      stream.on('finish', function () {
        console.log('file has been written');
        res.end('file has been written');
      });
    
      stream.write(Buffer.from(req.body), 'utf-8');
      stream.end();
    });
    
    app.listen(PORT, () => console.log(`Server running at ${PORT}`));
    
                  


Итог:


  • Для чего нужны кодировки
  • Общее представление бинарных данных
  • Рекомендуемый способ отправки байтов
  • Объект буфера
  • Объект FileReader
  • Простая реализация на хуках

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 33

    +3
    Вы забыли выкинуть jQuery React из этого поста.
      –2

      В web приложениях React занимает достойно положение, с этим надо смирится

        +7
        Вот и выросло новое поколение джейквери :) Видимо и правда надо просто смириться.
          0

          Эммм… а что значит jQuery? Его как бы нет в посте )). Не понимаю в чем проблема, если какая та часть в посте вам не понятна, то я могу по крайней мере постараться вам объяснить

            0
            но в посте есть axios
              0

              Да, есть )). Но это никак не связано с jQuery. Это гибкая либа для интерактивности с бэком.

                0

                А чем обоснован выбор axios? Он же огромный оверхед в мире с fetch и его полифиллом (на крайний случай)

              +6
              Когда-то было время, когда jQuery пихали во все и везде. jQuery подключали даже чтобы просто атрибут на странице поменять один раз. В статьях — аналогично. «Модуль для jQuery ...» — в котором от jQuery только $(document).ready().

              У вас сейчас аналогичная ситуация. «Обработка изображений ReactJS — NodeJS».
              Расскажите мне пожалуйста, как React относится к обработке изображений?
              Вы подключаете огромный шаблонизатор чтобы отобразить input-file на странице?

              Вам хватит обычного fetch и… да и все в общем-то :) Кода будет в половину меньше и никаких зависимостей.

              UPD: Я понимаю, что вы его используете потому что «это удобно и вообще че пристали». Просто когда новичек такое видит, он реально начнет на каждый пустяк пихать Реакт. И будет как с jQuery.
                0

                React не nodejs, там нет шаблонизатора. Есть формат jsx — функция js по созданию react элемента.
                Во первых jQuery — это не React. Это разные вещи. Почему react? Банально, на нем просто пишут все и я в том числе )). Я в принципе тяжело уже представляю фронт без норм фрэймворков и на автоматизме юзаю reactjs, хотя вы правы — это действительно можно было написать и на простом js и реализация не совсем отличалась бы от текущего вида, но зато мы имеем готовый модуль реакт хуки — это сэкономит время для реакт разработчиков

                  +5
                  Ну. В общем, запомните этот коммент и вернитесь сюда спустя лет пять разработки. Поймете о чем я говорю :)
      0
      Автор а почему не сделать обычную отправку файла через стандартный протокол и нативный fetch? зачем все эти танцы с бубном?
      var input = document.querySelector('input[type="file"]')
      
      var data = new FormData()
      data.append('file', input.files[0])
      
      fetch('/avatars', {
        method: 'POST',
        body: data
      })


      Тут можно винилу заменить на React, но зато на backend не нужно гемороиться, а заюзать готовые пакеты, типо express-fileupload
        –1

        Во первых, задача стояла не использовать сторонние либы для бэка )).
        Во вторых, вы пробовали сами отправить files[0] в formData без представления его байтов?

          0
          но вы же используете на express, тогда уже нужно юзать http.createServer…
          да и на сколько помню именно так он в formData отправляется
          developer.mozilla.org/en-US/docs/Web/API/FormData/Using_FormData_Objects
            –1

            Да, вы правы. Но проблема не заключалась написать create http server, проблема заключалась в ключевой части, загрузить изображение непосредственно нативными методами, поэтому этот момент можно отпустить )

        0
        Ну и да, тут нет ни строчки про обработку изображения, тем более не строчки про обработку на React, это очередной неудачный пример того, как просто загрузить картинку на сервер
          –1

          Очень сомневаюсь, что пример указанный вами работает )). Кинь ссылку на песочницу, гляну твой результат. Reactjs — как бы и есть js es6+. Обработка указана и на клиенте и на сервере )). Будьте повнимательнее, а ваш пример нерабочий ))

            +1
            developer.mozilla.org/ru/docs/Web/API/Fetch_API/Using_Fetch

            Раздел «Загрузка файла на сервер» :)
              +1
              боюсь что автор ожидает application/json на вход, вместо multipart/form-data
                +1
                Он вообще ничего не ожидает. Он просто пишет req.body в файл и все тут.
                В принципе, это будет работать при любых данных на входе :)
                  –1

                  Так ваша реализация рабочая? Или все таки нет? ))

                +2
                «Обработка» — это можно было бы назвать обработкой если бы изменяли изображение в canvas, накладывали фильтры, делали хоть что то с изображением, но вы всего лишь считали байтики перевели в нужный формат данные и отправили на сервер, это называется загрузка, но никак не обработка и на сервере вы получили данные, конвернули и записали в файлик, где тут обработка?
                  –3

                  Почитайте пожалуйста за обработчики во фронтэнде. Судя по вашему мнению на фронте «обработчик» не мыслимое понятие ))

              +1

              Добавьте в заголовок "Часть 1: Загрузка на сервер" и это решит большинство упоминаемых в комментариях проблем :)

                0

                Вообще это странное решение. В сторону велосипеда.
                Оптимально это Nginx или аналог который уже умеет оптимально отдавать изображения.
                Вам в БД нужна только ссылка или что-то на основании чего можно сгенерировать ссылку.
                Так же загрузка реализуется через Nginx, который затем вам отправляет путь по которому сохранено изображение.

                  –1

                  Да, все верно. Можно установить в качестве статики указанную директорию в конфигах nginx и пользоваться этим )). Но я предпочитаю пользоваться прокси сервером для выдачи статики на ноде. А так классный вариант, юзал такое

                    +1
                    Вам пытаются донести мысль про производительность, ну и сюрприз на 10к+ файлов в одной папке будет вам хорошим напоминании об этой статье
                  0
                  была задача загружать на сервер кучу фоток за раз (600++) с предварительным уменьшением до заданного размера (задавалось на странице, по умолчанию 600*700px). Для ускорения всего процесса фотки были сложены в зип архив 1,5++ Гиг. задача решилась без всякого fw, с применение только одной одной либы zip.js, для распаковки входного файла-архива… отправка простым XMLHttpRequest.
                  чистый js. серверная часть — любая способная принять XMLHttpRequest.
                  вот буквально по этому совету habr.com/ru/post/491548/#comment_21368528
                    –1
                    Круто )), если грузить все за раз >= 600 картинок.
                    А причем тут совет? )) У меня проект на react, в любом случае я бы реализовывал эту идею под свой проект. А во-вторых react — это и есть js )
                    0

                    Можно и так:


                    const url = '/upload-path';
                    const file = input.files[0];
                    
                    const xhr = new XMLHttpRequest();
                    xhr.open('POST', url);
                    xhr.setRequestHeader('Content-Type', file.type);
                    // обрабатываем xhr.onload, xhr.onerror
                    xhr.send(file);

                    Или через fetch:


                    fetch(url, {
                     method: 'POST',
                     headers: {
                       'Content-Type': file.type
                      },
                      body: file
                    })
                      0

                      Дополню свой комментарий. Поскольку передаются сырые данные прямо в теле, то для Node.js достаточно (нужно, конечно, еще добавить обработку ошибок и исключений, например, обрыв соединения) выполнить pipe для потоков запроса и файла:


                      req.pipe(writeFileStream);
                        –1
                        Ты избегаешь самую главную часть. Идея заключалась отправить все произвольными байтами ))
                          0

                          А зачем? В моем примере с фронта и так отправляются сырые данные, причем потоково. Вы же сначала грузите весь файл в память (а что если он весит 10 Гб?), а потом отправляете. На стороне Node.js опять же обрабатываете максимально неэффективно — сохраняете целым куском (stream.write(Buffer.from(req.body), 'utf-8')) и преимущества потоков не используются.


                          Разбивка на байты могла бы пригодиться для реализации возможности докачки, но есть же file.slice.

                    Only users with full accounts can post comments. Log in, please.