Как мы заставили npm-пакеты работать в браузере
В ходе начальной разработки проекта CodeSandbox я всегда игнорировал поддержку npm-зависимостей. Я думал, что невозможно установить в браузер произвольное, случайное количество пакетов, мой мозг просто отказывался об этом думать.
Сегодня поддержка npm — одна из определяющих возможностей CodeSandbox, так что как-то нам удалось это реализовать. Чтобы фича работала при любых сценариях, пришлось сделать немало итераций, много раз переписывая код, и даже сегодня мы всё ещё можем улучшить логику. Я расскажу, с чего у нас начиналась поддержка npm, что имеем сегодня и что можем сделать для её улучшения.
«Первая» версия
Я просто не знал, как за это браться, так что начал с очень простой версии поддержки npm:
Первая версия, импорт стилизованных компонентов и React (25 ноября 2016 года)
Эта версия была очень простой. Настолько, что на самом деле поддержки-то и не было, я просто локально установил зависимости и для каждого вызова сделал заглушку в виде уже установленной зависимости. Конечно, масштабируемостью до 400 тысяч пакетов разных версий тут и не пахнет.
Хотя эта версия бесполезна, было приятно увидеть, что я заставил работать в sandbox-среде хотя бы две зависимости.
WebPack-версия
Первой версией я был удовлетворён, и мне казалось, что её достаточно для MVP (первого релиза CodeSandbox). Я не представлял, что вообще можно без применения магии установить любую зависимость. До тех пор пока не наткнулся на https://esnextb.in/. Ребята уже поддерживали любые зависимости из npm, их достаточно было определить в package.json — и всё волшебным образом работало!
Это стало для меня большим уроком. Я даже в мыслях не покушался на такую поддержку npm, потому что считал её нереальной. Но, увидев вживую доказательство реальности, я начал много над этим размышлять. Сначала нужно было изучить возможности, прежде чем отбрасывать идею.
В своих раздумьях я слишком усложнил задачу. Моя первая версия не помещалась в уме, так что я нарисовал схему:
Первая идея, вероятно, ошибочная
У этого подхода было одно преимущество: настоящая реализация гораздо проще, чем ожидалось!
Я узнал, что плагин WebPack DLL мог собирать зависимости в пачку и выдавать JS-пакет (bundle) с манифестом. Этот манифест выглядел так:
Каждый путь сопоставлен с ID модуля. Если мне потребуется react, то достаточно вызвать dll_bundle(3) , и я получу React! Для нас это было идеально, так что я придумал вот такую реальную систему:
Исходный код сервиса лежит тут. Сервис также содержит код для публикации любых песочниц в npm, позже мы отказались от этой функции
Для каждого запроса к упаковщику я создавал новую директорию в /tmp/:hash , запускал yarn add $ и позволял WebPack собирать пакет. Результат я сохранял в gcloud в качестве кеша. Получается гораздо проще, чем на схеме, в основном потому, что я заменил установку зависимостей на Yarn и собирал в пакеты с помощью WebPack.
При загрузке песочницы, прежде чем проводить выполнение, мы сначала проверяли наличие манифеста и пакета. В ходе анализа мы для каждой зависимости вызывали dll_bundle(:id) . Это решение прекрасно работало, я создал первую версию с нормальной поддержкой npm-зависимостей!
Ура! У нас интерфейс в стиле Material Design и динамически исполняемый React! (24 декабря 2016 года)
У системы всё ещё было большое ограничение: она не поддерживала файлы, отсутствовавшие в графе зависимостей WebPack. Это означает, что, к примеру, require(‘react-icons/lib/fa/fa-beer’) работать не станет, потому что он никогда не будет в первую очередь запрошен входной точкой зависимости.
Всё же я создал релиз CodeSandbox с такой поддержкой и связался с автором WebPackBin Кристианом Альфони. Для поддержки npm-зависимостей мы использовали очень похожие системы и столкнулись с одинаковыми ограничениями. Поэтому решили объединить усилия и создать абсолютный упаковщик!
WebPack с записями
«Абсолютный» упаковщик получил ту же функциональность, что и предыдущий, за исключением созданного Кристианом алгоритма, который добавлял файлы в пакет в зависимости от их важности. Мы вручную добавляли входные точки, чтобы удостовериться, что WebPack тоже упакует эти файлы. После многократных настроек системы она стала работать при любой (?) комбинации. Так что мы уже могли запрашивать React-иконки и CSS-файлы.
Новая система получила архитектурный апгрейд: у нас был лишь один dll-сервис, обслуживающий балансировщик нагрузки и кеш. Упаковкой занимались несколько упаковщиков, которые могли добавляться динамически.
Мы хотели, чтобы наш сервис упаковки стал доступным для всех. Поэтому сделали сайт, на котором объяснялась работа сервиса и варианты использования. Это принесло нам известность, нас даже упомянули в блоге CodePen!
Но у «абсолютного» упаковщика были некоторые ограничения и недостатки. По мере роста популярности экспоненциально росли и затраты, и мы кешировали по комбинации пакетов. Это означает, что при добавлении зависимости приходилось пересобирать всю комбинацию.
Бессерверная обработка (serverless)
Мне всегда хотелось попробовать эту классную технологию — бессерверную (serverless) обработку. С её помощью можно определять функцию, которая будет исполняться по запросу.
Она запустится, обработает запрос и через какое-то время убьёт себя. Это означает очень высокую масштабируемость: если получаете тысячу одновременных запросов, то можете мгновенно запустить тысячу серверов. Но при этом платите только за реальное время работы серверов.
Звучит идеально для нашего сервиса: он не работает постоянно, и нам нужна высокая согласованность на случай одновременного получения многочисленных запросов. Так что я начал экспериментировать с фреймворком с соответствующим названием Serverless.
Изменение нашего сервиса шло спокойно (благодаря Serverless!), через два дня у меня была рабочая версия. Я создал три serverless-функции:
- Обработчик метаданных: эта служба разрешала (resolve) версии и peerDependencies, а также запрашивала функцию упаковщика.
- Упаковщик: эта служба устанавливала и собирала зависимости в пакеты.
- Минификатор (Uglifier) отвечал за асинхронную минификацию (uglifying) получающихся пакетов.
Я запустил новый сервис рядом со старым, всё прекрасно работало! Мы спрогнозировали расходы на уровне 0,18 доллара в месяц (по сравнению с предыдущими 100 долларами), а время отклика улучшилось на 40—700 %.
Через несколько дней я заметил одно ограничение: лямбда-функция имела всего 500 Мб пространства на диске. Значит, некоторые комбинации зависимостей не могли быть установлены. Это было недопустимо, и я снова вернулся к рисованию схем.
Пересмотр бессерверной обработки
Прошла пара месяцев, и я выпустил новый упаковщик для CodeSandbox. Он был очень мощный и поддерживал больше библиотек наподобие Vue и Preact. Благодаря этому у нас появились интересные запросы. Например: если вы хотите использовать React-библиотеки в Preact, то нужно связать require(‘react’) с require(‘preact-compat’) . Для Vue вам может понадобиться разрешить (resolve) @/components/App.vue для sandbox-файлов. Наш упаковщик не делает этого для зависимостей, а другие делают.
Я начал думать, что, быть может, мы переложим задачу по упаковке на упаковщик браузера. Если просто отправлять соответствующие файлы в браузер, то в результате его упаковщик будет собирать зависимости в пакеты. Так будет быстрее, потому что мы обрабатываем не весь пакет, а лишь часть.
У этого подхода большое преимущество: мы можем независимо устанавливать и кешировать зависимости. Или просто объединять файлы зависимостей на клиенте. Это означает, что, если вы запрашиваете новую зависимость поверх существующих, нам нужно лишь собрать файлы для новой зависимости! Это решает проблему 500 Мб для AWS Lambda, потому что мы устанавливаем лишь одну зависимость. Так что можно выкинуть WebPack из упаковщика, потому что он теперь полностью отвечает за вычисление релевантных файлов и их отправку.
Распараллеливание упаковки наших зависимостей
Примечание: можно выкинуть упаковщик и динамически запрашивать каждый файл из unpkg.com. Пожалуй, это быстрее моего подхода. Но я решил пока оставить упаковщик (как минимум для редактора), потому что хочу предоставлять офлайн-поддержку. Это возможно, лишь если у вас есть все возможные релевантные файлы.
Работа на практике
Запрашивая комбинацию зависимостей, мы сначала проверяем, хранится ли она уже в S3. Если нет, то запрашиваем комбинацию у API-сервиса, а тот запрашивает у всех упаковщиков отдельно по каждой зависимости. Если получаем в ответ 200 OK, то опять запрашиваем S3.
Упаковщик устанавливает зависимости с помощью Yarn и, обходя AST всех файлов в директории входной точки, находит все релевантные файлы. Он ищет выражения require и добавляет в список файлов. Это делается рекурсивно, и в результате мы получаем граф зависимостей. Пример выходных данных ( react@latest ):
< "aliases": < "asap": "asap/browser-asap.js", "asap/asap": "asap/browser-asap.js", "asap/asap.js": "asap/browser-asap.js", "asap/raw": "asap/browser-raw.js", "asap/raw.js": "asap/browser-raw.js", "asap/test/domain.js": "asap/test/browser-domain.js", "core-js": "core-js/index.js", "encoding": "encoding/lib/encoding.js", "fbjs": "fbjs/index.js", "iconv-lite": "iconv-lite/lib/index.js", "iconv-lite/extend-node": false, "iconv-lite/streams": false, "is-stream": "is-stream/index.js", "isomorphic-fetch": "isomorphic-fetch/fetch-npm-browserify.js", "js-tokens": "js-tokens/index.js", "loose-envify": "loose-envify/index.js", "node-fetch": "node-fetch/index.js", "object-assign": "object-assign/index.js", "promise": "promise/index.js", "prop-types": "prop-types/index.js", "react": "react/index.js", "setimmediate": "setimmediate/setImmediate.js", "ua-parser-js": "ua-parser-js/src/ua-parser.js", "whatwg-fetch": "whatwg-fetch/fetch.js" >, "contents": < "react/index.js": < "requires": [ "./cjs/react.development.js" ], "content": "/* code */" >, "object-assign/index.js": < "requires": [], "content": "/* code */" >, "fbjs/lib/emptyObject.js": < "requires": [], "content": "/* code */" >, "fbjs/lib/invariant.js": < "requires": [], "content": "/* code */" >, "fbjs/lib/emptyFunction.js": < "requires": [], "content": "/* code */" >, "react/cjs/react.development.js": < "requires": [ "object-assign", "fbjs/lib/warning", "fbjs/lib/emptyObject", "fbjs/lib/invariant", "fbjs/lib/emptyFunction", "prop-types/checkPropTypes" ], "content": "/* code */" >, "fbjs/lib/warning.js": < "requires": [ "./emptyFunction" ], "content": "/* code */" >, "prop-types/checkPropTypes.js": < "requires": [ "fbjs/lib/invariant", "fbjs/lib/warning", "./lib/ReactPropTypesSecret" ], "content": "/* code */" >, "prop-types/lib/ReactPropTypesSecret.js": < "requires": [], "content": "/* code */" >, "react/package.json": < "requires": [], "content": "/* code */" >>, "dependency": < "name": "react", "version": "16.0.0" >, "dependencyDependencies": < "asap": "2.0.6", "core-js": "1.2.7", "encoding": "0.1.12", "fbjs": "0.8.16", "iconv-lite": "0.4.19", "is-stream": "1.1.0", "isomorphic-fetch": "2.2.1", "js-tokens": "3.0.2", "loose-envify": "1.3.1", "node-fetch": "1.7.3", "object-assign": "4.1.1", "promise": "7.3.1", "prop-types": "15.6.0", "setimmediate": "1.0.5", "ua-parser-js": "0.7.14", "whatwg-fetch": "2.0.3" >>
Преимущества
Экономия
Я развернул новый упаковщик 5 октября, и за два дня мы заплатили смехотворные 0,02 доллара! И это за создание кеша. Гигантская экономия по сравнению со 100 долларами в месяц.
Выше производительность
Новую комбинацию зависимостей вы можете получить за 3 секунды. Любую комбинацию. На старой системе иногда это занимало минуту. Если комбинация закеширована, вы получите её через 50 миллисекунд при быстром подключении. Мы кешируем с помощью Amazon Cloudfront по всему миру. Также наша песочница работает быстрее, потому что теперь мы парсим и исполняем только релевантные JS-файлы.
Больше гибкости
Наш упаковщик теперь обрабатывает зависимости, словно это локальные файлы. Это означает, что наши отслеживания стека ошибок (error stack traces) стали гораздо чище, теперь мы можем включать любые файлы из зависимостей (.scss, .vue и т. д.) и легко поддерживать алиасы. Всё это работает так, словно зависимости установлены локально.
Релиз
Я начал использовать новый упаковщик рядом со старым, чтобы построить кеш. За два дня было закешировано 2000 разных комбинаций и 1400 разных зависимостей. Я хочу интенсивно протестировать новую версию, прежде чем полностью перейти на неё. Можете попробовать, включив её в своих настройках.
Исходный код. Он пока непригляден, скоро я его почищу, напишу README.md и т. д.
Используйте Serverless!
Я очень впечатлён этой технологией, она невероятно облегчает масштабирование и управление серверами. Единственное, что меня всегда удерживало, — это очень сложная настройка, но разработчики из serverless.com сильно её упростили. Очень благодарен за их работу, думаю, что serverless — это будущее многих форм приложений.
Будущее
Мы всё ещё можем во многом улучшить нашу систему. Я хочу исследовать динамические запросы, требуемые во встраивании и офлайн-сохранении. Трудно соблюсти баланс, но это должно быть возможно. Можно начать независимо кешировать зависимости в браузере, опираясь на то, что он позволяет делать. В этом случае вам иногда даже не понадобится скачивать новые зависимости при посещении новой песочницы с другой комбинацией зависимостей. Также я хочу лучше исследовать разрешение зависимостей. В новой системе есть вероятность конфликта версий, которую я хочу исключить перед отказом от старой версии.
В любом случае я очень доволен результатом и намерен работать над нововведениями для CodeSandbox!
- Блог компании VK
- Веб-разработка
- Open source
- JavaScript
- Системы сборки
Ссылки на CDN
These docs are old and won’t be updated. Go to react.dev for the new React docs.
See Add React to an Existing Project for the recommended ways to add React.
Как React, так и ReactDOM доступны через CDN.
script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"> script> script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"> script>
Указанные выше версии предназначены только для разработки приложения и не подходят для использования в продакшен-окружении. Минифицированные и оптимизированные для продакшена версии React перечислены ниже:
script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"> script> script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"> script>
Для загрузки конкретной версии react и react-dom , замените 18 на номер нужной версии.
Зачем нужен атрибут crossorigin ?
Если вы загружаете React из CDN, мы рекомендуем использовать атрибут crossorigin :
script crossorigin src=". "> script>
Желательно также проверить, что используемый сервис CDN устанавливает HTTP-заголовок Access-Control-Allow-Origin: * :
Такая практика позволит улучшить обработку ошибок в React 16 и более новых версиях.
Ссылки CDN
Вышеперечисленные версии предназначены только для разработки и не подходят для продакшена. Минимизированные и оптимизированные продакшен-версии React доступны по следующим адресам:
script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"> script> script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"> script>
Для загрузки определённых версий react и react-dom , замените 16 на требуемый номер версии.
Зачем используется атрибут crossorigin ?
Если вы подключаете React через CDN, мы рекомендуем сохранить атрибут crossorigin :
script crossorigin src=". "> script>
Мы также рекомендуем проверить, что используемый вами CDN устанавливает HTTP-заголовок Access-Control-Allow-Origin: * :
Это позволяет улушчить обработку ошибок в React 16 и более новых версиях.
React это просто JavaScript
В экосистеме React регулярно происходят изменения и многие начинают переутомляться от поступающей информации. Существует мнение, что все что происходит в React — магия, отчего очень тяжело начать писать на React без использования инструментов вроде create-react-app.
В действительности же, хоть это и слабо документировано, достаточно потратить 5 минут на то, чтобы использовать React на любой странице. Дэн Абрамов делает отличную работу в привлечении внимания к этой идее, позвольте и мне убедить вас, что это возможно за 5 минут.
Как подключить React в вашем приложении?
Первый шаг — найти js файлы для подключения. Один из простейших способов это сделать — использовать UNPKG. Это что-то вроде огромного CDN для популярных библиотек и фреймворков.
Если вы пройдете по ссылке https://unpkg.com/react@16/ вы обнаружите две директории:
Директория UMD (Universal Module Definition) — то, что нам нужно: браузер понимает этот формат js-модулей без каких-либо преобразований и нам не нужно использовать сборщик.
Нам также понадобится подключить библиотеку React Dom (версию для разработки). Ссылка на UNPKG: https://unpkg.com/react-dom@16/
Итого у нас есть две ссылки:
- https://unpkg.com/react@16/umd/react.development.js
- https://unpkg.com/react-dom@16/umd/react-dom.development.js
Интересно что находится в директории cjs ?
Это директория с CommonJS модулями, которые могут быть использованы NPM’ом. Эту версию вы можете использовать, к примеру, в ваших node.js проектах.
Создаем HTML файл
Для старта нам необходим простейший HTML файл. В VSCode для этого достаточно ввести ! и нажать клавишу Tab : все, простейшая HTML страница готова для дальнейшей работы и подключения скриптов. Вот что получилось у меня:
Используя эту функцию, мы можем создать простой div с текстом внутри и классом, который мы можем стилизировать используя CSS.
Все, мы освоили основы создания элементов на React, теперь давайте добавим немного интерактивности, используя Состояния.
Добавляем немного интерактивности
В этой части мы создадим самое лучшее приложение на свете. Я серьезно: оно будет отображать фото случайного пёсика и иметь кнопку переключения фото. Фото мы будем брать с https://dog.ceo/api/breed/akita/images/random. Поверьте мне, это будет здорово!
Первое что нам понадобится — создать простой React.Component . Главное отличие React.Component от обычной JavaScript функции в том, что компонент имеет состояние (state), в то время как у функции его нет.
Состояние — это набор данных специфичных для компонента, значения которых могут меняться со временем. Технически состояние это обычный JavaScript-объект, содержимое которого определяется пользователем., см. https://reactjs.org/docs/react-component.html#state
Также мы должны иметь ввиду, что отрисованный элемент должен перерисовываться при изменении состояния.
В нашем случае состоянием будет объект с ссылкой на изображение. Теперь наш компонент Wrapper выглядит следующим образом:
Ух ты! Мы многое изменили, давайте рассмотрим все изменения по-отдельности:
Теперь мы используем класс вместо функции, вы можете прочитать подробнее об использовании классов в Mozilla Developer Docs. Мы также отнаследовали наш класс от React.Component что дало нам возможность использовать методы родительского класса в классе Wrapper.
Метод constructor — специальный метод, необходимый для создания и инициализации объектов, созданных с помощью класса. В классе может быть только один метод с именем constructor. Исключение типа SyntaxError, будет выброшено, если класс содержит более одного вхождения метода constructor. см. https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Classes
Конструктор может использовать ключевое слово super для вызова конструктора родительского класса.
Далее с помощью выражения this.state = ; мы устанавливаем начальное состояние компонента. Почему null? — На момент создания экземпляра компонента у нас еще нет ссылки изображения. Ну, и метод render возвращает HTML элемент, созданный с помощью react.
Следующий шаг — создание элемента изображения ( img ), который будет отображать милого пса:
Мы уже делали что-то подобное при создании нашего компонента Wrapper . Единственное, что мы изменили здесь — добавили аргумент props в функцию. Это нужно для того, чтобы мы могли передать в функцию объект, в которой должно присутствовать свойство source, необходимое для инициализации атрибута src изображения ( img ). Также здесь мы не используем третий аргумент метода React.createElement , поскольку у изображения не может быть дочерних элементов.
Что-то похожее мы сделаем и для кнопки переключения:
Здесь мы создали функцию которая которая будет менять фото при нажатии на кнопку.
Соединяем все вместе
У нас готовы все части приложения и сейчас самое время использовать функции Img и Button внутри компонента Wrapper .
Наш Wrapper будет выглядеть примерно так:
Теперь пришло время получить изображение, и для этого мы будем использовать fetch api, доступный в большинстве современных браузеров.
Давайте сделаем это в отдельной функции и вызовем ее, когда компонент будет подключаться (событие mount ) на странице:
Первое, что мы здесь сделали — создали функцию getImage , в которой мы получаем изображение. Когда изображение получено, вызываем setState , чтобы изменить состояние компонента. Прямое изменение состояния через присвоение переменной не будет работать поскольку React не будет знать, что состояние изменилось и не перерисует компонент.
В componentDidMount мы вызвали функцию getImage , которую мы только что создали. Вызов getImage изменит значение this.state.image , а React, в свою очередь, передаст измененное значение в функцию Img . Все, приложение только что показало фото первого пса! Также мы передали функцию getImage в функцию Button для того, чтобы пользователь мог обновить фото.
Мы использовали bind в конструкторе из-за того, что функция в JavaScript имеет свою собственную область видимости (scope), отличную от области видимости класса, частью которого она является. При использовании такой функции вне класса могут возникнуть проблемы при использовании ключевого слова this , которое может указывать уже не на экземпляр класса (как мы может это ожидать). Если у вас есть вопросы по тому, как работает ключевое слово this или вы слышите о нем впервые — вы можете прочитать об этом в Mozilla Developer Docs.
Есть два варианта решения проблемы с this : установить контекст выполнения явным образом, используя функцию bind в конструкции вида this.getImage = this.getImage.bind(this) , либо мы можем использовать стрелочную функцию, которая не создает собственную область видимости. В данном случае, для ясности я решила использовать первый подход.
Вы можете попробовать рабочую версию нашей замечательной страницы здесь: https://react-no-jsx.now.sh
Я хочу немного магии!
Вы же хотите немного JSX, правда?
Ты любишь авантюры — мне это нравится! Для начала я бы посоветовала пойти в песочницу Babel и немного поиграться там. Как только вы поймете, как работает JSX и захотите использовать JSX в нашем приложении — вы можете использовать это официальное руководство.
После этого, вы можете начать экспериментировать с JSX на платформе codeandbox, которая предоставляет необходимый онлайн-инструментарий.
Добавление JSX в проект похоже на добавление CSS препроцессора: это единственная команда, которую вы можете запустить, чтобы преобразовать все ваши теги скриптов. Вам не нужно устанавливать webpack или аналогичные инструменты.
Заключение
Надеюсь, что вы избавились от чувства, что React делает какую-то магию за вас. Даже, если вы уже знали React, я надеюсь, что это помогло вам понять какие-то основы, о которых вы, возможно, и не подозревали.
А теперь берите React и пишите крутые приложения! ⚛️
Спасибо Дэну Абрамову за то, что начал раскрывать эту тему!