Node.JS: библиотека для модификации http ответов

Некоторое время назад писал сайт с бэкендом на Express/Node.JS. Возникла проблема с минификацией ответов. Нашел много готовых пакетов, но у всех была проблема — не минифицировался html после шаблонов. В итоге принял решение написать свой маленький и родной велосипед — библиотеку web-minify, позволяющую встроить хук перед отправкой клиенту и модифицировать ответ.

Установка пакета


npm i @dmitriym09/web-minify --save

Думаю, самое лучшее описание библиотеки для разработчика — пример кода=)

Пример


web-minify — middleware-функция:

const htmlminify = require('html-minifier').minify;

const csso = require('csso').minify;
const postcss = require('postcss');
const precss = require('precss');
const autoprefixer = require('autoprefixer');

const minify = require('web-minify');

app.use(minify([
  {
    contentType: /css/,
    minify: async (data, req, res) => {
      let resData = (await postcss([precss, autoprefixer]).process(data, { from: undefined })).css;
      
      resData = csso(resData).css;

      return resData;
    }
  }
]));

В данном примере будут перехватываются все ответы с Content-Type, содержащие подстроку «css». Тело ответа обрабатывается с помощью csso, postcss, precss, autoprefixer. В параметре contentType передается String (будет искаться вхождение String.prototype.indexOf()) или RegExp (RegExp.prototype.test()). Параметр minify — функция Function(data:String, req:Request, res:Response), должна возвращать String с новым телом или Promise, который в свою очередь разрешается String. При неотловленом исключении клиент получит ответ 500.

Как уже сказал, большинство существующих популярных библиотек с похожим функционалом хорошо минифицирует статические файлы. Однако минификация сгенерированных в коде (например html шаблонизатором) ответов не работает. Одна из проблем — ответ может отправляться частями, а для обработки обычно нужны полные данные. Соответственно нужно перехватывать все отправки пользователю, собирать и уже в конце обрабатывать и отсылать. Это нужно учитывать при использовании web-minify: тот терабайтный файл, который вы хотите отправить клиенту и который попадает под contentType, накапливаться в памяти.

Примеры


Минификация HTML с помощью html-minifier из юнит-тестов


const htmlminify = require('html-minifier').minify;
it('HTML', (done) => {
	const app = createServer([minify([
		{
			contentType: 'html', 
			minify: (data) => { 
				let res = htmlminify(data, {
					removeAttributeQuotes: true,
					collapseWhitespace: true,
					conservativeCollapse: false,
					decodeEntities: true,
					keepClosingSlash: false,
					preserveLineBreaks: false,
					preventAttributesEscapin,
					processConditionalComments: true,
					removeAttributeQuotes: true,
					removeComments: true,
					trimCustomFragments: true,
					useShortDoctype: true
	    		});
	   			return res;
	  		}
		}
	])], function(req, res) {
		res.setHeader('Content-Type', 'text/html; charset=utf-8');
	    res.end(`<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8">

</head>

<body>
<h1>Test</h1>

<p>Test</p>
</body>`);
	});


	request(app)
	  .get('/')
	  .set('Accept', 'text/html; charset=utf-8')
	  .expect('Content-Type', 'text/html; charset=utf-8')
	  .expect('<!doctype html><html lang=en><head><meta charset=utf-8></head><body><h1>Test</h1><p>Test</p></body></html>')
	  .expect(200)
	  .end(done)
});

Модификации JSON и кода ответа с возвратом Promise из юнит-тестов


it('JSON', (done) => {
	const app = createServer([minify([
		{
			contentType: /json/,
			minify: (data, req, res) => {
				return new Promise(function(resolve, reject) {
					try {
						res.statusCode = 456;
						let o = JSON.parse(data);
						o.dt = new Date('2018-09-28T11:05:13.492Z') 
						resolve(JSON.stringify(o))
					}
					catch(exc) {
						reject(exc)
					}
				})
				
			}
		}
	])], function(req, res) {
		res.setHeader('Content-Type', 'application/json; charset=utf-8');
	    res.end(JSON.stringify({a: 12}));
	});

	request(app)
	  .get('/')
	  .set('Accept', 'applicatio3n/json; charset=utf-8')
	  .expect('Content-Type', 'application/json; charset=utf-8')
	  .expect('{"a":12,"dt":"2018-09-28T11:05:13.492Z"}')
	  .expect(456)
	  .end(done)
});

Web-minify доступна на github и в npm под лицензией MIT.

Спасибо за внимание! Критика, предложения и комментарии приветствуются!
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 19

    +3
    Извините, за банальный вопрос, но зачем? Почему нельзя выполнить нужные операции один раз?
      0

      Ну статику конечно можно и нужно пожать 1 раз и раздавать. Но есть же и динамический контент. Например html, созданный шаблонами. Конечно можно и его жать сразу в шаблонизаторе, но есть такой подход.

        0
        Это должен делать какойнить дженкинс и складывать на сервер, зачем заставлять ноду это делать? зачем тянуть кучу лишних зависимостей в продакшин?
          0

          Как какойнить дженкинс будет обрабатывать динамически создаваемый контент и складывать на сервер?

            +1
            Извините если я не понял вашу идею, но тогда давайте разберемся, если у Вас полная динамика css/html то минификацию нужно делать сразу на момент сохранения данных, но никак ни на момент каждого запроса к серверу, да я понимаю что у вас там возможно есть кэш, но при каждом запросе проверять данные из кеша не айс когда можно сразу nginx натравить на ваши динамические html/css и все.
            Или же у вас css зависит от того какой пришел запрос?
              0

              Есть статика, как, наверное, у любого сайта. Они собирается webpack. Сами html страницы генерятся при запросе с помощью шаблонов, в проекте о котором и шла в частности используется ejs. Можно конечно заморочиться и сделать так, чтобы после шаблонизатора получался уже минифицированный ответ. Но там есть нюансы и на мой вкус лучше обрабатывать уже полностью собранный ответ. Кеш конечно используется — я ставлю перед node-приложением nginx. Вообще эта библиотека не обязательно должна использоваться для минификации чего-то. Она просто позволяет хукнуть ответ так, чтобы было полное тело и что то можно сделать с ним перед отправкой. Можно, например, вместо webpack собирать бандлы прям на приложении при запросе, а потом кешировать их уже на проксире. Конечно тут куча сложностей и в большинстве случаев так делать не надо, но я вполне могу представить себе кейс где бы это было хорошо. Ну например код после какого-либо конструктора сайта. Или же нужно менять то, что генерит какая-нибудь 3rd-либа без возможности влезть в нее. Или же у вас есть кеча inline-стилей, которые можно собрать во внешний бандл. Естественно я решал не каждодневную задачу — она не стоит перед всеми, раз нет в стандартной реализации. В общем я решал проблему, не нашел удобного мне способа в стандартной реализации, навелосипедил и решил выложить в opensource. Может быть кому-нибудь поможет когда-нибудь. Естественно нужно думать головой когда и как ее использовать, чтобы было лучше, а не хуже.

                0
                на практике пришли к выводу, что подобная оптимизация не входит в задачи приложения. остановились на PageSpeed, там оптимизация намного глубже чем просто пожать html/css/картинки.
                  0
                  А что с динамическим контентом? Как его жмете?
                    0
                    PageSpeed работает на уровне запросов, как прокси в nginx. Все равно как был получен контент, статический файл или на лету сгенерированный. Приложение отдает ответ на запрос, PageSpeed его обрабатывает, кеширует и т.д.
                      0
                      Ну да, это удобно. А какие есть ограничения? Сколько запросов максимум? Платно\бесплатно?

                      Когда используешь PageSpeed для статистики сайта то он вроде кешируешь на какое-то время — сразу после внесения изменений не видит их. Как это решено через их API? На что он ориентируется для кеша?

                      Но нужно учитывать, что не все ответы можно отдавать 3-ей стороне. Бывает содержатся и приватные данные. Конечно их можно выдрать и отправлять отдельным запросом.

                      Например, я воспользовался данной библиотекой для того, чтобы после после конструктора сайта, создающего довольно трешовый код, выдерать inline-стили и потом оптимизировать их. Что-то уникальное часто руками приходится писать.

                      В общем любая проблема решается 100500 способами и каждый разработчик должен сам думать. А наличие большого разнообразия возможностей для решения — это хорошо.

                        0
                        Несмотря на похожее название, между PageSpeed Insights (сервис оценки производительности страницы) и developers.google.com/speed/pagespeed/module достаточно большая разница. То, что я описываю, ставится на ваш локальный apache/nginx. Ни о api, ни о ограничении количества запросов речь не идет. Просто прокси + какая то собственная админка для управления (статистика, кеши и вот это все)

                        Относительно неплохо срабатывание не просто минимизация html, но и включение внешних javascript/css файлов непосредственно в страницу, сборка нескольких файлов в бандлы, оптимизация картинок и т.д. Более подробно лучше посмотреть в их документации.

                        А наличие большого разнообразия возможностей для решения — это хорошо.

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

                        Моя мысль заключается в том, что подобная оптимизация для серьезного продукта более уместна вне приложения, а именно настройками окружения.
        0
        Откройте для себя spa и ssr.
          –1
          Открыл уже давно — есть такие проекты у меня. Но считаю, что не серебрянная пуля и не панацея. Нужно исходить и задач и кейса. Обычно эти 3-х буквенные сокращения идут рядом с реактивностью, а она не всегда бывает нужна. Кроме того, не все серверные рендеры дают минифицированный html.
          +1
          Вместо того, чтобы экономить на спичках и удалять из HTML и JSON пробелы и кавычки, лучше сжать ответ с помощью gzip. К тому же так можно сжимать потоки и не надо накапливать весь ответ (тем более 1ТБ в памяти).

          Посмотрите например www.npmjs.com/package/compression
            0
            «Экономия на спичках» дает в некоторых ситуациях сжатие до 10%.Cжатый html — это еще и плюс для SEO. Кроме того, одно другому не мешает, а дополняет: можно сначала пожать код, а потом gzip использовать. Я кстати так и делаю. Только я ставлю перед node приложением кеширующий проксер (nginx например), который и gzip-ает ответ клиенту. Кавычки из JSON не удаляются, а то это будет уже невалидный json)
              +2
              Cжатый html — это еще и плюс для SEO

              Можете предоставить ссылку на пруф, что именно сжатый HTML влияет на СЕО?
              Где указано, что влияет именно HTML без лишних пробелов и кавычек, а не просто более оптимальный HTML без лишних тегов или более правильный семантически.

              Кроме того, одно другому не мешает, а дополняет: можно сначала пожать код, а потом gzip использовать.

              Давайте проведем эксперимент. Возьмем довольно большую страницу, например habr.com/post/425351 и сожмем ее по разному:
              • Исходный размер HTML: 582KB
              • Размер после html-minifier: 543KB (-7%)
              • Размер после html-minifier и gzip: 78KB (-87%)
              • Размер после одного gzip: 81KB (-86%)

              Т.е. при условии, что HTML сжимается с помощь gzip, минификация дает крайне минимальный эффект (1% всего) — вот это я и называю «экономия на спичках».

              Лучше процессорное время потратить на обработку другого запроса, чем минифицировать HTML (а тем более JSON)

              я ставлю перед node приложением кеширующий проксер (nginx например)

              Это, безусловно, правильное решение.
                0
                Можете предоставить ссылку на пруф, что именно сжатый HTML влияет на СЕО?
                Где указано, что влияет именно HTML без лишних пробелов и кавычек, а не просто более оптимальный HTML без лишних тегов или более правильный семантически.


                Не было такого утверждения. Было написано
                Cжатый html — это еще и плюс для SEO.

                Не раскрывалось в чем плюс.

                … минифицировать HTML (а тем более JSON)

                Где здесь минифицируется JSON??

                Лучше процессорное время потратить на обработку другого запроса

                И это лично ваше мнение. Очень сильно зависит от нагрузки и целей. Я думаю каждый разработчик сам должен принимать решение.

                Т.е. при условии, что HTML сжимается с помощь gzip, минификация дает крайне минимальный эффект (1% всего) — вот это я и называю «экономия на спичках».

                Опять все зависит от ситуации. Может кто-то хочет уместить ответ в минимальное количество tcp пакетов и 1% может повлиять на результат.

                Вообще здесь приведены абстрактные решения в вакууме для демонстрации функциональности. Например, я воспользовался данной библиотекой для того, чтобы после после конструктора сайта, создающего довольно трешовый код, выдерать inline-стили и потом оптимизировать их. Естественно нужно думать головой когда и как что использовать, чтобы было лучше, а не хуже. Каждый разработчик принимает решение в зависимости от многих критериев для свое ситуации. И я считаю, что лишний инструмент, расширяющий возможности — это хорошо.
                  +1
                  Не было такого утверждения. Было написано
                  «Cжатый html — это еще и плюс для SEO.»


                  Ok, раскройте, в чем именно плюс. И желательно с ссылкой на пруф.
                    0
                    немного притянутый за уши, но плюс в том что скорость загрузки страницы будет меньше, а это незначительно но влияет на SEO

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