На проект меня вдохновил паблик "Гайды по игре Жизнь", захотелось написать игру в сеттинге IT отрасли, где можно создать персонажа айтишника, сражаться с мобами-тасками, получать опыт, прокачивать скилы, брать перки и т.д. Задумывался проект изначально как обучающий - мне показалось что будет полезно своими руками выполнить все работы от разработки требований до деплоя и поддержки, к тому же у меня никогда не было опыта старта проекта с нуля и конфигурирования окружения. А ещё, созданный и прокачанный персонаж копирующий мои навыки и умения будет оригинальным дополнением к резюме, а сам проект в целом будет неплохой демонстрацией скилов. На практике же всё вышло далеко не так радужно: то что задумывалось как интересное упражнение стало для меня настоящим испытанием на выносливость. Работа в общей сложности продолжалась 4 месяца, а последний месяц я буквально не видел белого света. Но и образовательная ценность всего этого также превзошла все ожидания: за время работы я опробовал множество новых для меня технологий и подходов, столкнулся с многими сложнослями и подводными камнями и приобрел бесценный опыт - то что не нагуглишь и в документации не прочитаешь.
- MySQL (8.0.15) - для БД
- cloudinary - для хостинга аватарок
- Sequelize (4.43.0) - для ORM
- Node.js (10.15.1) + Express.js (4.16.4) - для серверной логики
- OpenAPI (3) - для документации и тестирования API
- JavaScript (2016, 2017, 2018), TypeScript (3.3.4000), React (16.8.3) - для клиентской логики
- CSS3, SASS - для стилей
- Webpack (4.29.6), Babel (7.3.4) и компания - для сборки
Демо версия - https://ant478-it-roleplay-a9c4bce8c3fe.herokuapp.com/
На данном этапе проект представляет собой демонстрацию ролевой системы в духе старых RPG, позволяет создать персонажа, повышать его уровень, прокачивать характеристики, навыки, технологии и перки. На странице списка присутствует краткое описание персонажей, при клике на персонажа открывается страница профиля. На странице профиля есть 6 табок с информацией о персонаже. Чтобы создать своего персонажа нужно зарегистрироваться или войти кликнув на пункт меню в верхнем правом углу. После успешной авторизации появится кнопка "Создать персонажа", открывающая страницу создания, где нужно выставить изначальные характеристики, выбрать класс и т.д. и нажать кнопку "Сохранить". Далее можно повышать уровень нажимая на кнопку "Повысить уровень" на первой табке профиля. При повышении уровня нужно выбрать класс и затем распределить доступные очки аттрибутов, навыков, технологий и способностей (или приберечь их для следующих уровней) и нажать кнопку "Сохранить". После чего можно повысить персонажа до следующего уровня опять кликнув на кнопку "Повысить уровень" на первой табке профиля и так далее сколько нужно. Созданный и сохраненный персонаж появляется на странице списка. Своему персонажу можно поменять имя и аватарку кликнув на соответствующую фиолетовую иконку на первой табке профиля. (Отзывы пользователей показали что это всё ни разу не очевидно, нужно будет добавить обучающий режим с подсказками или как-то ещё решить этот вопрос) А ещё контейнер можно поворачивать потянув мышкой за край экрана, на обратной стороне есть "телевизор" (ну так, по приколу, надо же было что-то там разместить).
Для документации API использова использовал стандарт OpenAPI 3 и сервис Swagger. Сервис предоставляет инструменты для более удобного написания и красивый UI для отображения документации. Посмотреть как это выглядит можно здесь - https://app.swaggerhub.com/apis/ant478/IT-Roleplay/0.1.0. Стандарт OpenAPI поддерживается различными инструментами и сервисами, я использовалл Dredd - инструмент для автоматического тестирования соответствия реального API документации. На практике же хоть тесты и называются автоматическими, чтобы всё работало как надо нужно немного потанцевать с бубном, и поддержка стандарта хоть и заявляется, но в некоторых местах работает криво. Вот так выглядят готовые тесты - dreddHooks.js. Dredd пока не поддерживает стандарт версии 3, поэтому перед запуском тестов документация конвертируется в версию 2 - Gruntfile.js
Весь клиентский код написан на TypeScript, думаю мне не нужно рассказывать что это, скажу только что мой опыт использования TypeScript самый положительный. Строгая типизация вынуждает писать более строгий и безопасный код, страхует от некоторых глупых ошибок, а явное указаниие типов делает код более наглядным, позволяет лучше понять его работу. Рефакторить крупный проект на TypeScript вообще одно удовольствие. К тому же TypeScript частично выполняет функцию интеграционных тестов - контролирует правильность использования интерфейсов модулей. С непривычки "борьба с тайпскриптом" занимает некоторое время, но постепенно это приучает писать хороший код сразу, и речь не идет о каких-то хаках "чтобы ТС не ругался", а о действительно лучшем понимании и лучшем качестве кода. Я использовал самые строгие настройки компилятора - tsconfig.json
В своём коде я использовал все новшества ECMAScript стандартов 2016, 2017, 2018. Поддерживаются они как известно разными браузерами по-разному, поэтому чтобы обеспечить кроссбраузерность нужно скомпилировать их в понятный браузеру стандарт при помощи Babel. Я использовал для этого Babel с пресетом preset-env и интеграцией browserslist. browserslist позволяет задать список поддерживаемых браузеров - (package.json, Поддерживаемые браузеры) и preset-env подключает только те преобразования и полифилы которые необходимы именно для этих браузеров. Плюсы этого в том что уменьшается размер бандла, т.к. ничего лишнего в него не попадает, а код в новом стандарте имеет меньший размер чем он же скомпилированный в старый. browserslist использует данные caniuse.com, и при изменении статистики использования браузеров автоматически обновляются и настройки компиляции. Также browserslist можно интегрировать и с другими инструментами для тех же целей, например у меня используется postcss-loader с плагином postcss-preset-env. Аналогичным образом, используя конфиг browserslist, он компилирует последний стандарт css в понятный браузеру (правда у меня он используется только для автоматического добавления префиксных свойств, новые фичи css не использовал). Благодаря всему этому разработка под IE 11 прошла так легко что я аж сам удивился. Вот так выглядит webpack.config.js - webpack.config.js
Приложение использует большое колличество изображений и сам бандл имеет немаленький размер, чтобы всё загружалось красиво был реализован экран загрузки. Для этого я сделал две точки входа и соответственно два бандла - main и initial - webpack.config.js. Загрузка происходит следующим образом: на странице index.html элементы link и script тригерят загрузку initial бандла. initial включает в себя только одну функцию - loadApp. Она показывает экран загрузки, и тригерит загрузку основного бандла и других ресурсов. Когда всё загружено экран загрузки скрывается.
Основа приложения - ролевая система (Модуль RolePlayingSystem), включает в себя всю игровую логику. На данном этапе это логика работы с данными персонажа, логика повышения уровня, доступные классы, аттрибуты, навыки и т.д. Логика работы с каждой из характеристик (аттрибут, навык, технология, перк) персонажа инкапсулирована в базовый класс характеристики, а каждый вид характектеристики (Сила, Ловкость и т.д. для аттрибутов) инкаплулирован в отдельный класс наследник. Классы полностью самодостаточны и включают в себя всю необходимую логику, это позволяет добавлять новые перки, технологии и др. в ролевую систему без танцев с бубном всего лишь добавляя новые классы.
В дизайне везде используются относительные единицы em и rem - приложение выглядит одинаково при любом разрешении - app.scss. Насколько это удачное решение ещё предстоит выяснить. Пока готова только десктопная версия.
Обычно если на странице что-либо загружается асинхронно, например делается запрос на сервер, это делается как-то так:
class Page extends React.Component {
constructor() {
this.state = {
isLoaded: false,
characters: [],
};
}
async componentDidMount() {
const characters = await charactersService.getCharacters();
this.setState({ isLoaded: true, characters });
}
render() {
// if (!this.state.isLoaded) {
// return <Loader />
// }
return (
<CharactersList characters={this.state.characters} />
);
}
}
В таком случае на долю секунды отображается лоадер или, если не использовать лоадер, то пустая страница. Меня такое не устраивало, я хотел создать провдоподобную иммитацию виртуальной реальности, а в виртуальной реальности ничего не должно дергаться или напоминать стандартное поведение браузера. Поэтому мне нужно было чтобы новая страница рендерилась только после того как загрузятся все необходимые ей ресурсы. Усложнялось дело тем что так обычно не делают и никакой инфы по такому решению я не нашел. А реализовал я это вот так: CharactersPage.tsx, App.tsx. В двух словах, у каждого компонента страницы есть статический метод preload, который загружает необходимые ресурсы. При переходе на другую страницу меняются пропсы App.tsx, вызывается componentWillReceiveProps в котором вызывается метод preload следующей страницы, но App.tsx пока не перерендеривается из-за переопределенного componentDidMount. Когда ресурсы загружены, они кладутся в стейт и это тригерит ещё один перерендер, но на этот раз уже загруженные ресурсы передаются в props следующей страницы - она сразу рендерится как нужно и пользователь не видит никаких недозагруженных страниц. Также я добавил лоадер (MainLoader.tsx), который появляется если асинхронная операция идет более 500 миллисекунд, но исчезает лоадер не раньше чем через 1 секунду - опять же чтобы ничего не дергалось и не мерцало.
Для дизайна ключевое значение имело создать эффект виртуальной реальности, дергания и мерцания при загрузке недопустимы. Тоже касается и картинок - картинка должна отображаться сразу, а не подгружаться "сверху вних". Поэтому в приложении повсеместно используется прелоад изображений. На странице списка персонажей и профиля персонажа предзагрузка аватарок добавлена в метод preload, чтобы страница рендерилась сразу с уже загруженными аватарками. При создании персонажа только что аплоаднутая пользователем аватарка презагружается сразу же т.к. в дальнейшем пользователь перейдет на страницу профиля и с уже предзагруженной аватаркой переход произойдет без задержки. При изменении аватарки у существующего персонажа отображается аватарка из локального файла, а новая прелоадится в фоне т.к. в дальнейшем пользователь может опять зайти на страницу профиля своего персонажа.
Все знают что такое бесконечный скролл. Я пошел дальше и чтобы субъективно ускорить загрузку для пользователя, реализовал бесконечный скролл с предзагрузкой на странице списка персонажей - CharactersPage.tsx. Работает это так: когда пользователь заходит на страницу списка персонажей в методе preload загружаются первая пачка персонажей и их аватарки и после этого страница рендерится. Сразу же после этого начинается предзагрузка новой пачки вместе с аватаркими в фоне и загруженная пачка будет храниться в стейте. Когда пользователь скролит вниз - без всяких задержек рендерится уже предзагруженная пачка и следом начинается прелоад новой. Аватарки оптимизированы средствами cloudinary (CharacterAvatarService.ts). Замеры показали, что при скорости 500 кБит/с (средняя скорость интернета в Буркина-Фасо) и средне-быстром скроле всё работает без задержек.
HighTechContainer.tsx - это контейнер в котором всё находится. Тут всё стандартно: используется requestAnimationFrame, слушаются события мыши, сетается transform. Тут я пытался создать иллюзию реального объекта, создать ощущение что его можно потрогать. Контейнер также оптимизирован для адекватной работы при падении фреймрейта.
Я провел небольшой эксперемент, на клиенте каждый компонент, страница, сервис и вообще всё представляет из себя инкапсулированные модули с файлом index.ts в котором описан его интерфейс и в котором также импортятся другие необходимые модулю ресурсы - картинки, стили, шрифты, конфиги, дочерние компоненты и все они включаются внутрь модуля. Пример 1 - MainLoader.tsx, Пример2 - ErrorPage.tsx, Пример3 - RolePlayingSystem.tsx. Модуль вместе со всеми ресурсами представляет собой единое целое, не нужно импортить стили в каком-нибудь main.css, и не нужно складывать картинки в отдельную папку - при импорте модуля он добавляет в сборку всё что ему нужно. Если у модуля есть внешние зависимости, например от других компонентов или сервисов, это зависисимости от модулей, а не от отдельных файлов, что как мне кажется понятнее и положительно сказывается на переиспользуемости. Юнит тесты пока ещё не написаны, но я планирую включать в каждый модуль также и тесты для него. Тем не менее подход ещё не до конца отработан - стили модулей используют переменные и миксины из variables.scss, нужно подумать как лучше решить этот вопрос. Но подход мне кажется перспективный.
Рендеринг React компонентов оптимизирован при помощи shouldComponentUpdate и PureComponent. Пример 1 - TabInfoSection.tsx, Пример 2 - IntegerPropertyControl.tsx. Крупные компоненты разделены на более мелкие для лучшей читабельности и опять же оптимизации.
Документация React рекомендует использовать CancelablePromises вместо isMounted для того чтобы избежать вызовов setState на анмаунтнотом компоненте в асинхронных операциях. Я реализовал это в своих компонентах, и не сказал бы что это хорошая альтернатива - такие промисы получаются громоздкими и не очень удобными. Зато CancelablePromises отлично подходят для контроля цепочек асинхронных операций. В компоненте UndergroundBroadcast.tsx (телевизор) сначала некоторое время отображаются помехи, потом начинает грузиться следующаяя картинка и в это время тоже отображаются помехи, потом картинка некоторое время показывается и опять отображаются помехи и начинает грузиться следующая. При этом если контейнер перевернуть телевизор "выключается" и цепочка отменяется, если опять перевернуть - всё начинается заново и CancelablePromises подходят для этого как нельзя лучше.
Приложение поддерживает локализацию - LocalisationService.ts, Пример использования. Локаль пока одна - русская
У меня есть 2 варианта развития проекта и я пока не определился. Возможно проект будет развиваться в сторону однопользовательской игры - в таком случае нужно работать над геймплейной логикой ролевой системы, или возможно это будет система внутренних резюме какой-нибудь компании - в таком случае нужно переработать концепцию и дизайн, добавить админку и т.д.
На данном этапе проект скорее демо чем готовый продукт, множество критически выжных для продакшена вещей не доделано и работы ещё поле непахано, но не нужно думать что если чего-то нет то я этого не умею делать) Проект длится уже 4 месяца и нужно было уже на чем-то остановиться. С планом дальнейших работ вы можете ознакомиться здесь. Ставьте звездочку если считаете проект интересным, также буду рад адекватным ревьюерам и конструктивным отзывам. Всем добра)
