Русский
Русский
English
Статистика
Реклама

Webpack

Минифицируем приватные поля в TypeScript. Доклад Яндекса

13.06.2020 14:36:10 | Автор: admin
Меня зовут Лёша Гусев, я работаю в команде разработки видеоплеера Яндекса. Если вы когда-нибудь смотрели фильмы или трансляции на сервисах Яндекса, то использовали именно наш плеер.

Я сделал небольшую оптимизацию размера бандла минификацию приватных полей. В докладе на Я.Субботнике я рассказал об использовании Babel-плагинов, трансформеров TypeScript и о том, насколько в итоге уменьшился размер продакшен-сборки проекта.


Конспект и видео будут полезны разработчикам, которые ищут дополнительные способы оптимизации своего кода и хотят узнать, как webpack, Babel и TypeScript могут в этом помочь. В конце будут ссылки на GitHub и npm.

С точки зрения структуры наш видеоплеер довольно типичный фронтенд-проект. Мы используем webpack для сборки, Babel для транспиляции кода, скин нашего плеера мы пишем на React, весь код написан на TypeScript.

Мы также активно используем опенсорсные библиотеки, чтобы реализовывать адаптивный видеостриминг. Одна из таких библиотека shaka-player, которая разрабатывается в Google, и нужна она для того, чтобы поддерживать в вебе адаптивный формат MPEG-DASH.

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



В процессе работы с этой библиотекой я познакомился с инструментом, который называется Google Closure Compiler. Наверняка многие из вас слышали про этот инструмент. Возможно, кто-то пользовался вот такой классной онлайн-страничкой, куда можно вставить кусок JavaScript-кода и минифицировать его с помощью Google Closure Compiler.



Кто-то, может быть, использовал плагин webpack-closure-compiler. Здесь на слайде сравнение webpack-closure-compiler с другими инструментами минификации webpack, и, как видно, closure-compiler по этому сравнению лидирует.



Почему он такой классный? Дело в том, что в closure-compiler есть так называемый advanced-уровень оптимизации. На этой страничке он кроется за переключалкой в radio button.

Что из себя представляют advanced-оптимизации и почему они такие классные? Это некоторый набор оптимизаций, которых нет в других инструментах. Рассмотрим некоторые из них.



Closure-compiler умеет инлайнить функции. Здесь он просто заинлайнил тело функции f и удалил объявление самой функции, так как она больше нигде не используется.



Он умеет удалять неиспользуемый код. Здесь объявлен класс A и класс B. По факту используется только один метод из класса A. Класс B был удален совсем. Из класса A method() был заинлайнен, и мы в итоге получили только console.log.



Closure-compiler умеет инлайнить и вычислять значения переменных на этапе сборки.



Еще он умеет минифицировать поля объектов. Здесь объявлен класс A со свойством prop, и после обработки closure-compiler свойство prop заменилось на короткий идентификатор a, за счет чего код стал меньше весить.

Это не все оптимизации. В статье можно подробно почитать, что еще может closure-compiler. Там довольно много чего крутого.

Когда я про это узнал, мне очень понравилось. Я захотел притащить все эти оптимизации к нам в проект. Что мне особенно понравилось, так это минификация полей объектов, а именно минификация приватных полей для TypeScript.



Если вы никогда не писали на TypeScript, то что такое приватные поля? Это такой синтаксический сахар, который просто удаляется при сборке, при компиляции TS-кода в JavaScript. Но если вы используете приватное поле за пределами класса, вы получите ошибку компиляции.

Почему мне понравилась идея минифицировать приватные поля?



У нас в проекте довольно много React-компонентов, написанных в ООП-стиле с классом. И есть TypeScript-код, в котором используются классы и приватные поля.

Давайте рассмотрим вот такой компонент. В нем есть приватное поле clickCount.



Сейчас при сборке и компиляции кода TypeScript оставляет название этого поля как есть, просто удаляет модификатор private. Было бы клево, если бы clickCount заменился на короткий идентификатор A.

Чтобы достичь этого, давайте попробуем использовать Closure Compiler в advanced-режиме как минификатор.

И тут можно столкнуться с проблемами. Давайте рассмотрим пример. Объявлен объект с полем foobar. Обратимся к этому полю. Здесь все хорошо. Closure Compiler отработает такой код корректно. Поле foobar будет переименовано.


Ссылка со слайда

Но если мы вдруг зачем-то будем обращаться к этому полю через строковой литерал, то после сборки получим no reference, ошибку в коде. Она связанна с тем, что в поле идентификатор foobar Closure Compiler переименует, а строковые литералы оставит как есть.


Ссылка со слайда

Следующий пример. Здесь мы объявляем метод у объекта, который внутри использует ключевое слово this. После сборки с помощью Closure Compiler и удаления мертвого кода, как видно на слайде, идентификатор this станет глобальным. Вы опять же получите ошибку в коде.

Примеры немножко утрированные, надеюсь, такие конструкции у себя в проектах вы не применяете. Тем не менее, есть более сложные случаи, когда advanced-оптимизации могут сломать ваш код. По ссылке вы можете посмотреть документацию Closure Compiler, какие ограничения он привносит на ваш код. Тем более нельзя гарантировать, что ваши внешние npm-зависимости после обработки Closure Compiler будут работать корректно.


Ссылка со слайда

Для нашего проекта это еще критичнее. Как я говорил, мы разрабатываем видеоплеер. Видеоплеер это встраиваемая библиотека. Если Closure Compiler переименует поле в нашей встраиваемой библиотеке, переименует публичный метод, то внешний код сломается и не сможет взаимодействовать с плеером.

Наверное, как-то нужно сказать Closure Compiler о том, что какие-то поля можно минифицировать, какие-то нельзя, какие-то, очевидно, публичные, какие-то приватные.

Но выходит, что advanced-оптимизации в общем случае не безопасны. Их можно сделать безопасными, если вы используете Closure Compiler на полную мощность.



Я немножко слукавил. Closure Compiler не просто минификатор, а целый комбайн. Он заменяет собой webpack, Babel и TypeScript. За счет чего и как ему это удается?


Ссылка со слайда

В Closure Compiler есть своя модульная система goog.provide, goog.require.


Ссылка со слайда

Есть своя транспиляция и вставка полифиллов для различных таргетов, для разных версий ECMASCRIPT.


Ссылка со слайда

Еще там есть свои аннотации типов. Только описываются они не как в TypeScript, а в JSDoc. Точно так же там можно пометить, например, модификаторы доступа public, private и подобные.



Если сравнивать микросистему webpack-Babel-TypeScript с Closure Compiler, то, на мой вкус, Closure Compiler проигрывает. У него чуть хуже документация, им умеют пользоваться меньше разработчиков. В целом не самый удобный инструмент.

Но я все-таки хочу оптимизации. Может, можно как-то взять Closure Compiler, взять TypeScript и объединить их?



Такое решение есть. Называется оно tsickle.


Ссылка со слайда

Это проект, который разрабатывается в Angular и занимается тем, что компилирует TypeScript-код в JS-код с аннотациями Closure Compiler.


Ссылка со слайда

Есть даже webpack loader, tsickle-loader называется, который внутри использует tsickle и заменяет собой tsickle loader. То есть он подгружает TypeScript-код в webpack и эмитит JavaScript с аннотациями. После чего можно запустить Closure Compiler как минификатор.



Проблема в том, что в README tsickle явно написано, что работа над проектом в процессе, и Google внутри его использует, но снаружи большого опыта использования этого проекта нет. И завязываться на отдельный компилятор от TypeScript не очень-то и хочется, потому что тогда вы будете отставать по фичам от TypeScript и не получать какие-то новые возможности.

Собственно, tsickle loader имеет ту же проблему, так как он основан на tsickle. В общем, это решение сейчас не выглядит как хорошее продакшен-решение.



Какие еще есть варианты? В TypeScript есть issue про предложение о минификации. Там идет обсуждение похожей реализации на Closure Compiler: оптимизировать код, используя знания о типах. Проблема в том, что этот issue открыт 15 июля 2014 года, там до сих пор ничего не происходит. Вернее, там происходит 145 комментариев, но результатов пока нет. Судя по всему, команда TypeScript не считает, что компилятор TypeScript должен заниматься минификацией. Это задача других инструментов.

Что делать? Получается, хорошего решения у нас нет. Что делает разработчик, когда его не устраивают существующие решения? Естественно, пишет свой велосипед. Если мы говорим о велосипеде, который как-то модифицирует исходный код, на ум сразу приходит Babel.



Не так давно в Babel появилась поддержка TypeScript. Существует babel/preset-ypescript, который добавляет в Babel возможность парсить TypeScript-код и эмитить JavaScript. Он делает это путем удаления всех TypeScript-модификаторов.



Мы, кстати, не так давно перешли с ts-TS loader на Babel с использованием babel/preset-typescript и этим сильно ускорили сборку. Наконец-то настроили конкатенацию модулей в webpack, сделали еще некоторые оптимизации и настроили разные сборки под ES5- и ES6-браузеры. Про это можно узнать подробнее из доклада моего коллеги.

Окей, давайте попробуем написать babel-plugin, который будет минифицировать приватные поля, раз Babel умеет работать с TypeScript.


Ссылка со слайда

Как Babel работает? На эту тему есть много хороших материалов, статей и докладов. Можно начать, например, с этого материала на Хабре. Я лишь бегло расскажу, как происходит процесс обработки кода через Babel.

Итак, Babel, как и любой транспойлер кода, сначала парсит его и строит абстрактное синтаксическое дерево, abstract syntax tree, AST. Это некоторое дерево, которое описывает код.



Давайте попробуем на коротком кусочке кода посмотреть, как строится AST. Наша маленькая программа состоит из двух выражений. Первое выражение это Variable Declaration, объявление перемены. Из чего оно состоит? Из оператора VAR на схеме это Variable Declarator. У оператора есть два операнда идентификатор A, мы создаем переменную A, и Numeric Literal, мы присваиваем им значение 3.

Второе выражение в программе это Expression Statement, просто выражение. Оно состоит из бинарного выражения, то есть операции, у которой есть два аргумента. Это выражение +, первый аргумент a, второй 5, числовой литерал. Примерно так строятся абстрактные статические деревья.



Если вы хотите подробнее в это окунуться или когда-нибудь будете писать свой плагин для Babel, вам очень сильно поможет инструмент AST Explorer. Это онлайн-приложение, куда вы можете просто скопировать ваш код и посмотреть, как строится для него абстрактное синтаксическое дерево.

Здесь есть поддержка Babel и TypeScript. Очень удобный, практически незаменимый инструмент, чтобы заниматься написанием плагинов для Babel.


Ссылка со слайда

Когда мы построили AST, мы его трансформируем. Трансформация это превращение одного AST в новое, измененное. Как происходит трансформация? Это тот самый процесс, который вы описываете с помощью настроек Babel в файлике .babelrc или где-то еще.

Вы задаете список плагинов, которые вы трансформируете в ваше AST дерево.



Что такое плагин? Это просто функция, которая имплементирует паттерн Visitor. Babel обходит AST в глубину, в том порядке, как указано на схеме на слайде. Babel вызывает функцию вашего плагина, который возвращает объект с некоторыми методами.

В зависимости от того, в каком узле мы сейчас находимся, вызывается соответствующий метод объекта, который вернул плагин. Для идентификатора вызовется Identifier, для строки вызовется StringLiteral и т. д.

Подробно об этом можно узнать из документации Babel и воспользоваться AST Explorer, чтобы понять, какие операции в коде каким узлам в AST- дереве соответствуют.


Ссылка со слайда

Когда мы вызвали метод на узле, узел можно модифицировать, чтобы трансформировать AST-дерево. После этого мы получаем новое AST. Тогда Babel запускает генерацию и эмитит из AST-дерева итоговый код. Примерно так выглядит вся схема работы Babel и, на самом деле, практически любого другого инструмента, который занимается транспиляцией кода.

Давайте напишем плагин, который модифицирует приватные поля. Спойлер: у меня ничего не получилось. Расскажу, почему.

Как будет работать наш плагин? Давайте возьмем какой-нибудь класс, в котором есть приватное поле. Наш плагин Visitor будет заходить во все узлы AST-дерева, соответствующего классу, и в MemberExpression. Это узел, соответствующий операции доступа к полю в объекте.


Ссылка со слайда

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


Ссылка со слайда

Для этого нужно подняться вверх по AST-дереву и найти декларацию этого поля. Тут нам повезло: она имеет модификатор private, значит, можно переименовать это поле.


Ссылка со слайда

Магия! Все работает, классно. Плагин готов.

Так я думал, пока не начал активнее его тестировать.


Ссылка со слайда

Рассмотрим пример. Тут мы добавили в наш класс еще один метод, внутри которого обращаемся к приватному полю не this, а другого инстанса этого класса. Это валидный код, мы обращаемся к приватному полю внутри класса, здесь все ок.

Но мой плагин при обработке этого кода делал вот такое. Почему так происходило? Потому что я сделал так, что мы ищем доступы к объекту this. Если же мы обращаемся к другому объекту, эти узлы AST-дерева мы не рассматриваем.

Окей, можно написать тут костыль, который будет рассматривать все MemberExpression и искать, а в данном случае пытаться искать, поднимаясь вверх по AST-дереву, декларацию идентификатору foo. Здесь это легко, она описывается в теле функции, в заголовке функции. Можно понять, что у нее стоит тип foo, значит, это поле тоже нужно переименовать.

Звучит как какой-то костыль, будто нам не хватает информации о знании типа.


Ссылка со слайда

Рассмотрим еще один пример. Здесь мы this присваиваем переменную и обращаемся к полю bar из этой переменной. Это тоже валидный код. Но в итоге я получал такое. Здесь тоже нужен костыль, который будет разбирать такие обращения, искать, что this foo на самом деле this, что у него тип foo. И в этом случае bar нужно точно так же переименовать.

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

Я расстроился и уже было похоронил эту идею.



Но потом я вернулся в тот долгий тред про минификацию в TypeScript и увидел там комментарий Евгения Тимохова.



Он писал, что реализовал TypeScript в трансформер, который занимается минификацией приватных полей. И этот трансформер вроде бы даже работает.



У Евгения немножко другой подход. В своем трансформере он не переименовывает поля полностью, как я хотел, а добавляет к ним префикс, нижнее подчеркивание private нижнее подчеркивание.



Чтобы потом минифицировать такие поля, он предлагает использовать uglifyJS или terser, то есть uglifyJS с поддержкой ES6, и настроить минификацию полей, название которых соответствует регулярным выражениям.

Почему трансформер Евгения работает, а подход, который выбрал я, ни к чему не привел?


Ссылка со слайда

Тут нам потребуется немножко поговорить о том, как работает TypeScript. TypeScript это ведь тоже транспойлер. Он занимается тем, что берет TypeScript-код и парсит его. Но вместе с парсингом TypeScript еще запускает Type Checker. И при построении AST-дерева TypeScript обогащает его информацией о типах идентификаторов. Это как раз та самая информация, которой мне не хватало в моем Babel-плагине.

Дальше компилятор TypeScript может трансформировать AST-дерево точно так же, как Babel. Построить новое AST-дерево и эмитить из него JavaScript-код.

На этапе трансформации можно подключить кастомный трансформер. Это точно такой же плагин, точно такая же сущность, как плагин Babel, только для TypeScript. Она использует такой же паттерн Visitor и реализует примерно те же идеи.

Как это все использовать? Проблема в том, что в CLI компилятора TypeScript нет возможности подключать кастомные трансформации. Если вы хотите такое делать, вам потребуется пакет ttypescript.


Ссылка со слайда

Это не опечатка, а обертка над компилятором TypeScript, которая позволяет в настройках компилятора в tsconfig указать возможность использовать кастомную трансформацию. Здесь кастомная трансформация будет просто браться из node_modules.


Ссылка со слайда

Такая фича есть и в ts-loader. Там тоже можно задать функцию getCustomTransformers, которая позволит вам применить кастомный трансформер на этапе сборки.



Когда я решил попробовать этот подход, то столкнулся с проблемой. У нас-то в проекте используется Babel и babel/preset-typescript. Как вы помните из рассказа, мы на него переехали из ts-loader и получили кучу профита, сделали кучу оптимизаций. Откатываться обратно и терять все это мне не хотелось.

Окей, будем делать свой велосипед еще раз. Как выглядит сейчас пайплайн сборки в моем проекте? Мы подгружаем TypeScript-код в Babel loader и эмитим из него JS. Тут мне нужна сущность, которая перед Babel позволит запускать TypeScript-трансформер.

В Babel этого сделать нельзя, потому что он не запускает компилятор TypeScript. Как я говорил, он просто вырезает модификаторы TypeScript из кода.


Ссылка со слайда

Идею такой сущности я подсмотрел в проекте react-docgen-typescript-loader. Это такой loader для webpack, который пригодится, если вы используете Storybook. Storybook инструмент, который позволяет строить визуальные гайды и документацию к вашим React-компонентам.



Чем занимается этот loader? Он подгружает TypeScript-код, обрабатывает его и эмитит TypeScript-код с дополнительными полями у React-компонентов. Поля называются docgenInfo, и в них содержится информация для Storybook, чтобы построить документацию к React-компоненту, используя не propTypes, а аннотации TypeScript.

Потом этот код, заэмиченный в react-docgen-typescript-loader, как-то обрабатывается. Например, с помощью TS loader и Babel. В итоге, когда он попадает в Storybook, тот успешно строит документацию по полям docgenInfo.

Мне нужна похожая штука. Мне нужен webpack loader. Как это сделать?


Ссылка со слайда

Webpack loader это просто функция. Она принимает исходный код файла в виде строки и возвращает исходный код, тоже может его как-то модифицировать.

Здесь на слайде очень глупый loader, который занимается тем, что все ваши файлы превращает в код, содержащий console.log(Hello World!).


Ссылка со слайда

Loader может быть и синхронный. Можно получить callback, сделать асинхронную операцию. Вернее, что-то прочитать с диска или сделать что-то подобное и вызвать callback с новым модифицированным source.



Какой пайплайн должен быть у меня? Мне нужен loader, который подгружает TypeScript, запускает на нем кастомный трансформер. Я хочу применить трансформер, который минифицирует приватные поля и эмитит TypeScript.

Дальше я смогу его обработать с помощью Babel loader, как я делал это сейчас, и эмитить JS. А нашлепка из моего кастомного loader будет опциональной. Если что-то пойдет не так, я всегда смогу ее отключить, и максимум, что я здесь потеряю, минификацию приватных полей. И это не потребует от меня перестройки всего остального пайплайна сборки.

Окей, мы разобрались, как писать loader. Функция, которая обрабатывает source. Теперь нужно понять, как применить на наш файл кастомную TypeScript-трансформацию.


Ссылка со слайда

Для этого нам потребуется TypeScript Transformation API как раз тот самый программный API, который позволяет обработать исходный код на TypeScript, применить к нему трансформацию и заэмитить TypeScript-код. Это то, что нужно моему loader.

Примерно как это работает? Сначала нужно получить TS program, это объект, который содержит коллекцию файлов и настройки компилятора TypeScript. Нам нужно распарсить исходный файл и получить для него AST. Потом мы трансформируем это дерево и подключаем здесь myCustomTransformer, нашу кастомную трансформацию. И получаем в переменной result новое AST. Дальше мы его можем сериализовать обратно в TypeScript-код. Этим занимается компонент printer.

Кажется, ничего не мешает использовать это в webpack loader. Единственная проблема: документация по Transformation API не очень хорошая. И вообще, документация по внутренним сущностям компилятора в TypeScript сильно проигрывает аналогичной документации Babel. Но если вы захотите окунуться в это, начать можно с пул-реквеста в репозитории в TypeScript, где Transformation API сделали публичной.



Итак, что в итоге делает мой loader? Подгружает TypeScript-код и с помощью TypeScript Transformation API применяет на него кастомную трансформацию. Эмитит уже модифицированный TypeScript-код обратно. Дальше я скармливаю его Babel, который эмитит JavaScript.

Итоговую реализацию loader я выложил в npm, можно посмотреть исходный код на GitHub и даже подключить и использовать в вашем проекте:

npm install -D ts-transformer-loader

Всю эту прекрасную конструкцию мы даже покатили в продакшен.



Какой профит дала вся эта возня? Сырой непожатый код нашего бандла уменьшился на 31 килобайт, это почти 5%. Результаты в gzip и brotli не такие классные, потому что код и повторяющиеся идентификаторы там и так хорошо сжимаются. Но выигрыш порядка 2%.

Уменьшение непожатого кода на 5% не очень крутой выигрыш. Но 2% в минифицированном коде, который вы гоняете по сети, можно даже заметить на мониторингах.

Вот ссылка на мои заметки. Спасибо.
Подробнее..

Из песочницы Module Federation в Webpack 5, плагин для обмена модулями между Javascript приложениями, описание и пример

14.06.2020 18:20:27 | Автор: admin
В пятой версии сборщика Webpack появился набор плагинов для обмена модулями между Javascript приложениями.

Эта статья краткое описание и пример на основе двух небольших приложений построенных на фреймворке ReactJS.

Плагин Module Federation позволяет приложению экспортировать один или несколько модулей в отдельный JS файл. Отличный способ строить микрофронтенд приложения. Сторонние приложения могут импортировать себе готовые модули, это могут быть например реакт компоненты. Причём, импорт зависимостей Webpack берёт на себя. Отличие от NPM в том, что импорт в runtime.



Приложение-источник в конфиге webpack явно указывает:

  • модули для экспорта
  • имя файла экспорта например export.js
  • зависимости, которые экспортировать не нужно например export.js

В HTML приложения-получателя импортируется JS файл приложения-источника как обычный JS скрипт:

<script src="http://personeltest.ru/away/source-app.com/export.js"> 

А в webpack конфиге, указывается какие модули брать из файла.

Вот как это выглядит


Приложение-источник должно прописать экспорт в настройках webpack.config.js
В настройках плагина указываем название экспортируемого контейнера, какие модули в него войдут и как будет называться файл.

пример конфига
const HtmlWebpackPlugin = require('html-webpack-plugin');const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin'); module.exports = { ... plugins: [   new ModuleFederationPlugin({     name: 'home',     library: { type: 'var', name: 'home' },     filename: 'export.js',     exposes: {       ProductCarousel: './src/ProductCarousel'     },     shared: ['react', 'react-dom', '@material-ui/core', '@material-ui/icons']   }), ]};

Приложение-получатель

  • В index.html импортирует js файл приложения-источника

    пример
    <!DOCTYPE html><html lang="en"> <head>   <script src="http://personeltest.ru/away/source-app.com/export.js"></script>
    

  • В webpack.config.js, в настройках плагина указываем название нужного контейнера, плагин сам найдёт его в JS файле

    пример конфига
    const HtmlWebpackPlugin = require('html-webpack-plugin');const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin'); module.exports = {... plugins: [   new ModuleFederationPlugin({     remotes: {home: 'home' } //указываем контейнер   }), ]};
    

  • В JSX импортируем модуль из нужного контейнера, как будто это обычный локальный модуль

    пример кода
    import React from 'react';import ProductCarousel from 'home/ProductCarousel'; function App() { return (     <ProductCarousel /> );} export default App;
    


Вот более подробный пример со ссылкой на репозиторий


  1. Склонируйте репо github.com/jherr/wp5-intro-video-code
  2. Установите зависимости и запустите проект

    yarn installyarn start
    
  3. Запустятся три простых реакт приложения на разных портах
    home (packages/home) localhost:3001/
    search page (packages/search) localhost:3002/
    nav (packages/nav) localhost:3003/

  4. Обратите внимание на вебпак в home: packages/home/webpack.config.js, там уже настроен экспорт компонента ProductCarousel

    new ModuleFederationPlugin({     name: 'home',     library: { type: 'var', name: 'home' },     filename: 'remoteEntry.js', //в этом файле будет весь экспорт приложения для внешних получателей     remotes: {       nav: 'nav'     },     exposes: {       ProductCarousel: './src/ProductCarousel'  //экспортируем один модуль, назовём его ProductCarousel     },     shared: ['react', 'react-dom', '@material-ui/core', '@material-ui/icons'] //это должно быть у получателя   }),
    

    Вот как выглядит этот компонент в приложении home (я обвёл его красной рамкой)


  5. Теперь импортируем этот компонент в приложении SearchPage

    1. В packages/search/public/index.html добавьте импорт JS файла который экспортирует home, он называется remoteEntry.js (обратите внимание на порт 3001 это порт приложения home)

      <!DOCTYPE html><html lang="en"> <head>   <script src="http://personeltest.ru/away/localhost:3003/remoteEntry.js"></script>   <script src="http://personeltest.ru/away/localhost:3001/remoteEntry.js"></script> </head> <body>   <div id="root"></div> </body></html>
      
    2. В настройках вебпак опишите контейнер который мы импортируем. Для этого в remotes просто добавьте название контейнера который вы импортируете, ModuleFederation сам найдёт его. (На самом деле для импорта никакие другие настройки плагина для импорта не нужны, вы можете удалить все настройки кроме remotes и импорт останется рабочим)

      module.exports = { ... plugins: [   new ModuleFederationPlugin({     name: 'search',     library: { type: 'var', name: 'search' },     filename: 'remoteEntry.js',     remotes: {       nav: 'nav',       home: 'home'     },     exposes: {     },     shared: ['react', 'react-dom', '@material-ui/core', '@material-ui/icons']   }),
      
    3. Теперь просто втавьте этот компонент в коде SearchPage, packages/search/src/App.jsx

      import ProductCarousel from 'home/ProductCarousel';function App() { return (   <Container fixed>     <CssBaseline />     <Header />     <Typography variant="h3">       Search Page.     </Typography>     <ProductCarousel />   </Container> );}
      

  6. Остановите скрипт yarn и запустите заново, что бы изменения вебпак вступили в силу. Откройте приложение SearchPage и обратите внимание что там появилась карусель из приложения home localhost:3002/
    Было Стало

Полезные ссылки


Подробнее..

Генерация вспомогательных файлов реэкспорт, экспортный объект, валидаторы из моделей можно ли подружить с Webpack?

18.06.2020 21:44:10 | Автор: admin

При разработке SPA довольно много времени уходит на работу с импортом и экспортом различных файлов, а также на создание валидационных схем. Эти задачи достаточно просто автоматизируются, но, как это обычно бывает, "есть нюансы" попробуем разобраться.


За основу проекта возьму код из этой статьи, так как оформляю несколько текстов в виде более-менее связанного цикла.


Про реэкспорт и главные файлы компонентов


ES6-синтаксис импорта файлов позволяет делать это вот так (разумеется, предпочтительнее именованные деструктурируемые импорты):


import { ComponentOne } from 'someFolder/ComponentOne/ComponentOne.tsx';import { ComponentTwo } from 'someFolder/ComponentTwo/ComponentTwo.tsx';

В этом коде два довольно назойливых недостатка необходимость прописывать путь вплоть до файла (получается семантический дубляж в данном случае и проникновение во внутреннюю реализацию, а именно в структуру и именование файлов внутри компонента) и необходимость писать 2 отдельных импорта, что приводит к раздуванию файла. Первая решается указанием на главный файл компонента двумя господствующими подходами:


  • создание в каждой папке компонента файла index.ts с содержанием export * from './ComponentOne.tsx';. Недостатки файл проходит через все стадии компиляции, увеличивая время сборки, включается в бандл, увеличивая размер, и создает дополнительную нагрузку на компилятор (Typescript в данном случае). Также быстрый переход в IDE, как правило, ведет на этот индексный файл, и приходится "проваливаться" дальше, а в списке открытых файлов копятся одинаковые по названию и бесполезные для разработки index.ts. Иногда в этот же файл записывают реэкспорты других файлов, кроме главного но это приводит лишь к путанице с другим типом файлов, о котором расскажу дальше.
  • создание в каждой папке компонента файла package.json с содержанием { "main": "ComponentOne.tsx", "types": "ComponentOne.tsx" }. Это явно технический файл, который фактически никогда не открывается и не мешает целевой разработке, при этом позволяя сохранить семантичные названия файлов. Недостаток только один watch-режим Webpack не умеет "на лету" подхватывать этот файл при добавлении в папку, требуется ручной перезапуск сборки.

Какой бы способ вы ни выбрали, импорты сокращаются до следующих:


import { ComponentOne } from 'someFolder/ComponentOne';import { ComponentTwo } from 'someFolder/ComponentTwo';

Следующий шаг создание реэкспортного файла. В папке someFolder тоже создается главный файл компонента (который на этот раз является реэкспортным) и указывающий на него файл по схеме выше. Для данного типа файлов я предпочитаю выбирать названия по схеме _someFolder.ts нижнее подчеркивание одновременно позволяет ему быть всегда наверху списка и семантически отделяет от других файлов (например, нескольких десятков файлов утилит в папке utils) или папок, указывая на его особое назначение и то, что руками трогать не стоит (у javascript-разработчиков ввиду отсутствия private-переменных в классах издавна есть привычка "околоприватные", "технические" функции называть, начиная со знака подчеркивания, так что к аргументам добавляется еще и "привычность"). Содержание в данном случае будет следующим:


export * from './ComponentOne';export * from './ComponentTwo';

Таким образом, первоначальные импорты можно сократить до


import { ComponentOne, ComponentTwo } from 'someFolder';

Следует быть немного более внимательным при таких импортах если их использовать внутри самих компонентов типа ComponentOne, то возникнет циклическая зависимость, что приведет к неявным багам, поэтому внутри папки взаимные импорты должны быть либо относительными, либо по несокращенной схеме.


Генерация реэкспортных файлов


Так как редактировать и синхронизировать с содержанием папки подобные файлы быстро надоедает, можно создать утилиты для генерации. Выглядеть это будет следующим образом:


webpack-custom/utils/generateFiles.ts
import fs from 'fs';import path from 'path';import chalk from 'chalk';import { ESLint } from 'eslint';import { env } from '../../env';import { paths } from '../../paths';const eslint = new ESLint({  fix: true,  extensions: ['.js', '.ts', '.tsx'],  overrideConfigFile: path.resolve(paths.rootPath, 'eslint.config.js'),});const logsPrefix = chalk.blue(`[WEBPACK]`);const pathsForExportFiles = [  {    folderPath: path.resolve(paths.sourcePath, 'const'),    exportDefault: false,  },  {    folderPath: path.resolve(paths.validatorsPath, 'api'),    exportDefault: true,  },];type TypeProcessParams = { changedFiles?: string[] };class GenerateFiles {  _saveFile(params: { content?: string; filePath: string; noEslint?: boolean }) {    const { content, filePath, noEslint } = params;    if (content == null) return false;    const oldFileContent = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : '';    return Promise.resolve()      .then(() => (noEslint ? content : this._formatTextWithEslint(content)))      .then(formattedNewContent => {        if (oldFileContent === formattedNewContent) return false;        return fs.promises.writeFile(filePath, formattedNewContent, 'utf8').then(() => {          if (env.LOGS_GENERATE_FILES) console.log(`${logsPrefix} Changed: ${filePath}`);          return true;        });      });  }  _excludeFileNames(filesNames, skipFiles?: string[]) {    const skipFilesArray = ['package.json'].concat(skipFiles || []);    return filesNames.filter(      fileName => !skipFilesArray.some(testStr => fileName.includes(testStr))    );  }  _formatTextWithEslint(str: string) {    return eslint.lintText(str).then(data => data[0].output || str);  }  generateExportFiles({ changedFiles }: TypeProcessParams) {    const config =      changedFiles == null        ? pathsForExportFiles        : pathsForExportFiles.filter(({ folderPath }) =>            changedFiles.some(filePath => filePath.includes(folderPath))          );    if (config.length === 0) return false;    return Promise.all(      config.map(({ folderPath, exportDefault }) => {        const { base: folderName } = path.parse(folderPath);        const generatedFileName = `_${folderName}.ts`;        const generatedFilePath = path.resolve(folderPath, generatedFileName);        return Promise.resolve()          .then(() => fs.promises.readdir(folderPath))          .then(filesNames => this._excludeFileNames(filesNames, [generatedFileName]))          .then(filesNames =>            filesNames.reduce((template, fileName) => {              const { name: fileNameNoExt } = path.parse(fileName);              return exportDefault                ? `${template}export { default as ${fileNameNoExt} } from './${fileNameNoExt}';\n`                : `${template}export * from './${fileNameNoExt}';\n`;            }, '// This file is auto-generated\n\n')          )          .then(content =>            this._saveFile({              content,              filePath: generatedFilePath,              noEslint: true,            })          );      })    ).then(filesSavedMarks => filesSavedMarks.some(Boolean));  }  process({ changedFiles }: TypeProcessParams) {    const startTime = Date.now();    const isFirstGeneration = changedFiles == null;    let filesChanged = false;    // Order matters    return Promise.resolve()      .then(() => this.generateExportFiles({ changedFiles }))      .then(changedMark => changedMark && (filesChanged = true))      .then(() => {        if (isFirstGeneration || filesChanged) {          const endTime = Date.now();          console.log(            '%s Finished generating files within %s seconds',            logsPrefix,            chalk.blue(String((endTime - startTime) / 1000))          );        }        return filesChanged;      });  }}export const generateFiles = new GenerateFiles();

Схема работы следующая: запускается generateFiles.process({ changedFiles: null }), в который можно передать либо null (в этом случае будут перегенерированы все файлы), либо массив путей изменившихся файлов и тогда будут созданы реэкспортные файлы только в папках с изменившимися файлами. Из списка файлов внутри реэкспортного будет исключен он сам + package.json, в базовом случае этого достаточно. Я также сделал поддержку не только "общего" реэкспорта в виде export * from './MyComponent', но и только дефолтного экспорта в виде именованного export { default as MyComponent } from './MyComponent', это пригодится дальше.


Следующим шагом компонент необходимо отформатировать в соответствии с текущими настройками ESLint и Prettier для этого у них есть node.js интерфейс, однако его использование довольно затратно по времени на моей машине занимает в районе 0.1 секунды, а это довольно существенно по моим меркам, поэтому реимплементировал форматирование вручную, расставив символы переноса строк таким образом, время генерации десятка файлов сократилось до тысячных долей секунды. Также хотел бы обратить внимание, что если контент файла не изменился, то я не пересоздаю файл, так как это повлекло бы за собой срабатывание вотчеров (например, пересборку Webpack).


Генерация экспортных объектов


Иногда удобнее генерировать один именованный экспорт вместо перечисления всех файлов, особенно если требуется специфичная обработка названий. Я использую такую схему для работы с ассетами, чтобы код выглядел вот так:


// icons.tsxexport const icons = {  arrowLeft: require('./icons/arrow-left.svg'),  arrowRight: require('./icons/arrow-right.svg'),};// MyComponent.tsximport { icons } from 'assets/icons';import { icons } from 'assets'; // Либо сокращенно с реэкспортным файлом<img src={icons.arrowLeft} />

Подобная схема удобнее, чем import { arrowLeft } from 'assets/icons';, так как в данном случае названия лучше не деструктурировать, чтобы не путать с другими сущностями на странице. Таким образом, в файл генератора добавится метод, похожий на предыдущий:


webpack-custom/utils/generateFiles.ts
const pathsForAssetsExportFiles = [  {    folderPath: path.resolve(paths.assetsPath, 'icons'),    exportDefault: false,  },  {    folderPath: path.resolve(paths.assetsPath, 'images'),    exportDefault: true,  },];class GenerateFiles {  generateAssetsExportFiles({ changedFiles }: TypeProcessParams) {    const config =      changedFiles == null        ? pathsForAssetsExportFiles        : pathsForAssetsExportFiles.filter(({ folderPath }) =>            changedFiles.some(filePath => filePath.includes(folderPath))          );    if (config.length === 0) return false;    return Promise.all(      config.map(({ folderPath, exportDefault }) => {        const { base: folderName, dir: parentPath } = path.parse(folderPath);        const generatedFileName = `${folderName}.ts`;        const generatedFilePath = path.resolve(parentPath, generatedFileName);        return Promise.resolve()          .then(() => fs.promises.readdir(folderPath))          .then(filesNames => {            const exportObject = this._createExportObjectFromFilesArray({              folderName,              filesNames,              exportDefault,            });            return `// This file is auto-generated\n\nexport const ${folderName} = ${this._objToString(              exportObject            )}`;          })          .then(content =>            this._saveFile({              content,              filePath: generatedFilePath,              noEslint: true,            })          );      })    ).then(filesSavedMarks => filesSavedMarks.some(Boolean));  } }

В данном случае отличие в том, что используется require-синтаксис и отсутствует префикс _ у сгенерированного файла, так как этот файл создает новую сущность в виде объекта с преобразованными ключами, соответственно не является техническим, и в него вполне можно "проваливаться" для изучения набора возможных значений. А то, что он является автоматически генерируемым просто подчеркивается комментарием в самом верху файла.


Думаю, тем, кто работал с Webpack пришла в голову мысль почему для этих задач не использовать require.context, чтобы "на лету" собирать подобные файлы без отдельной генерации. Ответ прост TS, IDE и разработчик не могут узнать, что же собрал этот самый require.context, соответственно не будет подсказок, автодополнений, быстрых переходов, проверки типов и т.п. так что этот вариант не рекомендую рассматривать.


Генерация валидаторов из Typescript-моделей


Как я люблю говорить задача тривиальная, прочитать файл ts-компилятором и заменить типы на функции какой-либо библиотеки для проверки значений. В итоге хотелось бы 1 раз написать модель, и получить файл с валидационной функцией в отдельной папке. С этой задачей справится вот эта небольшая утилита, однако она принимает параметром только 1 файл за раз, создавая каждый раз новый инстанс компилятора соответственно на ровном месте для десятка файлов будет работать несколько секунд. Компилятор при этом умеет работать с массивами файлов, так что для эффективной работы нужно форкнуть эту утилиту и заменить метод компиляции на следующий:


  public static compile(    filePaths: string[],    options: ICompilerOptions = {      ignoreGenerics: false,      ignoreIndexSignature: false,      inlineImports: false,    }  ) {    const createProgramOptions = { target: ts.ScriptTarget.Latest, module: ts.ModuleKind.CommonJS };    const program = ts.createProgram(filePaths, createProgramOptions);    const checker = program.getTypeChecker();    return filePaths.map(filePath => {      const topNode = program.getSourceFile(filePath);      if (!topNode) {        throw new Error(`Can't process ${filePath}: ${collectDiagnostics(program)}`);      }      const content = new Compiler(checker, options, topNode).compileNode(topNode);      return { filePath, content };    });  }

Следующим шагом нужно создать файл примера, например для запроса к апи (типы разделил для наглядности):


src/api/auth.ts
type ApiRoute = {  url: string;  name: string;  method: 'GET' | 'POST';  headers?: any;};type RequestParams = {  email: string;  password: string;};type ResponseParams = {  email: string;  sessionExpires: number;};type AuthApiRoute = ApiRoute & { params?: RequestParams; response?: ResponseParams };export const auth: AuthApiRoute = {  url: `/auth`,  name: `auth`,  method: 'POST',};

И натравить на него всеядный и потому толстеющий генератор файлов:


webpack-custom/utils/generateFiles.ts
import { Compiler } from '../../lib/ts-interface-builder';const pathsForValidationFiles = [  {    folderPath: path.resolve(paths.sourcePath, 'api'),  },];const modelsPath = path.resolve(paths.sourcePath, 'models');class GenerateFiles {  generateValidationFiles({ changedFiles }: TypeProcessParams) {    const config =      changedFiles == null        ? pathsForValidationFiles        : pathsForValidationFiles.filter(({ folderPath }) =>            changedFiles.some(              filePath => filePath.includes(folderPath) || filePath.includes(modelsPath)            )          );    if (config.length === 0) return false;    return Promise.all(      config.map(({ folderPath }) => {        const { base: folderName } = path.parse(folderPath);        const generatedFileName = `_${folderName}.ts`;        const generatedFolderPath = path.resolve(paths.validatorsPath, folderName);        if (!fs.existsSync(generatedFolderPath)) fs.mkdirSync(generatedFolderPath);        return Promise.resolve()          .then(() => fs.promises.readdir(folderPath))          .then(filesNames => this._excludeFileNames(filesNames, [generatedFileName]))          .then(filesNames => filesNames.map(fileName => path.resolve(folderPath, fileName)))          .then(filesPaths =>            Promise.all(              Compiler.compile(filesPaths, { inlineImports: true }).map(({ filePath, content }) => {                const { base: fileName } = path.parse(filePath);                const generatedFilePath = path.resolve(generatedFolderPath, fileName);                return this._saveFile({ filePath: generatedFilePath, content });              })            )          );      })    ).then(filesSavedMarks => _.flatten(filesSavedMarks).some(Boolean));  }}

Обратить внимание тут можно на то, что вручную такие файлы не отформатировать, поэтому приходится прогонять через eslint с соответствующими временными затратами. В параметрах утилиты стоит { inlineImports: true } для того, чтобы импорты типов включались непосредственно в итоговый файл (иначе они не будут проверяться), а также включена проверка filePath.includes(modelsPath), чтобы при изменении моделей триггерился этот процессинг. Вытаскивать дерево зависимостей нетривиальная задача, поэтому поддерживать этот функционал в предлагаемой мной версии предполагается вручную.


Таким образом, при запуске данного метода будет сгенерирован файл:


src/validators/api/auth.ts
import * as t from 'ts-interface-checker';// tslint:disable:object-literal-key-quotesexport const ApiRoute = t.iface([], {  url: 'string',  name: 'string',  method: t.union(t.lit('GET'), t.lit('POST')),  headers: t.opt('any'),});export const RequestParams = t.iface([], {  email: 'string',  password: 'string',});export const ResponseParams = t.iface([], {  email: 'string',  sessionExpires: 'number',});export const AuthApiRoute = t.intersection(  'ApiRoute',  t.iface([], {    params: t.opt('RequestParams'),    response: t.opt('ResponseParams'),  }));const exportedTypeSuite: t.ITypeSuite = {  ApiRoute,  RequestParams,  ResponseParams,  AuthApiRoute,};export default exportedTypeSuite;

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


import { createCheckers } from 'ts-interface-checker';import * as apiValidatorsTypes from 'validators/api';const apiValidators = _.mapValues(apiValidatorsTypes, value => createCheckers(value));function validateRequestParams({ route, params }) {  return Promise.resolve()    .then(() => {      const requestValidator = _.get(apiValidators, `${[route.name]}.TypeRequestParams`);      return requestValidator.strictCheck(params);    })    .catch(error => {      throw createError(        errorsNames.VALIDATION,        `request: (request params) ${error.message} for route "${route.name}"`      );    });}

Таким образом, при отправке запроса при несовпадении типов будет создана человекопонятная ошибка. К сожалению, библиотека ts-interface-checker не работает с дженериками, но для моих нужд текущего функционала в виде проверки объектов с глубокой вложенностью подходит отлично.


Интеграция с процессом сборки


Перед билдом запустить все созданные рецепты генерации просто, достаточно выполнить дополнительный скрипт (в моем случае он уже есть webpackBuider.ts), в котором запускается этап generateFiles.process({}) (с пустым changedFiles значит, будет проведена масштабная операция по созданию файлов и перезаписыванию на новые, если они изменились). А вот в случае пересборки начинается самое интересное, так как придется взаимодействовать с Webpack "вещью в себе" по двум возможным сценариям:


  • Интеграция в compiler.hooks.watchRun:

webpack-custom/plugins/pluginChangedFiles.ts
import webpack from 'webpack';import { generateFiles } from '../utils/generateFiles';class ChangedFiles {  apply(compiler: webpack.Compiler) {    compiler.hooks.watchRun.tapAsync('GenerateFiles_WatchRun', (comp, done) => {      const watcher = comp.watchFileSystem.watcher || comp.watchFileSystem.wfs.watcher;      const changedFiles = Object.keys(watcher.mtimes);      return changedFiles.length        ? generateFiles.process({ changedFiles }).then(() => done())        : done();    });  }}export const pluginChangedFiles: webpack.Plugin = new ChangedFiles();

Таким образом, перед стартом пересборки будет выполняться асинхронное действие, а по его завершении колбэк продолжения процесса сборки. При параллельной изоморфной сборке такое решение, примененное в одной из сборок, разумеется не прокатит, так как должно выполняться перед обеими, учитывая измененные файлы обеих. Для этого мне пришлось форкнуть parallel-webpack и настраивать общение между тремя процессами через node-ipc, блокируя соседние сборки до завершения общей генерации. Подробно на этом останавливаться не буду, можно посмотреть в итоговом репозитории, если интересно, но у данного решения в целом есть масса недостатков:


  • сгенерированные файлы не попадают в текущий цикл сборки, несмотря на асинхронность хука. Таким образом, если первая сборка сгенерировала файл _const.ts, то после ее завершения будет запущена вторая, так как Webpack watcher посчитает это новым изменением.
  • при удалении какого-либо файла, ссылка на который была в реэкспортном файле, до хука watchRun процесс не доходит, а заранее падает с ошибкой приходится перезапускать.
  • при добавлении нового файла в папку он, разумеется, не подтягивается, так как не находится в области видимости Webpack.

Первые две проблемы мне удалось решить в данном случае следующим плагином:


new webpack.ProgressPlugin(percentage => {    if (percentage === 0) generateFilesSync.process({ changedFiles });})

Однако он работает только синхронно, что невозможно соблюсти ввиду асинхронного характера некоторых рецептов. В общем, после тщательного исследования исходников Webpack, мне так и не удалось подключиться к его наблюдательной системе, блокируя пересборку и добавляя в нее измененные файлы.


  • Отдельный процесс с наблюдением за измененными файлами:

webpack-custom/webpackBuilder.ts
function startFileWatcher() {  let changedFiles = [];  let isGenerating = false;  let watchDebounceTimeout = null;  watch(paths.sourcePath, { recursive: true }, function fileChanged(event, filePath) {    if (filePath) changedFiles.push(filePath);    if (isGenerating) return false;    clearTimeout(watchDebounceTimeout);    watchDebounceTimeout = setTimeout(() => {      isGenerating = true;      generateFiles.process({ changedFiles }).then(() => {        isGenerating = false;        if (changedFiles.length > 0) fileChanged(null, null);      });      changedFiles = [];    }, 10);  });}function afterFirstBuild() {  startFileWatcher();}

В данном случае просто запускается слежка за целой папкой или несколькими, соответственно файлы создаются независимо от Webpack, а значит корректно обрабатывается удаление и создание файлов. В конфиге Webpack есть параметр aggregateTimeout (который в моей версии задается через env-параметры), это задержка от времени изменения файла до старта пересборки. Соответственно, если генерация файлов заняла времени меньше, чем эта цифра, то будет запущена всего 1 пересборка с итоговыми файлами, а это как раз тот результат, к которому хотелось бы придти. Однако это константная величина, и файлы могут иногда генерироваться за большее время, что приведет к двум пересборкам вместо одной.


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


Я использую данный генератор не только для упрощения тех рутинных задач, которые привел выше, но и как часть крупного архитектурного решения (микрофронтенды, только не смейтесь), надеюсь дойду через несколько статей и до этой темы.


Репозиторий: https://github.com/dkazakov8/habr_helpers-generator


Комфортного кодинга!

Подробнее..

Из песочницы Как я выкинул webpack и написал babel-plugin для транспила scsssass

03.08.2020 12:13:23 | Автор: admin

Предыстория


Как-то субботним вечером я сидел и искал способы сборки UI-Kit с помощью webpack. В качестве демо UI-kit я пользуюсь styleguidst. Конечно же, webpack умный и все файлы, которые есть в рабочем каталоге он запихивает в один бандл и оттуда всё крутится и вертится.

Я создал файл entry.js, импортнул туда все компоненты, затем оттуда же экспортнул. Вроде всё ок.

import Button from 'components/Button'import Dropdown from 'components/Dropdown 'export {  Button,  Dropdown }

И после сборки всего этого, я получил на выходе output.js, в котором как и ожидалось было всё все компоненты в куче в одном файле. Тут возник вопрос:
А как мне собрать все кнопочки, дропдауны и прочее по отдельности, что бы импортировать в других проектах?
А я ведь хочу это ещё и в npm залить как пакет.

Хм Поехали по порядку.

Multiple entries


Конечно, первая идея, которая может прийти в голову спарсить все компоненты в рабочем каталоге. Пришлось немножко погуглить про парсинг файлов, т.к с NodeJS я работаю очень редко. Нашёл такую штуку, как glob.

Погнали писать multiple entries.

const { basename, join, resolve } = require("path");const glob = require("glob");const componentFileRegEx = /\.(j|t)s(x)?$/;const sassFileRegEx = /\s[ac]ss$/;const getComponentsEntries = (pattern) => {  const entries = {};  glob.sync(pattern).forEach(file => {    const outFile = basename (file);    const entryName = outFile.replace(componentFileRegEx, "");    entries[entryName] = join(__dirname, file);  })  return entries;}module.exports = {  entry: getComponentsEntries("./components/**/*.tsx"),  output: {    filename: "[name].js",    path: resolve(__dirname, "build")  },  module: {    rules: [      {        test: componentFileRegEx,        loader: "babel-loader",        exclude: /node_modules/      },      {        test: sassFileRegEx,        use: ["style-loader", "css-loader", "sass-loader"]      }    ]  }  resolve: {    extensions: [".js", ".ts", ".tsx", ".jsx"],    alias: {      components: resolve(__dirname, "components")    }  }}

Готово. Собираем.

После сборки в каталог build упало 2 файла Button.js, Dropdown.js заглядываем внутрь. Внутри лицензии react.production.min.js, тяжелочитаемый минимизированный код, и куча всякой фигни. Окей, попробуем использовать кнопку.

В демо файле кнопки меняем импорт на импорт из каталога build.

Вот так выглядит простая демка кнопки в styleguidist Button.md

```javascriptimport Button from '../../build/Button'<Button>Кнопка</Button>```

Заходим посмотреть на кнопочку иии

Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.


На этом этапе уже отпали идея и желание собирать через webpack.

Ищем другой путь сборки без webpack


Идём за помощью к бабелю без вебпака. Пишем скрипт в package.json, указываем файл конфига, расширения, директорию где лежат компоненты, директорию куда собрать:

{  //...package.json всякие штуки-дрюки о которых обычно не паримся  scripts: {    "build": "babel --config-file ./.babelrc --extensions '.jsx, .tsx' ./components --out-dir ./build"  }}

запускаем:

npm run build

Вуаля, у нас в каталоге build появились 2 файла Button.js, Dropdown.js, внутри файлов красиво оформленный ванильный js + некоторые полифилы и одинокий requre(styles.scss). Явно это не сработает в демке, удаляем импорт стилей(в этот момент меня гложила надежда, что я найду плагин для транспила scss), собираем ещё раз.

После сборки у нас остался читсый JS. Повторяем попытку интеграции собранного компонента в styleguidist:

```javascriptimport Button from '../../build/Button'<Button>Кнопка</Button>```

Скомпилировалось работает. Только кнопочка без стилей.

Ищем плагин для транспила scss/sass


Да, сборка компонентов работает, компоненты работают, можно собирать, паблишить в npm или свой рабочий нехус(nexus). Ещё бы только стили сохранить Окей, снова гугл нам поможет (нет).

Гугления плагинов не принесли мне каких-то результатов. Один плагин генерирует строку из стилей, другой вообще не работает да ещё требует импорта вида:import styles from styles.scss (никогда так не импортировал стили и мое личное мнение не надо так делать, но о вкусах не спорят)

Единственная надежда была на этот плагин: babel-plugin-transform-scss-import-to-string, но он просто генерирует строку из стилей (а я уже говорил выше. Блин...). Дальше всё стало ещё хуже, я дошел до 6 страницы в гугле (а на часах уже 3 утра). Да и вариантов особо уже не будет что-то найти. Да и думать то нечего либо webpack + sass-loader, которые хреново это делают и не для моего случая, либо ШТО-ТО ДРУГОЕ. Нервы Я решил немного передохнуть, попить чай, спать всё равно не хочется. Пока делал чай, идея написать плагин для транспила scss/sass все больше и больше влетала в мою голову. Пока мешал сахарочек, редкие звоны ложки в моей голове отдавались эхом: Пиши плааагин. Ок, решено, буду писать плагин.

Плагин не найден. Пишем сами


За основу своего плагина я взял babel-plugin-transform-scss-import-to-string, упомянутый выше. Я прекрасно понимал, что сейчас будет геморрой с AST деревом, и прочими хитростями. Ладно, поехали.

Делаем предварительные подготовочки. Нам нужны node-sass и path, а так же регулярочки для файлов и расширений. Идея такая:

  • Получаем из строки импорта путь до файла со стилями
  • Парсим через node-sass стили в строку (спасибо babel-plugin-transform-scss-import-to-string)
  • Создаем style теги по каждому из импортов (плагин бабеля запускается на каждом импорте)
  • Надо как-то идентифицировать созданный стиль, что бы не накидывать одно и то же на каждый чих hot-reload. Впихнем ему какой-нибудь аттрибут (data-sass-component) со значением текущего файла и названием файла стилей. Будет что-то вроде этого:

          <style data-sass-component="Button_style">         .button {            display: flex;         }      </style>
    

В целях разработки плагина и тестирования на проекте, на уровне с каталогом components я создал babel-plugin-transform-scss каталог, запихнул туда package.json и запихнул туда каталог lib, а в него уже закинул index.js.
Что бы вы были вкурсе конфиг бабеля лезет за плагином, который указан в директиве main в package.json, для этого пришлось его запихать.
Указываем:

{  //...package.json опять всякие штуки-дрюки о которых обычно не паримся, да и кроме main ничего нету  main: "lib/index.js"}

Затем, пихаем в конфиг бабеля (.babelrc) путь до плагина:

{  //Тут всякие пресеты  plugins: [    "./babel-plugin-transform-scss"    //тут остальные плагины для сборки  ]}

А теперь напихиваем в index.js магию.

Первый этап проверка на импорт именно scss или sass файла, получение имени импортируемых файлов, получение имени самого js файла(компонента), транспил в css строку scss или sass. Подрубаемся через WebStorm к npm run build через дебаггер, ставим точки останова, смотрим аргументы path и state и выуживаем имена файлов, обрабатываем руглярочками:

const { resolve, dirname, join } = require("path");const { renderSync } = require("node-sass");const regexps = {  sassFile: /([A-Za-z0-9]+).s[ac]ss/g,  sassExt: /\.s[ac]ss$/,  currentFile: /([A-Za-z0-9]+).(t|j)s(x)/g,  currentFileExt: /.(t|j)s(x)/g};function transformScss(babel) {  const { types: t } = babel;  return {    name: "babel-plugin-transform-scss",    visitor: {      ImportDeclaration(path, state) {        /**         * Проверяем, содержит ли текущий файл scss/sass расширения в импорте         */        if (!regexps.sassExt.test(path.node.source.value)) return;        const sassFileNameMatch = path.node.source.value.match(          regexps.sassFile        );        /**         * Получаем имя текущего scss/sass файла и текущего js файла         */        const sassFileName = sassFileNameMatch[0].replace(regexps.sassExt, "");        const file = this.filename.match(regexps.currentFile);        const filename = `${file[0].replace(          regexps.currentFileExt,          ""        )}_${sassFileName}`;        /**         *         * Получаем полный путь до scss/sass файла, транспилим в строку css         */        const scssFileDirectory = resolve(dirname(state.file.opts.filename));        const fullScssFilePath = join(          scssFileDirectory,          path.node.source.value        );        const projectRoot = process.cwd();        const nodeModulesPath = join(projectRoot, "node_modules");        const sassDefaults = {          file: fullScssFilePath,          sourceMap: false,          includePaths: [nodeModulesPath, scssFileDirectory, projectRoot]        };        const sassResult = renderSync({ ...sassDefaults, ...state.opts });        const transpiledContent = sassResult.css.toString() || "";        }    }}

Fire. Первый успех, получена строка css в transpiledContent. Дальше самое страшное лезем в babeljs.io/docs/en/babel-types#api за API по AST дереву. Лезем в astexplorer.net пишем там код запихивания в head документа стилей.

В astexplorer.net пишем Self-Invoking функцию, которая будет вызываться на месте импорта стиля:

(function(){  const styles = "generated transpiledContent" // ".button {/n display: flex; /n}/n"   const fileName = "generated_attributeValue" //Button_style  const element = document.querySelector("style[data-sass-component='fileName']")  if(!element){    const styleBlock = document.createElement("style")    styleBlock.innerHTML = styles    styleBlock.setAttribute("data-sass-component", fileName)    document.head.appendChild(styleBlock)  }})()

В AST explorer тыкаем в левой части на строки, объявления, литералы, справа в дереве смотрим структуру объявлений, по этой структуре лезем в babeljs.io/docs/en/babel-types#api, курим всё это и пишем замену.

A few moments later

Спустя 1-1,5 часа, бегая по вкладкам из ast в babel-types api, затем в код, я написал замену импорта scss/sass. Разбирать отдельно дерево ast и babel-types api я не буду, будет ещё больше буковок. Показываю сразу результат:

const { resolve, dirname, join } = require("path");const { renderSync } = require("node-sass");const regexps = {  sassFile: /([A-Za-z0-9]+).s[ac]ss/g,  sassExt: /\.s[ac]ss$/,  currentFile: /([A-Za-z0-9]+).(t|j)s(x)/g,  currentFileExt: /.(t|j)s(x)/g};function transformScss(babel) {  const { types: t } = babel;  return {    name: "babel-plugin-transform-scss",    visitor: {      ImportDeclaration(path, state) {        /**         * Проверяем, содержит ли текущий файл scss/sass расширения в импорте         */        if (!regexps.sassExt.test(path.node.source.value)) return;        const sassFileNameMatch = path.node.source.value.match(          regexps.sassFile        );        /**         * Получаем имя текущего scss/sass файла и текущего js файла         */        const sassFileName = sassFileNameMatch[0].replace(regexps.sassExt, "");        const file = this.filename.match(regexps.currentFile);        const filename = `${file[0].replace(          regexps.currentFileExt,          ""        )}_${sassFileName}`;        /**         *         * Получаем полный путь до scss/sass файла, транспилим в строку css         */        const scssFileDirectory = resolve(dirname(state.file.opts.filename));        const fullScssFilePath = join(          scssFileDirectory,          path.node.source.value        );        const projectRoot = process.cwd();        const nodeModulesPath = join(projectRoot, "node_modules");        const sassDefaults = {          file: fullScssFilePath,          sourceMap: false,          includePaths: [nodeModulesPath, scssFileDirectory, projectRoot]        };        const sassResult = renderSync({ ...sassDefaults, ...state.opts });        const transpiledContent = sassResult.css.toString() || "";        /**         * Имплементируем функцию, написанную в AST Explorer и заменяем импорт методом          * replaceWith аргумента path.         */        path.replaceWith(          t.callExpression(            t.functionExpression(              t.identifier(""),              [],              t.blockStatement(                [                  t.variableDeclaration("const", [                    t.variableDeclarator(                      t.identifier("styles"),                      t.stringLiteral(transpiledContent)                    )                  ]),                  t.variableDeclaration("const", [                    t.variableDeclarator(                      t.identifier("fileName"),                      t.stringLiteral(filename)                    )                  ]),                  t.variableDeclaration("const", [                    t.variableDeclarator(                      t.identifier("element"),                      t.callExpression(                        t.memberExpression(                          t.identifier("document"),                          t.identifier("querySelector")                        ),                        [                          t.stringLiteral(                            `style[data-sass-component='${filename}']`                          )                        ]                      )                    )                  ]),                  t.ifStatement(                    t.unaryExpression("!", t.identifier("element"), true),                    t.blockStatement(                      [                        t.variableDeclaration("const", [                          t.variableDeclarator(                            t.identifier("styleBlock"),                            t.callExpression(                              t.memberExpression(                                t.identifier("document"),                                t.identifier("createElement")                              ),                              [t.stringLiteral("style")]                            )                          )                        ]),                        t.expressionStatement(                          t.assignmentExpression(                            "=",                            t.memberExpression(                              t.identifier("styleBlock"),                              t.identifier("innerHTML")                            ),                            t.identifier("styles")                          )                        ),                        t.expressionStatement(                          t.callExpression(                            t.memberExpression(                              t.identifier("styleBlock"),                              t.identifier("setAttribute")                            ),                            [                              t.stringLiteral("data-sass-component"),                              t.identifier("fileName")                            ]                          )                        ),                        t.expressionStatement(                          t.callExpression(                            t.memberExpression(                              t.memberExpression(                                t.identifier("document"),                                t.identifier("head"),                                false                              ),                              t.identifier("appendChild"),                              false                            ),                            [t.identifier("styleBlock")]                          )                        )                      ],                      []                    ),                    null                  )                ],                []              ),              false,              false            ),            []          )        );        }    }}

Итоговые радости


Ура!!! Импорт заменился на вызов функции, которая напихала в head документа стиль с этой кнопкой. И тут я подумал, а что если я стартану всю эту байдарку через вебпак, выкосив sass-loader? Будет ли оно работать? Окей, выкашиваем и проверяем. Запускаю сборку вебпаком, жду ошибку, что я должен определить loader для этого типа файла А ошибки-то нет, всё собралось. Открываю страницу, смотрю, а стиль воткнулся в head документа. Интересно получилось, я ещё избавился от 3 лоадеров для стилей(очень довольная улыбка).

Если вам была интересна статья поддержите звездочкой на github.

Так же ссылка на npm пакет: www.npmjs.com/package/babel-plugin-transform-scss

Примечание: Вне статьи добавлена проверка на импорт стиля по типу import styles from './styles.scss'
Подробнее..

HTML, CSS, JavaScript Знакомство с Rome компилятор, сборщик, линтер, тесты в одном флаконе

24.08.2020 10:10:39 | Автор: admin

Почти две недели назад вышла запись в блоге по поводу Rome.

Rome представляет собой целый набор инструментов линтер, компилятор, сборщик, тест раннер и даже больше. Нацелен он на JS, TS, HTML, JSON, Markdown, CSS. Проект пытается унифицировать набор инструментов необходимых для фронт-енд разработки.

Rome монолитный и включает все традиционные инструменты фронт-енд экосистемы. Команда разработчиков называет это toolchain. Важным моментом является, то, что это не попытка объединить существующий набор инструментов, а разработать полностью новый набор инструментов. Все это должно быть в одном пакете и единой кодовой базой.

Rome разрабатывается как замена Babel, ESLint, Webpack, Prettier, Jest и прочих.

Посмотрим к чему это приведет, если честно меня устраивает некоторая конкуренция и не монолитный подход в инструментах разработки. Да, для новичка вход не очевиден, слишком большое изобилие инструментов, которые в большинстве своем взаимозаменяемы. Возможно в таком нюансе, Rome немного снизит порог входа.

Текущее состояние




Rome находится в бете и уже справляется с некоторыми своими обязанностями. Например у линтера уже поддерживается 100 правил, включая те, что часто используемые при использовании Typescript и React. Полный список правил можно найти тут

Опенсорс проекта начался с начала года и у проекта уже 70 контрибьютеров и более 600 комитов. В целом проект развивается и пока каких либо затуханий не видно.

Немного истории


В 2014 году был создан 6to5 (сейчас называется Babel). Это JavaScript транспайлер который копмпилировал новый код на ES6 в ES5. В тот момент проект не ставил каких либо целей, но с ростом популярности приходилось корректировать разработку.

6to5 переименовали в Babel и новой задачей было стать общей платформой для статических трансформаций JavaScript. Это означало систему плагинов и поддержку новых фичей будущих JavaScript стандартов и даже proposals.

Команда разработчиков думала пойти еще дальше. Babel должен быть под капотом минифайеров, линтеров, форматтеров, подсветки синтаксиса, проверщиков типов, инструментов компиляции и так далее.

В 2016 году создатель Babel покинул проект. Со временем стало понятно, что подход с предоставлением широкого API (буквально всех внутренностей) слишком трудно поддерживать. Для поддержки вышеописанного инструментария, потребовалось бы переписать буквально все. Архитектура проекта связана с решениями которые автор делал еще в 2014 году, только изучая как работают парсеры AST и компиляторы. Изменения бы затронули большую часть API без возможности обратной совместимости.

Разработчики инструментов под JavaScript, тратят огромное количество времени на обработку исходного кода. Babel, ESlint, Webpack, частично занимаются одним и тем же.

Линтеры в экосистеме JavaScript равнозначны компиляторам. Оба принимают на вход исходный код, оба его обрабатывают и на выход выдают код и ошибки (если есть). Отличается лишь полученный код. В случае с линтером, это отформатированный исходный код с с исправлениями.
Чем сильней ваша инфрастуктура компиляции, тем сильней линтер. То же самое относится и к другим инструментам, которые так или иначе обрабатывают код. Мы можем создавать более мощные инструменты, используя общую базу, которую легко адаптировать.

Rome духовный преемник Babel. Я усвоил уроки и поставил четкие цели. Вместо того, чтобы предоставлять слишком объемный публичный API для других инструментов, мы собираем их все в одном месте. Я очень рад попробовать что-то новое, чего раньше не видел JavaScript и веб-экосистема.

Sebastian McKenzie

Использование


Rome можно установить используя Yarn или NPM:

yarn add rome

npm install rome


Создание проекта


Для того, что бы Rome смог найти ваши файлы, ему нужна конфигурация. Для автоматического создания проекта можно использовать:

rome init


Эта команда создаст директорию .config и поместит туда rome.rjson, с конфигурацией проекта.

Если у вас есть существующий проект, то можно сразу автоматически применить форматирование и исправления:

rome init --apply


RJSON это расширение JSON, которое добавляет некоторые фичи. Например комментарии в JSONе.


Запустить линтер можно с помощью

rome check


Линтинг


Rome разрабатывается с мыслью, предоставления максимального количества информации для исправления ошибок. У проекта есть своего рода кредо:

Богатый UI (результат в консоли): Хорошо отформатированная информация, подсветка синтаксиса, ссылки, списки и прочее.

Исправления: Rome может исправить большое количество ошибок (как линтер) и применить их автоматически. Если есть несколько путей решения, то инструмент предоставляет вам выбор.

Процесс ревью: CLI интерактивно богат и позволяет прямо в консоли принимать решения по исправлениям и пробегаться по всем ошибкам.

Редакторы: Rome интегрируется с редакторами, что позволяет делать форматирование на сохранения, подсветку ошибок и тд.

Сохранность: Rome кеширует оригинальные файлы перед тем как сделать изменения. Используя rome recover, можно откатить исправления.



Вместо заключения


Пока этот инструмент еще находится в активной разработке, но его уже как-то можно попробовать. Главное, что бы не получилось вот так:



Офф документация
Подробнее..

Основы настройки Webpack

15.09.2020 12:21:41 | Автор: admin
Для начала установим webpack, делается это с помощью команд:

yarn add webpack webpack-cli -D, если используете менеджер пакетов yarn
npm i webpack webpack-cli --save-dev, для менеджера пакетов npm

Настраивается Webpack с помощью конфигурационного файла webpack.config.js, который хранится в корневой директории проекта.

Пример конфигурационного файла
const path = require('path')module.exports = {   watch: true,   entry: "./src/index.js",   output: {       filename: "bundle.js",       path: path.resolve(__dirname,'build'),       publicPath: "/"   }};

Начальная конфигурация представляет собой следующее:
  • watch заставляет webpack отслеживать изменения в файлах и заново выполнять сборку;
  • entry Указывает на точку входа в проект, и откуда нужно начать построение графа внутренних зависимостей проекта;
  • output Указывает путь, где будет располагаться создаваемый файл и как он будет называться;

Так же необходимо установить webpack-dev-server, который понадобится для разработки и отладки приложения.

yarn add webpack-dev-server для менеджера пакетов yarn или
npm i webpack-dev-server если используется npm

Для настройки webpack-dev-server добавим devServer в нашем конфигурационном файле.

Параметры для webpack-dev-server:
module.exports = {    //...    devServer: {        port: 8000,        historyApiFallback: true,        hot: true,    },};

Также нам нужно добавить/заменить в нашем package.json файле скрипт запуска проекта
"start": "webpack-dev-server --mode development",

и скрипт для сборки билда
"build": "webpack --mode production"

Загрузчики


Загрузчики (loaders) это специальные модули, которые используются для загрузки других модулей. Так же они позволяют предварительно обрабатывать файлы по мере их импорта или загрузки.

Загрузчики могут преобразовывать файлы, например TypeScript в JavaScript, sass в css. Они могут даже позволить нам делать такие вещи, как импорт файлов CSS и HTML непосредственно в наши модули JavaScript. Для их использования необходимо прописать нужные загрузчики в разделе module.rules файла конфигурации.

Примеры загрузчиков:
  • babel-loader использует babel для загрузки файлов ES2015.
  • file-loader для загрузки различных файлов (изображения, музыкальные дорожки и т.д.) в выходную директорию
  • style-loader используется для загрузки стилей
  • css-loader включает загрузку файлов стилей
  • @svgr/webpack лоадер, позволяющий использовать svg изображения как jsx элементы

Для использования babel-loader необходимо установить babel/core. Также установим пресет babel/preset-env, который компилирует ES2015+ в ES5 путем автоматического определения необходимых Babel плагинов и полифайлов. Далее создадим файл .babelrc и в него добавим ранее установленный пресет.

{ "presets": [   "@babel/preset-env" ]}

Теперь добавим загрузчик в нашу конфигурацию для преобразования файлов Javascript. Это позволит нам использовать синтаксис ES2015 + в нашем коде (который будет автоматически конвертироваться в ES5 в окончательной сборке).

{   test: /\.(js|jsx)$/,   exclude: /node_modules/,   use: {       loader: 'babel-loader',   },},

Пример конфигурации с лоадером file-loader

{   test: /\.(png|jpe?g|gif)$/i,   use: [       {           loader: 'file-loader',       },   ],},

Пример конфигурации для лоадера @svgr/webpack

{   test : /\.(svg)$/,   use: [       {           loader: "@svgr/webpack"       }   ]}

Плагины


Плагины являются основой webpack, так как по сути вся его работа построена на системе плагинов. Они значительно расширяют возможности загрузчиков.

Загрузчики выполняют предварительную обработку файлов любого формата. Они работают на уровне отдельных файлов во время или до создания пакета. После того как отработают загрузчики наступает очередь плагинов. Плагины как правило отрабатывают только одну функцию.

Для их использования необходимо добавить нужные плагины в разделе plugins файла конфигурации.

Примеры плагинов:
  • html-webpack-plugin используется для создания html файлов
  • copy-webpack-plugin копирует отдельные файлы или целые каталоги, которые уже существуют, в каталог сборки.
  • definePlugin позволяет создавать глобальные константы
  • HotModuleReplacementPlugin включает HMR режим, обновляет только ту часть, которая изменилась, не перезагружая полностью приложение.

Пример конфигурации с добавленными плагинами:

plugins: [   new webpack.HotModuleReplacementPlugin(),   new webpack.DefinePlugin({       'process.env': JSON.stringify(process.env),   }),   new HtmlWebpackPlugin({       template: "./public/index.html",   }),   new CopyWebpackPlugin({       patterns: [           { from: './public/favicon.ico', to: './public'}       ]   }),],

Также добавим плагин UglifyjsWebpackPlugin, который минимизирует js код, для этого нужно установить uglifyjs-webpack-plugin и добавить его в разделе optimization

optimization: {   minimizer: [new UglifyJsPlugin()]},
Подробнее..

Как я разработал мобильную игру на Android с использованием React.js и выложил её в Google Play Store

30.12.2020 18:06:06 | Автор: admin

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

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

Скриншот готовой игрыСкриншот готовой игры

Порядок заголовков не является структурированным по каким-либо параметрам, кроме хронологической последовательности.

Предыстория

Добрый день, уважаемые читатели. Конечно же мы с Вами не знакомы, однако если бы были, то Вы бы знали, что ещё с раннего детства после приобретения отцом персонального компьютера у меня была мечта стать разработчиком игр, что в дошкольном возрасте выражалось в "создании" платформеров мелом на асфальте для ребят во дворе, а в школьном - отдельными играми в тетрадках, в том числе и во время уроков.

Разработчиком игр (пока что) так я и не стал, но стал веб-разработчиком, имея на данный момент 8+ лет коммерческого опыта. Однажды на глаза попалась весьма интересная библиотека-фреймворк для создания пошаговых игр, документация которой была незамедлительно изучена. Предлагалось создавать такие игры, как крестики-нолики или шахматы, однако это показалось скучным и мне захотелось пойти дальше - сделать нечто посложнее и похожее на то, во что я играл ещё в детстве. Вероятно, старички заметят некоторое сходство финального результата с той игрой, на которую делался акцент.

Построение и прорисовка мира

Сперва было принято решение создать изометрический мир, который мог бы поддерживать бесконечную прокрутку по горизонтали. После тщательного изучения статей на тему "Как создавать изометрические миры" и им подобных, а также просмотра библиотек для JavaScript и React, которые якобы должны помогать выполнять данную задачу, дело перешло к практике.

Времени на это было потрачено немало и готовых подходящих решений так и не было найдено. Ну что ж, напишем своё. По сути весь наш мир это набор квадратиков-тайлов, которые находятся рядом друг с другом и на соседний квадратик можно перейти в восьми направлениях.

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

Сперва отрисуем ячейки мира построчно. Пускай они будут размером 64x64 пикселя. Далее развернём наш контейнер таким образом, чтобы он выглядел изометрично:

.rotate {  transform: rotateX(60deg) rotateZ(45deg);  transform-origin: left top;}

При имплементации данного подхода к отрисовке можно наблюдать, что необходимые нам "строки" мира на самом деле идут не прямо, а зигзагом, так как мы развернули нашу карту. Таким образом, каждую из ячеек необходимо позиционировать абсолютно с учетом текущего индекса строки, а также индекса колонки:

const cellOffsets = {};export function getCellOffset(n) {  if (n === 0) {    return 0;  }  if (cellOffsets[n]) {    return cellOffsets[n];  }  const result = 64 * (Math.floor(n / 2));  cellOffsets[n] = result;  return result;}

Использование:

import { getCellOffset } from 'libs/civilizations/helpers';// ...const offset = getCellOffset(columnIndex);// ...style={{  transform: `translateX(${(64 * rowIndex) + (64 * columnIndex) - offset}px) translateY(${(64 * rowIndex) - offset}px)`,}}

Для увеличения производительности нам необходимо перерисовывать только те ячейки карты, которые сейчас являются видимыми на экране. Для этого был использован компонент FixedSizeGrid из модифицированной версии библиотеки react-window с учетом нашего поворота и расположений ячеек, код которого здесь приводить не буду. Из того, что не получилось - это сделать бесконечную прокрутку мира. После изучений исходного различных библиотек для бесконечного скролла / слайдеров и тп. подходящего решения найдено не было. Что ж, значит наш мир будет с границами по всем бокам.

Графика

Для отображения поворотов наших юнитов, а также анимации атаки, используются png-спрайты. Поиск графических элементов занял очень большой промежуток времени, было очень сложно найти хотя бы какие-то картинки возможных юнитов для нашей игры. Изначально ещё до поиска игра выглядела вот так:

Игра до поиска графических элементовИгра до поиска графических элементов

Вообще сам я не дизайнер и попросить кого-либо нарисовать тоже возможности нет, потому каждый кадр спрайта пришлось ручками размещать в необходимую позицию. Для примера, финальный спрайт вертолёта выглядит вот так:

Спрайт вертолётаСпрайт вертолёта

Локализация

Игра поддерживает 4 языка и, если честно, мне непонятно, зачем в несложных приложениях разработчики подключают массивные библиотеки типа react-i18next. Давайте напишем похожее кастомное решение, которое уместится в чуть более чем 100 строк с учетом красивой разметки кода, а также будет поддерживать определение языка девайса пользователя, переключение языков в реальном времени и сохранение последнего выбора пользователя. Здесь используется redux, однако данный код можно адаптировать и под другие реактивные хранилища. Да, здесь нет некоторых фишек больших библиотек типа поддержки переменных в строках, однако в таком проекте нам это и не нужно. И да, эту библиотеку можно использовать как легковесную замену react-i18next (или подобным) в уже существующем проекте.

import React, { Component } from 'react';import PropTypes from 'prop-types';import { connect } from 'react-redux';import get from 'lodash/get';import set from 'lodash/set';import size from 'lodash/size';import { emptyObj, EN, LANG, PROPS, langs } from 'defaults';import { getLang } from 'reducers/global/selectors';import en from './en';export function getDetectedLang() {  if (!global.navigator) {    return EN;  }  let detected;  if (size(navigator.languages)) {    detected = navigator.languages[0];  } else {    detected = navigator.language;  }  if (detected) {    detected = detected.substring(0, 2);    if (langs.indexOf(detected) !== -1) {      return detected;    }  }  return EN;}const options = {  lang: global.localStorage ?    (localStorage.getItem(LANG) || getDetectedLang()) :    getDetectedLang(),};const { lang: currentLang } = options;const translations = {  en,};if (!translations[currentLang]) {  try {    translations[currentLang] = require(`./${currentLang}`).default;  } catch (err) {} // eslint-disable-line}export function setLang(lang = EN) {  if (langs.indexOf(lang) === -1) {    return;  }  if (global.localStorage) {    localStorage.setItem(LANG, lang);  }  set(options, [LANG], lang);  if (!translations[lang]) {    try {      translations[lang] = require(`./${lang}`).default;    } catch (err) {} // eslint-disable-line  }}const mapStateToProps = (state) => {  return {    lang: getLang(state),  };};export function t(path) {  const { lang = get(options, [LANG], EN) } = get(this, [PROPS], emptyObj);  if (!translations[lang]) {    try {      translations[lang] = require(`./${lang}`).default;    } catch (err) {} // eslint-disable-line  }  return get(translations[lang], path) || get(translations[EN], path, path);}function i18n(Comp) {  class I18N extends Component {    static propTypes = {      lang: PropTypes.string,    }    static defaultProps = {      lang: EN,    }    constructor(props) {      super(props);      this.t = t.bind(this);    }    componentWillUnmount() {      this.unmounted = true;    }    render() {      return (        <Comp          {...this.props}          t={this.t}        />      );    }  }  return connect(mapStateToProps)(I18N);}export default i18n;

Использование:

import i18n from 'libs/i18n';// ...static propTypes = {  t: PropTypes.func,}// ...const { t } = this.props;// ...{t(['path', 'to', 'key'])}// ...или тоже самое, но слегка медленнее{t('path.to.key')}// ...export default i18n(Comp);

Мультиплеер

Игра поддерживает мультиплеер в реальном времени для устройств с Android 9 или выше (возможно, будет работать и на 8-м, однако данное предположение не проверялось) с рейтингом и таблицей лидеров.

Сам движок не поддерживает ходы в реальном времени, потому многопользовательский режим построен таким образом, что все события происходят в один и тот же ход, в это же время существуют кулдауны на определенные действия, которые реализованы через requestAnimationFrame. На Android 7 и ранее такой подход почему-то просто-напросто не работает.

Кастомный код библиотеки, которая регистрирует requestAnimationFrame, для интересующихся (не забывайте также, что после регистрации колбека его нужно и отрегистрировать, что обычно происходит по завершении колбека или на анмаут компонента):

import isFunction from 'lodash/isFunction';let lastTime = 0;const vendors = ['ms', 'moz', 'webkit', 'o'];for (let x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {  window.requestAnimationFrame = window[`${vendors[x]}RequestAnimationFrame`];  window.cancelAnimationFrame = window[`${vendors[x]}CancelAnimationFrame`] || window[`${vendors[x]}CancelRequestAnimationFrame`];}if (!window.requestAnimationFrame) {  window.requestAnimationFrame = (callback) => {    const currTime = new Date().getTime();    const timeToCall = Math.max(0, 16 - (currTime - lastTime));    const id = window.setTimeout(() => { callback(currTime + timeToCall); },      timeToCall);    lastTime = currTime + timeToCall;    return id;  };}if (!window.cancelAnimationFrame) {  window.cancelAnimationFrame = (id) => {    clearTimeout(id);  };}let lastFrame = null;let raf = null;const callbacks = [];const loop = (now) => {  raf = requestAnimationFrame(loop);  const deltaT = now - lastFrame;  // do not render frame when deltaT is too high  if (deltaT < 160) {    let callbacksLength = callbacks.length;    while (callbacksLength-- > 0) {      callbacks[callbacksLength](now);    }  }  lastFrame = now;};export function registerRafCallback(callback) {  if (!isFunction(callback)) {    return;  }  const index = callbacks.indexOf(callback);  // remove already existing the same callback  if (index !== -1) {    callbacks.splice(index, 1);  }  callbacks.push(callback);  if (!raf) {    raf = requestAnimationFrame(loop);  }}export function unregisterRafCallback(callback) {  const index = callbacks.indexOf(callback);  if (index !== -1) {    callbacks.splice(index, 1);  }  if (callbacks.length === 0 && raf) {    cancelAnimationFrame(raf);    raf = null;  }}

Использование:

import { registerRafCallback, unregisterRafCallback } from 'client/libs/raf';// ...registerRafCallback(this.cooldown);// ...componentWillUnmount() {  unregisterRafCallback(this.cooldown);}

Стандартная имплементация Lobby из библиотеки движка мне не подходила, так как она открывала ещё одно новое websocket-подключение на каждый инстанс игры, но мне также нужно было передавать данные пользователя и таблицу лидеров по своему уже существующему websocket-подключению, потому, чтобы не плодить подключения, здесь снова было использовано собственное решение на основе библиотеки primus. На стороне клиента подключение хендлится сбилдженной библиотекой от примуса, которое также выложил на npm с именем primus-client. Вы можете сами сбилдить себе подобную клиентскую библиотеку для определенной версии примуса через функцию save на стороне сервера.

Видео геймплея многопользовательского режима можно наблюдать ниже:

Звук и музыка

В игре присутствует несколько звуков - старта игры, атаки и победы. Для проигрывания звука - также кастомная библиотека (для музыки - схожий подход):

import { SOUND_VOLUME } from 'defaults';const Sound = {  audio: null,  volume: localStorage.getItem(SOUND_VOLUME) || 0.8,  play(path) {    const audio = new Audio(path);    audio.volume = Sound.volume;    if (Sound.audio) {      Sound.audio.pause();    }    audio.play();    Sound.audio = audio;  },};export function getVolume() {  return Sound.volume;}export function setVolume(volume) {  Sound.volume = volume;  localStorage.setItem(SOUND_VOLUME, volume);}export default Sound;

Использование:

import Sound from 'client/libs/sound';// ...Sound.play('/mp3/win.mp3');
Окно настроек игрыОкно настроек игры

Сборка проекта

Сборка web-части осуществляется вебпаком. Однако тут нужно учитывать особенности путей к файлам, ведь в процессе разработке на локалхосте или на сервере в продакшене они являются относительными корня домена, а для приложения в Cordova наши файлы будут размещены по протоколу file:// и потому после сборки нам необходимо провести некоторые преобразования, а именно:

const replace = require('replace-in-file');const path = require('path');const options = {  files: [    path.resolve(__dirname, './app/*.css'),    path.resolve(__dirname, './app/*.js'),    path.resolve(__dirname, './app/index.html'),  ],  from: [/url\(\/img/g, /href="\//g, /src="\//g, /"\/mp3/g],  to: ['url(./img', 'href="./', 'src="./', '"./mp3'],};replace(options)  .then((results) => {    console.log('Replacement results:', results);  })  .catch((error) => {    console.error('Error occurred:', error);  });

Итоги

Приложение разрабатывалось в течении года, находится в Google Play Store с середины сентября, а значит уже прошло три месяца. Общее количество установок - 46, из которых ещё непонятно, сколько там на самом деле настоящих людей. Если коротко, то это провал. Однако был приобретен первичный опыт как разработки игр, так и мобильных приложений.

Из того, что было задумано, но не получилось:

  1. Более сложный геймплей

  2. Бесконечная прокрутка карты по горизонтали

  3. Продвинутый ИИ компьютера

  4. Поддержка мультиплеера на всех устройствах

Дальнейшие планы

Сейчас понятно, что подобные игры мало кому интересны, так что в прогрессе изучение Unity, и возможно через некоторое время появится ещё одна игра в жанре tactical rts.

Можно потыкать?

Можно. Для интересующихся - ссылка на приложение на Google Play Store.

P.S. Отдельное спасибо музыканту Anton Zvarych за предоставленную фоновую музыку.

Подробнее..

Как готовить микрофронтенды в Webpack 5

27.04.2021 16:10:26 | Автор: admin

Всем привет, меня зовут Иван и я фронтенд-разработчик.

На моём комментарии про микрофронтенды набралось целых три лайка, поэтому я решил написать статью с описанием всех шишек, что наш стрим набил и набивает в результате внедрения микрофронтендов.

Начнём с того, что ребята с Хабра (@artemu78, @dfuse, @Katsuba) уже писали про Module Federation, так что, моя статья - это не что-то уникальное и прорывное. Скорее, это шишки, костыли и велосипеды, которые полезно знать тем, кто собирается использовать данную технологию.

Причина

Причина, по которой решено было внедрять микросервисный подход на фронте, довольно простая - много команд, а проект один, нужно было как-то разделить зоны ответственности и распараллелить разработку. Как раз в тот момент, мне на глаза попался доклад Павла Черторогова про Webpack 5 Module Federation. Честно, это перевернуло моё видение современных веб-приложений. Я очень вдохновился и начал изучать и крутить эту технологию, чтобы понять, можно ли применить это в нашем проекте. Оказалось, всё что нужно, это дописать несколько строк в конфиг Webpack, создать пару компонентов-хелперов, и... всё завелось.

Настройка

Итак, что же нужно сделать, чтобы запустить микрофронтенды на базе сборки Webpack 5?

Для начала, убедитесь, что используете Webpack пятой версии, потому что Module Federation там поддерживается из коробки.

Настройка shell-приложения

Так как, до внедрения микрофронтендов, у нас уже было действующее приложение, решено было использовать его в качестве точки входа и оболочки для подключения других микрофронтендов. Для сборки использовался Webpack версии 4.4 и при обновлении до 5 версии возникли небольшие проблемы с некоторыми плагинами. К счастью, это решилось простым поднятием версий плагинов.

Чтобы создать контейнер на базе сборки Webpack и при помощи этого контейнера иметь возможность импортировать ресурсы с удаленных хостов добавляем в Webpack-конфиг следующий код:

const webpack = require('webpack');// ...const { ModuleFederationPlugin } = webpack.container;const deps = require('./package.json').dependencies;module.exports = {  // ...  output: {    // ...    publicPath: 'auto', // ВАЖНО! Указывайте либо реальный publicPath, либо auto  },  module: {    // ...  },  plugins: [    // ...    new ModuleFederationPlugin({      name: 'shell',      filename: 'shell.js',      shared: {        react: { requiredVersion: deps.react },        'react-dom': { requiredVersion: deps['react-dom'] },        'react-query': {          requiredVersion: deps['react-query'],        },      },      remotes: {        widgets: `widgets@http://localhost:3002/widgets.js`,      },    }),  ],  devServer: {    // ...  },};

Теперь нам нужно забутстрапить точку входа в наше приложение, чтобы оно запускалось асинхронно, для этого создаем файл bootstrap.tsx и кладем туда содержимое файла index.tsx

// bootstrap.tsximport React from 'react';import { render } from 'react-dom';import { App } from './App';import { config } from './config';import './index.scss';config.init().then(() => {  render(<App />, document.getElementById('root'));});

А в index.tsx вызываем этот самый bootstrap

import('./bootstrap');

В общем то всё, в таком виде уже можно импортировать ваши микрофронтенды - они указываются в объекте remotes в формате <name>@<адрес хоста>/<filename>. Но нам такая конфигурация не подходит, ведь на момент сборки приложения мы ещё не знаем откуда будем брать микрофронтенд, к счастью, есть готовое решение, поэтому возьмем код из примера для динамических хостов, так как наше приложение написано на React, то оформим хэлпер в виде React-компонента LazyService:

// LazyService.tsximport React, { lazy, ReactNode, Suspense } from 'react';import { useDynamicScript } from './useDynamicScript';import { loadComponent } from './loadComponent';import { Microservice } from './types';import { ErrorBoundary } from '../ErrorBoundary/ErrorBoundary';interface ILazyServiceProps<T = Record<string, unknown>> {  microservice: Microservice<T>;  loadingMessage?: ReactNode;  errorMessage?: ReactNode;}export function LazyService<T = Record<string, unknown>>({  microservice,  loadingMessage,  errorMessage,}: ILazyServiceProps<T>): JSX.Element {  const { ready, failed } = useDynamicScript(microservice.url);  const errorNode = errorMessage || <span>Failed to load dynamic script: {microservice.url}</span>;  if (failed) {    return <>{errorNode}</>;  }  const loadingNode = loadingMessage || <span>Loading dynamic script: {microservice.url}</span>;  if (!ready) {    return <>{loadingNode}</>;  }  const Component = lazy(loadComponent(microservice.scope, microservice.module));  return (    <ErrorBoundary>      <Suspense fallback={loadingNode}>        <Component {...(microservice.props || {})} />      </Suspense>    </ErrorBoundary>  );}

Хук useDynamicScript нужен нам, чтобы в рантайме прикреплять загруженный скрипт к нашему html-документу.

// useDynamicScript.ts  import { useEffect, useState } from 'react';export const useDynamicScript = (url?: string): { ready: boolean; failed: boolean } => {  const [ready, setReady] = useState(false);  const [failed, setFailed] = useState(false);  useEffect(() => {    if (!url) {      return;    }    const script = document.createElement('script');    script.src = url;    script.type = 'text/javascript';    script.async = true;    setReady(false);    setFailed(false);    script.onload = (): void => {      console.log(`Dynamic Script Loaded: ${url}`);      setReady(true);    };    script.onerror = (): void => {      console.error(`Dynamic Script Error: ${url}`);      setReady(false);      setFailed(true);    };    document.head.appendChild(script);    return (): void => {      console.log(`Dynamic Script Removed: ${url}`);      document.head.removeChild(script);    };  }, [url]);  return {    ready,    failed,  };};

loadComponent это обращение к Webpack-контейнеру, по сути - обычный динамический импорт.

// loadComponent.tsexport function loadComponent(scope, module) {  return async () => {    // Initializes the share scope. This fills it with known provided modules from this build and all remotes    await __webpack_init_sharing__('default');    const container = window[scope]; // or get the container somewhere else    // Initialize the container, it may provide shared modules    await container.init(__webpack_share_scopes__.default);    const factory = await window[scope].get(module);    const Module = factory();    return Module;  };}

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

// types.tsexport type Microservice<T = Record<string, unknown>> = {  url: string;  scope: string;  module: string;  props?: T;};
  • url - имя хоста + имя контейнера (например, http://localhost:3002/widgets.js), с которого мы хотим подтянуть модуль

  • scope - параметр name, который мы укажем в удаленном конфиге ModuleFederationPlugin

  • module - имя модуля, который мы хотим подтянуть

  • props - опциональный параметр, если вдруг наш микросервис требует пропсы, нужно их типизировать

Вызов компонента LazyService происходит следующим образом:

import React, { FC, useState } from 'react';import { LazyService } from '../../components/LazyService';import { Microservice } from '../../components/LazyService/types';import { Loader } from '../../components/Loader';import { Toggle } from '../../components/Toggle';import { config } from '../../config';import styles from './styles.module.scss';export const Video: FC = () => {  const [microservice, setMicroservice] = useState<Microservice>({    url: config.microservices.widgets.url,    scope: 'widgets',    module: './Zack',  });  const toggleMicroservice = () => {    if (microservice.module === './Zack') {      setMicroservice({ ...microservice, module: './Jack' });    }    if (microservice.module === './Jack') {      setMicroservice({ ...microservice, module: './Zack' });    }  };  return (    <>      <div className={styles.ToggleContainer}>        <Toggle onClick={toggleMicroservice} />      </div>      <LazyService microservice={microservice} loadingMessage={<Loader />} />    </>  );};

В общем то, по коду видно, что мы можем динамически переключать наши модули, а основной url хранить, например, в конфиге.

Так, с shell-приложением вроде разобрались, теперь нужно откуда-то брать наши модули.

Настройка микрофронтенда

Для начала проделываем все те же манипуляции что и в shell-приложении и убеждаемся, что версия Webpack => 5

Настраиваем ModuleFederationPlugin, но уже со своими параметрами, эти параметры указываем при подключении модуля в основное приложение.

// ...new ModuleFederationPlugin({      name: 'widgets',      filename: 'widgets.js',      shared: {        react: { requiredVersion: deps.react },        'react-dom': { requiredVersion: deps['react-dom'] },        'react-query': {          requiredVersion: deps['react-query'],        },      },      exposes: {        './Todo': './src/App',        './Gallery': './src/pages/Gallery/Gallery',        './Zack': './src/pages/Zack/Zack',        './Jack': './src/pages/Jack/Jack',      },    }),// ...

В объекте exposes указываем те модули, которые мы ходим отдать наружу, точку входа в приложение так же нужно забутстрапить. Если в микрофронтенде нам не нужны модули с других хостов, то компонент LazyService тут не нужен.

Вот и всё, получен работающий прототип микрофронтенда.

Выглядит круто, работает тоже круто. Общие зависимости не грузятся повторно, версии библиотек рулятся плагином, можно динамически переключать модули, в общем, сказка. Если копать глубже, то это очень гибкая технология, можно использовать её не только с React и JavaScript, но и со всем, что переваривает Webpack, то есть теоретически можно подружить части приложения написанные на разных фреймворках, это конечно не очень хорошо, но сделать так можно. Можно собрать модули и положить на CDN, можно использовать контейнер как общую библиотеку компонентов для нескольких приложений. Возможностей реально много.

Проблемы

Когда удалось запустить это в нашем проекте я был доволен, нет, очень доволен, но это длилось недолго, после того как началась реальная работа над микрофронтендами, начали всплывать наши любимые подводные камни, а теперь поговорим он них подробнее.

Потеря контекстов в React-компонентах

Как только понадобилось работать с контекстом библиотеки react-router, то возникли проблемы, при попытке использовать в микрофронтенде хук useLocation, например, приложение вылетало с ошибкой.

Ошибка при попытке обращения к контексту shell-приложения из микрофронтендаОшибка при попытке обращения к контексту shell-приложения из микрофронтенда

Для взаимодействия с бэкендом мы используем Apollo, и хотелось, чтобы ApolloClient объявлялся только единожды в shell-приложении. Но при попытке из микрофронтенда просто использовать хук useQuery, в рантайме приложение вылетало с такой же ошибкой как и для useLocation.

Экспериментальным путём было выяснено, для того чтобы контексты правильно работали, нужно в микрофронтендах использовать версию npm-пакета не выше, чем в shell-приложение, так что за этим нужно внимательно следить.

Дублирование UI-компонентов в shell-приложении и микрофронтенде

Так как разработка ведётся разными командами, есть шанс, что разработчики напишут компоненты с одинаковым функционалом и в shell-приложении и в микрофронтенде. Чтобы этого избежать, есть несколько решений:

  1. Выносить UI-компоненты в отдельный npm-пакет и использовать его как shared-модуль

  2. "Делиться" компонентами через ModuleFederationPlugin

В принципе, у обоих подходов есть свои плюсы, но мы выбрали первый, потому что так удобнее и прозрачнее управлять библиотекой компонентов. Да и саму технологию Module Federation хотелось использовать как механизм для построения микрофронтендов, а не аналог npm.

Заключение

Пока что выглядит так, что переход на Webpack 5 Module Federation решает проблему, которая стояла перед нашим стримом, а именно - разделение зоны ответственности и распараллеливание разработки. При этом, нет больших накладных расходов при разработке, а настройка довольно проста даже для тех, кто не знаком с этой технологией.

Минусы у этого подхода конечно же есть, накладные расходы для развертывания зоопарка микрофронтендов будут значительно выше, чем для монолита. Если над вашим приложением работает одна-две команды и оно не такое большое, то наверное не стоит делить его на микросервисы.

Но для нашей конкретной проблемы, это решение подошло хорошо, посмотрим, как оно покажет себя в будущем, технология развивается и уже появляются фреймворки и библиотеки, которые под капотом используют Module Federation.

Полезные ссылки

Репозиторий из примера

Документация Module Federation в доках Webpack 5

Примеры использования Module Federation

Плейлист по Module Federation на YouTube

Подробнее..

Перевод Почему мы перешли с Webpack на Vite

06.05.2021 20:17:21 | Автор: admin
image


Миссия Replit сделать программирование более доступным. Мы предоставляем людям бесплатные вычисления в облаке, чтобы они могли создавать приложения на любом устройстве. Одним из самых популярных способов создания приложений в Интернете на сегодняшний день является React. Однако исторически инструменты React были медленными на Replit. В то время как экосистема JavaScript создала отличные инструменты для профессиональных разработчиков, многие из самых популярных из них, такие как Create React App и Webpack, становятся все более сложными и неэффективными.

К счастью, мы заметили, как сообщество JavaScript осознало эту проблему и перешло к созданию более быстрых и эффективных инструментов, что означает, что мы, наконец, можем предоставить опыт, который наши пользователи ожидают от нас.

Этот новый опыт основан на Vite, инструменте сборки JavaScript, который обеспечивает быструю и экономичную разработку. Vite поставляется с рядом функций, включая HMR или Hot Module Replacement, команду сборки, которая объединяет ваши инструменты с Rollup, и встроенную поддержку TypeScript и JSX.

Vite ускоряет разработку с React. Очень сильно ускоряет. С HMR изменения, которые вы вносите, визуализируются в течении миллисекунд, что значительно ускоряет создание прототипов пользовательского интерфейса. Имея это в виду, мы решили переписать наш шаблон React, используя Vite, и были шокированы, увидев, насколько он стал быстрее. Вот как он выглядит по сравнению с нашим старым шаблоном CRA:



За последние несколько недель наш новый шаблон React помог нашим дизайнерам прототипировать сложные компоненты пользовательского интерфейса и позволил многим нашим кандидатам в инженеры создать целое веб-приложение всего за несколько часов на наших (виртуальных) сайтах. Когда вы видите это в действии, легко понять, почему:

image

Как это работает


Vite работает, по-разному обрабатывая ваш исходный код и ваши зависимости. В отличие от вашего исходного кода, зависимости не так часто меняются во время разработки. Vite использует этот факт, предварительно связывая ваши зависимости с помощью esbuild. Esbuild это сборщик JS, написанный на Go, который связывает зависимости в 10-100 раз быстрее, чем альтернативы на основе JavaScript, такие как Webpack и Parcel.

Затем он обслуживает ваш исходный код через собственные модули ES или ESM, что позволяет браузеру обрабатывать фактический бандл.

Наконец, Vite поддерживает HMR, который гарантирует, что при редактировании файла заменяются только соответствующие модули, а не перестраивает весь бандл, что вызывает перезагрузку страницы и сбрасывает состояние. В отличие от других сборщиков, Vite выполняет HMR над собственными модулями ES, что означает, что ему нужно только аннулировать затронутые модули при редактировании файла. Это гарантирует, что время обновления будет постоянно быстрым, а не линейно масштабироваться по мере роста вашего приложения.

Начнем


Для начала просто сделайте форк нашего шаблона React или выберите React.js в раскрывающемся списке при создании нового репла.

Vite также не зависит от фреймворка, поэтому, если React вам не нравится, вы также можете использовать наши шаблоны Vue и Vanilla JS.

Мы надеемся, что это поможет воплотить ваши идеи в жизнь еще быстрее, и с нетерпением ждем того, что вы создадите!
Подробнее..

Перевод 7 лучших библиотек для создания молниеносно быстрых приложений ReactJS

27.05.2021 18:04:31 | Автор: admin

Некоторые необходимые инструменты для rock-star разработчика

Привет, Хабр. В рамках набора на курс "React.js Developer" подготовили перевод материала.

Всех желающих приглашаем на открытый демо-урок "Webpack и babel". На занятии рассмотрим современные и мощные фишки JavaScript Webpack и Babel. Пошагово покажем, как с нуля создать проект на React, используя Webpack. Присоединяйтесь!


ReactJS по умолчанию обладает высокой производительностью. Но время от времени у вас появляется шанс сделать его еще лучше. И замечательное сообщество React придумало для этого несколько фантастических библиотек.

Сегодня мы поговорим о семи таких библиотеках, которые могут улучшить качество вашего кода и при этомповысить производительность.

Давайте начнем.

. . .

1. React Query

Известно, что React Query, библиотека управления состоянием для React, отсутствует.

В документации к ней говорится: Получайте, кэшируйте и обновляйте данные в ваших приложениях React, не затрагивая никакого "глобального состояния".

Да. Это именно то, что она делает. Она помогает нам управлять состоянием сервера без лишних хлопот. Это может уменьшить необходимость использования библиотеки управления состоянием, такой как Redux.

Преимущества

  • Автоматическое кэширование

  • Автоматическое обновление данных в фоновом режиме

  • Значительно сокращает объем кода

До использования React Query

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

const useFetch = (url) => {  const [data, setData] = useState();  const [isLoading, setIsLoading] = useState(false);  const [error, setError] = useState(false);   useEffect(() => {    const fetchData = async () => {      setIsError(false);      setIsLoading(true);      try {        const result = await fetch(url);        setData(result.data);      } catch (error) {        setError(error);      }      setIsLoading(false);    };    fetchData();  }, [url]);    return {data , isLoading , isError}}

После (использования) React Query

Вот код, если мы хотим использовать React Query. Посмотрите, какой он маленький.

import { useQuery } from 'react-query'const { isLoading, error, data } = useQuery('repoData', () =>    fetch(url).then(res =>res.json()  ))

Посмотрите, насколько сильно сократился наш код.

. . .

2. React Hook Form

React Hook Form - это современная библиотека обработки форм, которая может поднять эффективность работы вашей формы на совершенно новый уровень.

Преимущества

  • Уменьшает объем кода

  • Сокращает ненужный ре-рендеринг.

  • Легко интегрируется с современными библиотеками пользовательского интерфейса (UI)

Ниже приведен пример, демонстрирующий, как React Hook Form может улучшить качество кода.

Без React Hook Form

Вот пример создания формы авторизации вручную.

function LoginForm() {  const [email, setEmail] = React.useState("");  const [password, setPassword] = React.useState("");  const handleSubmit = (e: React.FormEvent) => {    e.preventDefault();    console.log({email, password});  }    return (    <form onSubmit={handleSubmit}>          <input        type="email"        id="email"        value={email}        onChange={(e) => setEmail(e.target.value)}      />            <input        type="password"        id="password"        value={password}        onChange={(e) => setPassword(e.target.value)}      />          </form>  );}

С помощью React Form

Вот тот же пример с React Hook Form.

function LoginForm() {  const { register, handleSubmit } = useForm();    const onSubmit = data => console.log(data);     return (    <form onSubmit={handleSubmit(onSubmit)}>      <input {...register("email")} />      <input {...register("password")} />      <input type="submit" />    </form>  );}

Выглядит аккуратно и в то же время эффективно. Попробуйте.

. . .

3. React Window

React Window используется для рендеринга длинных списков. Представьте, что у вас есть список из 1 000 элементов. На экране отображаются только десять, но ваш код пытается визуализировать 1000 элементов одновременно.

Это может привести к серьезным задержкам в вашем приложении. Данная библиотека очень популярна и является обязательным инструментом в вашем арсенале.

Ручной рендеринг 1 000 элементов

import React, {useEffect, useState} from 'react';const names = [] // 1000 namesexport const LongList = () => {    return <div>       {names.map(name => <div> Name is: {name} </div>)}     <div/>}

Но этот код рендерит 1000 элементов одновременно, хотя на экране можно увидеть не более 10-20 элементов.

Использование React Window

Теперь давайте используем React Window.

import { FixedSizeList as List } from 'react-window'; const Row = ({ index, style }) => <div style={style}> Name is {names[index]}</div> const LongList = () => (  <List    height={150}    itemCount={1000}    itemSize={35}    width={300}  >    {Row}  </List>);

Данный код рендерит только то, что вы видите на экране. Поначалу это может быть непонятно, но необходимо, если вам нужно отобразить длинный список.

. . .

4. React LazyLoad

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

React LazyLoad - это библиотека, специально созданная для этой цели. Вы просто оборачиваете свой компонент, а эта библиотека позаботится обо всем остальном.

Преимущества

  • Повышенная производительность

  • Поддерживает рендеринг на стороне сервера

Без LazyLoad

Вот пример, в котором мы загружаем пять изображений вручную.

import React from 'react';const ImageList = () => {    return <div>    <img src ='image1.png' />    <img src ='image2.png' />    <img src ='image3.png' />    <img src ='image4.png' />    <img src ='image5.png' />  </div>}

С LazyLoad

Вот тот же пример с компонентом LazyLoad.

import React from 'react';import LazyLoad from 'react-lazyload';const ImageList = () => {    return <div>    <LazyLoad> <img src ='image1.png' /> <LazyLoad>    <LazyLoad> <img src ='image2.png' /> <LazyLoad>    <LazyLoad> <img src ='image3.png' /> <LazyLoad>    <LazyLoad> <img src ='image4.png' /> <LazyLoad>    <LazyLoad> <img src ='image5.png' /> <LazyLoad>  </div>}

. . .

5. Почему вы выполняете рендеринг (Why Did You Render)

Ненужный рендеринг может навредить производительности ваших React-приложений. Но иногда мы делаем это, даже не подозревая.

Этот замечательный пакет, Why Did You Render, помогает нам найти проблемы с производительностью и решить их. Вы просто включаете его в любом компоненте, и он сообщает вам, почему именно происходит рендеринг.

Ниже представлен компонент с возникающими проблемами рендеринга.

import React, {useState} from 'react'const WhyDidYouRenderDemo = () => {    console.log('render')        const [user , setUser] = useState({})    const updateUser = () => setUser({name: 'faisal'})    return <>        <div > User is : {user.name}</div>        <button onClick={updateUser}> Update </button>    </>}export default WhyDidYouRenderDemo

После включения эта библиотека будет записывать в консоль следующий результат.

Из этого журнала видно, что мы обновляем объект с одним и тем же значением, что плохо для производительности.

. . .

6. Reselect

Если вы используете Redux, то эта функция станет для вас спасительной. Мы знаем, что редьюсеры Redux могут хранить большое количество данных, и если вы передадите всё хранилище в какой-либо компонент, это приведет к тому, что он будет заново рендериться каждый раз, когда что-либо в этом хранилище будет обновляться.

Reselect решает эту проблему, меморизуя значения и передавая только то, что необходимо.

Преимущества (из документации)

  • Селекторы могут вычислять производные данные, что позволяет Redux хранить минимально возможное состояние.

  • Селекторы эффективны. Селектор не пересчитывается, если один из его аргументов не изменился.

  • Селекторы являются составными. Они могут быть использованы в качестве входных данных для других селекторов.

Пример

Ниже приведен пример получения значений из хранилища и их изменения в селекторе.

import { createSelector } from 'reselect'const shopItemsSelector = state => state.shop.itemsconst subtotalSelector = createSelector(  shopItemsSelector,  items => items.reduce((subtotal, item) => subtotal + item.value, 0))const exampleState = {  shop: {    items: [      { name: 'apple', value: 1.20 },      { name: 'orange', value: 0.95 },    ]  }}

. . .

7. Deep Equal

Deep Equal - это известная библиотека, которую можно использовать для сравнения. Это очень удобно. Ведь в JavaScript, несмотря на то, что два объекта могут иметь одинаковые значения, они считаются разными, поскольку указывают на разные области памяти.

Вот почему мы видим следующий результат.

const user1 = {    name:'faisal'}const user2 ={    name:'faisal'}const normalEqual = user1 === user2 // false

Но если нужно проверить равенство (для мемоизации), то это становится затратной (и сложной) операцией.

Если мы используем Deep Equal, то это повышает производительность в 46 раз. Ниже приведен пример того, как мы можем это сделать.

var equal = require('deep-equal');const user1 = {    name:'faisal'}const user2 ={    name:'faisal'}const deepEqual = equal(user1 , user2); // true -> exactly what we wanted!

. . .

Вот как это выглядит. Таковы некоторые из самых важных библиотек, которые вы можете использовать для обеспечения максимальной производительности вашего приложения React.

Оставляйте комментарии, если у вас на примете есть другие. Хорошего дня!

Ресурсы

  1. Веб-сайт React Query

  2. Веб-сайт React Hook Form

  3. Примеры React Window

  4. Пакет Why Did You Render

  5. Пакет React Lazy Load

  6. Reselect Репозиторий

  7. Пакет Deep Equal


Узнать подробнее о курсе "React.js Developer"

Смотреть открытый онлайн-урок "Webpack и babel"

Подробнее..

Как мы распилили монолит. Часть 4. И как Angular между приложениями пошарили

23.12.2020 10:20:51 | Автор: admin

В первой, второй и третьей частях мои коллеги рассказали, как и почему мы распиливали монолит.

Если коротко, то мы создали решение, которое позволило в рамках одной открытой страницы браузера запускать несколько независимых Angular-приложений, шарить между ними данные, управлять роутингом и аутентификацией. Мы научились бороться с утечками памяти и решать конфликты глобальных стилей приложений. Но одна проблема оставалась открытой каждое приложение несло в своем банде Angular, RxJS, zone.js и т. д. И в этой статье я расскажу, как мы ее решили.

Исследование

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

Итак, дано:

  • десятки приложений, созданных с помощью Angular CLI или nx;

  • доступ к конфигурации webpack через кастомные билдеры;

  • артефакты сборок webpack каждого приложения;

  • одна страница браузера.

Минимальная цель: запустить дочерние приложения на Angular, на котором уже работает Frame Manager.

Мы выделили для себя четыре гипотетических варианта решения проблемы:

  1. Monorepo.

  2. Micro application like a package.

  3. Webpack Externals.

  4. Webpack Module Federation.

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

Monorepo & Micro app like a package

Monorepo это подход, когда все приложения и библиотеки хостятся в одном репозитории.

Micro app like a package имеется в виду подход, когда каждое приложение собирается как npm-пакет и устанавливается в host-приложение.

Плюсы и минусы обоих подходов схожи (поэтому они и объединены в одном пункте).

Минусы

  • Максимально возможная оптимизация бандла средствами Angular.

  • Одни и те же версии библиотек у всех приложений.

  • Отсутствие костылей с инициализацией.

Плюсы

  • Всегда нужно релизить все.

  • Миграции всех приложений при апдейте зависимостей.

  • Много инфраструктурных изменений.

Вывод напрашивается сам (хотя он был понятен еще до осознания этих плюсов/минусов) оба решения совершенно нам не подходят.

Webpack Externals

Webpack Externals это конфигурационная опция webpack, позволяющая исключать зависимости из бандла. Если в пользовательском окружении есть доступ к библиотеке, например через глобальные переменные, то эта опция как раз для такого случая.

Плюсы

  • Можно добиться экстремально маленького бандла. То есть взять и исключить вообще все внешние библиотеки, оставив только код самого приложения.

Минусы

  • Исключенные библиотеки все равно нужно поставлять.

  • Ничего не умеет из коробки

И опять неутешительный вывод нам необходимо решение чуть более сложное чем просто вырезать Angular из бандла.

Webpack Module Federation

Webpack Module Federation это подход, когда несколько отдельных сборок формируют одно приложение. Эти отдельные сборки не должны иметь зависимости друг от друга, поэтому их можно разрабатывать и развертывать индивидуально. Звучит многообещающе!

Плюсы

  • Приложения остаются самодостаточными

  • Архитектурно ничего не меняется

Минусы

  • На момент исследования Angular не поддерживал webpack 5. Сейчас поддерживает экспериментально.

  • На момент исследования никто не знал, когда Angular будет поддерживать webpack 5. На момент публикации, можно сказать, ничего не изменилось.

Итого: нестабильность, неопределённость и разочарование.

Результат исследования

Нам не подошло ни одно из существующих решений.

Исследование 2.0

Да, мы запустили еще одно исследование, но уже на основе результатов предыдущего мы составили список требований к будущему решению:

  1. Приложения остаются самодостаточными. Каждое приложение продолжает так же независимо разрабатываться и имеет возможность самостоятельно запускаться как изолированно в iframe, так и без него.

  2. Архитектурные изменения минимальны. Решение быстро и безболезненно интегрируется в любое приложение без глобальных изменений в организации кода и кодовой базы.

  3. Приемлемое время и сложность имплементации.

  4. Фолбэки. В этом пункте под фолбэками мы имеем в виду возможность приложения самостоятельно догрузить библиотеку для работы, если никакое другое приложение этой библиотекой еще не поделилось.

  5. Библиотеки разных версий живут в своих бандлах.

В итоге этого исследования решение все-таки было найдено и через три дня уже был готов прототип.

@tinkoff/shared-library-webpack-plugin

Мы сделали webpack-плагин, который отвечает всем нашим требованиям. Если коротко, то плагин добавляет еще одну сущность в бандлы, которую мы назвали shared chunks. Эти сущности умеют использовать все приложения, собранные с помощью плагина. Если подробнее, то стоит начать с основных сущностей webpack.

Основные сущности webpack, которые выделили мы:

  • Runtime отвечает за запуск приложения, поставляет такие функции, как require, также умеет загружать lazy chunks и т. д.

  • Entries чанки, содержащие точку входа приложения.

  • Lazy chunks ленивые чанки, загружаемые в приложение по требованию.

Плагин добавляет еще одну сущность:

  • Shared chunks отдельный чанк, содержащий библиотеки, которые могут использоваться несколькими приложениями.

Как выглядит сборка на примере Angular

Допустим, мы имеем два приложения, сгенерированных с помощью Angular CLI. С помощью любого кастомного билдера с поддержкой модификации конфигурации webpack и в оба приложения добавляем плагин со следующими настройками:

В настройках мы указываем, что приложение будет делиться всеми используемыми пакетами Angular (@angular/** ), а также zone.js. Артефакты такой сборки будут выглядеть примерно так.

Здесь мы видим, что каждый пакет Angular выделен в свой чанк, как и библиотека zone.js. Когда первое приложение загрузится, оно запросит все свои чанки, включая Angular и zone.js, и запустится.

При загрузке второго приложения будут запрошены runtime, main и polyfills. И приложение запустится. Никакой повторной загрузки Angular и zone.js. Второе приложение использует уже загруженные ранее экземпляры.

Как плагин работает

При сборке плагин:

  1. Анализирует ресурсы и ищет библиотеки, которые указаны в настройках.

  2. Выделяет библиотеки для шаринга в отдельные чанки (те самые shared chunks), хэширует имена с учетом версии.

  3. Учит entries и runtime работать с shared chunks и сообщает каждому entry, какие shared chunks ему нужны для работы.

Как загружается приложение

  1. На клиенте загружается runtime и entries. Тут все стандартно.

  2. Каждая точка входа проверяет наличие обязательных для запуска шареных библиотек и сообщает о результатах runtime.

  3. Runtime скачивает недостающие библиотеки и маркирует их как загруженные.

  4. Runtime запускает приложение.

Кажется, что все просто. Но, как правило, просто только для автора кода. И то только первые пару месяцев.

А что насчет библиотек с разными версиями?

При формировании имен чанков плагин учитывает версию библиотеки. По умолчанию мы считаем, что каждая библиотека придерживается семантического версионирования и фикс версию можно упустить.

Это значит, что для модулей @angular/core@10.0.0 и @angular/core@10.0.1 будет сформировано одинаковое имя angularCore-10.0. Мы видим, что фикс-версия просто отсутствует. Именно поэтому чанки, имеющие одинаковые имена, грузятся единожды.

Если третье приложение имеет в зависимостях @angular/core/@9.x.x, то для shared chunk будет сформировано имя angularCore-9.x. Понятно, что в таком случае приложение загрузит свою версию библиотеки и будет работать с ней.

В случае проблем совместимости стандартное поведение формирования имени чанка можно изменить тремя параметрами: chunkname, suffix и separator.

Demo

Самым нетерпеливым ссылка на репозиторий

Дано:

  • Хост-приложение. В него входит верхний тулбар и навигация слева.

  • Два дочерних приложения. Каждое из них состоит из тулбара и некоего форматированного текста. В нашем случае это отрисованные md-файлы.

То есть на клиента загружается хост-приложение, которое в зависимости от роута подгружает дочернее приложение, предварительно подготавливая для него окружение.

Каждое приложение несет в себе свой экземпляр Angular, zone.js и т. д. Общий вес JavaScript после загрузки на клиента всех трех приложений составит 282.8kb в gzip.

В каждом приложении в сборку включаем плагин со следующими настройками:

const {  SharedLibraryWebpackPlugin,} = require('@tinkoff/shared-library-webpack-plugin');module.exports = {  plugins: [    new SharedLibraryWebpackPlugin({      libs: [        '@angular/core',        '@angular/common',        '@angular/common/http',        '@angular/platform-browser',        '@angular/platform-browser/animations',        '@angular/animations',        '@angular/animations/browser',        'zone.js/dist/zone',      ],    }),  ],};

Из конфигурации видно, что мы хотим пошарить между приложениями основные модули Angular и zone.js. Собираем, запускаем, открываем браузер и видим ужасную картину, когда размер хост-приложения увеличился на 58%(!).

Так происходит из-за отключения tree shaking для shared chunks. Причина отключения очень проста: заранее неизвестно, какую часть библиотеки будет использовать другое приложение.

Но при загрузке дочерних приложений мы видим совсем другую картину. Они стали грузить на 70% меньше JavaScript для запуска, так как Angular и zone.js уже были загружены. Общий размер загружаемого JavaScript упал на 19%.

Но можно ли еще уменьшить количество загружаемого кода? Оказывается, можно, стоит лишь чуть больше поиграть с webpack.

Используемые экспорты

Как я писал выше, мы блокируем возможность webpack вырезать неиспользуемый в приложении код, потому что никогда заранее не знаем, какую часть шареной библиотеки будет использовать дочернее приложение. А что, если знаем?

В теории хост-приложение должно поставить на клиента шареную библиотеку, которая содержит объединение экспортов всех дочерних приложений. С этой мыслью мы добавили новую опцию в конфигурацию плагина usedExports, которая принимает массив строк. Фактически это перечисление экспортов из библиотеки, которые webpack должен включить в shared chunk в дополнение к уже используемым в приложении.

Итак, пора проверить, как это работает на нашем демо. Для этого мы модифицируем настройки плагина:

const {  SharedLibraryWebpackPlugin,} = require('@tinkoff/shared-library-webpack-plugin');module.exports = {  plugins: [    new SharedLibraryWebpackPlugin({      libs: [        { name: '@angular/core', usedExports: [] },        { name: '@angular/common', usedExports: [] },        { name: '@angular/common/http', usedExports: [] },        { name: '@angular/platform-browser', usedExports: ['DomSanitizer'] },        { name: '@angular/platform-browser/animations', usedExports: [] },        { name: '@angular/animations', usedExports: [] },        { name: '@angular/animations/browser', usedExports: [] },        'zone.js/dist/zone',      ],    }),  ],};

Пустой массив в usedExports означает, что webpack включит в shared chunk только экспорты, используемые в самом приложении. Опытным путем мы узнали, что для корректной работы одному из приложений понадобится DomSanitizer из модуля @angular/platform-browser, что мы тоже отражаем в конфигурации.

Собираем, запускаем, открываем браузер и видим

Хост-приложение грузит 129kb, что на 12kb больше, чем загружает хост-приложение, собранное без плагина. Но что с остальными приложениями? Они грузят все те же 23kb. Итого суммарно на клиента попадает на 38% меньше JavaScript кода. То есть было 282.8kb, а стало 174.6kb. Неплохо? Кажется, что немного лучше, чем неплохо, и даже хорошо! Для 20+ приложений со схожей архитектурой количество профита будет еще больше!

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

Подведем итоги

При разработке плагина мы оглядывались на выявленные в ходе первого исследования требования и как результат все требования соблюдены и учтены (ставим мысленные пять чеков к пунктам из абзаца с требованиями). Количество загружаемого на клиента JavaScript-кода уменьшено. Дочерние приложения быстрее загружаются и инициируются, потребляют меньше памяти и вычислительных ресурсов. Мы перестали бояться webpack. И обогнали появление Webpack 5 в Angular на полгода. Тут также нужно учесть, что сейчас это даже не стабильная связка.

На этой ноте наше повествование о распиливании монолита официально закончено. Мы были рады поделиться своим опытом и будем рады почитать про ваш! Больше хардкорных тем в ленту Хабра!

Полезные ресурсы

Здесь оставлю ссылки на предыдущие части

Подробнее..

TypeScript для конфигурации WebPack (FE and BE)

30.12.2020 20:10:58 | Автор: admin

Легенда

Когда проект зародился, то нравился каждому. Белый листа бумаги и каждый смотрел на него с ожиданием и воображал какие перспективы откроются, какие проблемы решатся.

Вот на бумагу архитектор нанес первый блок. Сзади раздалась ругань. Это разработчики, спорили: Как лучше стартовать новый сервис и какой стартер выбрать. У архитектора по спине пронесся холодок. Не успела сложилась архитектура даже для Proof Of Concept, не то что для Minimal Valuable Product, но уже возникли препятствия. Выбор стартера наложит пока не очевидные рамки.

Одно было ясно, сборщик будет использоваться. Архитектор подошел к Team Lead и попросил использовать WebPack и чистый проект без стартера, так как по прошлым проектам с ним в той или иной мере знакомы разработчикам.

Мотивация

Каждый кто в 2020 использовал браузер - пользовался результатами сборки с помощью WebPack.

Среди разработчиков некоторые добавляли обработчик для специальных файлов или плагин для нужд проекта или использовали уже готовую конфигурацию, например в create-react-app.

Задач много и помнить параметры конфигурации смысла нет. Структура и часто используемые настройки сами отложатся в голове.

Готовые плагины и loader's сильно облегчают работу, задача на 95% заключается в прочтении первой страницы документации, чтобы сконфигурировать под конкретный проект. Даже в таком случае ошибки в синтаксисе случаются. Мало кто сходу вспомнит devtool или devtools. Некоторые директивы относились к другой версии WebPack. Учет этого будет полезным положить на плечи TypeScript.

Пару лет назад мне не нахватало подробного описания такой настройки, а на сайте самого WebPack только короткое описание: вот ссылка.

Особенности проекта в статье

В проекте для статьи нет цели написать всеобъемлющий мануал по настройке, будет базовый пример для backend и frontend.

Cервер будет отдавать статическую директорию с FE для нашего сайта. Сам же FE будет только выводить на страницу Hello World!. Зависимостями для BE будет node, для сборки webpack.

GitHub: тут

Структура директорий c описанием

Для удобства демонстрации я буду использовать моно-репозиторий с server и webapp в одном проекте

  • ~/projectfolder/ # Корень проекта -- инициализирован с помощью yarn init

    • /apps # директория приложений

      • /server # директория backend -- инициализирована с помощью yarn init

        • /src # исходный код сервера

        • файлы конфигурации (части относящиеся к BE)

      • /webapp # директория frontend -- инициализирована с помощью yarn init

        • /src # исходный код браузерного приложения

        • файлы конфигурации (части относящиеся к FE)

      • /utils # расширенные утилиты

    • общие части конфигурации

Зависимости проекта

  • Общие в директории ~/project_folder

yarn add -D @types/node @types/webpack concurrently cross-env nodemon ts-loader ts-node typescript webpack webpack-cli
  • Для сервера в директории /apps/server нам не понадобится дополнительных зависимостей помимо тех что есть в общей директории

  • Для веб-приложения в директории /apps/web_app нам понадобится html-webpack-plugin 5 версии так как он предназначен для использования с WebPack 5 версии. На Момент написания этот покет еще в beta доступе.

cd apps/web_appyarn add -D html-webpack-plugin@5

Настройки TypeScript

Браузер, server, и компьютер разработчика или runner - это три среды с личными особенностями:

Для сервера главное, nodeс помощью которой будет выполняться итоговый скрипт сервера. Что доступно в зависимости от версии наглядно показывается по ссылке:https://node.green

Конкретная настройка сервера apps/server/tsconfig.json не влияет на сборку, главное в конфигурации webpack указать правильны путь до файла для сборки сервера.

Для браузера, на конец 2020, лучше выбиратьES6если нет задачи поддерживать Internet Explorer 11. Хороший сайт для проверки доступных функций: https://caniuse.com

Файл: apps/web_app/tsconfig.json

Компьютер разработчика или runner где будет собираться проект тоже накладывает ограничения, которые в большинстве ситуаций легко устранимы. Для запуска также понадобиться конфигурация TS, она будет использоваться ts-node который будет запускаться под капотом webpack.

Spoiler

tsconfig.json

"compilerOptions": {    "module": "commonjs",    "target": "es5",    "esModuleInterop": true  }

Данный файл обязателен и частью для запуска самого webpack с конфигурацией написанной на typescript

Серверное приложение

Сервер для данной статьи предельно прост, раздачей файлов из одной папки. Код является копией статьи (ссылка) с сайта node, адаптированный под этот проект и с защитой от доступа к родительским папкам ..\..\secret в запрошенных файлах.

Spoiler

apps/server/src/index.ts

import { resolve, normalize, join } from 'path'import { createServer, RequestListener} from 'http'import { readFile } from 'fs' const webAppBasePath = '../web_app'; // Это путь до папки уже после build (в директории dist)const handleWebApp: RequestListener = (req, res) => {    const resolvedBase = resolve(__dirname ,webAppBasePath);    const safeSuffix = normalize(req.url || '')        .replace(/^(\.\.[\/\\])+/, '');    const fileLocation = join(resolvedBase, safeSuffix);    readFile(fileLocation, function(err, data) {        if (err) {            res.writeHead(404, 'Not Found');            res.write('404: File Not Found!');            return res.end();        }        res.statusCode = 200;        res.write(data);        return res.end();    });};const httpServer = createServer(handleWebApp)httpServer.listen("5000", () => {    console.info('Listen on 5000 port')})

Frontend приложение

Web приложение также предельно простое. В document.body монтируется простой &lt;div id="root">Hello world!&lt;/div>

Spoiler

apps/webapp/src/index.ts

const rootNode = document.createElement('div')rootNode.setAttribute('id', 'root')rootNode.innerText = 'Hello World!'document.body.appendChild(rootNode)

Настройка WebPack

Теперь нам осталось только настроить webpack.

Для удобства конфигурацию можно разбить на файлы. А так как мы используем TS, то мы получаем синтаксис import {serverConfig} from "./apps/server/webpack.part"; из-за этого основной файл становится предельно коротким.

Spoiler

webpack.config.ts

import {serverConfig} from "./apps/server/webpack.part";import {webAppConfig} from "./apps/web_app/webpack.part";import {commonConfig} from "./webpack.common";export default [    /** server  **/ {...commonConfig, ...serverConfig},    /** web_app **/ {...commonConfig, ...webAppConfig},]

В нем мы только импортируем конфигурации и экспортируем их в виде массива попутно объединяя с общей частью.

Общая часть

Общая часть может содержать все что можно переиспользовать между различными конфигурациями. В нашем случае это поля mode и resolve. Обратите внимание, что у константы объявлена типизация const commonConfig: Configuration, тип взят из import {Configuration} from "webpack";.

Spoiler

webpack.common.ts

import {Configuration, RuleSetRule} from "webpack";import {isDev} from "./apps/_utils";export const tsRuleBase: RuleSetRule = {    test: /\.ts$/i,    loader: 'ts-loader',}export const commonConfig: Configuration = {    mode: isDev ? 'development' : 'production',    resolve: {        extensions: ['.tsx', '.ts', '.js', '.json'],    },}

Также в этом файле лежит общая для проекта часть настройки правила для загрузки TS файлов const tsRuleBase: RuleSetRule, тип взят из import {RuleSetRule} from "webpack";.

isDev это простая проверка isDev = process.env.NODE_ENV === 'development'

Конфигурация FE и BE

Тут уже все максимально похоже на простую настройку webpack, только с подсказками благодаря типизации import {Configuration, RuleSetRule, WebpackPluginInstance} from "webpack";

Обратите внимание на WatchIgnorePlugin так как благодаря нему можно исключить какие-то файлы и директории и при изменениях в них не будет перекомпиляции.

Spoiler

apps/server/webpack.part.ts

import {Configuration, RuleSetRule, WatchIgnorePlugin, WebpackPluginInstance} from "webpack";import {join} from "path";import {tsRuleBase} from "../../webpack.common";const serverPlugins: WebpackPluginInstance[] = [    new WatchIgnorePlugin({        paths: [join(__dirname, '..', 'apps', 'web_app')]    })]const tsRuleServer: RuleSetRule = {    ...tsRuleBase,    options: {        configFile: join(__dirname, 'tsconfig.json')    }}export const serverConfig: Configuration = {    entry: join(__dirname, 'src', 'index.ts'),    output: {        path: join(__dirname, '..', '..', 'dist', 'server'),        filename: 'server.js'    },    target: 'node',    plugins: serverPlugins,    module: {        rules: [tsRuleServer]    }}
Spoiler

apps/webapp/webpack.part.ts

import {Configuration, RuleSetRule, WatchIgnorePlugin, WebpackPluginInstance} from "webpack";import HtmlWebpackPlugin from "html-webpack-plugin";import {join} from "path";import {tsRuleBase} from "../../webpack.common";const webAppPlugins: WebpackPluginInstance[] = [    new HtmlWebpackPlugin(),    new WatchIgnorePlugin({        paths: [join(__dirname, '..', 'apps', 'server')]    })]const tsRuleWebApp: RuleSetRule = {    ...tsRuleBase,    options: {        configFile: join(__dirname, 'tsconfig.json')    }}export const webAppConfig: Configuration = {    entry: join(__dirname, 'src', 'index.ts'),    output: {        path: join(__dirname, '..', '..', 'dist', 'web_app'),        filename: 'bundle.js'    },    target: 'web',    plugins: webAppPlugins,    module: {        rules: [tsRuleWebApp]    }}

Один из интересный моментов - это указание пути до файла конфигурации для ts-loader, выглядит это так configFile: join(__dirname, 'tsconfig.json'). Так как __dirname в каждом случае различен. То в случае backend все компилируется в целевую версию EcmaScript esnext, а для frontend в es6.

Заключение

Весь код приведенный в статью публикуется под "UNLICENSE". Что также указано в репозитории Github: тут.

Использование в проектах конфигурации через TS - это конечно не бизнес фича. Но привносит комфорт в процесс настройки. На небольших проектах это не так заметно, но если вы например используете micro-frontend c помощью ModuleFederationPlugin, то количество файлов конфигурации webpack растет с каждым микро-приложением и комфорт при настройке становится важен, тем более что время затраченное на именно TS тут минимальное.

PS. Хотелось бы узнать будет ли вам интересна настройка разработки через разворачивание в docker (для VSCode и JetBrains)

Подробнее..

Recovery mode TypeScript для конфигурации WebPack (FE and BE)

31.12.2020 00:22:22 | Автор: admin

Легенда

Когда проект зародился, то нравился каждому. Белый листа бумаги и каждый смотрел на него с ожиданием и воображал какие перспективы откроются, какие проблемы решатся.

Вот на бумагу архитектор нанес первый блок. Сзади раздалась ругань. Это разработчики, спорили: Как лучше стартовать новый сервис и какой стартер выбрать. У архитектора по спине пронесся холодок. Не успела сложилась архитектура даже для Proof Of Concept, не то что для Minimal Valuable Product, но уже возникли препятствия. Выбор стартера наложит пока не очевидные рамки.

Одно было ясно, сборщик будет использоваться. Архитектор подошел к Team Lead и попросил использовать WebPack и чистый проект без стартера, так как по прошлым проектам с ним в той или иной мере знакомы разработчикам.

Мотивация

Каждый кто в 2020 использовал браузер - пользовался результатами сборки с помощью WebPack.

Среди разработчиков некоторые добавляли обработчик для специальных файлов или плагин для нужд проекта или использовали уже готовую конфигурацию, например в create-react-app.

Задач много и помнить параметры конфигурации смысла нет. Структура и часто используемые настройки сами отложатся в голове.

Готовые плагины и loader's сильно облегчают работу, задача на 95% заключается в прочтении первой страницы документации, чтобы сконфигурировать под конкретный проект. Даже в таком случае ошибки в синтаксисе случаются. Мало кто сходу вспомнит devtool или devtools. Некоторые директивы относились к другой версии WebPack. Учет этого будет полезным положить на плечи TypeScript.

Пару лет назад мне не нахватало подробного описания такой настройки, а на сайте самого WebPack только короткое описание: вот ссылка.

Особенности проекта в статье

В проекте для статьи нет цели написать всеобъемлющий мануал по настройке, будет базовый пример для backend и frontend.

Cервер будет отдавать статическую директорию с FE для нашего сайта. Сам же FE будет только выводить на страницу Hello World!. Зависимостями для BE будет node, для сборки webpack.

GitHub: тут

Структура директорий c описанием

Для удобства демонстрации я буду использовать моно-репозиторий с server и webapp в одном проекте

  • ~/projectfolder/ # Корень проекта -- инициализирован с помощью yarn init

    • /apps # директория приложений

      • /server # директория backend -- инициализирована с помощью yarn init

        • /src # исходный код сервера

        • файлы конфигурации (части относящиеся к BE)

      • /webapp # директория frontend -- инициализирована с помощью yarn init

        • /src # исходный код браузерного приложения

        • файлы конфигурации (части относящиеся к FE)

      • /utils # расширенные утилиты

    • общие части конфигурации

Зависимости проекта

  • Общие в директории ~/project_folder

yarn add -D @types/node @types/webpack concurrently cross-env nodemon ts-loader ts-node typescript webpack webpack-cli
  • Для сервера в директории /apps/server нам не понадобится дополнительных зависимостей помимо тех что есть в общей директории

  • Для веб-приложения в директории /apps/web_app нам понадобится html-webpack-plugin 5 версии так как он предназначен для использования с WebPack 5 версии. На Момент написания этот покет еще в beta доступе.

cd apps/web_appyarn add -D html-webpack-plugin@5

Настройки TypeScript

Браузер, server, и компьютер разработчика или runner - это три среды с личными особенностями:

Для сервера главное, nodeс помощью которой будет выполняться итоговый скрипт сервера. Что доступно в зависимости от версии наглядно показывается по ссылке:https://node.green

Конкретная настройка сервера apps/server/tsconfig.json не влияет на сборку, главное в конфигурации webpack указать правильны путь до файла для сборки сервера.

Для браузера, на конец 2020, лучше выбиратьES6если нет задачи поддерживать Internet Explorer 11. Хороший сайт для проверки доступных функций: https://caniuse.com

Файл: apps/web_app/tsconfig.json

Компьютер разработчика или runner где будет собираться проект тоже накладывает ограничения, которые в большинстве ситуаций легко устранимы. Для запуска также понадобиться конфигурация TS, она будет использоваться ts-node который будет запускаться под капотом webpack.

Spoiler

tsconfig.json

"compilerOptions": {    "module": "commonjs",    "target": "es5",    "esModuleInterop": true  }

Данный файл обязателен и частью для запуска самого webpack с конфигурацией написанной на typescript

Серверное приложение

Сервер для данной статьи предельно прост, раздачей файлов из одной папки. Код является копией статьи (ссылка) с сайта node, адаптированный под этот проект и с защитой от доступа к родительским папкам ..\..\secret в запрошенных файлах.

Spoiler

apps/server/src/index.ts

import { resolve, normalize, join } from 'path'import { createServer, RequestListener} from 'http'import { readFile } from 'fs' const webAppBasePath = '../web_app'; // Это путь до папки уже после build (в директории dist)const handleWebApp: RequestListener = (req, res) => {    const resolvedBase = resolve(__dirname ,webAppBasePath);    const safeSuffix = normalize(req.url || '')        .replace(/^(\.\.[\/\\])+/, '');    const fileLocation = join(resolvedBase, safeSuffix);    readFile(fileLocation, function(err, data) {        if (err) {            res.writeHead(404, 'Not Found');            res.write('404: File Not Found!');            return res.end();        }        res.statusCode = 200;        res.write(data);        return res.end();    });};const httpServer = createServer(handleWebApp)httpServer.listen("5000", () => {    console.info('Listen on 5000 port')})

Frontend приложение

Web приложение также предельно простое. В document.body монтируется простой &lt;div id="root">Hello world!&lt;/div>

Spoiler

apps/webapp/src/index.ts

const rootNode = document.createElement('div')rootNode.setAttribute('id', 'root')rootNode.innerText = 'Hello World!'document.body.appendChild(rootNode)

Настройка WebPack

Теперь нам осталось только настроить webpack.

Для удобства конфигурацию можно разбить на файлы. А так как мы используем TS, то мы получаем синтаксис import {serverConfig} from "./apps/server/webpack.part"; из-за этого основной файл становится предельно коротким.

Spoiler

webpack.config.ts

import {serverConfig} from "./apps/server/webpack.part";import {webAppConfig} from "./apps/web_app/webpack.part";import {commonConfig} from "./webpack.common";export default [    /** server  **/ {...commonConfig, ...serverConfig},    /** web_app **/ {...commonConfig, ...webAppConfig},]

В нем мы только импортируем конфигурации и экспортируем их в виде массива попутно объединяя с общей частью.

Общая часть

Общая часть может содержать все что можно переиспользовать между различными конфигурациями. В нашем случае это поля mode и resolve. Обратите внимание, что у константы объявлена типизация const commonConfig: Configuration, тип взят из import {Configuration} from "webpack";.

Spoiler

webpack.common.ts

import {Configuration, RuleSetRule} from "webpack";import {isDev} from "./apps/_utils";export const tsRuleBase: RuleSetRule = {    test: /\.ts$/i,    loader: 'ts-loader',}export const commonConfig: Configuration = {    mode: isDev ? 'development' : 'production',    resolve: {        extensions: ['.tsx', '.ts', '.js', '.json'],    },}

Также в этом файле лежит общая для проекта часть настройки правила для загрузки TS файлов const tsRuleBase: RuleSetRule, тип взят из import {RuleSetRule} from "webpack";.

isDev это простая проверка isDev = process.env.NODE_ENV === 'development'

Конфигурация FE и BE

Тут уже все максимально похоже на простую настройку webpack, только с подсказками благодаря типизации import {Configuration, RuleSetRule, WebpackPluginInstance} from "webpack";

Обратите внимание на WatchIgnorePlugin так как благодаря нему можно исключить какие-то файлы и директории и при изменениях в них не будет перекомпиляции.

Spoiler

apps/server/webpack.part.ts

import {Configuration, RuleSetRule, WatchIgnorePlugin, WebpackPluginInstance} from "webpack";import {join} from "path";import {tsRuleBase} from "../../webpack.common";const serverPlugins: WebpackPluginInstance[] = [    new WatchIgnorePlugin({        paths: [join(__dirname, '..', 'apps', 'web_app')]    })]const tsRuleServer: RuleSetRule = {    ...tsRuleBase,    options: {        configFile: join(__dirname, 'tsconfig.json')    }}export const serverConfig: Configuration = {    entry: join(__dirname, 'src', 'index.ts'),    output: {        path: join(__dirname, '..', '..', 'dist', 'server'),        filename: 'server.js'    },    target: 'node',    plugins: serverPlugins,    module: {        rules: [tsRuleServer]    }}
Spoiler

apps/webapp/webpack.part.ts

import {Configuration, RuleSetRule, WatchIgnorePlugin, WebpackPluginInstance} from "webpack";import HtmlWebpackPlugin from "html-webpack-plugin";import {join} from "path";import {tsRuleBase} from "../../webpack.common";const webAppPlugins: WebpackPluginInstance[] = [    new HtmlWebpackPlugin(),    new WatchIgnorePlugin({        paths: [join(__dirname, '..', 'apps', 'server')]    })]const tsRuleWebApp: RuleSetRule = {    ...tsRuleBase,    options: {        configFile: join(__dirname, 'tsconfig.json')    }}export const webAppConfig: Configuration = {    entry: join(__dirname, 'src', 'index.ts'),    output: {        path: join(__dirname, '..', '..', 'dist', 'web_app'),        filename: 'bundle.js'    },    target: 'web',    plugins: webAppPlugins,    module: {        rules: [tsRuleWebApp]    }}

Один из интересный моментов - это указание пути до файла конфигурации для ts-loader, выглядит это так configFile: join(__dirname, 'tsconfig.json'). Так как __dirname в каждом случае различен. То в случае backend все компилируется в целевую версию EcmaScript esnext, а для frontend в es6.

Заключение

Весь код приведенный в статью публикуется под "UNLICENSE". Что также указано в репозитории Github: тут.

Использование в проектах конфигурации через TS - это конечно не бизнес фича. Но привносит комфорт в процесс настройки. На небольших проектах это не так заметно, но если вы например используете micro-frontend c помощью ModuleFederationPlugin, то количество файлов конфигурации webpack растет с каждым микро-приложением и комфорт при настройке становится важен, тем более что время затраченное на именно TS тут минимальное.

PS. Хотелось бы узнать будет ли вам интересна настройка разработки через разворачивание в docker (для VSCode и JetBrains)

Подробнее..

От одного приложения к сотне. Путь микрофронтенда в Тинькофф Бизнес

16.06.2021 12:16:05 | Автор: admin

Привет, меня зовут Ваня, недавно я выступил на CodeFest 11, где рассказал про путь Тинькофф Бизнеса на фронтенде от одного приложения к сотне. Но так как в ИT очень быстро все меняется, а ждать запись еще долго, сейчас я тезисно расскажу о нашем шестилетнем путешествии в дивный мир микрофронтенда!

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

Этапы развития

  1. Одно приложение на AngularJS в 20142015 годах.

  2. Миграция на Angular2.

  3. Утяжеление десяти приложений новой функциональностью.

  4. Переход к микросервисам и разбиение на 100 приложений.

На дворе начало 2015 года. К нам приходит бизнес и говорит: Мы хотим сделать зарплатный проект! Посмотрите, что есть сейчас на рынке по технологиям, и сделайте. Выбираем AngularJS, быстро создаем приложение. Спустя некоторое время аппетиты вырастают, мы создаем еще два сервиса. На этот момент фронтенд-приложения никак не взаимодействуют друг с другом.

Компания растет, мы доходим до десяти приложений в 2017 году. Команды еще не так сильно разрослись, но уже чувствуется локализация некоторой экспертизы. Для шаринга информации выделяется особая роль архитектор. С одной стороны, это самый технически прокачанный человек, с другой евангелист наших подходов, который на одном языке общается как с бизнесом, так и с новичком в компании.

В 20172018 годах мы подумали, что уже хватит отдельных репозиториев, и стали добавлять фичи в существующие проекты и репозитории вместо создания новых.

В одном из проектов у нас было пять фронтенд-команд, в каждой по 35 человек, то есть в самый лучший момент в одном проекте работали 25 фронтендеров! Иногда было действительно больно: ты вот-вот замержишь свою задачку, но нет! Перед тобой кто-то успевает и все твои пайплайны начинают проходить заново! До сих пор мне не по себе от этих воспоминаний.

В какой-то момент мы осознали, что сборка приложения занимает уже полчаса и будет только расти, и решили распиливать наши монолиты на микросервисы.

Вот так быстро и пролетели шесть лет эволюции наших подходов. Теперь взглянем на наш путь через призму своих решений для микрофронтенда.

Сайдбар

Первые три приложения мы подружили между собой с помощью сайдбара.

В каждое приложение он встраивался с помощью ссылки на скрипт, который загружал на клиент полноценное приложение. Сайдбар отвечал за авторизацию, продукты клиента и текущий роутинг страницы, мог отправлять уведомления и загружал мессенджер на страницу. Как мы видим, довольно много возможностей у одного сервиса.

По технической части это iframe со всеми плюсами и минусами, о которых довольно подробно расписано в докладе Яндекса. Но больше всего нам не нравилась перезагрузка страницы на каждую смену продукта. Это мешало переиспользованию состояния клиента, требовало каждый раз обращаться к бекенду и добавляло некрасивые мерцания страницы при переходах.

Подсвеченная область отдельное приложение СайдбарПодсвеченная область отдельное приложение Сайдбар

Frame Manager

Именно рваные переходы мы убрали с появлением Frame Manager'а (далее буду называть его ФМ).

Подсвеченная область отдельное приложение Frame ManagerПодсвеченная область отдельное приложение Frame Manager

В отличие от сайдбара, который встраивался в приложение с помощью iframe, ФМ находился на странице всегда и сам встраивал в себя приложения.

Слева концепция сайдбара (было), справа Frame Manager'а (стало)Слева концепция сайдбара (было), справа Frame Manager'а (стало)

Для клиента ФМ так же не заметен, как и сайдбар, но для нас он выполняет уже больше функций. Кроме рейтинга, списка продуктов и авторизации он может передавать состояние между приложениями, например через глобальный window. Пользователь больше не замечает, что ходит между несколькими отдельными приложениями, так как верхняя часть у него никуда не пропадает и визуально весь процесс выглядит как подгрузка контента на одну страницу.

В плане интеграции приложения тоже все поменялось:

  • Раньше приложению-клиенту достаточно было подключить необходимый скрипт к себе в index.html.

  • Теперь все приложения ФМа хранятся в отдельной конфигурации и используются как единый источник правды.

Минус этого подхода мы все равно остались с iframe, который нам не особо нравился из-за особенностей работы с ним.

Однажды через поддержку к нам обратились пользователи с ситуацией: Раньше у меня работал плагин для Google Chrome, а с недавнего времени именно на вашем сайте перестал. Почините, пожалуйста! Обычно на такие просьбы не реагируют: пользователь что-то себе установил пусть сам и разбирается. Но только не в нашей компании. Команда долго изучала вопрос, смотрела, какое окружение у клиента, версия браузера и все-все, но ответа так и не было. В итоге мы полностью повторили окружение, загрузили себе плагины и путем дебагинга установили, что данный плагин не работает, если у iframe динамически менять атрибут src или пересоздавать фрейм. К сожалению, мы так и не смогли исправить такое поведение, поскольку на этой концепции построено все взаимодействие ФМ и дочерних приложений.

Бесфрейм-менеджер

Однажды мы собрались и подумали: Несколько лет страдаем от iframe. Как перестать страдать? Давайте просто уберем его! Сказано сделано. Так и появился бесфрейм-менеджер с фантазией у нас, конечно, не фонтан ;-)

Ключевые отличия от предыдущей версии самописная изоляция и работа с микрофронтендом вместо фрейма. На мой взгляд, лучше всего мотивацию создания отражает слайд из моей презентации:

В решении три составляющие:

  1. Webpack-плагин основа нашего решения, подробнее о которой можно прочитать в статье Игоря.

  2. Angular builder обвязка для настройки и запуска плагина.

  3. Angular schematics скрипт для упрощения работы с файловой структурой с помощью AST.

В 2021 году плагин становится менее актуальным, потому что вышел Webpack 5 с Module Federation, но напомню, что мы вели разработку в 2018 году, а Angular стал поддерживать последнюю версию вебпака лишь с двенадцатой версии, которая вышла 12 мая 2021 года. Мы пока не уверены, сможет ли MF заменить наше решение, и изучаем комбинацию подходов.

Что же касается других решений, на которые можно было перейти для отказа от iframe, то это Single SPA. Он всем хорош и очень популярен, но в плане Angular есть небольшой дисклеймер.

http://personeltest.ru/aways/single-spa.js.org/docs/ecosystem-angular/https://single-spa.js.org/docs/ecosystem-angular/

Мы понимали, что глобально менять концепцию фреймворка нам никто не даст, потому решили доделать имеющийся.

Что же касается Angular builder и schematics, то они нужны, чтобы разработчики, которые будут интегрировать наше решение к себе, не выполняли километровую инструкцию, а просто написали в консоли:

ng update @scripts/deframing

И вся магия подключения произойдет у них на глазах, но не потребует никакого ручного вмешательства. Подробнее с работой билдеров и схематиков можно ознакомиться в классной статье.

Тестирование

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

  1. Можно прогонять тесты совместно с локальным ФМом. Так разработчики всегда могут быть уверены, что на текущей продовой сборке все работает и выглядит так, как и задумывалось.

  2. Сам ФМ определяет несколько жизненно важных процессов, работоспособность которых гарантирует при любых условиях: это авторизация, роутинг, работа с данными приложений. Для этого создаются приложения-стабы (stub), суть которых подключиться к ФМу и выполнить одну из вышеперечисленных функций. То есть на каждое изменение кодовой базы ФМа будет гарантированно работать эта функция.

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

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

.my-pretty-header {    display: none;}

Если у кого-то из следующих приложений есть такое же название класса, этот стиль применится так же!

Пример: диалог решил спрятаться под меню, чтобы пользователь не догадался, что от него требуется:

Этот тип багов мы решили путем префикса по id приложения для всех стилей, чтобы они не имели глобальную видимость.

Сторонние библиотеки если на одной странице два и более приложения используют библиотеку, которая на старте создает новый инстанс, то получается такая картина:

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

Microzord

Вот мы и прошли шесть лет технического развития нашего решения. И что может быть лучше, чем поделиться этим опытом с сообществом? Все наработки будут публиковаться под npm scope @microzord с открытым кодом на Гитхабе.

В планах предоставить клиент не только для Angular, но и для работы с другими фреймворками. Сейчас лишь небольшая часть вынесена на Гитхаб, но будьте уверены, ребята не заставят себя ждать и в одной из следующих статей расскажут о гитхабе поподробнее.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru