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

Vuejs

Проблемы рендера 7-и тысяч элементов на Vuetify

03.06.2021 08:11:45 | Автор: admin

Предисловие

На момент написания статьи я готовился к диплому и писал дипломный проект для нужд Московского Политеха. Моя задача - перенести существующий функционал из PHP-таблицы во что-то современное с кучей проверок, после чего дополнить данный функционал. Движок - Nuxt, материал-фреймворк: Vuetify.

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

Первые попытки

Что делает разработчик, когда сталкивается с проблемой? Идет гуглить. Это было первое, что я сделал. Как оказалось, проблема медленного рендера таблицы Vuetify встречается с куда меньшим числом элементов, чем у меня. Что советуют:

  • Рендерить элементы по частям через setInterval

  • Ставить условие, чтобы не рендерить элементы, пока не сработает хук жизненного цикла mounted()

  • Использовать v-lazy для последовательной отрисовки

При этом было предложение использовать компонент Virtual Scroller, позволяющий отрисовывать элементы по мере скролла, а предыдущие разрендеривать. Но этот компонент Vuetify не работает с таблицами Vuetify -_-

С "радостью" прочитав, что в Vuetify 3 (релиз через ~полгода) производительность улучшится на 50%, я стал пробовать решения. Рендер элементов по частям ничего не дал, так как на условном тысячном элементе отрисовка начинала лагать, а к семи тысячам снова всё висло. Рендер элементов на mounted не дал вообще ничего, всё зависало, но зато после того, как страница загрузится (эээ, ура?). v-lazy хоть и рендерился быстрее, но рендерить 14 тысяч компонентов (Vuetify и Transition от Vue) тоже грустное занятие.

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

ТаблицаТаблица

Решение 1. Intersection Observer

Итак, что мы имеем. v-lazy отрисовывать невозможно, это 14 тысяч компонентов. Vuetify Virtual Scroller не поддерживается в Vuetify Data Table из-за его структуры. Выходит, нужно писать свою реализацию. Кто умеет определять, докрутил ли пользователь до элемента? Intersection Observer. Internet Explorer нам не нужен, так что можем приступать.

Первая логичная попытка: использовать директиву v-intersect от самого Vuetify. И 7 тысяч директив также привели к длительному рендеру страницы =(. Значит, выбора нет и придется работать руками.

mounted() {  //Цепляем его на таблицу с overflow: auto  //Требуемая видимость элемента для триггера: 10%this.observer = new IntersectionObserver(this.handleObserve, { root: this.$refs.table as any, threshold: 0.1 });//Почему нельзя повесить observe на все нужные элементы? Ну ладноfor (const element of Array.from(document.querySelectorAll('#intersectionElement'))) {this.observer.observe(element);}},

Теперь взглянем на сам handleObserve:

async handleObserve(entries: IntersectionObserverEntry[]) {const parsedEntries = entries.map(entry => {const target = entry.target as HTMLElement;  //Предварительно задали data-атрибутыconst project = +(target.dataset.projectId || '0');const speciality = +(target.dataset.specialityId || '0');return {          isIntersecting: entry.isIntersecting,          project,          speciality,};    });//Чтобы точно было реактивно    this.$set(this, 'observing', [      //Не добавляем дубликаты      ...parsedEntries.filter(x => x.isIntersecting && !this.observing.some(y => y.project === x.project && y.speciality === x.speciality)),      //Убираем пропавшие      ...this.observing.filter(entry => !parsedEntries.some(x => !x.isIntersecting && x.project === entry.project && x.speciality === entry.speciality)),     ]);//Иначе функция стриггерится несколько раз     Array.from(document.querySelectorAll('#intersectionElement')).forEach((target) => this.observer?.unobserve(target));     //Даем Vuetify перерендериться await this.$nextTick(); //Ждем 300мс, чтобы не триггернуть лишний раз, отрисовка тоже грузит браузер     await new Promise((resolve) => setTimeout(resolve, 500));     //Вновь обсервим элементы     Array.from(document.querySelectorAll('#intersectionElement'))          .forEach((target) => this.observer?.observe(target));},

Итак, мы имеем 7 тысяч элементов, на которых смотрит наш Intersection Observer. Есть переменная observing, в которой содержатся все элементы с projectId и specialityId, по которым мы можем определять, нужно ли показывать нужный элемент в таблице. Осталось всего-лишь повесить v-if на нужный нам элемент и отрисовывать вместо него какую-нибудь заглушку. Ура!

 <template #[`item.speciality-${speciality.id}`]="{item, headers}" v-for="speciality in getSpecialities()"><div id="intersectionElement" :data-project-id="item.id" :data-speciality-id="speciality.id"><ranking-projects-table-itemv-if="observing.some(x => x.project === item.id && x.speciality === speciality.id)":speciality="speciality":project="item"/><template v-else>Загрузка...</template></div></template>

А на саму таблицу вешаем v-once. Таблице будет запрещено менять свой рендер без $forceUpdate. Не очень красивое решение, но Vuetify непонятно чем занимается при скролле, запретим ему это делать.

<v-data-table v-bind="getTableSettings()"   v-once   :items="projects"   @update:expanded="$forceUpdate()">

Итоги:

  • Рендер занимает около секунды

  • Элементы разрендериваются вне их зоны видимости

  • Идет ререндер на каждое действие внутри таблицы

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

Загрузка...Загрузка...

Однако было ясно (и заказчик это подчеркнул), что работать с таблицей тяжело. Нужно было что-то решать, но на момент тогда я зашел в тупик: Vuetify Data Table имеет ужасные проблемы с производительностью, которые признают даже разработчики фреймворка.

Не переписывать же мне их таблицу и создавать свой аналог?

Решение 2. Переписать таблицу и создать свой аналог

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

Что мы поняли из первого решения:

  1. Таблицы Vuetify лагучий отстой

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

  3. Нужно как-то сократить время на первичную отрисовку, чтобы пользователь не видел бесконечную "Загрузку"

  4. Можно заставить Intersection Observer реагировать чаще, чтобы рендерить элементы по мере скролла, а не когда скролл остановится или среагирует событие (300мс задержка)

Возвращаемся к Virtual Scroller. Он не работает в таблице Vuetify, так? А что если мы напишем свою таблицу с блекджеком и display: grid? Зачем-то же его придумали.

Что нужно для Virtual Scroller? Фиксированная высота каждого элемента. Что нужно для Grid'ов? Фиксированная ширина каждого элемента и информация о количестве элементов. Бахнем CSS-переменные для последующего использования в CSS:

<div class="ranking-table" :style="{    '--projects-count': getSettings().projectsCount,    '--specialities-count': getSettings().specialitiesCount,    '--first-column-width': `${getSettings().firstColumnWidth}px`,    '--others-columns-width': `${getSettings().othersColumnsWidth}px`,    '--cell-width': `${getSettings().firstColumnWidth + getSettings().othersColumnsWidth * getSettings().specialitiesCount}px`,    '--item-height': `${getSettings().itemHeight}px`  }">

Потом пишем гриды по типу аля

display: grid;grid-template-columns:var(--first-column-width)repeat(var(--specialities-count), var(--others-columns-width));

И так далее для элементов внутри. Класс! А еще мы, зная ширину и количество элементов, можем задать ширину нашему Virtual Scroller (он сам по умолчанию туповат), а заодно и всем нашим элементам, чтобы не дай боже кто-то вышел за доступные ему границы

.ranking-table_v2__scroll::v-deep {.v-virtual-scroll {&__container, &__item, .ranking-table_v2__project {    width: var(--cell-width);}}}

Примечание: если вы сделаете просто <style>без scoped и решите, что будет хорошей идеей редактировать глобальные стили вне окружения компонента, то у меня для вас плохие новости: лучше так не делать вне каких-то App.vue, и стоит ознакомиться с тем, что это за v-deep.

Поехали: добавляем Virtual Scroller, пихаем в него проекты, после чего выводим последние. Сразу скажу: поддержки Expandable Items у нас тут нет, я вынес информацию о проекте во всплывающее окно. Жаль, конечно, что нельзя это отображать прямо в таблице, как делал Vuetify, но тогда придется помучаться с их скроллером, а он и так не особо хорошо работает. В общем, к делу:

<v-virtual-scroll   class="ranking-table_v2__scroll"   :height="getSettings().commonHeight":item-height="getSettings().itemHeight":items="projects"><template #default="{item}"><div class="ranking-table_v2__project" :key="item.id"><!-- ... -->

Итого: на страницу, допустим, помещается 6 проектов (высота же у всех максимальная по факту), итого рендерится 6 строк + шапка. Колонок 50. Итого рендерится около 300 сложных компонентов. А вот это уже задача не уровня мстителей, 300 мы рендерить умеем.

Вспоминаем про лучший инструмент всех времен и народов v-lazy: он позволяет отрендерить элемент один раз и потом не перерендеривать его. Раньше мы пытались отрендерить 14 тысяч компонентов, сейчас 600 простых. Ну и оборачиваем все наши колонки (кроме шапки) в v-lazy. При горизонтальном скролле элементы подгружаются, и остаются отрендеренными до тех пор, пока сама строка таблицы не пропадет из области видимости и не разрендерится.

 <v-lazy class="ranking-table_v2__item ranking-table_v2__item--speciality"v-for="(speciality, index) in specialities":key="speciality.id">   <!-- Колонка в строке таблицы --></v-lazy>

Видео с разницей, можно сравнить:

Плюсы такого решения:

  • Элементы перестали скакать как ненормальные

  • Нет нужды писать свою реализацию логики рендера/отрендера

  • Можем отказаться от v-once и всяких принудительных ререндеров аля $forceUpdate

  • Куда больше гибкости при верстке таблицы

Минусы:

  • Если используются выпадающие элементы (expand), нужно писать свою реализацию

  • Нужно верстать таблицу самому, без инструментов движка

  • Нет средств сортировки/группировки и прочего (мне было не нужно)

  • Для того, чтобы шапка всегда была отрендеренной, но скроллилась вместе со всеми, мне пришлось отслеживать событие Scroll и подгонять скролл шапки к скроллу основных элементов

  • Чтобы отображать таблицу в нужном мне проценте от высоты страницы, мне пришлось смотреть на window.innerHeight и применять его в CSS переменных и в значении высоты у VirtualScroll

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

Заключение

Я привел 2 варианта решения проблем с производительностью отрисовки таблиц в Vuetify. Оба решения достаточно сложные, просто сложные в разных аспектах. Оба так или иначе помогли мне решить мою проблему, и у обоих есть свои ограничения. Например, второй вариант мне нравится больше, но отсутствие средств Vuetify (сортировка/группировка и пр.) может оттолкнуть тех, кому это позарез нужно.

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

И да: я пользовался дебагером производительности от Vue и смотрел, кто её потребляет. Зачастую там был буквально один-два компонента, и, заменив их на какой-то другой с похожей логикой, проблема не решалась - дело в их количестве, а не сложности (не считая таблицу Vuetify - там передается множество props'ов из компонента в компонент).

Надеюсь, что приведенные мной варианты натолкнут кого-то на решение его проблемы, а кто-то просто узнает что-то новое =). Будем вместе ждать стабильный Vue 3 со всей его экосистемой, как минимум Nuxt 3. Что-то обещают множество улучшений, может, часть костылей из этой статьи даже пропадет.

Подробнее..

Перевод Как создать приложение-чат за двадцать минут

09.11.2020 12:16:47 | Автор: admin
image

Мой отец любит напоминать мне, что, будучи компьютерным инженером в 1970-х, он был программистом до того, как программирование стало модным. Пару раз он даже показывал старые скрипты Fortran и COBOL. Прочитав этот код, я с уверенностью могу сказать, что программирование сегодня определенно круче.

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

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

Сколько было бы построено небоскребов, если бы строители сами добывали себе сталь?

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

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

Как сделать приложение для чата


Давайте быстро создадим что-нибудь, что раньше занимало бы дни или недели. Мы сделаем Public Chat Room приложение, которое использует WebSockets для обмена сообщениями в реальном времени.

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

  • 8base управляемый GraphQL API
  • VueJS JavaScript фреймворк

Стартовый проект и полный файл README можно найти в этом репозитории GitHub. Если вы хотите просмотреть только готовое приложение, загляните в ветку public-chat-room.

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

Начнем.

Семь шагов для создания чат приложения:


1. Настройка проекта


Клонируйте стартовый проект и перейдите в директорию группового чата. Вы можете сами определить, использовать yarn или npm для установки зависимостей проекта. В любом случае, нам нужны все NPM пакеты, обозначенные в файле package.json.

# Клонируем проектgit clone https://github.com/8base/Chat-application-using-GraphQL-Subscriptions-and-Vue.git group-chat# Переходим в директориюcd group-chat# Устанавливаем зависимостиyarn

Чтобы взаимодействовать с GraphQL API, мы должны настроить три переменные среды. Создайте файл .env.local в корневой директории с помощью следующей команды, и приложение Vue после инициализации автоматически установит переменные среды, которые мы добавили в этот файл.

echo 'VUE_APP_8BASE_WORKSPACE_ID=<YOUR_8BASE_WORKSPACE_ID>
VUE_APP_8BASE_API_ENDPOINT=https://api.8base.com
VUE_APP_8BASE_WS_ENDPOINT=wss://ws.8base.com' \
> .env.local


Оба значения VUE_APP_8BASE_API_ENDPOINT и VUE_APP_8BASE_WS_ENDPOINT менять не нужно. Необходимо только установить значение VUE_APP_8BASE_WORKSPACE_ID.

Если у вас есть воркспейс 8base, который вы хотите использовать для создания чат-приложения по нашему руководству, обновите файл .env.local, указав свой идентификатор воркспейса. Если нет получите идентификатор воркспейса, выполнив шаги 1 и 2 из 8base Quickstart.

2. Импорт схемы


Теперь нам нужно подготовить серверную часть. В корне этого репозитория вы должны найти файл chat-schema.json. Чтобы импортировать его в рабочую область, нужно просто установить командную строку 8base и залогиниться, а затем импортировать файл схемы.

# Установка 8base CLIyarn global add 8base-cli# Аутентификация CLI8base login# Импортируем схему в нашу рабочую область8base import -f chat-schema.json -w <YOUR_8BASE_WORKSPACE_ID>

3. Доступ к API


Последняя задача по бэкенду разрешить публичный доступ к GraphQL API.

В консоли 8base перейдите в App Services > Roles > Guest. Обновите разрешения, установленные как для сообщений, так и для пользователей, чтобы они были или отмечены галочкой, или установлены как All Records (как показано на скриншоте ниже).

Роль Guest определяет, что разрешено делать пользователю, отправившему неаутентифицированный запрос к API.

image
Редактор ролей в консоли 8base.

4. Пишем GraphQL запросы


На этом этапе мы собираемся определить и выписать все запросы GraphQL, которые нам понадобятся для нашего компонента чата. Это поможет нам понять, какие данные мы будем читать, создавать и прослушивать (через WebSockets) с помощью API.

Следующий код следует поместить в файл src / utils / graphql.js. Прочтите комментарии над каждой экспортированной константой, чтобы понять, что выполняет каждый запрос.

/* gql преобразует строки запроса в документы graphQL */import gql from "graphql-tag";/* 1. Получение всех пользователей онлайн-чата и последних 10 сообщений */export const InitialChatData = gql`{  usersList {    items {      id      email    }  }  messagesList(last: 10) {    items {      content      createdAt      author {        id        email      }    }  }}`;/* 2. Создание новых пользователей чата и назначение им роли гостя */export const CreateUser = gql`mutation($email: String!) {  userCreate(data: { email: $email, roles: { connect: { name: "Guest" } } }) {    id  }}`;/* 3. Удаление пользователя чата*/export const DeleteUser = gql`mutation($id: ID!) {  userDelete(data: { id: $id, force: true }) {    success  }}`;/* 4. Подписка на создание и удаление пользователей чата */export const UsersSubscription = gql`subscription {  Users(filter: { mutation_in: [create, delete] }) {    mutation    node {      id      email    }  }}`;/* 5. Создание новых сообщений чата и связывание их с автором */export const CreateMessage = gql`mutation($id: ID!, $content: String!) {  messageCreate(    data: { content: $content, author: { connect: { id: $id } } }  ) {    id  }}`;/* 6. Подписка на создание сообщений чата. */export const MessagesSubscription = gql`subscription {  Messages(filter: { mutation_in: create }) {    node {      content      createdAt      author {        id        email      }    }  }}`;


5. Настройка Apollo клиента для подписок


Когда наши запросы GraphQL написаны, самое время настроить наши модули API.

Во-первых, давайте займемся клиентом API с помощью ApolloClient с его обязательными настройками по умолчанию. Для createHttpLink мы предоставляем наш полностью сформированный эндпоинт воркспейса. Этот код находится в src/utils/api.js.

import { ApolloClient } from "apollo-boost";import { createHttpLink } from "apollo-link-http";import { InMemoryCache } from "apollo-cache-inmemory";const { VUE_APP_8BASE_API_ENDPOINT, VUE_APP_8BASE_WORKSPACE_ID } = process.env;export default new ApolloClient({link: createHttpLink({  uri: `${VUE_APP_8BASE_API_ENDPOINT}/${VUE_APP_8BASE_WORKSPACE_ID}`,}),cache: new InMemoryCache(),});// Note: Чтобы узнать больше о параметрах, доступных при настройке // ApolloClient, обратитесь к их документации.

Затем займемся клиентом подписки, используя subscriptions-transport-ws и isomorphic-ws. Этот код немного длиннее, чем предыдущий, поэтому стоит потратить время на чтение комментариев в коде.

Мы инициализируем SubscriptionClient, используя наш эндопоинт WebSockets и workspaceId в параметрах connectionParams. Затем мы используем этот subscriptionClient в двух методах, определенных в экспорте по умолчанию: subscribe() и close().

subscribe позволяет нам создавать новые подписки с обратными вызовами данных и ошибок. Метод close это то, что мы можем использовать, чтобы закрыть соединение при выходе из чата.

import WebSocket from "isomorphic-ws";import { SubscriptionClient } from "subscriptions-transport-ws";const { VUE_APP_8BASE_WS_ENDPOINT, VUE_APP_8BASE_WORKSPACE_ID } = process.env;/*** Создайте клиент подписки, используя соответствующие*переменные среды и параметры по умолчанию.*/const subscriptionClient = new SubscriptionClient(VUE_APP_8BASE_WS_ENDPOINT,{  reconnect: true,  connectionParams: {    /**      * Workspace ID ОБЯЗАТЕЛЬНО должен быть установлен, иначе *конечная точка Websocket не сможет*сопоставить запрос с соответствующим воркспейсом      */    workspaceId: VUE_APP_8BASE_WORKSPACE_ID,  },},/**  * Конструктор для реализации WebSocket, совместимой с W3C. *Используйте это, если ваше окружение не имеет встроенного собственного *WebSocket (например, с клиентом NodeJS)  */WebSocket);export default {/**  * Принимает запрос подписки, любые переменные и обработчики колбэков *'data и 'error  */subscribe: (query, options) => {  const { variables, data, error } = options;  /**    * Запускает новый запрос на подписку.    */  const result = subscriptionClient.request({    query,    variables,  });  /**    * Функцию отписки можно использовать для закрытия *определенной подписки, в отличие от ВСЕХ подписок, *поддерживаемых subscriptionClient    */  const { unsubscribe } = result.subscribe({    /**      * При получении события результат передается в *колбэк данных, указанный разработчиком.      */    next(result) {      if (typeof data === "function") {        data(result);      }    },    /**      * Каждый раз при получении ошибки она передается в колбэк ошибок, указанный разработчиком.      */    error(e) {      if (typeof error === "function") {        error(e);      }    },  });  return unsubscribe;},/**  * Закрывает subscriptionClient соединение.  */close: () => {  subscriptionClient.close();},};// Примечание. Чтобы узнать больше о SubscriptionClient и его параметрах, // пожалуйста, обратитесь к их документации.

6. Написание компонента Vue


Теперь у нас есть все необходимое для создания публичного чата. Осталось только написать один компонент GroupChat.vue.

Загрузите компонент с помощью yarn serve, и продолжим.

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

Скрипт компонента


Сначала нам нужно импортировать наши модули, простые стили и GraphQL запросы. Всё это находится в нашем каталоге src / utils.
Объявите следующие импорты в GroupChat.vue.

/* API модули */import Api from "./utils/api";import Wss from "./utils/wss";/* graphQL операции */import {InitialChatData,CreateUser,DeleteUser,UsersSubscription,CreateMessage,MessagesSubscription,} from "./utils/graphql";/* Стили */import "../assets/styles.css";

Компонентные данные


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

/* imports ... */export default {name: "GroupChat",data: () => ({  messages: [],  newMessage: "",  me: { email: "" },  users: [],}),};

Хуки жизненного цикла


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

/* ипорты... */export default {/* остальные параметры ... *//**  * Хук жизненного цикла, выполняющийся при создании компонента.  */created() {  /**    * Подписка на событие, которое срабатывает при создании или удалении пользователя    */  Wss.subscribe(UsersSubscription, {    data: this.handleUser,  });  /**    * Подписка на событие, которое срабатывает при создании сообщения    */  Wss.subscribe(MessagesSubscription, {    data: this.addMessage,  });  /**    * Получение начальные данные чата (пользователи и последние 10 сообщений)    */  Api.query({    query: InitialChatData,  }).then(({ data }) => {    this.users = data.usersList.items;    this.messages = data.messagesList.items;  });  /**    * Колбэк вызывается при обновлении страницы, чтобы закрыть чат    */  window.onbeforeunload = this.closeChat;},/**  * Хук жизненного цикла, выполняющийся при уничтожении компонента.  */beforeDestroy() {  this.closeChat();},};

Методы компонента


Мы должны добавить определенные методы, предназначенные для обработки каждого вызова / ответа API (createMessage, addMessage, closeChat, и т.д.). Все они будут сохранены в объекте методов нашего компонента.

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

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

/* импорты ... */export default {/* остальные параметры ... */methods: {  /**    * Создание нового пользователя, используя указанный адрес электронной почты.    */  createUser() {    Api.mutate({      mutation: CreateUser,      variables: {        email: this.me.email,      },    });  },  /**    * Удалиние пользователя по его ID.    */  deleteUser() {    Api.mutate({      mutation: DeleteUser,      variables: { id: this.me.id },    });  },  /**    * Наши пользователи подписываются на события создания и обновления, и поэтому *нам нужно выбрать соответствующий метод для обработки ответа в зависимости от *типа мутации.**Здесь у нас есть объект, который ищет тип мутации по имени, возвращает *его и выполняет функцию, передавая узел события.    */  handleUser({    data: {      Users: { mutation, node },    },  }) {    ({      create: this.addUser,      delete: this.removeUser,    }[mutation](node));  },  /**    * Добавляет нового пользователя в массив users, сначала проверяя, *является ли добавляемый пользователь текущим пользователем.    */  addUser(user) {    if (this.me.email === user.email) {      this.me = user;    }    this.users.push(user);  },  /**    * Удаляет пользователя из массива users по ID.    */  removeUser(user) {    this.users = this.users.filter(      (p) => p.id != user.id    );  },  /* Создать новое сообщение */  createMessage() {    Api.mutate({      mutation: CreateMessage,      variables: {        id: this.me.id,        content: this.newMessage,      },    }).then(() => (this.newMessage = ""));  },  /**    * Наша подписка на сообщения проверяет только событие создания.  *Поэтому все, что нам нужно сделать, это поместить его в наш массив *сообщений.    */  addMessage({    data: {      Messages: { node },    },  }) {    this.messages.push(node);  },  /**    * Мы хотим закрыть наши подписки и удалить пользователя. Этот метод можно вызвать в нашем хуке жизненного цикла beforeDestroy и любом другом соответствующем колбэке.    */  closeChat () {    /* Закрытие подписки перед выходом */    Wss.close()    /* Удаление участника */    this.deleteUser();    /* Установка значения по умолчанию */    this.me = { me: { email: '' } }  }},/* Хуки ... */}

Шаблон компонента


И последнее, но не менее важное: у нас есть компонент GroupChat.vue.

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

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

Как всегда, читайте встроенные комментарии к коду.

<template><div id="app">  <!--    Представление чата должно отображаться только в том случае, если текущий пользователь имеет идентификатор. В противном случае отображается форма регистрации..    -->  <div v-if="me.id" class="chat">    <div class="header">      <!--        Поскольку мы используем подписки, которые работают в режиме реального времени, количество пользователей, которые сейчас находятся в сети, будет динамически корректироваться.        -->      {{ users.length }} Online Users      <!--       Пользователь может выйти из чата, вызвав функцию closeChat..        -->      <button @click="closeChat">Leave Chat</button>    </div>    <!--    Каждое сообщение, которое мы храним в массиве сообщений, мы будем отображать в этом div. Кроме того, если идентификатор участника сообщения совпадает с идентификатором текущего пользователя, мы присвоим ему класс me.      -->    <div      :key="index"      v-for="(msg, index) in messages"      :class="['msg', { me: msg.participant.id === me.id }]"    >      <p>{{ msg.content }}</p>      <small        ><strong>{{ msg.participant.email }}</strong> {{ msg.createdAt        }}</small      >    </div>    <!--Инпут сообщения привязан к свойству данных newMessage.      -->    <div class="input">      <input        type="text"        placeholder="Say something..."        v-model="newMessage"      />      <!--       Когда пользователь нажимает кнопку отправки, мы запускаем функцию createMessage.        -->      <button @click="createMessage">Send</button>    </div>  </div>  <!--   Процесс регистрации просит пользователя ввести адрес электронной почты. Как только инпут теряет фокус, вызывается метод createUser.    -->  <div v-else class="signup">    <label for="email">Sign up to chat!</label>    <br />    <input      type="text"      v-model="me.email"      placeholder="What's your email?"      @blur="createUser"      required    />  </div></div></template>

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

7. Заключение и тестирование


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

Надеюсь, вы также узнали, как инициализировать ApolloClient и SubscriptionClient для эффективного выполнения запросов GraphQL, мутаций и подписок в воркспейсе 8base, а также немного о VueJS.

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

Создайте чат-приложение с 8base


8base это готовый к работе бессерверный бэкенд-как-сервис, созданный разработчиками для разработчиков. Платформа 8base позволяет разработчикам создавать потрясающие облачные приложения с использованием JavaScript и GraphQL. Узнайте больше о платформе 8base здесь.
Подробнее..

CSR vs SSR для одностраничных приложений на VueJS

16.11.2020 00:15:06 | Автор: admin


Архитектура одностраничных приложений набрала обороты благодаря популярности реактивных фреймворков: VueJS, ReactJS, AngularJS.

Все больше компаний стремятся отделить логику Backend и Frontend приложений, опираясь на принципы микросервисной архитектуры.

Разберемся, чем рендеринг на стороне клиента (CRR) отличается от серверного (SSR).
Серверный рендеринг происходит в четыре этапа. Допустим, вы ввели в адресной строке URL и зашли на сайт:

  • Сервер отдает клиенту готовый к рендерингу HTML ответ
  • Браузер рендерит полученный HTML и загружает JS код. На этом этапе страница доступна к просмотру
  • Браузер исполняет JS код, а вместе с ним и код Vue/React
  • Страница становится интерактивной

image
При клиентском рендеринге страница доступна после выполнения кода. Благодаря чему пользователь не видит голый, не оживленный JS-ом скелет страницы:

  • Сервер присылает ответ клиенту
  • Браузер загружает JS
  • Браузер исполняет Vue/React
  • Страница доступна и интерактивна

Способ показа страницы пользователю не единственное различие CSR и SSR. Нередко маршрутизация SPA тоже делается на клиенте. У этого подхода есть недостатки с точки зрения CEO, но об этом дальше. Сначала рассмотрим преимущества SPA.

Преимущества одностраничных приложений


Новый подход не получил бы такого распространения, не имея существенных преимуществ

  • Выше скорость работы приложения
  • Лучше User Experience
  • Меньше нагрузка на сервер
  • Универсальная платформа
  • Отзывчивый интерфейс


Скорость


Благодаря внутренним оптимизации VDOM скорость обновления нод на странице выше, чем при использовании VanillaJS. В реактивных фреймворках используются и другие оптимизации, которые повышают отзывчивость интерфейса и улучшают user experience.

Сервер


Благодаря клиентской маршрутизации снижается нагрузка не сервер. Сервер не отдает заново набор повторяющихся скриптов и контента. При переходе между страницами вся работа по отрисовке контента делегирована JS. Фактически происходит эмуляция постраничной навигации. С точки зрения сервера страница остается такой же.

Универсальность


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

Архитектура одностраничных приложений


Фреймворки для создания одностраничных приложений реализуют MVC паттерн, позволяя разделить клиентское приложение на модель, представление и контроллер.


Контроллер


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

routes.js


Модель


Модель отвечает за хранение и работу с данными. В качестве хранилища данных во Vue используются Vuex, Vue Apollo, EventBus, RxJS. Взаимодействие с API для удобства выносится в отдельных файл. Взаимодействие с хранилищем происходит в компонентах.

Представление


Представление во Vue это шаблон, заключенный в тэг template. Представление отвечает за отрисовку данных пользователю.
Одностраничные приложения делятся по способу рендеринга на client side rendering и server side rendering.

Преимущества клиентского рендеринга


  • Не перезагружает контент страницы при постраничной навигации
  • После первоначальной загрузки работа с контентом происходит быстрее
  • Удобно для работы в режиме development благодаря горячей перезагрузке

Противниками CSR выступают CEO-специалисты. Одностраничное CSR приложение плохо индексируется поисковыми роботами.

Преимущества серверного рендеринга


Для SSR в экосистеме Vue существует библиотека vue-server-renderer:

  • Загрузка страницы быстрее, чем CSR
  • Лучше для SEO
  • Время первичной отрисовки контента ниже так, как не ожидается загрузка JS
  • Взаимодействие с сайтом происходит быстрее благодаря клиентской навигации VueJS
  • Нет необходимости в излишних запросах к серверу, которые происходят при первоначальной загрузке страницы при клиентском рендеринге

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

Что выбрать?


Нередко компании настраивают 2 варианта билда одностраничных приложений: CSR и SSR. Их используются в зависимости от задач и потребностей бизнеса. Решение о способе рендеринга одностраничных приложений выносится на основании запросов бизнеса. Для некоторых приложений СЕО-оптимизация не является критичной. Их создатели делают ставку на отзывчивость интерфейса, удобство разработки.
При выборе SSR для VueJS лучше воспользоваться NuxtJS. Это минималистичный фреймворк с доступной документацией.
Подробнее..
Категории: Javascript , Vuejs , Spa frontend

Полный стек на примере списка задач (React, Vue, TypeScript, Express, Mongoose)

24.12.2020 12:19:48 | Автор: admin


Доброго времени суток, друзья!

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

Наше приложение будет иметь стандартный функционал:

  • добавление новой задачи в список
  • обновление индикатора выполнения задачи
  • обновление текста задачи
  • удаление задачи из списка
  • фильтрация задач: все, активные, завершенные
  • сохранение задач на стороне клиента и в базе данных

Выглядеть наше приложение будет так:


Для более широкого охвата аудитории клиентская часть приложения будет реализована на чистом JavaScript, серверная на Node.js. В качестве абстракции для ноды будет использован Express.js, в качестве базы данных сначала локальное хранилище (Local Storage), затем индексированная база данных (IndexedDB) и, наконец, облачная MongoDB.

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

В статье будут приведены примеры реализации клиентской части на React и Vue, а также фуллстек-тудушки на React + TypeScript + Express + Mongoose.

Исходный код всех рассматриваемых в статье проектов находится здесь.

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

Демо нашего приложения:


Итак, поехали.

Клиент


Начнем с клиентской части.

Создаем рабочую директорию, например, javascript-express-mongoose:

mkdir javascript-express-mongoosecd !$code .

Создаем директорию client. В этой директории будет храниться весь клиентский код приложения, за исключением index.html. Создаем следующие папки и файлы:

client  components    Buttons.js    Form.js    Item.js    List.js  src    helpers.js    idb.js    router.js    storage.js  script.js  style.css

В корне проекта создаем index.html следующего содержания:

<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <title>JS Todos App</title>    <!-- Подключаем стили -->    <link rel="stylesheet" href="client/style.css" />  </head>  <body>    <div id="root"></div>    <!-- Подключаем скрипт -->    <script src="client/script.js" type="module"></script>  </body></html>

Стили (client/style.css):
@import url('https://fonts.googleapis.com/css2?family=Stylish&display=swap');* {  margin: 0;  padding: 0;  box-sizing: border-box;  font-family: stylish;  font-size: 1rem;  color: #222;}#root {  max-width: 512px;  margin: auto;  text-align: center;}#title {  font-size: 2.25rem;  margin: 0.75rem;}#counter {  font-size: 1.5rem;  margin-bottom: 0.5rem;}#form {  display: flex;  margin-bottom: 0.25rem;}#input {  flex-grow: 1;  border: none;  border-radius: 4px;  box-shadow: 0 0 1px inset #222;  text-align: center;  font-size: 1.15rem;  margin: 0.5rem 0.25rem;}#input:focus {  outline-color: #5bc0de;}.btn {  border: none;  outline: none;  background: #337ab7;  padding: 0.5rem 1rem;  border-radius: 4px;  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.5);  color: #eee;  margin: 0.5rem 0.25rem;  cursor: pointer;  user-select: none;  width: 102px;  text-shadow: 0 0 1px rgba(0, 0, 0, 0.5);}.btn:active {  box-shadow: 0 0 1px rgba(0, 0, 0, 0.5) inset;}.btn.info {  background: #5bc0de;}.btn.success {  background: #5cb85c;}.btn.warning {  background: #f0ad4e;}.btn.danger {  background: #d9534f;}.btn.filter {  background: none;  color: #222;  text-shadow: none;  border: 1px dashed #222;  box-shadow: none;}.btn.filter.checked {  border: 1px solid #222;}#list {  list-style: none;}.item {  display: flex;  flex-wrap: wrap;  justify-content: space-between;  align-items: center;}.item + .item {  border-top: 1px dashed rgba(0, 0, 0, 0.5);}.text {  flex: 1;  font-size: 1.15rem;  margin: 0.5rem;  padding: 0.5rem;  background: #eee;  border-radius: 4px;}.completed .text {  text-decoration: line-through;  color: #888;}.disabled {  opacity: 0.8;  position: relative;  z-index: -1;}#modal {  position: absolute;  top: 10px;  left: 10px;  padding: 0.5em 1em;  background: rgba(0, 0, 0, 0.5);  border-radius: 4px;  font-size: 1.2em;  color: #eee;}


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


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

Дополнительные элементы: 1) заголовок; 2) счетчик количества невыполненных задач.

Приступаем к созданию компонентов (сверху вниз). Компоненты Form и Buttons являются статическими, а List и Item динамическими. В целях дифференциации статические компоненты экспортируются/импортируются по умолчанию, а в отношении динамических компонентов применяется именованный экспорт/импорт.

client/Form.js:

export default /*html*/ `<div id="form">  <input      type="text"      autocomplete="off"      autofocus      id="input"  >  <button    class="btn"    data-btn="add"  >    Add  </button></div>`

/*html*/ обеспечивает подсветку синтаксиса, предоставляемую расширением для VSCode es6-string-html. Атрибут data-btn позволит идентифицировать кнопку в скрипте.

Обратите внимание, что глобальные атрибуты id позволяют обращаться к DOM-элементам напрямую. Дело в том, что такие элементы (с идентификаторами), при разборе и отрисовке документа становятся глобальными переменными (свойствами глобального объекта window). Разумеется, значения идентификаторов должны быть уникальными для документа.

client/Buttons.js:

export default /*html*/ `<div id="buttons">  <button    class="btn filter checked"    data-btn="all"  >    All  </button>  <button    class="btn filter"    data-btn="active"  >    Active  </button>  <button    class="btn filter"    data-btn="completed"  >    Completed  </button></div>`

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

client/Item.js (самый сложный компонент с точки зрения структуры):

/** * функция принимает на вход задачу, * которая представляет собой объект, * включающий идентификатор, текст и индикатор выполнения * * индикатор выполнения управляет дополнительными классами * и текстом кнопки завершения задачи * * текст завершенной задачи должен быть перечеркнут, * а кнопка для изменения (обновления) текста такой задачи - отключена * * завершенную задачу можно сделать активной*/export const Item = ({ id, text, done }) => /*html*/ `<li  class="item ${done ? 'completed' : ''}"  data-id="${id}">  <button    class="btn ${done ? 'warning' : 'success'}"    data-btn="complete"  >    ${done ? 'Cancel' : 'Complete'}  </button>  <span class="text">    ${text}  </span>  <button    class="btn info ${done ? 'disabled' : ''}"    data-btn="update"  >    Update  </button>  <button    class="btn danger"    data-btn="delete"  >    Delete  </button></li>`

client/List.js:

/** * для формирования списка используется компонент Item * * функция принимает на вход список задач * * если вам не очень понятен принцип работы reduce * https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce*/import { Item } from "./Item.js"export const List = (todos) => /*html*/ `  <ul id="list">    ${todos.reduce(      (html, todo) =>        (html += `            ${Item(todo)}        `),      ''    )}  </ul>`

С компонентами закончили.

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

src/helpers.js:

/** * данная функция будет использоваться * для визуализации нажатия одной из кнопок * для фильтрации задач * * она принимает элемент - нажатую кнопку и класс - в нашем случае checked * * основной контейнер имеет идентификатор root, * поэтому мы можем обращаться к нему напрямую * из любой части кода, в том числе, из модулей*/export const toggleClass = (element, className) => {  root.querySelector(`.${className}`).classList.remove(className)  element.classList.add(className)}// примерные задачиexport const todosExample = [  {    id: '1',    text: 'Learn HTML',    done: true  },  {    id: '2',    text: 'Learn CSS',    done: true  },  {    id: '3',    text: 'Learn JavaScript',    done: false  },  {    id: '4',    text: 'Stay Alive',    done: false  }]

Создадим базу данных (пока в форме локального хранилища).

src/storage.js:

/** * база данных имеет два метода * get - для получения тудушек * set - для записи (сохранения) тудушек*/export default (() => ({  get: () => JSON.parse(localStorage.getItem('todos')),  set: (todos) => { localStorage.setItem('todos', JSON.stringify(todos)) }}))()

Побаловались и хватит. Приступаем к делу.

src/script.js:

// импортируем компоненты, вспомогательную функцию, примерные задачи и хранилищеimport Form from './components/Form.js'import Buttons from './components/Buttons.js'import { List } from './components/List.js'import { Item } from './components/Item.js'import { toggleClass, todosExample } from './src/helpers.js'import storage from './src/storage.js'// функция принимает контейнер и список задачconst App = (root, todos) => {  // формируем разметку с помощью компонентов и дополнительных элементов  root.innerHTML = `    <h1 id="title">      JS Todos App    </h1>    ${Form}    <h3 id="counter"></h3>    ${Buttons}    ${List(todos)}  `  // обновляем счетчик  updateCounter()  // получаем кнопку добавления задачи в список  const $addBtn = root.querySelector('[data-btn="add"]')  // основной функционал приложения  // функция добавления задачи в список  function addTodo() {    if (!input.value.trim()) return    const todo = {      // такой способ генерации идентификатора гарантирует его уникальность и соответствие спецификации      id: Date.now().toString(16).slice(-4).padStart(5, 'x'),      text: input.value,      done: false    }    list.insertAdjacentHTML('beforeend', Item(todo))    todos.push(todo)    // очищаем поле и устанавливаем фокус    clearInput()    updateCounter()  }  // функция завершения задачи  // принимает DOM-элемент списка  function completeTodo(item) {    const todo = findTodo(item)    todo.done = !todo.done    // рендерим только изменившийся элемент    renderItem(item, todo)    updateCounter()  }  // функция обновления задачи  function updateTodo(item) {    item.classList.add('disabled')    const todo = findTodo(item)    const oldValue = todo.text    input.value = oldValue    // тонкий момент: мы используем одну и ту же кнопку    // для добавления задачи в список и обновления текста задачи    $addBtn.textContent = 'Update'    // добавляем разовый обработчик    $addBtn.addEventListener(      'click',      (e) => {        // останавливаем распространение события для того,        // чтобы нажатие кнопки не вызвало функцию добавления задачи в список        e.stopPropagation()        const newValue = input.value.trim()        if (newValue && newValue !== oldValue) {          todo.text = newValue        }        renderItem(item, todo)        clearInput()        $addBtn.textContent = 'Add'      },      { once: true }    )  }  // функция удаления задачи  function deleteTodo(item) {    const todo = findTodo(item)    item.remove()    todos.splice(todos.indexOf(todo), 1)    updateCounter()  }  // функция поиска задачи  function findTodo(item) {    const { id } = item.dataset    const todo = todos.find((todo) => todo.id === id)    return todo  }  // дополнительный функционал  // функция фильтрации задач  // принимает значение кнопки  function filterTodos(value) {    const $items = [...root.querySelectorAll('.item')]    switch (value) {      // отобразить все задачи      case 'all':        $items.forEach((todo) => (todo.style.display = ''))        break      // активные задачи      case 'active':        // отобразить все и отключить завершенные        filterTodos('all')        $items          .filter((todo) => todo.classList.contains('completed'))          .forEach((todo) => (todo.style.display = 'none'))        break      // завершенные задачи      case 'completed':        // отобразить все и отключить активные        filterTodos('all')        $items          .filter((todo) => !todo.classList.contains('completed'))          .forEach((todo) => (todo.style.display = 'none'))        break    }  }  // функция обновления счетчика  function updateCounter() {    // считаем количество невыполненных задач    const count = todos.filter((todo) => !todo.done).length    counter.textContent = `      ${count > 0 ? `${count} todo(s) left` : 'All todos completed'}    `    if (!todos.length) {      counter.textContent = 'There are no todos'      buttons.style.display = 'none'    } else {      buttons.style.display = ''    }  }  // функция повторного рендеринга изменившегося элемента  function renderItem(item, todo) {    item.outerHTML = Item(todo)  }  // функция очистки инпута  function clearInput() {    input.value = ''    input.focus()  }  // делегируем обработку событий корневому узлу  root.onclick = ({ target }) => {    if (target.tagName !== 'BUTTON') return    const { btn } = target.dataset    if (target.classList.contains('filter')) {      filterTodos(btn)      toggleClass(target, 'checked')    }    const item = target.parentElement    switch (btn) {      case 'add':        addTodo()        break      case 'complete':        completeTodo(item)        break      case 'update':        updateTodo(item)        break      case 'delete':        deleteTodo(item)        break    }  }  // обрабатываем нажатие Enter  document.onkeypress = ({ key }) => {    if (key === 'Enter') addTodo()  }  // оптимизация работы с хранилищем  window.onbeforeunload = () => {    storage.set(todos)  }}// инициализируем приложения;(() => {  // получаем задачи из хранилища  let todos = storage.get('todos')  // если в хранилище пусто  if (!todos || !todos.length) todos = todosExample  App(root, todos)})()

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

Однако, с использованием локального хранилища в качестве базы данных сопряжено несколько проблем: 1) ограниченный размер около 5 Мб, зависит от браузера; 2) потенциальная возможность потери данных при очистке хранилищ браузера, например, при очистке истории просмотра страниц, нажатии кнопки Clear site data вкладки Application Chrome DevTools и т.д.; 3) привязка к браузеру невозможность использовать приложение на нескольких устройствах.

Первую проблему (ограниченность размера хранилища) можно решить с помощью IndexedDB.

Индексированная база данных имеет довольно сложный интерфейс, поэтому воспользуемся абстракцией Jake Archibald idb-keyval. Копируем этот код и записываем его в файл src/idb.js.

Вносим в src/script.js следующие изменения:

// import storage from './src/storage.js'import { get, set } from './src/idb.js'window.onbeforeunload = () => {  // storage.set(todos)  set('todos', todos)}// обратите внимание, что функция инициализации приложения стала асинхронной;(async () => {  // let todos = storage.get('todos')  let todos = await get('todos')  if (!todos || !todos.length) todos = todosExample  App(root, todos)})()

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

React, Vue

Ниже приводятся примеры реализации клиентской части тудушки на React и Vue.

React:


Vue:


База данных


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

  1. Создаем аккаунт в MongoDB Atlas
  2. Во вкладке Projects нажимаем на кнопку New Project
  3. Вводим название проекта, например, todos-db, и нажимаем Next
  4. Нажимаем Create Project
  5. Нажимаем Build a Cluster
  6. Нажимаем Create a cluster (FREE)
  7. Выбираем провайдера и регион, например, Azure и Hong Kong, и нажимаем Create Cluster
  8. Ждем завершения создания кластера и нажимаем connect
  9. В разделе Add a connection IP address выбираем либо Add Your Current IP Address, если у вас статический IP, либо Allow Access from Anywhere, если у вас, как в моем случае, динамический IP (если сомневаетесь, выбирайте второй вариант)
  10. Вводим имя пользователя и пароль, нажимаем Create Database User, затем нажимаем Choose a connection method
  11. Выбираем Connect your application
  12. Копируем строку из раздела Add your connection string into your application code
  13. Нажимаем Close












В корневой директории создаем файл .env и вставляем в него скопированную строку (меняем <username>, <password> и <dbname> на свои данные):

MONGO_URI=mongodb+srv://<username>:<password>@cluster0.hfvcf.mongodb.net/<dbname>?retryWrites=true&w=majority

Сервер


Находясь в корневой директории, инициализируем проект:

npm init -y// илиyarn init -yp

Устанавливаем основные зависимости:

yarn add cors dotenv express express-validator mongoose

  • cors отключает политику общего происхождения (одного источника)
  • dotenv предоставляет доступ к переменным среды в файле .env
  • express облегчает создание сервера на Node.js
  • express-validator служит для проверки (валидации) данных
  • mongoose облегчает работу с MongoDB

Устанавливаем зависимости для разработки:

yarn add -D nodemon open-cli morgan

  • nodemon запускает сервер и автоматически перезагружает его при внесении изменений в файл
  • open-cli открывает вкладку браузера по адресу, на котором запущен сервер
  • morgan логгер HTTP-запросов

Далее добавляем в package.json скрипты для запуска сервера (dev для запуска сервера для разработки и start для продакшн-сервера):

"scripts": {  "start": "node index.js",  "dev": "open-cli http://localhost:1234 && nodemon index.js"},

Отлично. Создаем файл index.js следующего содержания:

// подключаем библиотекиconst express = require('express')const mongoose = require('mongoose')const cors = require('cors')const morgan = require('morgan')require('dotenv/config')// инициализируем приложение и получаем роутерconst app = express()const router = require('./server/router')// подключаем промежуточное ПОapp.use(express.json())app.use(express.urlencoded({ extended: false }))app.use(cors())app.use(morgan('dev'))// указываем, где хранятся статические файлыapp.use(express.static(__dirname))// подлючаемся к БДmongoose.connect(  process.env.MONGO_URI,  {    useNewUrlParser: true,    useUnifiedTopology: true,    useFindAndModify: false,    useCreateIndex: true  },  () => console.log('Connected to database'))// возвращаем index.html в ответ на запрос к корневому узлуapp.get('/', (_, res) => {  res.sendFile(__dirname + '/index.html')})// при запросе к api передаем управление роутеруapp.use('/api', router)// определяем порт и запускаем серверconst PORT = process.env.PORT || 1234app.listen(PORT, () => console.log(`Server is running`))

Тестируем сервер:

yarn dev// илиnpm run dev



Прекрасно, сервер работает. Теперь займемся маршрутизацией. Но перед этим определим схему данных, которые мы будем получать от клиента. Создаем директорию server для хранения серверных файлов. В этой директории создаем файлы Todo.js и router.js.

Структура проекта на данном этапе:

client  components    Buttons.js    Form.js    Item.js    List.js  src    helpers.js    idb.js    storage.js  script.js  style.cssserver  Todo.js  router.js.envindex.htmlindex.jspackage.jsonyarn.lock (либо package-lock.json)

Определяем схему в src/Todo.js:

const { Schema, model } = require('mongoose')const todoSchema = new Schema({  id: {    type: String,    required: true,    unique: true  },  text: {    type: String,    required: true  },  done: {    type: Boolean,    required: true  }})// экспорт модели данныхmodule.exports = model('Todo', todoSchema)

Настраиваем маршрутизацию в src/router.js:

// инициализируем роутерconst router = require('express').Router()// модель данныхconst Todo = require('./Todo')// средства валидацииconst { body, validationResult } = require('express-validator')/** * наш интерфейс (http://personeltest.ru/away/localhost:1234/api) * будет принимать и обрабатывать 4 запроса * GET-запрос /get - получение всех задач из БД * POST /add - добавление в БД новой задачи * DELETE /delete/:id - удаление задачи с указанным идентификатором * PUT /update - обновление текста или индикатора выполнения задачи * * для работы с БД используется модель Todo и методы * find() - для получения всех задач * save() - для добавления задачи * deleteOne() - для удаления задачи * updateOne() - для обновления задачи * * ответ на запрос - объект, в свойстве message которого * содержится сообщение либо об успехе операции, либо об ошибке*/// получение всех задачrouter.get('/get', async (_, res) => {  const todos = (await Todo.find()) || []  return res.json(todos)})// добавление задачиrouter.post(  '/add',  // пример валидации  [    body('id').exists(),    body('text').notEmpty().trim().escape(),    body('done').toBoolean()  ],  async (req, res) => {    // ошибки - это результат валидации    const errors = validationResult(req)    if (!errors.isEmpty()) {      return res.status(400).json({ message: errors.array()[0].msg })    }    const { id, text, done } = req.body    const todo = new Todo({      id,      text,      done    })    try {      await todo.save()      return res.status(201).json({ message: 'Todo created' })    } catch (error) {      return res.status(500).json({ message: `Error: ${error}` })    }  })// удаление задачиrouter.delete('/delete/:id', async (req, res) => {  try {    await Todo.deleteOne({      id: req.params.id    })    res.status(201).json({ message: 'Todo deleted' })  } catch (error) {    return res.status(500).json({ message: `Error: ${error}` })  }})// обновление задачиrouter.put(  '/update',  [    body('text').notEmpty().trim().escape(),    body('done').toBoolean()  ],  async (req, res) => {    const errors = validationResult(req)    if (!errors.isEmpty()) {      return res.status(400).json({ message: errors.array()[0].msg })    }    const { id, text, done } = req.body    try {      await Todo.updateOne(        {          id        },        {          text,          done        }      )      return res.status(201).json({ message: 'Todo updated' })    } catch (error) {      return res.status(500).json({ message: `Error: ${error}` })    }})// экспорт роутераmodule.exports = router

Интеграция


Возвращаемся к клиентской части. Для того, чтобы абстрагировать отправляемые клиентом запросы мы также прибегнем к помощи роутера. Создаем файл client/src/router.js:

/** * наш роутер - это обычная функция, * принимающая адрес конечной точки в качестве параметра (url) * * функция возвращает объект с методами: * get() - для получения всех задач из БД * set() - для добавления в БД новой задачи * update() - для обновления текста или индикатора выполнения задачи * delete() - для удаления задачи с указанным идентификатором * * все методы, кроме get(), принимают на вход задачу * * методы возвращают ответ от сервера в формате json * (объект со свойством message)*/export const Router = (url) => ({  // получение всех задач  get: async () => {    const response = await fetch(`${url}/get`)    return response.json()  },  // добавление задачи  set: async (todo) => {    const response = await fetch(`${url}/add`, {      method: 'POST',      headers: {        'Content-Type': 'application/json'      },      body: JSON.stringify(todo)    })    return response.json()  },  // обновление задачи  update: async (todo) => {    const response = await fetch(`${url}/update`, {      method: 'PUT',      headers: {        'Content-Type': 'application/json'      },      body: JSON.stringify(todo)    })    return response.json()  },  // удаление задачи  delete: async ({ id }) => {    const response = await fetch(`${url}/delete/${id}`, {      method: 'DELETE'    })    return response.json()  }})

Для того, чтобы сообщать пользователю о результате выполнения CRUD-операции (create, read, update, delete создание, чтение, обновление, удаление), добавим в src/helpers.js еще одну вспомогательную функцию:

// функция создает модальное окно с сообщением о результате операции// и удаляет его через две секундыexport const createModal = ({ message }) => {  root.innerHTML += `<div data-id="modal">${message}</div>`  const timer = setTimeout(() => {    root.querySelector('[data-id="modal"]').remove()    clearTimeout(timer)  }, 2000)}

Вот как выглядит итоговый вариант client/script.js:

import Form from './components/Form.js'import Buttons from './components/Buttons.js'import { List } from './components/List.js'import { Item } from './components/Item.js'import { toggleClass, createModal, todosExample } from './src/helpers.js'// импортируем роутер и передаем ему адрес конечной точкиimport { Router } from './src/router.js'const router = Router('http://localhost:1234/api')const App = (root, todos) => {  root.innerHTML = `    <h1 id="title">      JS Todos App    </h1>    ${Form}    <h3 id="counter"></h3>    ${Buttons}    ${List(todos)}  `  updateCounter()  const $addBtn = root.querySelector('[data-btn="add"]')  // основной функционал  async function addTodo() {    if (!input.value.trim()) return    const todo = {      id: Date.now().toString(16).slice(-4).padStart(5, 'x'),      text: input.value,      done: false    }    list.insertAdjacentHTML('beforeend', Item(todo))    todos.push(todo)    // добавляем в БД новую задачу и сообщаем о результате операции пользователю    createModal(await router.set(todo))    clearInput()    updateCounter()  }  async function completeTodo(item) {    const todo = findTodo(item)    todo.done = !todo.done    renderItem(item, todo)    // обновляем индикатор выполнения задачи    createModal(await router.update(todo))    updateCounter()  }  function updateTodo(item) {    item.classList.add('disabled')    const todo = findTodo(item)    const oldValue = todo.text    input.value = oldValue    $addBtn.textContent = 'Update'    $addBtn.addEventListener(      'click',      async (e) => {        e.stopPropagation()        const newValue = input.value.trim()        if (newValue && newValue !== oldValue) {          todo.text = newValue        }        renderItem(item, todo)        // обновляем текст задачи        createModal(await router.update(todo))        clearInput()        $addBtn.textContent = 'Add'      },      { once: true }    )  }  async function deleteTodo(item) {    const todo = findTodo(item)    item.remove()    todos.splice(todos.indexOf(todo), 1)    // удаляем задачу    createModal(await router.delete(todo))    updateCounter()  }  function findTodo(item) {    const { id } = item.dataset    const todo = todos.find((todo) => todo.id === id)    return todo  }  // дальше все тоже самое  // за исключением window.onbeforeunload  function filterTodos(value) {    const $items = [...root.querySelectorAll('.item')]    switch (value) {      case 'all':        $items.forEach((todo) => (todo.style.display = ''))        break      case 'active':        filterTodos('all')        $items          .filter((todo) => todo.classList.contains('completed'))          .forEach((todo) => (todo.style.display = 'none'))        break      case 'completed':        filterTodos('all')        $items          .filter((todo) => !todo.classList.contains('completed'))          .forEach((todo) => (todo.style.display = 'none'))        break    }  }  function updateCounter() {    const count = todos.filter((todo) => !todo.done).length    counter.textContent = `      ${count > 0 ? `${count} todo(s) left` : 'All todos completed'}    `    if (!todos.length) {      counter.textContent = 'There are no todos'      buttons.style.display = 'none'    } else {      buttons.style.display = ''    }  }  function renderItem(item, todo) {    item.outerHTML = Item(todo)  }  function clearInput() {    input.value = ''    input.focus()  }  root.onclick = ({ target }) => {    if (target.tagName !== 'BUTTON') return    const { btn } = target.dataset    if (target.classList.contains('filter')) {      filterTodos(btn)      toggleClass(target, 'checked')    }    const item = target.parentElement    switch (btn) {      case 'add':        addTodo()        break      case 'complete':        completeTodo(item)        break      case 'update':        updateTodo(item)        break      case 'delete':        deleteTodo(item)        break    }  }  document.onkeypress = ({ key }) => {    if (key === 'Enter') addTodo()  }};(async () => {  // получаем задачи из БД  let todos = await router.get()  if (!todos || !todos.length) todos = todosExample  App(root, todos)})()

Поздравляю, вы только что создали полноценную фуллстек-тудушку.

TypeScript

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



Заключение


Подведем краткие итоги.

Мы с вами реализовали полноценное клиент-серверное приложение для добавления, редактирования и удаления задач из списка, интегрированное с настоящей базой данных. На клиенте мы использовали самый современный (чистый) JavaScript, на сервере Node.js сквозь призму Express.js, для взаимодействия с БД Mongoose. Мы рассмотрели парочку вариантов хранения данных на стороне клиента (local storage, indexeddb idb-keyval). Также мы увидели примеры реализации клиентской части на React (+TypeScript) и Vue. По-моему, очень неплохо для одной статьи.

Буду рад любой форме обратной связи. Благодарю за внимание и хорошего дня.
Подробнее..

Перевод Какой будет новая версия Vuex?

27.12.2020 20:07:49 | Автор: admin
Vuex стейт менеджер для Vue приложений. Его следующая версия Vuex 4, которая практически готова к официальному релизу. Она добавит поддержку Vue 3, но не принесет никакой новой функциональности.

Несмотря на то, что Vuex считается отличным решением и многие разработчики выбирают его как основную библиотеку для управления состоянием, они надеются получить больше возможностей в будущих релизах. Поэтому, пока Vuex 4 только готовится к выходу, один из его разработчиков, Kia King Ishii (входит в состав core-команды) уже делится планами для следующей, 5 версии. Стоит заметить, что это только планы и некоторые вещи могут измениться, тем не менее основное направление уже похоже выбрано. О нем и пойдет речь.

С появлением Vue 3 и Сomposition API, разработчики стали создавать простые альтернативы. Например, в статье Вероятно вам не нужен Vuex демонстрируется простой, гибкий и надежный способ для создания сторов на основе Composition API совместно с provide/inject. Можно предположить, что этот и некоторые другие альтернативы вполне подойдут для небольших приложение, но как часто бывает, они имеют свои недостатки: документация, сообщество, соглашение в именовании, интеграция, инструменты разработчика.



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

Создание стора


Перед тем как делать что-то со стором, нам нужно его создать. Вo Vuex 4, это выглядит следующим образом:

import { createStore } from 'vuex'export const counterStore = createStore({  state: {    count: 0  },    getters: {    double (state) {      return state.count * 2    }  },    mutations: {    increment (state) {      state.count++    }  },    actions: {    increment (context) {      context.commit('increment')    }  }})

Стор все также состоит из 4 частей: состояние (state), где хранятся данные; геттеры (getters), предоставляющие вычисляемые состояния; мутации (mutations), необходимые для изменения состояния и экшены (actions), которые вызываются за пределами стора для выполнения операций над ним. Обычно экшены не просто вызывают мутацию (как в примере), а используются для выполнения асинхронных задач (потому что мутации должны быть синхронными) или реализуют какую-то более сложную логику. Как же будет выглядеть Vuex 5?

import { defineStore } from 'vuex'export const counterStore = defineStore({  name: 'counter',    state() {    return { count: 0 }  },    getters: {    double () {      return this.count * 2    }  },    actions: {    increment () {      this.count++    }  }})

Первое, что изменилось переименование createStore в defineStore. Чуть позже будет понятно почему. Следующее, появился параметр name для указания имени стора. До этого, мы разделяли сторы на модули, а имена модулей были в виде именованных объектов. Далее модули регистрировались в глобальном пространстве, из-за чего они не были самодостаточными и готовыми для переиспользования. В качестве решения, нужно было использовать параметр namespaced, чтобы не давать нескольким модулям реагировать на тот же тип мутаций и действий. Думаю многие сталкивались с этим, но ссылку на документацию я, тем не менее, добавлю. Теперь у нас нет модулей, каждый стор по-умолчанию отдельное и независимое хранилище.

После указания имени, нам нужно сделать state функцией, которая возвращает начальное состояние, а не просто устанавливает его. Это очень похоже на то, как выглядит data в компонентах. Изменения коснулись и геттеров, вместо state как параметра функции мы используем this, чтобы получить доступ к данным. Тот же подход применен и к экшенам, this вместо statе как параметра. И наконец, самое главное, мутации объединены с экшенами. Kia отмечал, что мутации довольно часто становятся простыми сеттерами, делая их многословными, видимо это и послужило причиной удаления. Он не упоминает, можно ли будет производить изменение состояния за пределам стора, например из компонентов. Тут, мы можем только сослаться на Flux паттерн, который не рекомендует этого делать и поощряет подход с изменением состояния именно из экшенов.

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

import { ref, computed } from 'vue'import { defineStore } from 'vuex'export const counterStore = defineStore('counter', {  const count = ref(0)  const double = computed(() => count.value * 2)    function increment () {    count.value++  }  return { count, double, increment }  })

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

Инициализация стора


Здесь нас ждут существенные изменения. Чтобы описать, как будет происходить инициализация стора в 5-ой версии, посмотрим как это происходит в 4-ой. Когда мы создаем стор через createStore, мы сразу же его инициализируем, чтобы затем использовать в app.use или напрямую.

import { createApp } from 'vue'import App from './App.vue'import store from './store'const app = createApp(App)app.use(store)app.mount('#app')// Теперь у всех компонентов есть доступ к `this.$store`// Или к `useStore()` в контексте Composition APIimport store from './store'store.state.count // -> 0store.commit('increment')store.dispatch('increment')store.getters.double // -> 4

В 5 версии, мы отдельно получаем доступ к каждому экземпляру Vuex, что дает гарантию независимости. Поэтому этот процесс выглядит иначе:

import { createApp } from 'vue'import { createVuex } from 'vuex'import App from './App.vue'const app = createApp(App)const vuex = createVuex()app.use(vuex)app.mount('#app')

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

import { defineComponent } from 'vue'import store from './store'export default defineComponent({  name: 'App',  computed: {    counter () {      return this.$vuex.store(store)    }  }})

Вызов $vuex.store создает и инициализирует стор. Теперь, каждый раз общаясь к этому хранилищу через $vuex.store, вам будет возвращаться уже созданный экземпляр. В примере это this.counter, который мы можем использовать дальше в коде. Так же, можно инициализировать стор через createVuex().

И конечно, вариант для Composition API, где вместо $vuex.store используется useStore.

import { defineComponent } from 'vue'import { useStore } from 'vuex' // import useStoreimport store from './store'export default defineComponent({  setup () {    const counter = useStore(store)    return { counter }  }})

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

import { createApp } from 'vue'import { createVuex } from 'vuex'import App from './App.vue'import store from './store'const app = createApp(App)const vuex = createVuex()app.use(vuex)app.provide('name', store)app.mount('#app')

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

import { defineComponent } from 'vue'export default defineComponent({  name: 'App',  inject: ['name']})// Composition APIimport { defineComponent, inject } from 'vue'export default defineComponent({  setup () {    const store = inject('name')    return { store }  }})

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

store.state.count                // Statestore.getters.double            // Gettersstore.commit('increment')   // Mutationsstore.dispatch('increment')  // Actions

В новой версии ожидается:

store.count          // Statestore.double         // Gettersstore.increment()  // Actions

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

Совместное использование


Последние момент, который стоит рассмотреть компоновка. Мы помним, что во Vuex 5 у нас больше нет именованных модулей и каждый стор является отдельным и независимым. Это дает возможность импортировать их когда нужно и использовать данные по мере необходимости, прямо как компоненты. Появляется логичный вопрос, как использовать несколько сторов вместе? В 4 версии все еще существует глобальное пространство имен и нам нужно использовать rootGetters и rootState, чтобы обращаться к разным сторам в этой области (так же как и в 3 версии). Подход в Vuex 5 иной:

// store/greeter.jsimport { defineStore } from 'vuex'export default defineStore({  name: 'greeter',  state () {    return { greeting: 'Hello' }  }})// store/counter.jsimport { defineStore } from 'vuex'import greeterStore from './greeter'export default defineStore({  name: 'counter',  use () {    return { greeter: greeterStore }  },    state () {    return { count: 0 }  },    getters: {    greetingCount () {      return `${this.greeter.greeting} ${this.count}'    }  }})

Мы импортируем стор, затем регистрируем его через use, и тем самым получаем к нему доступ. Все выглядит еще проще если использовать Сomposition API:

// store/counter.jsimport { ref, computed } from 'vue'import { defineStore } from 'vuex'import greeterStore from './greeter'export default defineStore('counter', ({use}) => {  const greeter = use(greeterStore)  const count = 0  const greetingCount = computed(() => {    return  `${greeter.greeting} ${this.count}`  })  return { count, greetingCount }})

Единственное, что стоит здесь упомянуть, это use, который работает точно также как vuex.store и отвечает за правильную инициализацию сторов.

Поддержка TypeScript


Благодаря изменениям в API и уменьшению количества абстракций, поддержка TypeScript в 4 версии будет намного лучше, но нам все еще потребуется много ручной работы. Выход же 5 версии даст возможность добавлять типы, там где необходимо, и там где мы этого хотим.

Заключение


Vuex 5 выглядит многообещающе и именно так как многие и ожидают (устранение старых недочетов, добавление гибкости). Полный список обсуждений и мнения основной команды можно найти в Vue RFCs репозитории.
Подробнее..

Nuxt.js app от UI-кита до деплоя

18.02.2021 14:23:54 | Автор: admin
Привет, Хабр!

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

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

  • создание и конфигурация проекта,
  • assets и static: стили, шрифты, изображения, посты,
  • создание компонентов,
  • создание страниц и layouts,
  • развертывание приложения (деплой).

Смотрите, что получилось!

Немного о Nuxt.js


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

Основные преимущества Nuxt:

  • SPA, SSR и пререндер уже настроены; всё, что от нас требуется, это указать. В данном приложении используем пререндер для продуктового режима, то есть заранее генерим все страницы сайта, а дальше деплоим их на хостинг для раздачи статики.
  • Отличный SEO для всех поисковых систем результат использования SSR или пререндера.
  • Быстрое взаимодействие с сайтом по сравнению со статическими сайтами. Это достигается благодаря подгрузке только необходимых js chunks, css styles и API запросов (большую часть этого процесса автоматизирует webpack 4, который работает под капотом Nuxt).
  • Отличные показатели Google Lighthouse / Page Speed. При правильной настройке можно получить 100/100 даже на слабом сервере.
  • CSS Modules, Babel, Postscc и другие крутые инструменты настроены заранее при использовании create-nuxt-app.
  • Заданная структура проекта позволяет комфортно работать в средних и больших командах.
  • Более 50 готовых модулей и возможность использовать любые пакеты из обширной экосистемы Vue.js.

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

Дизайн


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

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

Для разработки я использовал онлайн-сервис Figma. Дизайн и UI-кит доступны по ссылке. Вы можете скопировать этот шаблон и использовать его в своём проекте.

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


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

Инициализируем проект, указав его название:

npx create-nuxt-app nuxt-blog

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

Полный список выбранных опций вы можете посмотреть на Github.

Для этого проекта будет использована конфигурация с Typescript.

При разработке на Vue c Typescript можно использовать два API: Options API или Class API.

Функционально друг от друга они не отличаются, однако имеют разный синтаксис. Лично мне ближе синтаксис Options API, поэтому в нашем проекте будет использоваться именно он.

После создания проекта мы можем запустить наше приложение, используя команду: npm run dev. Теперь оно будет доступно на localhost:3000.

В качестве локального сервера Nuxt использует webpack-dev-server с установленным и настроенным HMR, что позволяет сделать разработку быстрой и комфортной.

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

Если ранее вы не касались этой темы, то советую обратить внимание на Jest очень простой, но при этом мощный инструмент, который поддерживает работу с Nuxt совместно с vue-test-utils.

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


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

-- Assets
-- Static
-- Pages
-- Middleware
-- Components
-- Layouts
-- Plugins
-- Store
-- nuxt.config.js
-- ...other files


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

Создание приложения


Перед тем, как писать код, давайте сделаем следующее:

1. Удалим стартовые компоненты и страницы, созданные Nuxt.
2. Установим pug и scss для нашего удобства и экономии времени при разработке. Выполним команду:

npm i --save-dev pug pug-plain-loader node-sass sass-loader fibers

После чего станет доступно использование атрибута lang для тегов template и style:

<template lang="pug"></template><style lang="scss"></style>

3. Добавим в конфигурацию stylelint поддержку глубокого селектора ::v-deep, который позволит применить стили к дочерним компонентам, игнорируя scoped. Подробнее об этом селекторе можно прочитать здесь.

{  rules: {      'at-rule-no-unknown': null,      'selector-pseudo-element-no-unknown': [        true,        {          ignorePseudoElements: ['v-deep'],        },      ],    },}

Все приготовления позади, переходим к следующему этапу.

Посты


Посты будут храниться в директории content/posts, которую мы создадим в корне проекта в виде набора markdown-файлов.

Давайте создадим 5 небольших файлов, чтобы далее можно было сразу начать с ними работать. Для простоты используем названия 1.md, 2.md и т. д.

В директории content создадим файл Posts.d.ts, в котором определим типы для объекта, содержащего всю необходимую информацию о посте:

export type Post = {    id: number    title: string  desc: string  file: string  img: string  }

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

Идём дальше. В этой же директории создадим ещё один файл под названием posts.ts со следующим содержимым:

import { Post } from './Post'  export default [    {    id: 1,      title: 'Post 1',      desc:        'A short description of the post to keep the user interested.' +        ' Description can be of different lengths, blocks are aligned' +        ' to the height of the block with the longest description',      file: 'content/posts/1.md',    img: 'assets/images/1.svg',  },    ...  {      id: 5,      title: 'Post 5',      desc:        'A short description of the post to keep the user interested.' +        ' Description can be of different lengths, blocks are aligned' +        ' to the height of the block with the longest description',      file: 'content/posts/5.md',    img: 'assets/images/5.svg',  },  ] as Post[]

В свойстве img мы ссылаемся на изображения в директории assets/images, но данную директорию мы ещё не создавали, давайте сделаем это сейчас.

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

Я возьму 5 изображений с unDraw. Этот отличный ресурс постоянно обновляется и содержит множество бесплатных svg-изображений.

Теперь, когда всё готово, директория content должна иметь следующий вид:

content/
-- posts.ts
-- Posts.d.ts
-- posts/
---- 1.md
---- 2.md
---- 3.md
---- 4.md
---- 5.md


А в директории assets должна была появиться поддиректория images со следующим содержимым:

assets/
-- images/
---- 1.svg
---- 2.svg
---- 3.svg
---- 4.svg
---- 5.svg
...


Динамическое получение файлов


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

Для этого создадим в директории plugins поддиректорию mixins, а в ней файл getDynamicFile.ts со следующим содержимым:

import Vue from 'vue'    export const methods = {    getDynamicFile(name: string) {      return require(`@/${name}`) },  }    Vue.mixin({    methods,  })

Всё, что нам остаётся, это подключить данный миксин в файле nuxt.config.js:

{  plugins: [      '~plugins/mixins/getDynamicFile.ts',    ],}

Шрифты


После этапа создания постов подключим шрифты. Самый простой вариант это сделать замечательная библиотека Webfontloader, которая позволяет получить любой шрифт с Google Fonts. Однако в коммерческой разработке чаще используют собственные шрифты, поэтому давайте разберём здесь именно такой случай.

В качестве шрифта для нашего приложения был выбран Rubik, который распространяется под лицензией Open Font License. Скачать его можно всё с того же Google Fonts.

Обратите внимание, что в скачанном архиве шрифты будут в формате otf, но, так как мы работаем с web, то лучшим выбором для нас будут форматы woff и woff2.Они имеют меньший размер, чем любые другие форматы, но при этом отлично поддерживаются во всех современных браузерах. Для конвертации otf в нужные нам форматы можно воспользоваться одним из множества бесплатных онлайн-сервисов.

Итак, у нас есть шрифты в нужных нам форматах, пришло время подключать их в проект. Для этого создадим в директории static поддиректорию fonts и добавим туда наши шрифты. Создадим в этой же директории файл fonts.css, который будет отвечать за подключение наших шрифтов в приложении со следующим содержимым:

@font-face {    font-family: "Rubik-Regular";    font-weight: normal;    font-style: normal;    font-display: swap;    src:    local("Rubik"),    local("Rubik-Regular"),    local("Rubik Regular"),    url("/fonts/Rubik-Regular.woff2") format("woff2"),    url("/fonts/Rubik-Regular.woff") format("woff");  }    ...

Полное содержимое файла можно увидеть в репозитории.

Стоит обратить внимание на 2 вещи:

1. Мы указываем font-display: swap;, определяя, как шрифт, подключенный через font-face, будет отображаться в зависимости от того, загрузился ли он и готов ли к использованию.
В данном случае мы не задаём период блокировки и задаем бесконечный период подмены. То есть загрузка шрифта происходит в фоне и не блокирует загрузку страницы, а шрифт отобразится по готовности.

2. В src мы указываем порядок загрузки по приоритетности. Сначала мы проверяем, установлен ли нужный шрифт у пользователя на устройстве, проверяя возможные варианты названия шрифта. Если не находим его, то проверяем, поддерживает ли браузер более современный формат woff2, и, если нет, то переходим к следующему формату woff. Есть вероятность, что пользователь использует устаревший браузер (например, IE < 9), в этом случае в дальнейшем мы укажем в качестве fallback встроенные в браузер шрифты.

После создания файла с правилами загрузки шрифтов нужно подключить его в приложении в файле nuxt.config.js в секции head:

{  head: {      link: [        {          as: 'style',          rel: 'stylesheet preload prefetch',          href: '/fonts/fonts.css',        },      ],    },}

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

Сразу добавим в директорию static favicon нашего приложения, который можно сгенерировать, воспользовавшись любым бесплатным онлайн-сервисом.

Теперь директория static выглядит так:

static/
-- fonts/
---- fonts.css
---- Rubik-Bold.woff2
---- Rubik-Bold.woff
---- Rubik-Medium.woff2
---- Rubik-Medium.woff
---- Rubik-Regular.woff2
---- Rubik-Regular.woff
-- favicon.ico


Переходим к следующему этапу.

Переиспользуемые стили


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

Cодержимое файла можно увидеть в репозитории.

Теперь мы должны подключить эти переменные в проект таким образом, чтобы они были доступны в любом нашем компоненте. В Nuxt для этой цели используется модуль @nuxtjs/style-resources.

Установим этот модуль:

npm i @nuxtjs/style-resources

И добавим в nuxt.config.js следующие строки:

{  modules: [    '@nuxtjs/style-resources',  ],  styleResources: {      scss: ['./assets/styles/variables.scss'],    },}

Отлично! В любом компоненте будут доступны переменные из этого файла.

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

Создадим в директории assets/styles поддиректорию global со следующими файлами:

1. typography.scss файл будет содержать все классы-помощники для текста, включая ссылки.
Обратите внимание, что эти классы-помощники меняют стили в зависимости от разрешения устройства пользователя: смартфона или ПК.

2. transitions.scss файл будет содержать глобальные стили анимаций, как для переходов между страницами, так и для анимаций внутри компонентов, если в будущем нам это понадобится.

3. other.scss файл будет содержать глобальные стили, которые пока не выделить в какую-то отдельную группу.

Класс .page будет использоваться как общий контейнер для всех компонентов на странице и будет формировать правильные отступы на странице.

Класс .section будет использован для обозначения границ логических блоков, а класс .content для ограничения ширины контента и его центрирования на странице.

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

4. index.scss общий файл, который будет использован как единая точка экспорта всех глобальных стилей.

Полное содержимое файлов можно увидеть на Github.

На данном этапе подключим эти глобальные стили, чтобы они стали доступны во всём приложении. Для этой задачи Nuxt предоставляет нам секцию css в файле nuxt.config.js:

{  css: ['~assets/styles/global'],}

Стоит сказать, что в дальнейшем при присваивании css-классов будет использована следующая логика:

1. Если у тега есть как классы-хелперы, так и локальные классы, то локальные классы будут напрямую добавлены к тегу, например, p.some-local-class, а классы-хелперы указаны в свойстве class, например, class=body3 medium.

2. Если у тега есть только классы-хелперы или только локальные классы, то они будут напрямую добавлены к тегу.

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

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

npm i reset-css

И подключим его в файле nuxt.config.js в уже знакомой нам секции css, которая теперь будет выглядеть так:

{  css: [    '~assets/styles/global',    'reset-css/reset.css',  ],}

Получилось? Если да, мы готовы переходить к следующему этапу!

Layouts


В Nuxt Layouts являются обёртками над страницами, которые позволяют переиспользовать между ними общие компоненты и реализовывать необходимую общую логику. Так как наше приложение предельно простое, то нам будет достаточно использовать layout по умолчанию default.vue.

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

Layouts в репозитории.

default.vue

Наш default.vue не будет иметь никакой логики и будет выглядеть следующим образом:

<template lang="pug">  div    nuxt  db-footer</template>

Здесь мы используем 2 компонента:

1. nuxt при сборке будет заменён на конкретную страницу, которую запросил пользователь.

2. db-footer это наш собственный компонент Footer (мы напишем его чуть позже), который будет автоматически добавлен на каждую страницу нашего приложения.

error.vue

По умолчанию при любой ошибке, возвращенной с сервера в статусе http, Nuxt делает редирект на layout/error.vue и передаёт через входной параметр с названием error объект, который содержит описание полученной ошибки.

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

<script lang="ts">  import Vue from 'vue'    type Error = {    statusCode: number    message: string  }    type ErrorText = {    title: string    subtitle: string  }    type ErrorTexts = {    [key: number]: ErrorText    default: ErrorText  }  export default Vue.extend({    name: 'ErrorPage',      props: {      error: {        type: Object as () => Error,        required: true,      },    },      data: () => ({      texts: {        404: {          title: '404. Page not found',          subtitle: 'Something went wrong, no such address exists',        },        default: {          title: 'Unknown error',          subtitle: 'Something went wrong, but we`ll try to figure out what`s wrong',        },      } as ErrorTexts,    }),    computed: {      errorText(): ErrorText {        const { statusCode } = this.error        return this.texts[statusCode] || this.texts.default      },    },  })  </script>

Что здесь происходит:

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

2. В объекте data мы создаём словарь, который будет содержать сообщения для всех ошибок, для которых мы хотим отображать уникальное сообщение и сообщение по умолчанию для всех остальных.

3. В вычисляемом свойстве errorText мы проверяем, есть ли в словаре полученная ошибка. Если ошибка есть, то возвращаем для неё сообщение. Если ошибки нет, возвращаем сообщение по умолчанию.

В этом случае наш шаблон будет выглядеть так:

<template lang="pug">  section.section    .content      .ep__container        section-header(          :title="errorText.title"          :subtitle="errorText.subtitle"        )        nuxt-link.ep__link(          class="primary"          to="/"        ) Home page  </template>

Обратим внимание, что здесь мы используем глобальные служебные классы .section и .content, которые создали ранее в файле assets/styles/global/other.scss. Они позволяют отображать контент по центру страницы.

Здесь используется компонент section-header, который ещё не создан, но в дальнейшем он будет являться универсальным компонентом для отображения заголовков. Мы реализуем его, когда будем говорить о компонентах.

Директория layouts выглядит так:

layouts/
-- default.vue
-- error.vue


Приступим к созданию компонентов.

Компоненты


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

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

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

Взглянем на секцию script этого компонента:

<script lang="ts">  import Vue from 'vue'  export default Vue.extend({    name: 'SectionHeader',    props: {      title: {        type: String,        required: true,      },      subtitle: {        type: String,        default: '',      },    },  })  </script>

Теперь посмотрим, как будет выглядеть шаблон:

<template lang="pug">  section.section    .content      h1.sh__title(        class="h1"      ) {{ title }}      p.sh__subtitle(        v-if="subtitle"        class="body2 regular"      ) {{ subtitle }}  </template>

Как мы видим, этот компонент является простой обёрткой для отображаемых данных и не содержит никакой логики.

LinkToHome

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

Этот компонент совсем крошечный, поэтому приведу весь его код сразу (без стилей):

<template lang="pug">  section.section    .content      nuxt-link.lth__link(        to="/"        class="primary"      )        img.lth__link-icon(          src="~/assets/icons/home.svg"          alt="icon-home"        )        | Home  </template>    <script lang="ts">  import Vue from 'vue'  export default Vue.extend({    name: 'LinkToHome',  })  </script> 

Обратите внимание, что мы запрашиваем иконку home.svg из директории assets/icons. Предварительно нужно создать данную директорию и добавить туда нужную иконку.

DbFooter

Компонент DbFooter очень прост. Он содержит copyright и ссылку для создания письма.
Требования понятны, давайте начнём реализацию с секции script:

<script lang="ts">  import Vue from 'vue'  export default Vue.extend({    name: 'DbFooter',    computed: {      copyright(): string {      const year = new Date().getUTCFullYear()      return ` ${year}  All rights reserved`    },    },  })  </script>

В DbFooter у нас есть всего одно вычисляемое свойство, которое возвращает текущий год, сконкатенированный с заданной строкой. Теперь взглянем на шаблон:

<template lang="pug">  section.section    .content      .footer        a.secondary(        href="mailto:example@mail.com?subject=Nuxt blog"      ) Contact us        p.footer__copyright(        class="body3 regular"      ) {{ copyright }}  </template>

При клике на ссылку Contact us мы будем открывать нативный почтовый клиент и сразу устанавливать тему сообщения. Это решение подойдёт для демо нашего приложения, но в реальной жизни более подходящим решением будет реализовать форму обратной связи для отправки сообщений напрямую с сайта.

PostCard

Карточка поста не несёт в себе никаких сложностей и является довольно простым компонентом.

<script lang="ts">  import Vue from 'vue'  import { Post } from '~/content/Post'  export default Vue.extend({    name: 'PostCard',    props: {      post: {        type: Object as () => Post,        required: true,      },    },    computed: {      pageUrl(): string {        return `/post/${this.post.id}`      },    },  })  </script>

В секции script мы определяем один входной параметр post, который будет содержать всю необходимую информацию о посте.

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

Шаблон будет выглядеть следующим образом:

<template lang="pug">  nuxt-link.pc(:to="pageUrl")    img.pc__img(      :src="getDynamicFile(post.img)"      :alt="`post-image-${post.id}`"    )    p.pc__title(class="body1 medium") {{ post.title }}    p.pc__subtitle(class="body3 regular") {{ post.desc }}  </template>

Обратите внимание, что корневым элементом шаблона является nuxt-link. Это сделано для того, чтобы у пользователя была возможность открыть пост в новом окне с помощью мышки.

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

PostList

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

Секция script для этого компонента:

<script lang="ts">  import Vue from 'vue'  import posts from '~/content/posts'  export default Vue.extend({    name: 'PostList',      data: () => ({      posts,    }),  })  </script>

Отметим, после импорта массива с постами мы добавляем их в объект data, чтобы в дальнейшем у шаблона был доступ к этим данным.

Сам шаблон выглядит так:

<template lang="pug">  section.section    .content      p.pl__count(class="body2 regular")        img.pl__count-icon(          src="~/assets/icons/list.svg"          alt="icon-list"        )        | Total {{ posts.length }} posts      .pl__items        post-card(          v-for="post in posts"          :key="post.id"          :post="post"        )  </template>

Чтобы всё корректно работало, не забудьте добавить иконку list.svg в директорию assets/icons.

PostFull

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

Для этого компонента нам потребуется модуль @nuxtjs/markdownit, который будет отвечать за преобразование md в html.

Установим его:

npm i @nuxtjs/markdownit

После этого добавим @nuxtjs/markdownit в секцию modules файла nuxt.config.js:

{  modules:  [    '@nuxtjs/markdownit',  ],}

Отлично! Начнем реализацию компонента. Как всегда, с секции script:

<script lang="ts">  import Vue from 'vue'  import { Post } from '~/content/Post'    export default Vue.extend({    name: 'PostFull',      props: {      post: {        type: Object as () => Post,        required: true,      },    },  })  </script>

В секции script мы определяем один входной параметр post, который будет содержать всю необходимую информацию о посте.

Переходим к шаблону:

<template lang="pug">  section.section    .content      img.pf__image(        :src="getDynamicFile(post.img)"        :alt="`post-image-${post.id}`"      )      .pf__md(v-html="getDynamicFile(post.file).default")  </template>

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

Я думаю, вы обратили внимание, что для рендеринга файла мы используем стандартный атрибут v-html, так как всю остальную работу за нас сделает @nuxtjs/markdownit. Невероятно просто!

Для доступа к кастомизации стилей нашего отрендеренного .md файла мы можем использовать селектор ::v-deep. Посмотрите на Github, как реализовано для этого компонента.

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

Страницы


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

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

Структура директории pages:

pages/
-- index.vue
-- post/
---- _id.vue


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

<template lang="pug">  .page    section-header(      title="Nuxt blog"      subtitle="The best blog you can find on the global internet"    )    post-list  </template>    <script lang="ts">  import Vue from 'vue'  export default Vue.extend({    name: 'HomePage',  })  </script>

Для установки правильных отступов мы использовали глобальный класс .page, который создали ранее в assets/styles/global/other.scss.

Отдельная страница поста будет выглядеть немного сложнее. Давайте посмотрим сначала на секцию script:

<script lang="ts">  import Vue from 'vue'  import { Post } from '~/content/Post'  import posts from '~/content/posts'export default Vue.extend({    validate({ params }) {      return /^\d+$/.test(params.id)    },      computed: {      currentId(): number {        return Number(this.$route.params.id)      },      currentPost(): Post | undefined {        return posts.find(({ id }) => id === this.currentId)      },    },  })  </script>

Мы видим метод validate. Этот метод отсутствует во Vue, его предоставляет нам Nuxt для валидации полученных от роутера параметров. Validate будет вызываться каждый раз при переходе к новому маршруту. В данном случае мы просто проверяем, что переданный нам id является числом. Если валидация не прошла, то пользователю будет возвращена страница ошибки error.vue.

Здесь реализованы 2 вычисляемых свойства. Рассмотрим подробнее, что они делают:

1. currentId это свойство возвращает нам текущий id поста (который был получен из параметров роутера), предварительно преобразовав его в number.

2. currentPost возвращает объект с информацией о выбранном посте из общего массива всех постов.

Кажется, с этим разобрались. Давайте взглянем на шаблон:

<template lang="pug">  .page  link-to-home    section-header(      :title="currentPost.title"    )    post-full(      :post="currentPost"    )</template>

Секция стилей для этой страницы так же, как и для главной страницы, отсутствует.
Код страниц на Github.

Деплой на Hostman


Ура! Наше приложение почти готово. Пришло время заняться его деплоем.

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

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

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

Сразу после этого автоматически запустится публикация и будет создан бесплатный домен в зоне *.hostman.site с установленным ssl сертификатом от Let's Encrypt.

С этого момента при каждом новом пуше в выбранную ветку (master по умолчанию) будет выполняться деплой новой версии приложения. Невероятно просто и удобно!

Заключение


Итак, что мы имеем:


Мы попробовали показать на практике, как работать с фреймворком Nuxt.js. Нам удалось создать простое приложение от начала и до конца, от создания UI-кита до организации деплоя.

Если вы выполнили все шаги, описанные в этой статье, вас можно поздравить с созданием своего первого приложения на Nuxt.js. Было сложно? Как вам работа с этим фреймворком? Если есть вопросы или пожелания, не стесняйтесь писать в комментариях.
Подробнее..

Экосистема JavaScript тренды в 2021 году. Всё ли так однозначно?

01.04.2021 12:23:08 | Автор: admin

В конце прошлого года на сайте State of JS 2020 было опубликовано исследование о состоянии экосистемы JavaScript в 2020 году с ретроспективой на предыдущие годы развития. Исследование основывалось на многочисленных опросах, в которых суммарно приняли участие более 23 тысяч человек из 137 стран мира.

Географическое распределение числа опрошенных.Географическое распределение числа опрошенных.

По результатам исследования можно сделать ряд прогнозов о том, какой будет экосистема JavaScript в 2021 году.

Языки, расширяющие возможности JavaScript

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

Тренды использования языка.Тренды использования языка.

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

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

Фреймворки

Наверно, многие помнят, когда в начале бурного развития фронтенд-экосистемы количество фреймворков росло, словно грибы после дождя. Часть из них уже канула в лету (Press F for Backbone.js, Marrionete.js, Prototype.js, [type anything].js), и за последние годы мы могли наблюдать стабилизацию позиций трёх основных конкурентов: React, Angular, Vue. И с каждым годом их доля присутствия на рынке только росла.

Тренды использования технологии.Тренды использования технологии.

Однако здесь не всё так однозначно. В 2019 году ворвался молодой Svelte, который за 2020 год в два раза увеличил свою долю использования среди разработчиков. И при этом в рейтингах проявления интереса и удовлетворённости от использования со стороны IT-сообщества Svelte занимает первое место. Фреймворк стал глотком свежего воздуха в подходе к созданию веб-приложений, и поэтому следует ожидать, что он будет наращивать своё присутствие в 2021 году всё больше и больше.

Тренды интереса к технологии.Тренды интереса к технологии.Тренды удовлетворённости от использования технологии.Тренды удовлетворённости от использования технологии.

Такжеинтересна позицияAngularкак одного из лучших в рейтинге использования.Несмотря на то, что им пользуется более половины опрошенных, интерес к изучению этого JavaScript-фреймворка плавно угасает.Разработчики едины во мнении, что Angular имеет высокий порог входа и применим в основном для разработки крупных и сложных проектов.Ожидается, что в 2021 году разработчиков будут привлекать фреймворки с меньшим порогом входа.

Главный лидер, React,по-прежнему занимает прочную позицию во фронтенд-сообществе, и это не изменится в 2021 году. Постоянное развитие этого инструмента в сочетании с некоторыми новаторскими идеями во фронтенде гарантирует, что это решение ориентировано на будущее.

Управление данными

Благодаря популярности React и его частому использованию совместно с Redux для управления состоянием приложения, последний на протяжении пяти лет исследований занимал первую позицию. И только в 2020 году его доля использования перестала расти, а удовлетворённость от использования стала резко падать. Возможно, это связано с гетерогенностью обвесов, которые применяются совместно с Redux и большим количеством шаблонного кода, который приходится писать при использовании этой технологии.

Тренды использования технологии.Тренды использования технологии.

Одновременно с этим GraphQL за последние годы медленно наращивал свои позиции. Инструментсейчас чрезвычайно популярен, становясь номером один в категориях удовлетворённости, интереса и осведомлённости.Легкость работы и отличная интеграция с бекендом становится ключом к успеху. И это даёт неплохие шансы для GraphQL на 2021 год в плане дальнейшей популяризации.

Тренды удовлетворённости от использования технологии.Тренды удовлетворённости от использования технологии.Тренды интереса к технологии.Тренды интереса к технологии.

Инструменты для тестирования

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

Mocha по-прежнему является достойной альтернативной. Но отсутствие явной привязанности к конкретному фреймворку смещает её на вторую позицию.

Jasmine является инструментом тестирования по умолчанию для проектов на Angular. И, возможно, спад его популярности связан с определенным спадом самого Angular.

Тренды использования технологии.Тренды использования технологии.

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

Тренды интереса к технологии.Тренды интереса к технологии.

Заключение

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

Подробнее..

Программа HolyJS нюансы DevTools, минусы GraphQL, инструменты a11y

01.04.2021 14:11:52 | Автор: admin


Осталось меньше месяца до конференции HolyJS (20-23 апреля, онлайн) пора рассказать, что именно там будет. Под катом описания докладов с разбивкой по тематическим блокам. А для начала несколько вопросов для затравки:


  • В чём недостатки GraphQL?
  • Зачем OCaml на фронтенде?
  • Чего вы не знаете о DevTools?
  • Как писать надёжные тесты для Vue?
  • Как сделать свой DSL-язык легко и непринуждённо?
  • Как добиться на дешёвом устройстве плавности дорогого?
  • Как отобразить 100500 метрик и не сойти с ума?
  • Как принести в JS типы ещё радикальнее, чем в TypeScript?

Ответы на всё это и многое другое в докладах.


Оглавление


Инструменты
Производительность
Фреймворки
Языки
Микрофронтенды
Основы
Визуал
Заключение



Инструменты


Всё, что вам нужно DevTools, Виталий Фридман


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


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


Чем хороша тема: DevTools использует каждый, но там столько подводных камней, что без помощи не разобраться! Возможно, сверхсеньоры и не найдут чего-то прорывного, но даже им будет интересно увидеть flow работы Виталия.




Сделать сайт доступным за 60 секунд, Глафира Жур и Денис Бирюк


Все вокруг говорят о доступности: она нужна, важна и должна быть добавлена в проект на самом раннем этапе. Но никто не говорит, как рядовому фронтенд-разработчику, путающему a11y и 11ty, разобраться в вопросе. Какой пакет подключить в Webpack, чтобы проблемы решались на этапе сборки? Где в консоли найти отчёт об ошибках a11y? И как проверить результат своей работы после деплоя?


Чем хороши спикеры: Два спикера два мира. Глафира эксперт по интерфейсам, Денис любит прикручивать автоматизацию к фронтенд-проектам.


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


Кому будет полезно: Знаете про доступность, но не практикуете, потому что ну это же сложно? Тут расскажут, что конкретно добавить в ваш проект, чтобы пользователям было удобнее.




WebXR в реальной жизни, Роман Пономарев


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


Чем хорош спикер: У Романа есть практический опыт в теме. Некоторые зрители также могут знать его по Фронтенд Юности.


Чем хороша тема: Даже в доковидные времена Virtual/Augmented Reality всё больше проникала в нашу жизнь, а уже теперь и подавно. Тем временем спецификация WebXR взрослеет и позволяет делать всё больше.




Make your authentication flow perfect, Anton Nemtsev


Есть очень много сайтов, где нужно зайти, ввести email, указать телефон, подтвердить это все через ввод кода из SMS. Авторизация и регистрация постоянно балансирует между удобно и секьюрно, причем чаще в сторону секьюрно.
Как сделать так, чтобы было и удобно, и секьюрно в докладе Антона Немцева.


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


Кому будет полезно: Если вы работали с формами авторизации и регистрации, либо вам интересно, как можно сделать их удобными




Воркшоп. GitLab + CI/CD + JavaScript = любовь, Виталий Слободин


Стать передовиком производства можно при помощи настроенных процессов. Это знают все. Но как настроить процессы вопрос сложный, лучше всего начинать с машин, потому что они податливее, чем люди. Первым шагом для настройки непрерывного цикла разработки может стать CI/CD. Цель Виталия показать вам трюки и рецепты для создания CI/CD, чтобы все инженеры были довольны и счастливы.


Чем хорош спикер: Ранее на HolyJS Виталий рассказывал про headless browsers, и доклад приняли отлично: глубоко знать тему ему помог личный опыт разработки PhantomJS. А теперь он работает в GitLab так что снова поговорит о том, к чему сам причастен.


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




Serverless и Edge Computing на практике, Алексей Тактаров


Две основные проблемы при построении веб-сервисов повысить пропускную способность и снизить время ответа от сервера. Это проблемы, которые обычно решают с помощью наращивания мощностей, горизонтального масштабирования и разнесения дата-центров по континентам. Такой подход надежен, но дорог и требует обслуживания. Лямбда-вычисления и Smart CDN относительно новые и дешевые технологии, призванные решить обе задачи.


В докладе Алексей рассмотрит практическое применение лямбд от Vercel и Cloudflare: проектирование фронтенд-микросервисов, их авторизацию, работу с CDN и особенности кеширования на пограничных (edge) серверах. Будут затронуты архитектурные приемы для построения легко масштабируемых сервисов на примере задач: рендеринг PDF/DOCX-документов, генерация above-the-fold CSS для страниц на лету в Cloudflare Workers, отрисовка OG-картинок.


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


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




Гиперавтоматизированный пайплайн, или Почему матрица должна победить, Алексей Золотых


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


Но настало время машин. Алексей расскажет о том, как быстро захватить власть в проекте на JavaScript или TypeScript. Как унижать и угнетать разработчиков так, чтобы они при этом оставались эффективными и счастливыми. И как не допустить появления Нео!


Чем хорош спикер: Пишет на JavaScript c 2007 года, давно участвует в HolyJS в качестве участника программного комитета.


Чем хороша тема: В наше девопсовое время вопросы автоматизации пайплайнов всё актуальнее.




Воркшоп: Знакомство с MobX, Назим Гафаров


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


В этом воркшопе мы напишем приложение на React и TypeScript, используя MobX для управления состоянием. Начнем с фундаментальных понятий, таких как наблюдаемые объекты, действия и реакции. Вместе с основами глубже рассмотрим варианты решения проблемы окрашивания асинхронных функций. В конце рассмотрим полезные архитектурные паттерны вроде композиции классов и способов внедрения модели.



Производительность


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


Вот реальная инженерная задача: есть SberBox за 3 000 рублей и SberPortal за 30 000. Производительность разная, но написанный на веб-технологиях интерфейс должен работать одинаково плавно. Павел Ремизов расскажет, как решали эту задачу, как ускоряли анимации и как использовали React, как кэшировали статику на девайсах и уменьшали размеры бандла, как отлаживали и измеряли производительность.


Чем хорош спикер: Имеет очень хорошую экспертизу в производительности и в реальных аспектах ее оценки


Чем хороша тема: Реальные устройства, юзкейсы, пользователи никаких коней в вакууме! Происходят реальные эксперименты на в боевых условиях, и их результаты не всегда совпадают с общепринятым мнением.




Оптимизация производительности высоконагруженного поиска на стороне фронтенда, Даниил Гоник, Ян Штефанец


Даниил Гоник и Ян Штефанец работают над редактором наподобие Google Docs.


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


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


Кому будет полезно: Если вы реализовывали подсветку элементов на сайте по тексту, если вы работали когда-либо с WYSIWYG




Стабильность React Native-приложения с круглосуточным up time, Евгений Гейманен


Евгений Гейманен расскажет поучительную историю про проблемы производительности одного React Native-приложения. Из нее вы узнаете:


  • Как оптимизировать перерисовку виртуального React DOM;
  • Как правильно мутировать redux-store, коннектить к нему компоненты и описывать селекторы;
  • Как находить проблемные места в Android-версии React Native-приложения;
  • Какими инструментами стоит обзавестись для того, чтобы держать руку на пульсе приложения и начать думать как приложение.

Чем хорош спикер: Евгений Lead Developer в Bolt и ему есть что рассказать как с точки зрения технологий, так и с точки зрения истории


Чем хороша тема: Мониторинг это одна боль. Мониторинг приложений установленных на куче девайсов другая. Это реальный опыт решения некоторого количества проблем производительности на примере React Native приложения




Производительность в полевых условиях, Александр Шушунов


Вы уже прочитали все статьи Эдди Османи. Выучили, как расшифровываются аббревиатуры RAIL, PRPL, LCP и прочие. Свежий перформанс-гайд Фридмана второй месяц висит в открытой вкладке. Но как все это применить к текущему проекту? Как улучшить производительность здесь и сейчас? Александр не знает и не будет давать общие советы зато расскажет, что сделал он. И надеется, что его опыт борьбы за секунды и байты покажется вам интересным.


Чем хороша тема: Сугубо практический взгляд на производительность от человека, повидавшего виды. Будет отличным дополнением к докладу Виталия Фридмана. Причем зачастую хороших результатов можно достичь малой кровью


Кому будет полезно: Если вам интересна тема производительности клиентских приложений. Особенно, если вы никогда специально не работали в этом направлении, и хотите посмотреть на свой текущий проект под другим углом




Фреймворки


А нужен ли нам GraphQL?, Павел Черторогов


Это далеко не первый доклад Павла про GraphQL, так что он уже глубоко в теме, но у него нет слепого фанатизма. Он понимает, что GraphQL это инструмент, а не серебряная пуля. И каждый инструмент удобен под свою категорию задач. Молотком шуруп не закрутишь, отверткой гвоздь не забьешь это практически нереально.


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




How we built our custom React SSR Engine, Erzhan Torokulov


Эржан Торокулов расскажет, как несколько лет назад в Toptal сделали React Rendering Engine: он уже тогда умел делать ряд вещей, которые теперь знакомы по инструментам вроде Gatsby и Next.js. Компания по-прежнему использует этот инструмент для своего сайта и продолжает его развивать.


Чем хорош спикер: Эржан имеет глубокий опыт не просто применения существующих решений, но и написания больших инструментов для компании.


Чем хороша тема: Никого не удивить сайтом на React, Vue или даже Svelte, и доклады про React стали очередными. Индустрия перестала строить велосипеды и догадалась положить рельсы. Но иногда хочется большего и это такой случай.




Как в GitLab @vue/test-utils обновляли, Илья Климов


За последний год Илья дважды обновлял @vue/test-utils в GitLab и оба раза это заняло огромное количество времени. Для того, чтобы это сделать, ему пришлось отправить более десятка pull-request'ов во @vue/test-utils, обсудить в RFC внутри GitLab возможность создания собственного форка, глубоко разобраться в механизмах реактивности Vue и нещадно воевать за качество кода и тестов как в GitLab, так и в самом @vue/test-utils.


Теперь Илья со зрителями HolyJS будет разбираться в следующих вопросах:


  • где спрятана сложность в тестировании подобных систем;
  • какие ошибки были допущены разработчиками @vue/test-utils и можно ли было их избежать;
  • как магия реактивности усложняет построение надежной системы и как с этим бороться;
  • как писать надежные тесты для Vue.

Доклад будет интересен не только тем, кто использует Vue.js в повседневной разработке, но и всем, кто верит в unit-тестирование как ключ к управляемости любого проекта.





Языки


Свой язык с поддержкой sourcemaps за полчаса, Дмитрий Карловский


DSL (Предметно-ориентированный язык) техника для высокоуровневого описания чего-либо (конфигурации, UI, баз-данных). С помощью DSL можно решить множество проблем бизнеса (и множество создать). Дмитрий поговорит об этой теме, раскроет нюансы sourcemaps и расскажет, как в два счёта реализовать свой язык с их поддержкой. А также как легко и просто трансформировать AST, даже если вы никогда с ним не работали и ничего не знаете про sourcemaps.


Чем хорош спикер: Является автором собственного инструмента для работы с DSL.


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




Зачем OCaml на фронтенде, Дмитрий Коваленко


Возможно, вы слышали о таких технологиях, как ReasonML/Resсript и bucklescript. Всё это OCaml на фронтенде. Но как всё это работает? Как OCaml компилируется в JS? Почему это круто? Всё это вы узнаете в данном докладе.


Чем хорош спикер: Дмитрий один из немногих людей, использующих OCaml на практике.


Чем хороша тема: Про Ocaml много кто слышал, но мало кто представляет, что это такое и чем он замечателен. Если вы хотите разнообразить свои знания мира языков транспилируемых в JS и узнать (а возможно и подсесть на) что-то новое вам сюда.




Strict Types in JavaScript, Виктор Вершанский


Говорят, что JavaScript динамически типизированный. А если хочешь другой типизации, надо использовать что-то вроде TypeScript, который не заставляет писать без ошибок типов в runtime.


Но что, если это не совсем так? Что, если можно у чистого JS заставить его runtime работать с типами по всей строгости и получать связанные с этим ошибки? Если вы думаете, что это невозможно нам есть о чём поговорить.


Чем хороша тема: Такое решение еще никто не представлял, это самый эксклюзивный доклад из всех возможных.


Кому будет полезно: Всем, кто:


  • задумывался про проблему строгой типизации в рантайме
  • устал ловить TypeError в рантайме
  • хочет поломать мозг неординарными решениями



How to outsmart time: Building futuristic JavaScript applications using Temporal, Ujjwal Sharma


Почти 25 лет JS-разработчики страдали каждый раз, когда нужно было что-то сделать со временем и датой. Было сломано много костылей, сорвано много подорожников, и прожжено много стульев. И вот, наконец, будущее наступило: появилось долгожданное Temporal API, которое позволяет работать с временем и датой, как все давно мечтали. Уджвал Шарма покажет, как это всё работает.


Чем хорош спикер: Данный спикер помогает разрабатывать этот стандарт (ТС39 делегат) и внедряет его в браузеры.


Чем хороша тема: Поскольку текущий Date API не слишком хорош, разработчики подключают внешние библиотеки для работы с датами вроде date-fns, moment, luxon и тд. Temporal предложение для самого языка, это будущий стандарт работы с датами.





Микрофронтенды


Микросервисы, которые делаем мы, Олег Сметанин


Олег представит обзор способов проектирования микросервисов, API, использования шаблонов и паттернов взаимодействия; DevOps и тестирования на основе обобщения опыта проектирования и аудирования приложений с микросервисной архитектурой для разных индустрий: финансы, ритейл, ресурсы. Вы узнаете, как использовать TypeScript/NestJS для реализации микросервисов под NodeJS для cloud-native-приложений и как применять TypeScript/React для фронта.


Чем хорош спикер: У спикера большой опыт в построении больших систем с высокой нагрузкой.


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




Микрофронтенды на модулях и веб-компонентах, Юрий Караджов


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


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


Чем хороша тема: Доклад про лёгкий и прозрачный способ внедрения микрофронтендов. Анализ преимуществ и недостатков. Практическая применимость и подводные камни для популярных фреймворков.




One logic to rule them all, from clot to wire, Владимир Минкин


Презентация концепции Strings API с библиотекой Wire для создания веб и мобильных приложений, которые повторно используют бизнес-логику в языке Dart. Цель этого доклада: продвинуть идеи создания программных систем, состоящих из плагинов отдельных компонентов и блоков, которые можно тестировать индивидуально, визуально и в модульных тестах.


Разделение ответственности это не новая идея и того же можно добиться с помощью уже существующих инструментов. Разница с Wire заключается в том, что он предоставляет простой и семантически красивый API с четким разделением задач на уровне API-библиотеки: общение в одном месте, данные в другом. В докладе будет уделено внимание архитектуре систем визуального ПО и тому, что всё это MVC, а также архитектуре Flux.


Чем хорош спикер: Владимир обладает хорошим опытов разработки приложений в ООП стиле. Автор библиотек. Имеет опыт боевой опыт разработки на Dart и Haxe. Он может помочь зрителю посмотреть на разработку под другим углом.


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





Основы


Напомни через минуту, или Как считать время в браузере, Никита Дубко


Пришла как-то к разработчику Мефодию задача: показывать через заданное пользователем время попап с напоминалкой, что время прошло. Написал Мефодий setTimeout, запушил, тикет закрыл. А пользователь так и не дождался своей напоминалки.


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


Чем хорош спикер: Обладает огромным опытом выступлений и предоставил очень интересную тему


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




Оптимизация синхронной асинхронности, Дмитрий Махнев


Дмитрий Махнёв хочет показать проблему обманчивости простоты async/await на реальном кейсе и приблизительные пути решения и профиты от этого.
Что вас ждет в докладе:
очевидное нахождение проблемы синхронной асинхронности в реальной задаче (индексе сайта);
удивительно неправильная попытка решения;
героическое ускорение примерно на порядок без переписывания на Rust;
неловкая ситуация с unhandledRejection, пролетающей сквозь try/catch;
пара полезных абстракций.


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


Чем хороша тема: async/await, которого боялись несколько лет назад, стал чем-то обыденным, повседневным, повсеместным. Раньше у нас был callback hell, then hell. Нет, await hell не выглядит чем-то страшным для читабельности кода. Он в определённых ситуациях критически влияет на производительность. Дима на собственном примере покажет, как правильно работать с async-await.




Machine Learning and JavaScript. Unleashing the power of sentiment analysis, Artur Nizamutdinov


Если вы работали с ML на Python и хотите перенести часть опыта в браузер (или не имели дела с МL, но очень хотите попробовать) приходите на доклад Артура Низамутдинова. Он покажет на живом примере, для каких задач можно использовать машинное обучение в браузере, и разберёт дообучение моделей.


Чем хорош спикер: У Артура сильная экспертиза. А еще он сильно переработал доклад конкретно для нашей аудитории. Так что тут будут не просто знания, а знания, поданные именно для слушателей конференции


Чем хороша тема: ML сейчас почти так же моден, как JavaScript. И этот доклад совмещает в себе эти два мира, показывая на практическом примере, для каких задач мы можем использовать ML в браузере.





Визуальное


Анализ больших географически распределенных данных через визуализацию на карте, Никита Швыряев


Доклад основан на опыте построения трёх систем для бизнес-анализа географически распределенных данных. В основном доклад будет построен вокруг использования библиотеки Mapbox GL JS. Рассмотрим востребованную функциональность, осветим проблемы и решения во многих задачах (от загрузки данных до кластеризации). Целевая аудитория JS-разработчики, которые работают или планируют работать с визуализацией на карте.


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


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




Как отобразить 100500 метрик распределенной системы и не сойти с ума, Андрей Гончаров


Представьте распределённый кластер из 100 машин. Каждая собирает по 100 разных метрик. А теперь представьте график со всеми метриками сразу. Получилось? Вот и у Hazelcast не получилось. Поговорим, что делать, когда хочется показать много и сразу в условиях ограниченного экранного пространства и конечного числа глаз пользователей. Вспомним, что такое интеграл. Вместе посмотрим, чем он может быть лучше арифметического среднего и медианы.


Чем хорош спикер: Андрей работает в компании, которая оперирует огромным количеством машин и метрик на них. Задача правильного и, что самое главное, доверительного отображения метрик первостепенная.


Чем хороша тема: Любой бизнес любит метрики продуктовые, технические. Почти любой разработчик знает что такое медиана, 75, 90, 95 перцентиль. Правда, не всегда понятно пора бежать чинить, мы горим. Андрей расскажет, как можно обрабатывать метрики, чтобы они лучше отображали существующую картину.




Браузерный игровой движок как pet-проект, Михаил Реммеле


Михаил расскажет о своем опыте работы над pet-проектом. Игровой движок штука достаточно сложная, это огромный источник всевозможных задач и очень нетипичный опыт. Писать его можно бесконечно долго. Интересующие темы могут меняться, но в таком проекте всегда можно подобрать подходящую для исследований задачу. Технологии в данном случае: JavaScript, WebGL, Webpack.


Чем хорош спикер: Опыт работы в геймдеве, крайне редкий среди спикеров JS-конференций. Опыт разработки собственного игрового движка под браузер.


Чем хороша тема: Не распространённый, а возможно и редкий опыт в мире фронтенда. В докладе будет рассказано как автор с нуля написал свой игровой движок и с какими архитектурными сложностями столкнулся.





Заключение


Доклады это хорошо. Но напомним, что HolyJS не сводится только к тому, чтобы сидеть и слушать:


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


Напоминаем, HolyJS пройдет с 20 по 23 апреля в онлайне. Вся информация и билеты на сайте.

Подробнее..

Dapp. Vue.js ethers.js

10.04.2021 12:12:31 | Автор: admin

Введение

В этой статье я попытаюсь максимально кратко и информативно пройтись по всем пунктам создания создания полноценного Децентрализованного приложения в сети Ethereum используя JavaScript фреймворк - Vue для создания веб-приложения и библиотеку ethers.js для общения со смарт контрактом. Также будут рассмотрены некоторые моменты о том, как установить и подключить кошелек, как задеплоить контракт в сеть используя truffle и др.

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

Что уже нужно знать перед тем как начать:

  • Неплохие знания js, в частности Vue.js

  • Понимание принципов работы Blockchain (смарт-контрактов)

  • Базовые знания языка пр. Solidity

Установка кошелька

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

Когда расширение установится - вас попросят создать кошелек или импортировать если такой имеется. После создания аккаунта у вас сгенерируется мнемоническая фраза. НИКОГДА, НИКОМУ ее не разглашайте. И воаля - у нас есть готовый адрес кошелька)

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

Здесь вы можете нафармить себе немного тестовых эфиров, чего нам хватит для работы.

Контракт

Пока не на долго забудем о нашем кошельке и приступим к написанию смарт-контракта. Для примера я буду использовать уже готовый контракт. Более подробно о нем вы можете ознакомиться с ним в README (он довольно простой). Основная идея - это купля-продажа. Продавец выставляет свой товар - покупатель ставит свою цену и делает предложение, продавец в свою очередь принимает предложение. Много нюансов упущено, но для для примера сойдет. Попробовать попользоваться контрактом вы можете в бесплатной IDE Remix.

Развертывание контракта

Для этой задачи мы будем использовать Truffle.js. Для его установки на вашей машине должен быть уже установлен node.js. Дальше открываем любой вам удобный редактор и в консоли через npm устанавливаем следующие пакеты (в моем случаи глобально): npm install -g truffle и npm install -g @truffle/hdwallet-provider После чего создайте папку, где будет хранится ваш проект и инициализируйте Truffle проект в нем командой truffle init.

В папке с проектом с начала обратим наше внимание на файл truffle-config.js. В нем хранятся конфигурации по развертыванию контрактов. Но прежде всего нам нужно будет зарегистрироваться в Infura и создать новый проект перейдя по вкладке Ethereum -> Create New Project соединяет интерфейс пользователя (UI) dApps с внешним смарт-контрактом на блокчейне Ethereum. Провайдер Infura может обрабатывать подписание транзакции, а также подключение к сети Ethereum без необходимости синхронизировать ноду и это именно то, что нам нужно. Дальше выбрав нужный созданный проект -> Settings -> Endpoints выбираем нашу сеть Ropsten и копируем указанный адрес.

Теперь можно перейти, непосредственно, к настройке truffle-config.js. Во-первых, создайте в корневой папке проекта файл .secret в котором будет хранится наша мнемоническая фраза. Лучше всего использовать dotenv, но для ознакомления оставим так, как предлагает truffle. Во-вторых, раскомментируйте следующие строки:

const HDWalletProvider = require('@truffle/hdwallet-provider');const fs = require('fs');const mnemonic = fs.readFileSync(".secret").toString().trim();

и также

ropsten: {    provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`),    network_id: 3,       // Ropsten's id    gas: 5500000,        // Ropsten has a lower block limit than mainnet    confirmations: 2,    // # of confs to wait between deployments. (default: 0)    timeoutBlocks: 200,  // # of blocks before a deployment times out  (minimum/default: 50)    skipDryRun: true     // Skip dry run before migrations? (default: false for public nets )    },

И вместо второго параметра в HDWalletProvider поместите скопированный адрес.

В-третьих, создайте новый файл формата .sol в папке contracts, и поместите туда весь код контракта. В папке migrations создайте js файл и поместите туда следующий код:

const SimpleMarketplace = artifacts.require("SimpleMarketplace");module.exports = function (deployer) {    deployer.deploy(SimpleMarketplace, 'description', 1000);};

Обратите внимание, что при создании нового файла в migrations название файла должно начинаться с цифры следующей по порядку от файла 1_initial_migrations.js, то есть 2.

Так как у нас контракт содержит конструктор с параметрами - мы методе deploy вторым и третьим аргументами ставим именно их: description и price в нашем случае. Если у вас контракт без конструктора или с конструктором, но без параметром, то оставляем просто переменную SimpleMarketplace.

Сохраняем и в терминале с проектом пробуем скомпилировать контракт введя команду truffle compile. Важно: Если у вас выкидывает ошибку, что не найден пакет @truffle/hdwallet-provider,(Error: Cannot find module '@truffle/hdwallet-provider'),то попробуйте установить этот пакет в текущую директорию командой npm install @truffle/hdwallet-provider. без опции-g и скомпилировать снова. После вывода в консоль Compiled successfully using... можно пробовать деплоить.

Для развертывания контракта в сети в консоль введите truffle migrate --network ropsten --reset. Где ropsten - это название сети, что мы указали в файле truffle-config.js, а --reset опция, которая просто передплоивает контракт наново если вы уже выполняли подобные действия (при первом разе можно и без нее).Важно: Если у вас выбрасывает подобную ошибку: var e = new Error('ETIMEDOUT'), то попробуйте в truffle-config.js в HDWalletProvider вставить другой адрес, что предлагает Infura - wss://ropsten.infura.io/ws/v3/YOUR-PROJECT-ID.

Если процесс прошел без ошибок и в Etherscan перейдя по своему адресу вы можете наблюдать транзакцию, о создании контракта - поздравляю, контракт находится в сети blockchain и можно идти дальше.

Создание Vue.js приложения

И так, если все выше перечисленные шаги вам удалось произвести, то можно начинать писать веб-приложения для нашего контракта. Установку vue/cli и создание проекта я упущу, так как это детально описано в официальной документации Vue.js. Установка. Создание проекта.

После того как ваше приложение было создано можно начать писать саму логику. Для начала давайте удалим из все assets (в нем будет хранится логотип Vue.js, который нам не нужен), удалим компонент HelloWorld.vue из components и в файле App.vue уберем все лишнее (импорт компонента HelloWorld.vue, саму инициализацию компонента в объекте components и оставим чистый шаблон с <div id="app">внутри).

<template>  <div id="app">  </div></template><script>export default {  name: 'App',  components: {  }}</script><style>#app {  font-family: Avenir, Helvetica, Arial, sans-serif;  -webkit-font-smoothing: antialiased;  -moz-osx-font-smoothing: grayscale;  text-align: center;  color: #2c3e50;  margin-top: 60px;}</style>

Запустив проект командой npm run serve можете проверить, что все работает и нет никаких ошибок, как и в прочем, ничего вообще пока что.

Чтобы взаимодействовать с таким простым контрактом нам понадобится всего лишь один новый компонент - это главная страница на которой мы будем делать UI для нашего смарт-контракта. В папке components создайте новый компонент и назовите его, к примеру, Marketplace.vue. Так же создайте папку core и в ней файл core.js и в проекте, в котором мы развертывали контракт, перейдите в папку ./build/contracts и скопируйте от туда JSON - файл с названием вашего файла контракта (или же запомните путь к нему). Этот файл - это как-бы инструкция, которая говорит вам как можно обратится к контракту. Не обязательно создавать все эти папки и называть так файлы, но это просто считается хорошим тоном. В дальнейшем хорошо структурированный проект будет проще поддерживать. Перед тем как продолжить - установите в пакет npm install ethers.

В файле core.js проимпортируйте установленную библиотеку, укажите путь в JSON - файлу и возьмите оттуда адрес нашего контракта:

const { ethers } = require('ethers')const ContractArtifact = require('./SimpleMarketplace.json')const CONTRACT_ADDRESS = ContractArtifact.networks['3'].address

Дальше создадим переменную ABI, которая должна быть массивом строк и занесем туда все необходимые нам функции контракта, что мы будем использовать (Вы можете просто скопировать строку из контракта и занести сюда. Если в контракте просто public поле, для него можете тоже отдельно написать public view returns функцию и ethers не должен на это ругаться):

const ABI = [    'function InstanceOwner () public view returns(address)',    'function Description () public view returns(string)',    'function AskingPrice () public view returns(int)',    'function InstanceBuyer () public view returns(address)',    'function OfferPrice () public view returns(int)',    'function MakeOffer(int offerPrice) public',    'function Reject() public',    'function AcceptOffer() public']

После чего нам нужно определить ряд переменных:

let provider = new ethers.providers.Web3Provider(window.ethereum)//С помощью провайдера мы подключаемся к сети Blockcainlet readOnlyContract = new ethers.Contract(CONTRACT_ADDRESS, ABI, provider)//Дает нам возможность читать view методы контрактаlet signer = provider.getSigner()//Нужен для подтверждения транзакцийlet contract = new ethers.Contract(CONTRACT_ADDRESS, ABI, signer)let contractSigner = contract.connect(signer)//Дают возможность выполнять транзакции

Дальше определим методы которые будем вызывать во Vue компонентах:

export default {    async getDescription() {        const description = await readOnlyContract.Description()        return {description: description}    }}

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

const { ethers } = require('ethers')const ContractArtifact = require('./SimpleMarketplace.json')const CONTRACT_ADDRESS = ContractArtifact.networks['3'].addressconst ABI = [    'function InstanceOwner () public view returns(address)',    'function Description () public view returns(string)',    'function AskingPrice () public view returns(int)',    'function InstanceBuyer () public view returns(address)',    'function OfferPrice () public view returns(int)',    'function MakeOffer(int offerPrice) public',    'function Reject() public',    'function AcceptOffer() public']let provider = new ethers.providers.Web3Provider(window.ethereum)let readOnlyContract = new ethers.Contract(CONTRACT_ADDRESS, ABI, provider)let signer = provider.getSigner()let contract = new ethers.Contract(CONTRACT_ADDRESS, ABI, signer)let contractSigner = contract.connect(signer)export default {    async getInstanceOwner() {        const instanceOwner = await readOnlyContract.InstanceOwner()        return {instanceOwner: instanceOwner}    },    async getDescription() {        const description = await readOnlyContract.Description()        return {description: description}    },    async getAskingPrice() {        const askingPrice = await readOnlyContract.AskingPrice()        return {askingPrice: askingPrice}    },    async getInstanceBuyer() {        const instanceBuyer = await readOnlyContract.InstanceBuyer()        return {instanceBuyer: instanceBuyer}    },    async getOfferPrice() {        const offerPrice = await readOnlyContract.OfferPrice()        return {offerPrice: offerPrice}    },    async makeOffer(offerPrice) {        const txResponse = await contractSigner.MakeOffer(offerPrice, {gasLimit: 300000})        const txReceipt = await txResponse.wait()        return {transaction: txReceipt.transactionHash}    },    async reject() {        const txResponse = await contractSigner.Reject({gasLimit: 300000})        const txReceipt = await txResponse.wait()        return {transaction: txReceipt.transactionHash}    },    async acceptOffer() {        const txResponse = await contractSigner.AcceptOffer({gasLimit: 300000})        const txReceipt = await txResponse.wait()        return {transaction: txReceipt.transactionHash}    }}

Дальше я в файле App.vue в mounted методе вызываю подключение кошелька и в свойство $root заношу проимпортированный заранее наш файл core.js:

const core = require('./core/core')/*Какой-то другой код*/mounted() {    window.ethereum.request({ method: 'eth_requestAccounts' })    this.$root.core = core.default  }

Дальше под каждое поле, что вы хотите читать с контракта создаете поле в в data и метод details в объекте methods :

data() {    return {      instanceOwner: '',      description: '',      askingPrice: '',      instanceBuyer: '',      offerPrice: ''    }  },  methods: {    async details() {      this.instanceOwner = (await this.$root.core.getInstanceOwner()).instanceOwner      this.description = (await this.$root.core.getDescription()).description      this.askingPrice = (await this.$root.core.getAskingPrice()).askingPrice      this.instanceBuyer = (await this.$root.core.getInstanceBuyer()).instanceBuyer      this.offerPrice = (await this.$root.core.getOfferPrice()).offerPrice    }  },

Тем временем в разметке выводим необходимые нам поля:

<button v-on:click="details">Get details</button><h3>Instance owner: {{ instanceOwner }}</h3><h3>Description: {{ description }}</h3><h3>Asking price: {{ askingPrice }}</h3><h3>Instance buyer: {{ instanceBuyer }}</h3><h3>Offer price: {{ offerPrice }}</h3>

Код компонента Marketplace.vue можете проверить в моем репозитории, дабы не захламлять статью лишним кодом.

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

И после этого при нажатии на кнопку Get details мы получим данные, которые вводили при развертывании контракта.

А так выглядит вывод если произвести транзакцию:

Заключение

Это моя первая статья. Буду рад каким-то вопросам и даже критике, так как я сам пока не профи во всем этом.

Ссылка на контракт, где вы можете проверить транзакции.

Подробнее..
Категории: Javascript , Vuejs , Solidity , Blockchain , Dapp

Микрофронтенды разделяй и властвуй

14.04.2021 16:05:26 | Автор: admin


Всем привет! Меня зовут Аня, я фронтенд-разработчик в Delivery Club. Хочу рассказать про использование микрофронтендов. Поговорим о том, что же это за зверь такой микрофронтенд, почему мы решили использовать этот подход в своих проектах и с какими проблемами столкнулись при внедрении.

Для чего они нам понадобились


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

  • Сложно жить с монолитом. Быстро писать код мешали устаревание подходов, фреймворков и зависимостей, а также связанность логики.
  • Совместная разработка. Долго решать конфликты, противоречивость изменений.
  • Проблема с тестированием. Когда прилетали баги от других задач и команд, было непонятно, в чём причины.
  • Развёртывание было зависимое и блокирующее.

Поэтому нам нужна была возможность:

  • использовать любые инструменты и технологии, снижая риск устаревания;
  • разрабатывать и деплоить независимо;
  • изолировать реализации.

Устройство проекта


Для начала расскажу, как сейчас устроен наш проект.

  • Основное старое приложение на AngularJS, к которому мы планируем подключать новые микроприложения.
  • Dashboard-приложение на Angular 6, подключенное через iframe (но оно со временем разрослось и от описанных выше проблем не избавило). К нему подключаются приложения, здесь хранятся старые страницы.
  • Приложения на VueJS, которые используют самописную библиотеку компонентов на VueJS.





Мы поняли, что ограничения тормозят развитие проекта. Поэтому сформулировали возможные пути:

  • Разделение приложения на страницы по маршрутам.
  • iframe.
  • Микрофронтенды.

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

Что такое микрофронтенды


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


Сейчас наш проект только переходит на микрофронтенды, наша команда стала первопроходцем. И мы столкнулись с рядом проблем.

Проблемы внедрения микрофронтендов


1. Ещё один iframe? Может, уже хватит?


К великому разочарованию, наше dashboard-приложение открывается внутри iframe (исторически так сложилось), и нам очень не хотелось делать один iframe внутри другого. Однако это был запасной план. Если вдруг что-то пойдёт не так, то просто заворачиваем микрофронтенд в iframe и готово рабочее приложение.

Мы видели несколько недостатков:

  • Неудобная навигация. Каждый раз для редиректа на внешнюю ссылку нужно использовать window.postMessage.
  • Сложно верстать в iframe.

К счастью, нам удалось этого всего этого избежать, и микрофрентенд мы подключили как веб-компонент с shadow dom: <review-ui-app></review-ui-app>. Такое решение выгодно с точки зрения изоляции кода и стилей. Веб-компонент мы сделали с помощью модифицированного vue-web-component-wrapper. Почитать подробнее о нём можно здесь.

Что мы сделали:

  1. Написали скрипт, который добавляет ссылку на сборку микрофронтенда в разделе head страницы при переходе на соответствующий маршрут.
  2. Добавили конфигурацию для микрофронтенда.
  3. Добавили в window.customElements тег review-ui-app.
  4. Подключили review-ui-app в dashboard-приложение.

Так мы сразу убили несколько зайцев. Во-первых, приложение представляет собой кастомный тег, как мы привыкли компонент. Во-вторых, кастомный тег принимает данные как свойства, следит за ними и выбрасывает события, которые слушает обёртка. Ну и в-третьих, избавились от iframe внутри iframe :)

2. А где стили?


Ещё одна неприятная проблема нас ждала дальше. Компоненты в микрофронтенде работали, только вот стили не отображались. Мы придумали несколько решений:

  • Первое: импортировать все стили в один файл и передать его во vue-wrapper (но это слишком топорно и пришлось бы добавлять вручную каждый новый файл со стилями).
  • Второе: подключить стили с помощью CSS-модулей. Для этого пришлось подкрутить webcomponents-loader.js, чтобы он вшивал собранный CSS в shadow dom. Но это лучше, чем вручную добавлять новые CSS-файлы :)

3. Теперь про иконки забыли!


Затем мы подключили внутреннюю библиотеку, но в ней не отображались SVG.
С иконками нам пришлось повозиться подольше. Для их подключения нам пришлось сделать несколько итераций:

  1. Сначала мы попытались подрубить спрайт так же, как и стили, через appendChild. Они подключились, но всё равно не отображались.
  2. Затем мы решили подключить через sprite.mount(this.shadowRoot). Добавили в вебпаке в svg-sprite-loader опцию spriteModule: path.resolve(__dirname, './src/renderers/sprite.js). Внутри sprite.js экспортировали BrowserSprite, и иконки начали отображаться! Мы, счастливые, подумали, что победили, но не тут-то было. Да, иконки отображались, и мы даже выкатились с этой версией в прод. Но потом нашли один неприятный баг: иконки пропадали, если походить по вкладкам dashboard-приложения.
  3. Наконец, во vue-wrapper мы подключили DcIconComponent (библиотечный компонент, позволяющий работать с библиотечными иконками) и в нём подключили иконки из нашего проекта. Получили отображение без багов :)

4. Без авторизации никуда!


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

  • Токен с авторизацией передаём с помощью свойств веб-компонента.
  • С помощью AuthRequestInterceptor подключаем токен-запросы для API.
  • Используем токен, пока он не протухнет. После протухания ловим ошибку 401 и кидаем в dashboard-приложение событие обнови токен, пожалуйста (ошибка обрабатывается в AuthResponseInterceptor).
  • Dashboard-приложение обновляет токен. Следим за его изменением внутри main-сервиса, и когда токен обновился, заворачиваем его в промис и подписываемся на обновления токена в AuthResponseInterceptor.
  • Дождавшись обновления ещё раз повторяем упавший запрос, но уже с новым токеном.

5. Нас волнуют зависимости


Проблема выглядела так: с каждым подключённым микрофронтом тянется бандл. И сколько микрофронтов подключено, столько и бандлов подтянуто. Чтобы избежать этого, мы применили общие зависимости. Вот как мы это реализовали:

  • В микрофронтенд-приложении указываем в webpack.config.prod.js в разделе externals те зависимости, которые хотим вынести:

    module.exports = {externals: {vue: Vue},
    

    Здесь мы указываем, что под именем Vue в window можно будет найти зависимость vue.
  • В рамках оболочки (в нашем случаем в dashboard-приложении) выполняем npm install vue (и другие npm install-зависимости).
  • В dashboard-приложении импортируем все зависимости:

    import Vue from vue(window as any).Vue = Vue;
    
  • Получаем удовольствие.

6. Разные окружения


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

Решили мы это следующим образом:

  1. Добавили в микрофронтенд файл, в котором определяем конфигурацию для приложения в runtime браузера. Также добавили в Docker системный пакет, который предоставляет команду envsubst. Она подставляет значения в env.js, который тянет микрофронтенд-приложение, и эти переменные пишутся в window['${APP_NAME}_envConfig'].
  2. Добавили переменные окружения отдельно для прода и отдельно для тестового окружения.

Так мы решили несколько проблем:

  • настроили локальные моки (раньше приходилось вручную включать их локально);
  • настроили тестовое окружение без дополнительных ручных переключений на прод и обратно.

Выводы


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

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

VueUse обязательная библиотека для Vue 3

27.04.2021 22:20:31 | Автор: admin

Для тех, кто незнаком с этой библиотекой, советую попробовать, так она может де-факто стать стандартом для использования в проектах на Vue 3, как стала, например, библиотека lodash для почти любых проектов на js.

Остальные наверное уже успели заценить весь обширный функционал, который она предоставляет. Некоторые уже использовали ее на Vue 2, но далеко не все новые функции поддерживают старую версию. Арсенал библиотеки впечатляет, тут и простые утилиты вроде клика вне элемента, и различные интеграции с Firebase, Axios, Cookies, QR, локальным хранилищем, браузером, RxJS, анимации, геолокации, расширения для стандартных Vue-хуков, медиа-плеер и многое другое. Среди спонсоров отмечен сам Эван Ю, что как бы намекает. Библиотека регулярно получает обновления, баги закрываются, а сообщество растет. В общем у нее есть все для успеха.

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

onClickOutside клики вне элемента

С установкой библиотеки вы справитесь, я уверен, поэтому перейдем сразу к интересным вещам. Для разогрева рассмотрим простой хук, который отслеживает клики вне заданного элемента onClickOutside. Существует множество пакетов, которые предоставляют данный функционал, не считая бесчисленные велосипеды, которые писал наверное каждый. Обычно его реализуют через добавление пользовательской Vue-директивы к нужному элементу, например v-clickOutside, а вот использование хука необычно.

Я использовал этот хук в своей тудушке, в компоненте ToDoItem.vue:

<template>    <li ref="todoItem">      <input type="checkbox" />      <span        v-if="!editable"        @click="editable = !editable"      >        {{ todo.text ? todo.text : "Click to edit Todo" }}      </span>      <input        v-else        type="text"        :value="todo.text"        @keyup.enter="editable = !editable"      />    </li></template><script lang="ts">  import { defineComponent, PropType, ref } from "vue"  import ToDo from "@/models/ToDoModel"  import { onClickOutside } from "@vueuse/core"  export default defineComponent({    name: "TodoItem",    props: {      todo: {        type: Object as PropType<ToDo>,        required: true      }    },    setup() {      const todoItem = ref(null)      const editable = ref(false)      onClickOutside(todoItem, () => {        editable.value = false      })      return { todoItem, editable }    }  })</script>

Я удалил лишний код, чтобы не отвлекал, но все еще компонент достаточно большой. Обратите внимание на код, который находится внутри хука setup, сначала мы создаем пустую ссылку todoItem, которую вешаем на нужный элемент в шаблоне, а потом передаем первым параметром в хук onClickOutside, а вторым параметром коллбэк с нужными нам действиями. При клике на тег span, он заменится на тег input, а если кликнуть вне тега li с атрибутом ref="todoItem", то input сменится тегом span.

useStorage реактивное локальное хранилище

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

// @/store/index.tsimport { createStore } from 'vuex'import Note from '@/models/NoteModel'import { useStorage } from '@vueuse/core'const localStorageNotes: unknown = useStorage('my-notes', [] as Note[])export default createStore({  state: {    notes: localStorageNotes as Note[]  },  mutations: {                  addNote(state) {      state.notes.push(note)    },   // mutations  },  actions: {    // actions  },  getters: {    // getters  },  strict: true})

Первым параметром функция useStorage принимает ключ, под которым она будет сохранять ваши данные, а вторым их начальное значение. Полученную переменную localStorageNotes передаем в state.notes. Массив notes можно использовать как обычно, например добавлять новую запись в мутациях. Теперь записи будут сохраняться даже после перегрузки страницы и самого браузера.

При написании этого кода TypeScript начал ругаться на отсутствие типа у переменной localStorageNotes. Этого следовала ожидать, так как эта переменная создается с помощью ref и не создана для использования вне хука setup. Я не нашел другого решения, кроме как присвоить ей значение any или unknown. Код работает, но выглядит не очень. Если кто знает лучшее решение, подскажите в комментариях. Будем надеяться, что авторы библиотеки создадут лучшую интеграцию с Vuex, где эта функциональность так необходима.

Для сравнения также полезно ознакомиться с примером использования useStorage от авторов. Разница в том, что в setup работать с реактивным хранилищем нужно не напрямую, а через его свойство value. В html-шаблоне же, все как обычно.\

useRefHistory история изменений

useRefHistory хук который позволит записывать историю изменений данных и предоставляет undo/redo функциональность. Я использовал ее для создания кнопок Undo и Redo на странице создания и редактирования записи со списком дел. Так как переменная currentNote, которая отвечает за хранение редактируемой записи, тоже находится во Vuex-хранилище. Я так же использовал ее именно там и так же получил ошибку типизации. Рассмотрим код получше:

import { createStore } from 'vuex'import Note from '@/models/NoteModel'import ToDo from "@/models/ToDoModel"import { useRefHistory } from '@vueuse/core'import { ref } from 'vue'const note: any = ref({  title: "",  todos: [] as ToDo[]})const { history, undo, redo, canUndo, canRedo, clear } = useRefHistory(note, {  deep: true})export default createStore({  state: {    currentNote: note as Note,    currentId: 0  },  mutations: {    // mutations    clearHistory() {      clear()    },    undoChanges() {      undo()    },    redoChanges() {      redo()    }  },  actions: {      },  getters: {    canUndo() {      return canUndo.value    },    canRedo() {      return canRedo.value    }  },  strict: true})

Создаем реактивную переменную с помощью ref, передаем ее в хук useRefHistory, в параметрах хука обозначаем deep: true, для вложенных объектов. С помощью деструктурирующего присваивания из useRefHistory получаем history, undo, redo, canUndo,canRedo и clear. Функции undo и redo, необходимо применять только в мутациях, чтобы Vuex не ругался. Свойства canUndo и canRedo можно передать через геттеры, которые потом повесить на атрибуты disabled в кнопках. clear необходима для очистки истории после окончания редактирования записей. Хук useManualRefHistory делает практически тоже самое, но сохранение в историю происходит только по вызову команды commit().


Я рассказал всего про 3 функции из большого арсенала инструментов VueUse для разработки на Vue 3. Для более глубокого изучения советую посетить сайт этой замечательной библиотеки. Документация все еще далека от совершенства, но она регулярно обновляется как и сама библиотека.

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

Подробнее..

Как я написал браузерный 3D FPS шутер на Three.js, Vue и Blender

07.05.2021 02:13:27 | Автор: admin
Стартовый экран игрыСтартовый экран игры

Мотивация

На пути каждого коммерческого разработчика (не только кодеров, но, знаю, у дизайнеров, например, также) рано или поздно встречаются топкие-болотистые участки, унылые мрачные места, блуждая по которым можно вообще забрести в мертвую пустыню профессионального выгорания и/или даже к психотерапевту на прием за таблетками. Работодатели-бизнес очевидно задействует ваши наиболее развитые скилы, выжимая по максимуму, стек большинства вакансий оккупирован одними и теми же энтерпрайз-инструментами, кажется, не для всех случаев самыми удачными, удобными и интересными, и вы понимаете что вам придется именно усугублять разгребать тонну такого легаси Часто отношения в команде складываются для вас не лучшим образом, и вы не получаете настоящего понимания и отдачи, драйва от коллег Умение тащить себя по-мюнхаузеновски за волосы, снова влюбляться в технологии, увлекаться чем-то новым [вообще и/или для себя, может быть смежной областью], имхо, не просто является важным качеством профессионала, но, на самом деле, помогает разработчику выжить в капитализме, оставаясь не только внешне востребованным, конкурентоспособным с наступающей на пятки молодежи, но, прежде всего, давая энергию и движение изнутри. Иногда приходится слышать что-нибудь вроде: а вот мой бывший говорил, что если бы можно было не кодить, он бы не кодил!. Да и нынешняя молодежь осознала что в сегодняшней ситуации честно и нормально зарабатывать можно только в айти, и уже стоят толпою на пороге HR-отдела... Не знаю, мне нравилось кодить с детства, а кодить хочется что-нибудь если не полезное, то хотя бы интересное. Короче, я далеко не геймер, но в моей жизни было несколько коротких периодов когда я позорно загамывал. Да само увлечение компьютерами в детстве началось, конечно же, с игр. Я помню как в девяностые в город завезли Спектрумы. Есть тогда было часто практически нечего, но отец все-таки взял последние деньги из заначки, пошел, отстоял невиданно огромную очередь и приобрел нам с братом нашу первую чудо-машину. Мы подключали его через шнур с разъемами СГ-5 к черно-белому телевизору Рекорд, картинка тряслась и моргала, игры нужно было терпеливо загружать в оперативную память со старенького кассетного магнитофона [до сих пор слышу ядовитые звуки загрузки], часто переживая неудачи... Несмотря на то что ранние программисты и дизайнеры умудрялись помещать с помощью своего кода в 48 килобайт оперативной памяти целые миры с потрясающим геймплеем, мне быстро надоело играть и я увлекся программированием на Бейсике)), рисовал спрайтовую графику (и векторная трехмерная тогда тоже уже была, мы даже купили сложную книжку), писал простую музыку в редакторе... Так вот, некоторое время назад мне опять все надоело, была пандемийная зима и на велике не покататься, рок-группа не репетировала Я почитал форумы и установил себе несколько более-менее свежих популярных игр, сделанных на Unity или Unreal Engine, очевидно. Мне нравятся РПГ-открытые миры-выживалки, вот это все... После работы я стал каждый вечер погружаться в виртуальные миры и рубиться-качаться, но хватило меня ненадолго. Игры все похожи по механикам, однообразный геймплей размазан по небольшому сюжету на кучу похожих заданий с бесконечными боями Но, самое смешное, это реально безбожно лагает в важных механиках. Лагают коммерческие продукты которые продают за деньги А любой баг, имхо, это сильное разочарование он мгновенно выносит из виртуальной среды, цифровой сказки в реальный мир Конечно, отличная графика, очень круто нарисовано. Но, утрируя, я понял что все эти поделки на энтерпрайзных движках, по сути даже не кодят. Их собирают менеджеры и дизайнеры, просто играясь с цветом кубиков, но сами кубики, при этом практически не меняются... Вообщем, когда стало совсем скучно, я подумал что а я ведь тоже так могу, да прямо в браузере на богомерзком непредназначенным для экономии памяти серьезного программирования джаваскрипте. Решил наконец полностью соответствовать тому что все время с умным видом повторяю сыну: уметь делать игры, намного интереснее чем в них играть. Одним словом, я задался целью написать свой кастомный браузерный FPS-шутер на открытых технологиях.

Итак, на данный момент, первый результат по этой долгоиграющей таски на самого себя можно тестить: http://robot-game.ru/

Стек и архитектура

Вполне может быть, что я не вкурсе чего-то (ммм на ум приходит что-нибудь вроде quakejs и WebAssembly), но, с основной технологией было, походу, особо без вариантов. Библиотека Three.js давно привлекала мое внимание. Кроме того, в реальной коммерческой практике, несколько раз, но уже приходилось сталкиваться с заказами на разработку с ее использованием. На ней я сделал собственно саму игру.

Очевидно, что нужно что-то вокруг для простого интерфейса пользователя: шкал, текстовых сообщений, инструкций, контролов настроек, вот этого всего. Я решил поленился, не усложнять себе жизнь и использовать любимый фреймворк Vue 2, хотя, надо было, конечно, писать на свежем, похожем по дизайну и еще более прогрессивном по сути молниеносном Svelte. Но так как хорошенько разобраться предстояло, прежде всего, с Three, думаю, это было правильное решение. Хорошо знакомый и предсказуемый, лаконичный, изящный, удобный и эффективный Vue, позволил практически не тратить время на внешний пользовательский интерфейс.

Когда-то давно я работал дизайнером на винде и достаточно бойко рисовал 2D в Иллюстраторе, но навыков 3D у меня никаких не было. А вот в процессе создания шутера пришлось пойти, скачать и установить одним кликом на свой нынешний Linux Blender. Я быстро научился рисовать с помощью примитивов мир, отдельные объекты, и даже научился делать UV-развертки на них. Но! В целях простоты, скорости работы и оптимизации объема ассетов в моей нынешней реализации не используются текстурные развертки. Я просто подгружаю чистые легковесные бинарные glTF: .glb-файлы и натягиваю на них всего несколько вариантов нескольких текстур уже в джаваскрипте. Это приводит к тому что текстуры на объектах искажаются в разных плоскостях, но на основном бетоне для стен, смотрится даже прикольно, такой разный, рваный ритм. Кроме того, сейчас персонажи не анимируются пока не было времени изучить скелетную анимацию. Одной из основных целей написания этой статьи является желание найти (по знакомым не получилось) специалиста который поможет довести проект до красоты (очень хочется) и согласится добавить совсем немного анимаций на мои .glb (об условиях договоримся). Тогда враги, будут погружаться в виде glTF со встраиванием: .gltf-файлов со встроенными текстурами и анимациями. Сейчас уже есть два вида врагов: ползающие-прыгающие наземные дроны-пауки и их летающая версия. Первых нужно научить шевелить лапками при движении и подбирать их в прыжке, а вторым добавить вращение лопастей.

Модель дрона-паука в BlenderМодель дрона-паука в Blender

Для того чтобы игру нельзя было тупо-легко прочитить через браузерное хранилище я добавил простенький бэкенд на Express с облачной MongoDB. Он хранит в базе данные о прогрессе пользователя по токену, который на фронте записывается в хранилище. Хотелось сделать не просто FPS-шутер, а привнести в геймплей элементы РПГ. Например, в нынешней реализации мир делиться на пять больших уровней-локаций между которыми можно перемещаться через перезагрузку. При желании локации можно быстро дорисовывать из уже имеющихся и добавлять в игру, указывая только двери входа и выхода, стартовую и конечную координату, хорошее направление камеры для них (при переходе живого персонажа через дверь текущее направление сохраняется-переносится). На каждом уровне есть только одна формальная цель найти и подобрать пропуск к двери на следующий уровень. Пропуски не теряются при проигрыше на локации (только при выборе перехода на стартовый уровень после выигрыша на последнем пятом). А вот враги и полезные предметы цветы и бутылки при переходе между локациями, проигрыше или перезагрузке страницы пока выставляются заново согласно основной glb-модели одновременно и схеме, и визуальной клетке локации об этом дальше. И тут вот первое важное про архитектуру: мой фронтенд это совсем примитивное SPA. Vue, например, ни для чего не нужен роутер. Вероятно, я получу негативную реакцию некоторых продвинутых читателей, после того, как сообщу что потратил кучу времени для того чтобы попробовать организовать перезагрузку-очистку сцены внутри системы и пока с самым провальным результатом. Вот к такой спорной мысли я пришел в процессе своих экспериментов: самый эффективный, простой, даже, в этой ситуации, правильный и при этом, конечно же, топорный подход, это нативный форс-релоад после того как мы сохраняем или обнуляем данные пользователя на бэкенде:

window.location.reload(true);

А потом просто дадада считываем их обратно )) и строим всю сцену заново, с чистого листа, так сказать. Тут, конечно, можно было бы улучшить прокидывать пользователя через хранилище вместо того чтобы ожидать разрешения запроса, но это не критично, в данном случае. Небольшое количество оптимизированных текстур (меньше полтора мегабайта сейчас), сильно компрессированного аудио (MP3, понятно: 44100Гц 16 бит, но с сильным сжатием 128 кбит/с меньше полтора мегабайта все вместе сейчас), основная модель-локация весящая около 100Кб и модели отдельных объектов каждая еще меньше... Я добился того что переход между локациями полная перезагрузка мира занимает вполне приемлемое время, судя по записи перфомансов примерно две с чем-то, три секунды. И это, кажется, меньше чем во всех шовных открытых мирах от энтерпрайза которые я видел. Продвинуто бесшовный я тоже один нашел и поиграл, но он лагал хуже всех, и когда сюжет наконец двинулся с мертвой точки вдруг перестали работать сейвы; тут я уже забил

Все использующиеся в игре текстурыВсе использующиеся в игре текстурыПерфомансПерфоманс

Хочется сразу сказать что техлиды и сеньоры с менторским тоном и заоблачной экспертизой в микробенчмаркинге в комментариях только приветствуются. Это же вообще самое забавное и интересное на Хабре когда лиды с сеньорами начинают рубиться в комментариях за стоимость операций в джаваскрипте и то, чей микробенчмаркинг заоблачнее! Остается только надеятся на то, что когда вы будете размазывать мой форсрелоад как дешёвое и сердитое средство изменения сцены вы обязательно продемонстрируете ваши работающие примеры в которых сцена Three с большим количеством разнообразных объектов на ней очищается и заново инициализируется через свои внутренние методы (например, без перезагрузки текстур и прочих ассетов, аудио). Я же не говорю что это невозможно, это очевидно дорого. Намного дороже чем просто сделать форсрелоад. Понятно что хороший проект это прежде всего кодовая база которая может и должна легко развиваться. Но невозможно прикрутить все фичи сразу, а использование дешевого релоада сейчас никак не блокирует добавление более сложного функционала в будущем. Да и кроме дешевизны более простой подход и идеологически привлекателен. Я убежден что хороший код это простой и понятный код, хороший подход простой подход, точно так же как и интерфейс который они предоставляют. Простое решение лучше сложного, особенно если мы только начинаем строить что-то.

Для того чтобы избежать лишних сложностей в моей реализации сцена практически неизменна. Она разворачивается, запускается и дальше функционирует в некотором постоянном виде [порождая и уничтожая только выстрелы и взрывы] пока не происходит переход в другую локацию (или проигрыш на этой). Конкретнее: cейчас я нигде кроме удаления не подлежащих внешнему учету выстрелов и взрывов не использую scene.remove(object.mesh) например при сборе героем полезных предметов, делая вместо этого:

// встроенное свойство на Object3D в Threeobject.mesh.visible = false;// кастомный флаг кастомного массива объектовobject.isPicked = true;

Поэтому мы, например, можем даже использовать свойство id: number mesh`ей вместо uuid: string для учета и идентификации объектов. Так как все подлежащие учету объекты всегда остаются на сцене мы можем быть уверены что Three не поменяет айдишники, сдвинув нумерацию под коробкой при удалении элемента (но если вы хотите все-таки удалять что-то такое просто опирайтесь на uuid при работе с этим).

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

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

. /public // статические ресурсы   /audio // аудио     ...   /images // изображения     /favicons // дополнительные фавиконки для браузеров       ...     /modals // картинки для информационных панелей       /level1 // для уровня 1         ...       ...     /models       /Levels         /level0 // модель-схема Песочницы (скрытый уровень 0 - тестовая арена)           Scene.glb         ...       /Objects          Element.glb          ...     /textures        texture1.jpg        ...   favicon.ico // основная фавиконка 16 на 16   index.html // статичный индекс   manifest.json // файл манифеста   start.jpg // картинка для репозитория ) /src   /assets // ассеты сорцов     optical.png // у меня один такой )))   /components // компоненты, миксины и модули     /Layout // компоненты и миксины UI-обертки над игрой       Component1.vue // копонент 1       mixin1.js // миксин 1       ...     /Three // сама игра        /Modules // готовые полезные модули из библиотеки          ...        /Scene           /Enemies // модули врагов             Enemy1.js             ...           /Weapon // модули оружия             Explosions.js // взрывы             HeroWeapon.js // оружие персонажа             Shots.js // выстрелы врагов           /World // модули различных элементов мира             Element1.js             ...           Atmosphere.js // модуль с общими для всех уровней объектами (общий свет, небо, звук ветра) и проверками взаимодействия между другими модулями           AudioBus.js // аудио-шина           Enemies.js // модуль всех врагов           EventsBus.js // шина событий           Hero.js // модуль персонажа           Scene.vue // основной компонент игры           World.js // мир   /store // хранилище Vuex     ...   /styles // стилевая база препроцессора SCSS     ...   /utils // набор утилитарных js-модулей для различных функциональностей     api.js // интерфейс для связи с бэкендом     constants.js // вся конфигурация игры и тексты-переводы     i18n.js // конфигурация переводчика     screen-helper.js // модуль "экранный помощник"     storage.js // модуль для взаимодействия с браузерным хранилищем     utilities.js // набор полезных функций-атомов   App.vue // "главный" компонент   main.js // эндпоинт сорцов Vue ... // все остальное на верхнем уровне проекта, как обычно: конфиги, gitignore, README.md и прочее

То как я работаю со стилями и оформлением компонент в UI-обертке подробно описано в моем первом масштабном тексте для сообщества. Целью данной статьи не является рассмотрение отдельных разрозненных особенностей перечисленных выше технологий и инструментов. Хочется рассказать об опыте, проблемах и найденных возможных путях их решениях которые возникли в результате практической попытки совместить данные технологии именно для решения конкретной задачи создания кастомного браузерного шутера.

Сейчас игра в спокойном состоянии когда потревоженных врагов нет или совсем мало, на компьютере с поддержкой GPU выдает практически коммерческие 60FPS в Google Chrome (ну или Yandex Bro). В Firefox игра запускается, но показатель производительности не менее чем в 2-3 раза ниже. А когда начинается мясо, появляется много потревоженных врагов, выстрелов и взрывов в Лисе процесс начинает лагать и может вообще повиснуть. Моя экспертиза в микробенчмаркинге сейчас пока не позволяет с умным видом рассуждать о причинах этой разницы. Будем считать что дело в более слабой поддержке WebGL и вычислительных способностях, что-то такое))...

Легенда

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

Земля, далекое будущее. Люди давным-давно перебили друг-друга в ядерных войнах, выясняя кто правый, кто левый, кто белый, а кто красный и прочее. На не затронутых бомбардировками атоллах в Тихом Океане размножилось несколько рас человекоподобных роботов. Например, более человекоподобные, имитирующие органику, двуполость и личные отношения Собутыльники, которые перерабатывают животных и растительность в жизненную силу и спецэффекты. Внутри них, по тонким крепким трубкам, течет специальный сброженный органический микс, схожий с человеческим вином, приводя их в движение. Или более машиноподбные однополые Кибер-Танцоры, проповедующие медитативный Дзинь-Нойз. На почве гендерных и религиозных разногласий между культурами, конечно же, понеслась жестокая война.

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

Робот-Собутыльник приходит в себя на полу пыточной камеры тюрьмы Однополых... Баки пусты... Его мучители, видимо, решили что он уже не жилец, и оставили подыхать... На стене висит портрет легендарного Последнего Президента идеологического предтечи и кумира Танцоров человека, когда-то развязавшего последнюю в истории человечества войну

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

В игре пока нет самих главных врагов, только их дроны (нужен специалист по скелетной анимации отзовись!), но уже присутствуют специфические объекты и отдельная механика позволяющая глубже раскрыть генезис Собутыльников, историю их борьбы против демократической диктатуры Танцоров. Это информационные панели которые включаются когда герой находится поблизости и важно в одном помещении с ними. Ниже об этом будет подробнее, когда будем разбирать постройку мира и взаимодействие модулей.

ДашбордДашборд

Если подойти к панели и нажать E открывается модаль с исторической справкой:

Рассказ о будущем внутриРассказ о будущем внутри

Это задел для дальнейшего развития в сторону РПГ. Например на таких дашбордах несложно организовать торговый ларек, в котором можно было бы менять поверженный и подобранный металлолом на полезные цветы и бутылки.

Геймплей

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

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

Цветы и бутылкиЦветы и бутылки

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

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

Уровни сложностиУровни сложности

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

  • Разное оружие. Виномет потеряет прицел, но станет работать как пулемет-автомат не нужно будет все время нажимать на левую кнопку мыши. С прицелом будет снайперская винтовка стреляющая стальными снарядами (которые, например, можно делать из собранных разрушенных врагов тут бы пригодился уже упомянутый торговый ларек на дашбордах).

  • Еще один вид бутылок с крепышом часовые мины: установил быстро отбегаешь. Будут полезны для разрушения Танков с огромным здоровьем или крупных скоплений любых врагов.

  • Новые типы врагов. Танки медленные, но очень живучие и с убойным выстрелом. Стационарные дроны-пушки умеющие стрелять не в горизонтальной плоскости навесом как делают дроны сейчас, а под разными углами и двойными зарядами. Рядовые бойцы Танцоры Роботы-Курицы мой барабанщик почему-то их именно так видит. В идеале они высаживаются как спезназ, приземляясь на челноке в центр третьей локации когда герой на нее заходит. В пятой локации может появиться босс: Робот-Блогер Финальный с ракетницей

  • Трубочных и двуполых Собутыльников нарисовать сложно, но в идеале было бы рассадить их по камерам Централа четвертой локации.

  • Можно добавить 2D-карту с врагами (внизу и по центру экрана)

Планов полно, но без скелетной анимации они бессмысленны, конечно

Но хватит лирики, перейдем к техническим решениям и собственно коду.

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

Особенный кайф от написания кастомной игры в том, что после того как вы доставили новые фичи или любые изменения в код вам просто необходимо расслабиться и их честно искренне протестировать. Ручками. Сделать несколько каток, по любому. Тесты тут никак и ничем не помогут, даже, убежден, наоборот будут мешать прогрессу, особенно если вы не работаете по заранее известному плану, а постоянно экспериментируете. Браузерная игра на джаваскрипт это в принципе превосходный пример того, когда статическая типизация или разработка через тестирование будут только мешать добиться действительно качественного результата. (А на чем тут необходимо проверять типы, господа сеньоры? Я до сих пор в замешательстве от React c CSS Modules и просто Flow, а не TS даже в котором авторы маниакально проверяли что каждый, еще и передаваемый по цепочке компонент, класс модулей для оформления !!! это string А тут что будем маниакально типизировать, вектора?). И даже сам Роберт Мартин в Идеальном программисте делает несколько пассажей на тему бессмысленности TDD, когда говорит о рисках при разработке GUI. В моей игре можно сказать что и нет практически ничего кроме тонны двумерного и трехмерного GUI, ну и логики для него. Любая ошибка либо вызовет исключение, либо неправильное поведение во вьюхе и геймплее, которое может быть очень быстро обнаружено с помощью визуальной проверки, но очень сомнительно что вообще способно быть покрыто тестом.

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

Все настройки настройки и значения влияющие на геймплей и дизайн (константа DESIGN), а также весь текстовый контент-переводы у меня сосредоточены в constants.js.

Контрол

На сайте библиотеки Three представлено большое количество полезных примеров с демо-стендами, самых разных реализаций, функциональностей которые стоит изучить и по возможности к месту использовать. Я отталкивался в своих исследованиях, прежде всего, вот от этого примера. Это правильный, мягкий инерционный контрол от первого лица который математически обсчитывает столкновения с клеткой-миром gld-моделью с помощью октодерева. Проверять столкновения можно для капсулы (для героя или врагов) или обычных сферы Sphere и луча Ray от Three. Этого в принципе достаточно для чтобы сделать FPS-игру: сделать так чтобы герой и враги не сталкивались с миром и между собой, выстрелы взрывались при попадании в другие объекты и тд.

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

// Controls// In First Person...

Но! Тут нюанс браузеры обязательно оставляют путь для панического отступления пользователю и резервируют клавишу Esc для того чтобы пользователь всегда мог разлочить указатель. Это касается нашего UI/UX в игре необходима клавиша P ставящая мир на паузу. Когда указатель залочен то бишь запущен игровой процесс нажатие на Esc, как уже сказано вызовет паузу. Но если мы попытаемся добавить обработку отпускания по 27ому коду даже только для режима паузы, все равно очень быстро увидим в консоли:

ОшибкаОшибка

Поэтому: забудьте про Esc. Пауза по клавише P. Есть еще одно ограничение и проблема связанная с созданием хорошего FPS-контрола: оружие. Я так понял что в энтерпрайзных реализациях руки-оружие это отдельный независимый план наложенный поверх мира. С Three, насколько я понимаю, сделать так не получится. Поэтому мой пока единственный в арсенале грозный виномет с оптическим прицелом это объект сцены который приделан к контролу. Я копирую вектор направления камеры на него. Но около зенита и надира в результате его начинает штормить он не может однозначно определить позицию. При взгляде совсем под ноги я его просто скрываю, а вот стрелять наверх нужно. Что делать с этим небольшим и не особо заметным багом я пока не придумал.

Оптический прицел винометаОптический прицел винометаВыстрел вверхВыстрел вверх

Пытаясь сделать скоростной задорный шутер на Three мы можем сразу забыть о тенях или дополнительных источниках освещения, особенно движущихся. Да, я пытался запилить качающиеся на ветру лампы для особенного мрачняка и криповости, движущиеся тени от них. Нет никак нельзя даже статичные точечные источники света сильно просаживают производительность (а нам еще врагов гонять). По поводу света я пришел к простому компромиссу: чтобы картинка не выглядела совсем сухо и скучно приделать мощный фонарик к контролу, герою. Фонарик можно выключать клавиша T.

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

Сцена

Основной компонент Scene.vue предоставляет:

  • всю стандартную кухню Three: Renderer, Scene и ее туман, Camera и Audio listener в ней, Controls

  • набор утилитарных переменных для использования в анимационных циклах низовых модулей

  • переменные для хранения коллекций примитивных дополнительных объектов превдоmesh`ей по которым работает кастинг

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

  • обрабатывает большинство (кроме тех, что удобно ловить в логике героя) событий клавиатуры, мыши и так далее

  • инициализирует Аудиошину, Шину Событий и Мир

  • анимирует Шину Событий, Героя и Мир

  • в наблюдателях значений важных геттеров добавляет игровой логики

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

Стандартный модуль героя, врагов, предмета или специфического объекта вроде двери или информационной панели в общем виде выглядит так:

import * as Three from 'three';import { DESIGN } from '@/utils/constants';function Module() {  let variable; // локальная переменная - когда очень удобна или необходима при инициализации или во всей логике    // ...  // Инициализация  this.init = (    scope,    texture1,    material1,    // ...  ) => {    // variable = ...    // ...  };  // Функция анимационного цикла для этого модуля - опционально (предметы, например, не нужно анимировать)  this.animate = (scope) => {    // А вот тут и в остальной логике стараемся использовать уже только переменные Scene.vue:    scope.moduleObjectsSore.filter(object => object.mode === DESIGN.ENEMIES.mode.active).forEach((object) => {      // scope.number = ...      // scope.direction = new Three.Vector3(...);      // variable = ... - так, конечно, тоже можно, главное не let variableNew;      // ...    });  };}export default Module;

Стор

Хранилище Vuex поделено на 3 простых модуля. layout.js отвечает за основные параметры игрового процесса: паузы-геймоверы и тд, взаимодействует с API-бекенда. В hero.js большое количество полей и их геттеров, но всего два экшена/мутации. Этот модуль позволяет в максимально унифицированной форме распространять изменения значений отдельных параметров, шкал, флагов на герое с помощью setScale или может пакетно установить эти значения через setUser.

Третий модуль совсем примитивный preloader.js и целиком состоит из однотипных boolean-полей с false по дефолту. Пока его поле isGameLoaded единственное в состоянии модуля с геттером с false не получает true при запуске или перезагрузке приложения пользователь будет видеть лоадер. Каждое из остальных полей обозначает подгрузку определенного ассета: текстуры, модели, аудио или постройку определенного типа объектов.

Если нам нужно подгрузить, например, текстуру песка:

import * as Three from 'three';import { loaderDispatchHelper } from '@/utils/utilities';function Module() {  this.init = (    scope,    // ...  ) => {    const sandTexture = new Three.TextureLoader().load(      './images/textures/sand.jpg',      () => {        scope.render(); // нужно вызвать рендер если объекты использующию эту текстуру заметны "на первом экране"          loaderDispatchHelper(scope.$store, 'isSandLoaded');      },    );  };}export default Module;
// В @/utils/utilities.js:export const loaderDispatchHelper = (store, field) => {  store.dispatch('preloader/preloadOrBuilt', field).then(() => {    store.dispatch('preloader/isAllLoadedAndBuilt');  }).catch((error) => { console.log(error); });};

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

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

Аудиошина

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

Аудио бывают:

1) Звучащие на контроле-герое и PositionalAudio на объектах

2) Луп или сэмпл

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

В Hero удобно записывать аудио в переменную чтобы можно было просто работать [в обход шины] с ними в специфической логике:

// В @/components/Three/Scene/Hero.js:import * as Three from "three";import {  DESIGN,  // ...} from '@/utils/constants';import {  loaderDispatchHelper,  // ...} from '@/utils/utilities';function Hero() {  const audioLoader = new Three.AudioLoader();  let steps;  let speed;  // ...  this.init = (    scope,    // ...  ) => {    audioLoader.load('./audio/steps.mp3', (buffer) => {      steps = scope.audio.addAudioToHero(scope, buffer, 'steps', DESIGN.VOLUME.hero.step, false);      loaderDispatchHelper(scope.$store, 'isStepsLoaded');    });  };  this.setHidden = (scope, isHidden) => {    if (isHidden) {      // ...      steps.setPlaybackRate(0.5);    } else {      // ...      steps.setPlaybackRate(1);    }  };  this.setRun = (scope, isRun) => {    if (isRun && scope.keyStates['KeyW']) {      steps.setVolume(DESIGN.VOLUME.hero.run);      steps.setPlaybackRate(2);    } else {      steps.setVolume(DESIGN.VOLUME.hero.step);      steps.setPlaybackRate(1);    }  };  // ...  this.animate = (scope) => {    if (scope.playerOnFloor) {      if (!scope.isPause) {        // ...        // Steps sound        if (steps) {          if (scope.keyStates['KeyW']            || scope.keyStates['KeyS']            || scope.keyStates['KeyA']            || scope.keyStates['KeyD']) {            if (!steps.isPlaying) {              speed = scope.isHidden ? 0.5 : scope.isRun ? 2 : 1;              steps.setPlaybackRate(speed);              steps.play();            }          }        }      } else {        if (steps && steps.isPlaying) steps.pause();        // ...      }    }  };}export default Module;

Казалось бы шаги нужно делать лупом? Ан нет, не получится. И, например, вешать еще один последний шаг на окончание движения не позволит само устройство инерционного контрола собирающего много быстрых событий. С ним даже не особо ясно когда это вообще происходит когда позиция героя перестаёт изменяться нам уже не надо. Найденное мною решение простое и хорошо работает, дает нужный эффект. Сэмпл шага это ровно два шага. Если появляется событие клавиатуры запускаем аудио. И оно всегда отыгрывает до конца. Если персонаж переходит на бег или начинает ползти просто меняем скорость.

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

if (!isLoop) audio.onEnded = () => audio.stop();

Имейте ввиду!

import * as Three from "three";import { DESIGN, OBJECTS } from '@/utils/constants';import { loaderDispatchHelper } from '@/utils/utilities';function Module() {  const audioLoader = new Three.AudioLoader();  // ...  let material = null;  const geometry = new Three.SphereBufferGeometry(0.5, 8, 8);  let explosion;  let explosionClone;  let boom;  this.init = (    scope,    fireMaterial,    // ...  ) => {    // Звук наземных врагов - загружаем в инициализации на объекты через шину    audioLoader.load('./audio/mechanism.mp3', (buffer) => {      loaderDispatchHelper(scope.$store, 'isMechanismLoaded');      scope.array = scope.enemies.filter(enemy => enemy.name !== OBJECTS.DRONES.name);      scope.audio.addAudioToObjects(scope, scope.array, buffer, 'mesh', 'mechanism', DESIGN.VOLUME.mechanism, true);     });    // Звук взрыва - то есть - "добавляемой и уничтожаемой" сущности - загружаем и записываем в переменную    material = fireMaterial;    explosion = new Three.Mesh(geometry, material);    audioLoader.load('./audio/explosion.mp3', (buffer) => {      loaderDispatchHelper(scope.$store, 'isExplosionLoaded');      boom = buffer;    });  };  // ...  // ... где-то в логике врагов:  this.moduleFunction = (scope, enemy) => {    scope.audio.startObjectSound(enemy.id, 'mechanism');    // ...    scope.audio.stopObjectSound(enemy.id, 'mechanism');    // ...  };  // При добавлении взрыва на шину взрывов:  this.addExplosionToBus = (    scope,    // ...  ) => {    explosionClone = explosion.clone();    // ..    scope.audio.playAudioOnObject(scope, explosionClone, boom, 'boom', DESIGN.VOLUME.explosion);    // ..  };}export default Module;

Попробуйте подобрать и принять зеленый цветок запускающий машину времени в игре, классно? ))

Шина событий и сообщения

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

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

Мир

Модель первой локацииМодель первой локации

В инициализации модуля мира по порядку:

  1. Загружаются все переиспользуемые в остальных модулях текстуры и создаются все такие материалы и геометрии.

  2. Загружается и разбирается модель уровня. На ее основе формируются массивы данных обо всех игровых объектах в OBJECTS и рабочие массивы, списки псевдообъектов для сущностей с одинаковой функциональностью в основном контексте.

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

  4. Двери переносятся в отдельную группу из основной модели. Из этих двух групп очищенной сцены и все дверей создаются стартовые октодеревья.

  5. Инициализируются все остальные модули.

Я разбираю один файл glb и как совершенно необходимый такой игре редактор уровней и как готовую модель для построения стартовых октодеревьев мира, и, отдельно дверей в нем, и как почти готовую не текстурированную основу самого примитивного визуального мира. Различать примитивы можно с помощью специфических маркеров в их наименовании. Это не самое надежное соглашение, оно чревато ошибками, но они легко обнаруживаются визуально при ручном тестировании. Изменения можно вносить очень быстро. Тут уже все зависит от вашей фантазии и выдуманного с помощью нее дизайна и геймплея, ну и количества времени которые вы можете на это потратить. Например, я использую маркер Mandatory если хочу чтобы цветок или бутылка были обязательными, если его нет постройка зависит от рандома. Или для механики включения-выключения информационных панелей собирается отдельный массив с их комнатами параллелепипедами определяющими объем в котором панель реагирует на персонажа. Для геометрии такого объекта следует сделать при инициализации:

room.geometry.computeBoundingBox();

room.visible = false;

И теперь у нас может быть вот такой публичный метод в модуле панелей и его забегая вперед можно будет использовать в модуле Атмосферы при проверках отношения панелей к герою:

// В @/components/Three/Scene/World/Screens.js:this.isHeroInRoomWithScreen = (scope, screen) => {scope.box.copy(screen.room.geometry.boundingBox).applyMatrix4(screen.room.matrixWorld); if (scope.box.containsPoint(scope.camera.position)) return true;return false;};

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

Псевдообъект-помощник для двериПсевдообъект-помощник для двериДверь не закрываетсяДверь не закрывается

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

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

Кастинг

Вот мы и добрались до самого интересного: кастинг и столкновения. Как сделать так, чтобы предметы можно было собирать, а герой и враги не сталкивались с миром и друг-другом. Для обоих механик я использую дополнительные невидимые примитивы, псевдоmesh`и. Они инициализируются и записываются в абстрактные объекты которыми оперирует система вместе с основным видимым и всеми необходимыми им флагами-свойствами. Для движущихся врагов еще записывается коллайдер Sphere. Псевдомеши идут на кастинг (предметы) или построение и обновление октодеревьев (враги). А коллайдер для проверки столкновения с ними.

Псевдообъекты-помощники для предметовПсевдообъекты-помощники для предметов

Геометрия и материал готовиться в мире перед инициализацией всех вещей и надежнее сделать материал двусторонними так кастинг будет работать даже если герой оказался внутри псевдообъекта:

// В @/components/Three/Scene/World.js:const pseudoGeometry = new Three.SphereBufferGeometry(DESIGN.HERO.HEIGHT / 2,  4, 4); const pseudoMaterial = new Three.MeshStandardMaterial({ color: DESIGN.COLORS.white, side: Three.DoubleSide,});new Bottles().init(scope, pseudoGeometry, pseudoMaterial);

В модуле конкретной вещи:

// В @/components/Three/Scene/World/Thing.js:import * as Three from 'three';import { GLTFLoader } from '@/components/Three/Modules/Utils/GLTFLoader';import { OBJECTS } from '@/utils/constants';import { loaderDispatchHelper } from '@/utils/utilities';function Thing() {  let thingClone;  let thingGroup;  let thingPseudo;  let thingPseudoClone;  this.init = (    scope,    pseudoGeometry,    pseudoMaterial,  ) => {    thingPseudo = new Three.Mesh(pseudoGeometry, pseudoMaterial);    new GLTFLoader().load(      './images/models/Objects/Thing.glb',      (thing) => {        loaderDispatchHelper(scope.$store, 'isThingLoaded'); // загружена модель        for (let i = 0; i < OBJECTS.THINGS[scope.l].data.length; i++) {          // eslint-disable-next-line no-loop-func          thing.scene.traverse((child) => {            // ... - тут "покраска" материалами частей вещи          });          // Клонируем объект и псевдо          thingClone = thing.scene.clone();          thingPseudoClone = thingPseudo.clone();          // Псевдо нужно дать правильное имя чтобы мы могли различать его при кастинге          thingPseudoClone.name = OBJECTS.THINGS.name;          thingPseudoClone.position.y += 1.5; // корректируем немного позицию по высоте          thingPseudoClone.visible = false; // выключаем рендер          thingPseudoClone.updateMatrix(); // обновляем          thingPseudoClone.matrixAutoUpdate = false; // запрещаем автообновление          // Делаем из обхекта и псевдо удобную группу          thingGroup = new Three.Group();          thingGroup.add(thingClone);          thingGroup.add(thingPseudoClone);          // Выставляем координаты из собранных из модели уровня данных          thingGroup.position.set(            OBJECTS.THINGS[scope.l].data[i].x,            OBJECTS.THINGS[scope.l].data[i].y,            OBJECTS.THINGS[scope.l].data[i].z,          );          // Записываем в "рабочие объеты" - по ним будем кастить и прочее          scope.things.push({            id: thingPseudoClone.id,            group: thingGroup,          });          scope.objects.push(thingPseudoClone);          scope.scene.add(thingGroup); // добавляем на сцену        }        loaderDispatchHelper(scope.$store, 'isThingsBuilt'); // построено      },    );  };}export default Thing;

Теперь мы можем тыкать направленным вперед лучом из героя в анимационном цикле Hero.js:

// В @/components/Three/Scene/Hero.js:import { DESIGN, OBJECTS } from '@/utils/constants';function Hero() {  // ...  this.animate = (scope) => {    // ...    // Raycasting    // Forward ray    scope.direction = scope.camera.getWorldDirection(scope.direction);    scope.raycaster.set(scope.camera.getWorldPosition(scope.position), scope.direction);    scope.intersections = scope.raycaster.intersectObjects(scope.objects);    scope.onForward = scope.intersections.length > 0 ? scope.intersections[0].distance < DESIGN.HERO.CAST : false;    if (scope.onForward) {      scope.object = scope.intersections[0].object;      // Кастим предмет THINGS      if (scope.object.name.includes(OBJECTS.THINGS.name)) {        // ...      }    }    // ...  };}export default Hero;

Кастинг очень полезен и для усовершенствования ИИ врагов. С помощью него возможно проверять имеет ли смысл, есть ли возможность двигаться-прыгать вперед, лететь вниз, делать выстрел. В утилитах:

// В @/utils/utilities.js:// let arrowHelper;const fixNot = (value) => { if (!value) return Number.MAX_SAFE_INTEGER; return value;};export const isEnemyCanMoveForward = (scope, enemy) => { scope.ray = new Three.Ray(enemy.collider.center, enemy.mesh.getWorldDirection(scope.direction).normalize()); scope.result = scope.octree.rayIntersect(scope.ray); scope.resultDoors = scope.octreeDoors.rayIntersect(scope.ray); scope.resultEnemies = scope.octreeEnemies.rayIntersect(scope.ray); // arrowHelper = new Three.ArrowHelper(scope.direction, enemy.collider.center, 6, 0xffffff); // scope.scene.add(arrowHelper); if (scope.result || scope.resultDoors || scope.resultEnemies) {   scope.number = Math.min(fixNot(scope.result.distance), fixNot(scope.resultDoors.distance), fixNot(scope.resultEnemies.distance));   return scope.number > 6; } return true;};

Для наглядной визуальной отладки подобных механик очень полезен объект Three ArrowHelper. Если мы включим его добавление на сцену в функции выше:

Отладка с включенными стрелочными помощникамиОтладка с включенными стрелочными помощниками

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

// В @/utils/utilities.js:export const isToHeroRayIntersectWorld = (scope, collider) => { scope.direction.subVectors(collider.center, scope.camera.position).negate().normalize(); scope.ray = new Three.Ray(collider.center, scope.direction); scope.result = scope.octree.rayIntersect(scope.ray); scope.resultDoors = scope.octreeDoors.rayIntersect(scope.ray); if (scope.result || scope.resultDoors) {   scope.number = Math.min(fixNot(scope.result.distance), fixNot(scope.resultDoors.distance));   scope.dictance = scope.camera.position.distanceTo(collider.center);   return scope.number < scope.dictance; } return false;};

Враги

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

// В @/utils/constatnts.js:export const DESIGN = {  DIFFICULTY: {    civil: 'civil',    anarchist: 'anarchist',    communist: 'communist',  },  ENEMIES: {    mode: {      idle: 'idle',      active: 'active',      dies: 'dies',      dead: 'dead',    },    spider: {      // ...      decision: {        enjoy: 60,        rotate: 25,        shot: {          civil: 40,          anarchist: 30,          communist: 25,        },        jump: 50,        speed: 20,        bend: 30,      },    },    drone: {      // ...      decision: {        enjoy: 50,        rotate: 25,        shot: {          civil: 50,          anarchist: 40,          communist: 30,        },        fly: 40,        speed: 20,        bend: 25,      },    },  },  // ...};
// В @/components/Three/Scene/Enemies.js:import { DESIGN } from '@/utils/constants';import {  randomInteger,  isEnemyCanShot,  // ...} from "@/utils/utilities";function Enemies() {  // ...  const idle = (scope, enemy) => {    // ...  };  const active = (scope, enemy) => {    // ...    // Где-то в логике агрессивного режима: решение на выстрел (если отдыхает)    scope.decision = randomInteger(1, DESIGN.ENEMIES[enemy.name].decision.shot[scope.difficulty]) === 1;    if (scope.decision) {      if (isEnemyCanShot(scope, enemy)) {        scope.boolean = enemy.name === OBJECTS.DRONES.name;        scope.world.shots.addShotToBus(scope, enemy.mesh.position, scope.direction, scope.boolean);        scope.audio.replayObjectSound(enemy.id, 'shot');      }    }  };  const gravity = (scope, enemy) => {    // ...  };  this.animate = (scope) => {    scope.enemies.filter(enemy => enemy.mode !== DESIGN.ENEMIES.mode.dead).forEach((enemy) => {      switch (enemy.mode) {        case DESIGN.ENEMIES.mode.idle:          idle(scope, enemy);          break;        case DESIGN.ENEMIES.mode.active:          active(scope, enemy);          break;        case DESIGN.ENEMIES.mode.dies:          gravity(scope, enemy);          break;      }    });  };}export default Enemies;

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

Но! Самое важное на что нужно обратить внимание: в idle спокойном режиме полноценно двигается некоторое случайное время только один выбранный случайным образом враг. Остальные поворачиваются на месте + может и должна быть запущена анимация. Такая оптимизация позволяет действительно полноценно разгрузить систему.

Столкновения

Октодеревом в данном тексте обозначается максимально упрощенная модель 3D-пространства которое занимает некоторая группа объектов с минимально необходимым и достаточным для обсчета количеством граней, рёбер и вершин.

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

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

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

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

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

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

// В @/utils/constatnts.js:export const DESIGN = {  OCTREE_UPDATE_TIMEOUT: 0.5,  // ...};
// В @/utils/utilities.js:// Обновить персональное октодерево врагов для одного врагаimport * as Three from "three";import { Octree } from "../components/Three/Modules/Math/Octree";export const updateEnemiesPersonalOctree = (scope, id) => {  scope.group = new Three.Group();  scope.enemies.filter(obj => obj.id !== id).forEach((enemy) => {    scope.group.add(enemy.pseudoLarge);  });  scope.octreeEnemies = new Octree();  scope.octreeEnemies.fromGraphNode(scope.group);  scope.scene.add(scope.group);};
// Столкновения враговconst enemyCollitions = (scope, enemy) => {  // Столкновения c миром - полом, стенами, стеклами и трубами  scope.result = scope.octree.sphereIntersect(enemy.collider);  enemy.isOnFloor = false;  if (scope.result) {    enemy.isOnFloor = scope.result.normal.y > 0;    // На полу?    if (!enemy.isOnFloor) {      enemy.velocity.addScaledVector(scope.result.normal, -scope.result.normal.dot(enemy.velocity));    } else {      // Подбитый враг становится совсем мертвым после падения на пол и тд      // ...    }    enemy.collider.translate(scope.result.normal.multiplyScalar(scope.result.depth));  }  // Столкновения c дверями  scope.resultDoors = scope.octreeDoors.sphereIntersect(enemy.collider);  if (scope.resultDoors) {    enemy.collider.translate(scope.resultDoors.normal.multiplyScalar(scope.resultDoors.depth));  }  // Делаем октодерево из всех врагов без этого, если давно не делали  if (scope.enemies.length > 1    && !enemy.updateClock.running) {    if (!enemy.updateClock.running) enemy.updateClock.start();    updateEnemiesPersonalOctree(scope, enemy.id);    scope.resultEnemies = scope.octreeEnemies.sphereIntersect(enemy.collider);    if (scope.resultEnemies) {      result = scope.resultEnemies.normal.multiplyScalar(scope.resultEnemies.depth);      result.y = 0;      enemy.collider.translate(result);    }  }  if (enemy.updateClock.running) {    enemy.updateTime += enemy.updateClock.getDelta();    if (enemy.updateTime > DESIGN.OCTREE_UPDATE_TIMEOUT && enemy.updateClock.running) {      enemy.updateClock.stop();      enemy.updateTime = 0;    }  }};

Своя атмосфера

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

Если вывалится за стену и забежать за край небаЕсли вывалится за стену и забежать за край неба

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

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

Пуленепробиваемые стеклаПуленепробиваемые стекла

Да, это вам не React c TS и тестами в финтех и банки!

Выводы которые я могу сделать на основе практики создания браузерной FPS на Three:

  • Мы не можем использовать тени и множество источников света

  • Мы должны экономить память в анимационном цикле и использовать в нем только готовые переменные

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

  • Статическая типизация и юнит-тесты ничем не могут помочь в данном эксперименте

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

Подробнее..

Vue.js и слоистая архитектура вынесение бизнес-логики в сервисы

10.05.2021 12:10:00 | Автор: admin

Когда нужно сделать код в проекте гибким и удобным, на помощь приходит разделение архитектуры на несколько слоев. Рассмотрим подробнее этот подход и альтернативы, а также поделимся рекомендациями, которые могут быть полезны как начинающим, так и опытным разработчикам Vue.js, React.js, Angular.

В старые времена, когда JQuery только появился, а о фреймворках для серверных языков лишь читали в редких новостях, веб-приложения реализовывали целиком на серверных языках. Зачастую для этого использовали модель MVC (Model-View-Controller): контроллер (controller) принимал запросы, отвечал за бизнес-логику и модели (model) и передавал данные в представление (view), которое рисовало HTML.

Объектно-ориентированное программирование (ООП) на тот момент только начинало формироваться, поэтому разработчики зачастую интуитивно решали, где и какой код надо писать. Таким образом, в мире разработки зародилось такое понятие, как Божественные объекты, которые первоначально отвечали практически за всю работу отдельных частей системы. Например, если в системе была сущность Пользователь, то создавался класс User и в нем писалась вся логика, так или иначе связанная с пользователями. Без разбиения на какие-то ещё файлы. И если приложение было большим, то такой класс мог содержать тысячи строк кода.

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

1. Выход есть

Как известно, Vue.js, React.js и прочие подобные фреймворки основаны на компонентах. То есть, по большому счету, приложение состоит из множества компонентов, которые могут заключать в себе и бизнес-логику и представление и много чего еще. Таким образом, разработчики во многих проектах пишут всю логику в компонентах и эти компоненты, как правило, начинают напоминать те самые божественные классы из прошлого. То есть, если компонент описывает какую-то крупную часть функционала с большим количеством (возможно сложной) логики, то вся эта логика и остается в компоненте. Появляются десятки методов и тысячи строк кода. А если учесть то, что, например, во Vue.js еще есть такие понятия как computed, watch, mounted, created, то логику пишут еще и во все эти части компонента. В итоге, чтобы найти какую-то часть кода, отвечающую за клик по кнопке, надо перелистать десяток экранов js-кода, бегая между methods, computed и прочими частями компонента.

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

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

Вот о таком разбиении кода на слои и пойдет речь, но уже применительно к frontend-фреймворкам, таким как Vue.js, React.js и прочим.

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

2. Создание удобной архитектуры приложения

Рассмотрим пример, в котором вся логика находится в одном компоненте.

2.1. Логика в компоненте

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

methods: {    duplicateCollage (collage) {      this.$store.dispatch('updateCollage', { id: collage.id, isDuplicating: true })      dataService.duplicateCollage(collage, false)        .then(duplicate => {          this.$store.dispatch('updateCollage', { id: collage.id, isDuplicating: false })        })        .catch(() => {          this.$store.dispatch('updateCollage', { id: collage.id, isDuplicating: false })          this.$store.dispatch('errorsSet', { api: `We couldn't duplicate collage. Please, try again later.` })        })    },    deleteCollage (collage, index) {      this.$store.dispatch('updateCollage', { id: collage.id, isDeleting: true })      photosApi.deleteUserCollage(collage)        .then(() => {          this.$store.dispatch('updateCollage', {            id: collage.id,            isDeleting: false,            isDeleted: true          })          this.$store.dispatch('setUserCollages', { total: this.userCollages.total - 1 })          this.$store.dispatch('updateCollage', {            id: collage.id,            deletingTimer: setTimeout(() => {              this.$store.dispatch('updateCollage', { id: collage.id, deletingTimer: null })              this.$store.dispatch('setUserCollages', { items: this.userCollages.items.filter(userCollage => userCollage.id !== collage.id) })               // If there is no one collages left - show templates              if (!this.$store.state.editor.userCollages.total) {                this.currentTabName = this.TAB_TEMPLATES              }            }, 3000)          })        })    },    restoreCollage (collage) {      clearTimeout(collage.deletingTimer)      photosApi.saveUserCollage({ collage: { deleted: false } }, collage.id)        .then(() => {          this.$store.dispatch('updateCollage', {            id: collage.id,            deletingTimer: null,            isDeleted: false          })          this.$store.dispatch('setUserCollages', { total: this.userCollages.total + 1 })        })    }}

2.2. Создание слоя сервисов для бизнес-логики

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

Один из классических способов хоть какого-то разбиения логики это деление на сущности. Например, почти всегда в проекте есть сущность Пользователь или, как в описываемом примере, Коллаж. Таким образом, можно создать папку services и в ней файлы user.js и collage.js. Такие файлы могут быть статическими классами или просто возвращать функции. Главное чтобы вся бизнес-логика, связанная с сущностью, была в этом файле.

services  |_collage.js  |_user.js

В сервис collage.js следует поместить логику дублирования, восстановления и удаления коллажей.

export default class Collage {  static delete (collage) {    // ЛОГИКА УДАЛЕНИЯ КОЛЛАЖА  }   static restore (collage) {    // ЛОГИКА ВОССТАНОВЛЕНИЯ  КОЛЛАЖА  }   static duplicate (collage, changeUrl = true) {    // ЛОГИКА ДУБЛИРОВАНИЯ КОЛЛАЖА  }}

2.3. Использование сервисов в компоненте

Тогда в компоненте надо будет лишь вызвать соответствующие функции сервиса.

methods: {  duplicateCollage (collage) {    CollageService.duplicate(collage, false)  },  deleteCollage (collage) {    CollageService.delete(collage)  },  restoreCollage (collage) {    CollageService.restore(collage)  }}

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

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

import axios from '@/plugins/axios' export default class Api {   static login (email, password) {    return axios.post('auth/login', { email, password })      .then(response => response.data)  }   static logout () {    return axios.post('auth/logout')  }   static getCollages () {    return axios.get('/collages')      .then(response => response.data)  }    static deleteCollage (collage) {    return axios.delete(`/collage/${collage.id}`)      .then(response => response.data)  }    static createCollage (collage) {    return axios.post(`/collage/${collage.id}`)      .then(response => response.data)  }}

3. Что и куда выносить?

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

Бизнес-логика это все то, что описано в требованиях к приложению. Например, ТЗ, документации, дизайны. То есть все то, что напрямую относится к предметной области приложения. Примером может быть метод UserService.login() или ListService.sort(). Для бизнес-логики можно создать сервисный слой с сервисами.

Логика это тот код, который не имеет прямого отношения к предметной области приложения и его бизнес-логике. Например, создание уникальной строки или поиск некоего объекта в массиве. Для логики можно создать слой хэлперов: например, папку helpers и в ней файлы string.js, converter.js и прочие.

Представление все то, что непосредственно связано с компонентом и его шаблоном. Например, изменение реактивных свойств, изменение состояний и прочее. Этот код пишется непосредственно в компонентах (methods, computed, watch и так далее).

login (email, password) {  this.isLoading = true  userService.login(email, password)    .then(user => {      this.user = user      this.isLoading = false    })}

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

Если же сервисы или хэлперы начнут разрастаться, то сущности всегда можно разделить на другие сущности. К примеру, если у пользователя в приложении маленький функционал в 3-5 методов и пара методов про заказы пользователя, то разработчик может вынести всю эту бизнес-логику в сервис user.jsрешить всю эту бизнес-логику написать в сервисе user.js. Если же у сервиса пользователя сотни строк кода, то можно все, что относится к заказам, вынести в сервис order.js.

4. От простого к сложному

В идеале можно сделать архитектуру на ООП, в которой будут, помимо сервисов, еще и модели. Это классы, описывающие сущности приложения. Те же User или Collage. Но использоваться они будут вместо обычных объектов данных.

Рассмотрим список пользователей.

Классический способ вывода ФИО пользователей выглядит так.

<template><div class="users">  <div    v-for="user in users"    class="user"  >    {{ getUserFio(user) }}  </div></div></template> <script>import axios from '@/plugins/axios' export default {  data () {    return {      users: []    }  },  mounted () {    this.getList()  },  methods: {    getList() {      axios.get('/users')        .then(response => this.users = response.data)    },    getUserFio (user) {      return `${user.last_name} ${user.first_name} ${user.third_name}`    }  }}</script>

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

Для начала следует создать модель Пользователь.

export default class User {  constructor (data = {}) {    this.firstName = data.first_name    this.secondName = data.second_name    this.thirdName = data.third_name  }   getFio () {    return `${this.firstName} ${this.secondName} ${this.thirdName}`  }}

Далее следует импортировать эту модель в компонент.

import UserModel from '@/models/user'

С помощью сервиса получить список пользователей и преобразовать каждый объект в массиве в объект класса (модели) User.

methods: {   getList() {     const users = userService.getList()     users.forEach(user => {       this.users.push(new UserModel(user))     })   },

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

<template><div class="users">  <div    v-for="user in users"    class="user"  >    {{ user.getFio() }}  </div></div></template>

К вопросу о том, какую логику выносить в модели, а какую в сервисы. Можно всю логику поместить в сервисы, а в моделях вызывать сервисы. А можно в моделях хранить логику, относящуюся непосредственно к сущности модели (тот же getFio()), а логику работы с массивами сущностей хранить в сервисах (тот же getList()). Как будет удобнее.

5. Заключение

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

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

Спасибо за внимание! Будем рады ответить на ваши вопросы.

Подробнее..

Keycloak интеграция со Spring Boot и Vue.js для самых маленьких

10.05.2021 18:20:09 | Автор: admin

Вы больше не можете создать сервер авторизации с помощью @EnableAuthorizationServer, потому что Spring Security OAuth задеприкейтили, а проект Spring Authorization Serverвсё ещё экспериментальный? Выход есть! Напишем авторизацию своими руками... Что?.. Нет?.. Не хочется? И вообще получаются какие-то костыли и велосипеды? Ну ладно, тогда давайте возьмём уже что-то готовое. Например, Keycloak.

Что, зачем и почему?

Как-то сидя на карантине захотелось мне написать pet-проект, да не простой, а с использованием микросервисной архитектуры (ну или около того). На начальном этапе одного сервиса для фронта и одного для бэка, в принципе, будет достаточно. Если вдруг в будущем понадобятся ещё сервисы, то будем добавлять их по мере необходимости. Для бэка будем использовать Spring Boot. Для фронта - Vue.js, а точнее Vuetify, чтобы не писать свои компоненты, а использовать уже готовые.

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

Для авторизации пусть будет отдельный сервис. И раз уж мы решили использовать Spring Boot, то сможет ли он нам чем-то помочь в создании этого сервиса? Например, каким-нибудь готовым решением, таким как Authorization Server? Правильно, не сможет. Проект Spring Security OAuth в котором находился Authorization Server задеприкейтили, а сам проект Authorization Server стал эксперементальным и на данный момент находится в активной разработке. Что делать? Как быть? Можно написать свой сервис авторизации. Если подсматривать в исходники задеприкейченого Authorization Server, то, вероятно, задача будет не такой уж и страшной. Правда, при этом возможны ситуации когда реализацию каких-то интересных фич будет негде подсмотреть и решать вопросы о том "быть или не быть", "правильно ли так делать или чё-то фигня какая-то" придётся исходя из собственного опыта, что может привести к получению на выходе большого количества неприглядных костылей.

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

Keycloak

Keycloak представляет из себя сервис, который предназначен для идентификации и контроля доступа. Что он умеет:

  • SSO (Single-Sign On) - это когда вы логинитесь в одном едином месте входа, получаете идентификатор (например, токен), с которым можете получить доступ к различным вашим сервисам

  • Login Flows - различные процессы по регистрации, сбросу пароля, проверки почты и тому подобное, а так же соответствующие страницы для этих процессов

  • Темы - можно кастомизировать страницы для Login Flows

  • Social Login - можно логиниться через различные социальные сети

  • и много чего ещё

И всё это он умеет практически из коробки, достаточно просто настроить требуемое поведение из админки (Admin Console), которая у Keycloak тоже имеется. А если вам всего этого вдруг окажется мало, то Keycloak является open sourceпродуктом, который распространяется по лицензии Apache License 2.0. Так что можно взять исходники Keycloak и дописать требуемый функционал, если он вам, конечно, настолько сильно нужен.

А ещё у Keycloak имеются достаточно удобные интеграции со Spring Boot и Vue.js, что значительно упрощает разработку связанную с взаимодействием с ним.

Getting Started with Keycloak

Запускать локально сторонние сервисы, требуемые для разработки своих собственных, лично я предпочитаю с помощью Docker Compose, т.к. наглядно и достаточно удобно в yml-файле описывать как и с какими параметрами требуется осуществлять запуск. А посему, Keycloak локально будем запускать с помощью Docker Compose.

В качестве докер-образа возьмём jboss/keycloak. Чтобы иметь возможность обращаться к Keycloak прокинем из контейнера порт 8080. Так же, чтобы иметь возможность заходить в админку Keycloak, требуется установить логин и пароль от админской учётки. Сделать это можно установив переменные окружения KEYCLOAK_USER для логина и KEYCLOAK_PASSWORD для пароля. Итоговый файл приведен ниже.

# For developmentversion: "3.8"services:  keycloak:    image: jboss/keycloak:12.0.2    environment:      KEYCLOAK_USER: admin      KEYCLOAK_PASSWORD: admin    ports:      - 8080:8080

Создание своих realm и client

Для того чтобы иметь возможность из своего клиентского приложения обращаться к Keycloak, например, для аутентификации или авторизации, нужно в Keycloak создать клиента (client), который будет соответствовать этому приложению. Клиента в Keycloak можно создать в определённом realm. Realm - это независимая область в которую входят пользователи, клиенты, группы, роли и много чего ещё.

По умолчанию уже создан один realm и называется он master. В нём будет находится админская учётка логин и пароль от которой мы задали при запуске Keycloak с помощью Docker Compose. Данный realm предназначен для администрирования Keycloak и он не должен использоваться для ваших собственных приложений. Для своих приложений нужно создать свой realm.

Для начала нам нужно залогиниться в админке Keycloak, запустить который можно с помощью файла Docker Compose, описанного ранее. Для этого можно перейти по адресу http://localhost:8080/auth/ и выбрать Administration Console.

После этого мы попадаем на страницу авторизации админки Keycloak. Здесь можно ввести логин и пароль от админской учётки для входа в Keycloak.

После входа откроется страница настроек realm master.

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

На странице создания realm достаточно заполнить только поле Name.

После нажатия на кнопку Createмы попадём на страницу редактирования этого realm. Но пока дополнительно в нашем realm ничего менять не будем.

Теперь перейдём в раздел Clients. Как можно заметить, по умолчанию уже создано несколько технических клиентов, предназначенных для возможности администрирования через Keycloak, например, для того чтобы пользователи могли менять свои данные или чтобы можно было настраивать realm'ы с помощью REST API и много для чего ещё. Подробнее про этих клиентов можно почитать тут.

Давайте создадим своего клиента. Для этого в разделе Clientsнеобходимо нажать на кнопку Create.

На странице создания клиента необходимо заполнить поля:

  • Client ID - идентификатор клиента, будет использоваться в различных запросах к Keycloak для идентификации приложения.

  • Root URL - адрес клиентского приложения.

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

Интеграция со Spring Boot

В первую очередь давайте создадим проект на Spring Boot. Сделать это можно, например, с помощью Spring Initializr. В качестве системы автоматической сборки проекта будем использовать Gradle. В качестве языка пусть будет Java 15. Никаких дополнительных зависимостей в соответствующем блоке Dependencies добавлять не требуется.

Для того чтобы в Spring Boot проекте появилась поддержка Keycloak, необходимо добавить в него Spring Boot Adapter и добавить в конфиг приложения конфигурацию для Keycloak.

Для того чтобы добавить Spring Boot Adapter, необходимо в проект подключить зависимость org.keycloak:keycloak-spring-boot-starter и сам adapter org.keycloak.bom:keycloak-adapter-bom. Сделать это можно изменив файл build.gradle следующим образом:

...dependencyManagement {imports {mavenBom 'org.keycloak.bom:keycloak-adapter-bom:12.0.3'}}dependencies {implementation 'org.springframework.boot:spring-boot-starter-web'implementation 'org.keycloak:keycloak-spring-boot-starter'testImplementation 'org.springframework.boot:spring-boot-starter-test'}...
Проблемы в Java 14+

Если запустить Spring Boot приложение на Java 14 или выше, то при обращении к вашим методам API, закрытым ролями кейклока, будут возникать ошибки видаjava.lang.NoClassDefFoundError: java/security/acl/Group. Связано это с тем, что в Java 9 этот, а так же другие классы из этого пакета были задеприкейчины и удалены в Java 14. Исправить данную проблему, вроде как, собираются в 13-й версии Keycloak. Чтобы решить её сейчас, можно использовать Java 13 или ниже, либо, вместо сервера приложений Tomcat, который используется в Spring Boot по умолчанию, использовать, например, Undertow. Для того чтобы подключить в Spring Boot приложение Undertow, нужно добавить в build.gradle зависимость org.springframework.boot:spring-boot-starter-undertow и исключить зависимоситьspring-boot-starter-tomcat.

...dependencies {implementation('org.springframework.boot:spring-boot-starter-web') {exclude module: 'spring-boot-starter-tomcat'}implementation ('org.keycloak:keycloak-spring-boot-starter') {exclude module: 'spring-boot-starter-tomcat'}implementation 'org.springframework.boot:spring-boot-starter-undertow'testImplementation 'org.springframework.boot:spring-boot-starter-test'}...

Теперь перейдём к конфигурации приложения. Вместо properties файла конфигурации давайте будем использовать более удобный (на мой взгляд, конечно же) yml. А так же, чтобы подчеркнуть, что данный конфиг предназначен для разработки, профиль dev. Т.е. полное название файла конфигурации будет application-dev.yml.

server:  port: 8082keycloak:  auth-server-url: http://localhost:8080/auth  realm: "list-keep"  resource: "list-keep"  bearer-only: true  security-constraints:    - authRoles:        - uma_authorization      securityCollections:        - patterns:            - /api/*

Давайте подробнее разберём данный конфиг:

  • server

    • port - порт на котором будет запущенно приложение

  • keycloak

    • auth-server-url - адрес на котором запущен Keycloak

    • realm - название нашего realm в Keycloak

    • resource - Client ID нашего клиента

    • bearer-only - если выставлено true, то приложение может только проверять токены, и в приложении нельзя будет залогиниться, например, с помощью логина и пароля из браузера

    • security-constraints - для описания ролевой политики

      • authRoles - список ролей Keycloak

      • securityCollections

        • patterns - URL-паттерны для методов REST API, которые требуется закрыть соответствующими ролями

      В данном конкретном случае мы закрываем ролью uma_authorization все методы, в начале которых присутствует путь /api/. Звёздочка в конце паттерна означает любое количество любых символов. Роль uma_authorization добавляется автоматически ко всем созданным пользователям, т.е. по сути данная ролевая политика означает что все методы /api/* доступны только авторизованным пользователям.

В общем-то, это все настройки которые нужно выполнить в Spring Boot приложении для интеграции с Keycloak. Давайте теперь добавим какой-нибудь тестовый контроллер.

@RestController@RequestMapping("/api/user")public class UserController {    @GetMapping("/current")    public User getCurrentUser(            KeycloakPrincipal<KeycloakSecurityContext> principal    ) {        return new User(principal.getKeycloakSecurityContext()                .getToken().getPreferredUsername()        );    }}
User.java
public class User {    private String name;    public User(String name) {        this.name = name;    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }}

В данном контроллере есть лишь один метод /api/user/current, который возвращает информацию по текущему юзеру, а именно Preferred Username из токена. По умолчанию в Preferred Username находится username пользователя Keycloak.

Исходники проекта можно посмотреть тут.

Интеграция с Vue.js

Начнём с создания проекта. Создать проект можно, например, с помощью Vue CLI.

vue create list-keep-front

После ввода данной команды необходимо выбрать версию Vue. Т.к. в проекте будет использоваться библиотека Vuetify, которая на данный момент не поддерживает Vue 3, нужно выбрать Vue 2.

После этого нужно перейти в проект и добавить Vuetify.

vue add vuetify

После добавления Vuetify вместе с самой библиотекой в проект будут добавлены каталоги components и assets. В components будет компонент HelloWorld, с примером страницы на Vuetify, а в assets ресурсы, использующиеся в компоненте HelloWorld. Эти каталоги нам не пригодятся, поэтому можно их удалить.

Для удобства разработки сконфигурируем devServer следующим образом: запускать приложение будем на порту 8081, все запросы, которые начинаются с /api/ будем проксировать на адрес, на котором запущенно приложение на Spring Boot.

module.exports = {  devServer: {    port: 8081,    proxy: {      '^/api/': {        target: 'http://localhost:8082'      }    }  }}

Перейдём к добавлению в проект поддержки Keycloak. Для начала обратимся к официальной документации. Там нам рассказывают о том, что в проект нужно добавить Keycloak JS Adapter. Сделать это можно с помощью библиотеки keycloak-js. Добавим её в проект.

yarn add keycloak-js

Далее нам предлагают добавить в src/main.js код, который добавит в наш проект поддержку Keycloak.

// Параметры для подключения к Keycloaklet initOptions = {  url: 'http://127.0.0.1:8080/auth', // Адрес Keycloak  realm: 'keycloak-demo', // Имя нашего realm в Keycloak  clientId: 'app-vue', // Идентификатор клиента в Keycloak    // Перенаправлять неавторизованных пользователей на страницу входа  onLoad: 'login-required'}// Создать Keycloak JS Adapterlet keycloak = Keycloak(initOptions);// Инициализировать Keycloak JS Adapterkeycloak.init({ onLoad: initOptions.onLoad }).then((auth) => {  if (!auth) {    // Если пользователь не авторизован - перезагрузить страницу    window.location.reload();  } else {    Vue.$log.info("Authenticated");        // Если авторизован - инициализировать приложение Vue    new Vue({      el: '#app',      render: h => h(App, { props: { keycloak: keycloak } })    })  }  // Пытаемся обновить токен каждые 6 секунд  setInterval(() => {    // Обновляем токен, если срок его действия истекает в течении 70 секунд    keycloak.updateToken(70).then((refreshed) => {      if (refreshed) {        Vue.$log.info('Token refreshed' + refreshed);      } else {        Vue.$log.warn('Token not refreshed, valid for '          + Math.round(keycloak.tokenParsed.exp          + keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds');      }    }).catch(() => {      Vue.$log.error('Failed to refresh token');    });  }, 6000)}).catch(() => {  Vue.$log.error("Authenticated Failed");});

С инициализацией Keycloak JS Adapter, вроде бы, всё понятно. А вот использование setInterval для обновления токенов мне показалось не очень практичным и красивым решением. Как минимум, кажется, что при бездействии пользователя на странице токены всё равно продолжат обновляться, хоть это и не требуется. На мой взгляд, обновление токенов лучше сделать так, как предлагает, например, автор данной статьи. Т.е. обновлять токены когда пользователь выполняет какое-либо действие в приложении. Автор указанной статьи выделяет три таких действия:

  • Взаимодействие с API (бэкендом)

  • Навигация (переход по страницам)

  • Переход на вкладку с нашим приложением, например, из другой вкладки

Приступим к реализации. Для того чтобы можно было обновлять токен из различных частей приложения, нам понадобится глобальный экземпляр Keycloak JS Adapter. Для этого во Vue.js существует функционал плагинов. Создадим свой плагин для Keycloak JS Adapter в файле /plugins/keycloak.js.

import Vue from 'vue'import Keycloak from 'keycloak-js'const initOptions = {    url: process.env.VUE_APP_KEYCLOAK_URL,    realm: 'list-keep',    clientId: 'list-keep'}const keycloak = Keycloak(initOptions)const KeycloakPlugin = {    install: Vue => {        Vue.$keycloak = keycloak    }}Vue.use(KeycloakPlugin)export default KeycloakPlugin

Значение адреса Keycloak, указанное в initOptions.url, может отличаться в зависимости от того где запущенно приложение (локально, на тесте, на проде), поэтому, чтобы иметь возможность указывать значения в зависимости от среды, будем использовать переменные окружения. Для локального запуска можно создать файл .env.local в корне проекта со следующим содержимым.

VUE_APP_KEYCLOAK_URL = http://localhost:8080/auth

Теперь нам достаточно импортировать файл с созданным нами плагином в main.js, и мы сможем из любого места приложения обратиться к нашему Keycloak JS Adapter с помощью Vue.$keycloak. Давайте это и сделаем, а так же создадим экземпляр Vue нашего приложения. Для этого изменим файл main.js следующим образом.

import Vue from 'vue'import App from './App.vue'import vuetify from './plugins/vuetify'import router from '@/router'import i18n from '@/plugins/i18n'import '@/plugins/keycloak'import { updateToken } from '@/plugins/keycloak-util'Vue.config.productionTip = falseVue.$keycloak.init({ onLoad: 'login-required' }).then((auth) => {  if (!auth) {    window.location.reload();  } else {    new Vue({      vuetify,      router,      i18n,      render: h => h(App)    }).$mount('#app')    window.onfocus = () => {      updateToken()    }  }})

Помимо инициализации Keycloak JS Adapter, здесь добавлен вызов функции updateToken() на событие window.onfocus, которое будет возникать при переходе пользователя на вкладку с нашим приложением. Наша функция updateToken() вызывает функцию updateToken() из Keycloak JS Adapter и, соответственно, обновляет токен, если срок жизни токена в секундах на данный момент меньше, чем значение TOKEN_MIN_VALIDITY_SECONDS, после чего возвращает актуальный токен.

import Vue from 'vue'const TOKEN_MIN_VALIDITY_SECONDS = 70export async function updateToken () {    await Vue.$keycloak.updateToken(TOKEN_MIN_VALIDITY_SECONDS)    return Vue.$keycloak.token}

Теперь добавим обновление токена на оставшиеся действия пользователя, а именно на взаимодействие с API и на навигацию. С API мы будем взаимодействовать с помощью axios. Помимо обновления токена нам в каждом запросе необходимо добавлять http-хидер Authorization: Bearer с нашим токеном для авторизации в нашем Spring Boot сервисе. Так же давайте будем перенаправлять на какую-нибудь страницу с ошибками, например, /error, если API будет возвращать нам ошибки. Для того чтобы выполнять какие-либо действие на любые запросы/ответы в axios существуют интерцепторы, добавить которые можно в App.vue.

<template>  <v-app>    <v-main>      <router-view></router-view>    </v-main>  </v-app></template><script>import Vue from 'vue'import axios from 'axios'import { updateToken } from '@/plugins/keycloak-util'const AUTHORIZATION_HEADER = 'Authorization'export default Vue.extend({  name: 'App',  created: function () {    axios.interceptors.request.use(async config => {      // Обновляем токен      const token = await updateToken()      // Добавляем токен в каждый запрос      config.headers.common[AUTHORIZATION_HEADER] = `Bearer ${token}`      return config    })        axios.interceptors.response.use( (response) => {      return response    }, error => {      return new Promise((resolve, reject) => {        // Если от API получена ошибка - отправляем на страницу /error        this.$router.push('/error')        reject(error)      })    })  },  // Обновляем токен при навигации  watch: {    $route() {      updateToken()    }  }})</script>

Помимо интерцепторов мы здесь добавили наблюдателя (watch), который будет отслеживать переход пользователя по страницам приложения и обновлять при этом токен.

Интеграция с Keycloak закончена. Давайте теперь добавим тестовую страницу /pages/Home.vue, на которой будем вызывать с помощью axios тестовый метод /api/user/current, который мы ранее добавили в Spring Boot приложение, и выводить имя полученного пользователя.

<template>  <div>    <p>{{ user.name }}</p>  </div></template><script>import axios from 'axios'export default {  name: 'Home',  data() {    return {      user: {}    }  },  mounted() {    axios.get('/api/user/current')        .then(response => {          this.user = response.data        })  }}</script>

Для того чтобы можно было попасть на данную страницу в нашем приложении необходимо добавить её в router.js. Данная страница будет доступна по пути /.

import Vue from 'vue'import VueRouter from 'vue-router'import Home from '@/pages/Home'import Error from '@/pages/Error'import NotFound from '@/pages/NotFound'Vue.use(VueRouter)let router = new VueRouter({    mode: 'history',    routes: [        {            path: '/',            component: Home        },        {            path: '/error',            component: Error        },        {            path: '*',            component: NotFound        }    ]})export default router

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

И ещё немного о страницах

Помимо страницы /pages/Home.vue в роутере присутствуют страницы /pages/Error.vue и /pages/NotFound.vue. НаError , как уже упоминалось ранее, происходит переход из интерцептора при получении ошибок от API. На NotFound - если будет переход на неизвестную страницу.

Для примера давайте рассмотрим содержимое страницы Error.vue. Содержимое NotFound.vue практически ничем не отличается.

<template>  <v-container      class="text-center"      fill-height      style="height: calc(100vh - 58px);"  >    <v-row align="center">      <v-col>        <h1 class="display-2 primary--text">          {{ $t('oops.error.has.occurred') }}        </h1>        <p>{{ $t('please.try.again.later') }}</p>        <v-btn            href="http://personeltest.ru/aways/habr.com/"            color="primary"            outlined        >          {{ $t('go.to.main.page') }}        </v-btn>      </v-col>    </v-row>  </v-container></template><script>export default {  name: 'Error'}</script>

В шаблоне данной страницы используется локализация. Работает она с помощью плагина vue-i18n. Для того чтобы прикрутить локализацию своих текстовок нужно добавить переводы в виде json файлов в проект. Например, для русской локализации можно создать файл ru.json и положить его в каталог locales. Теперь эти текстовки необходимо загрузить в VueI18n. Сделать это можно, например, следующим образом. Давайте код по загрузке текстовок вынесем в/plugins/i18n.js.

import Vue from 'vue'import VueI18n from 'vue-i18n'Vue.use(VueI18n)function loadLocaleMessages () {    const locales = require.context('@/locales', true,                                    /[A-Za-z0-9-_,\s]+\.json$/i)    const messages = {}    locales.keys().forEach(key => {        const matched = key.match(/([A-Za-z0-9-_]+)\./i)        if (matched && matched.length > 1) {            const locale = matched[1]            messages[locale] = locales(key)        }    })    return messages}export default new VueI18n({    locale: 'ru',    fallbackLocale: 'ru',    messages: loadLocaleMessages()})

После этого к этим текстовкам можно будет обращаться из шаблона страницы с помощью $t.

Так же привожу содержимое /plugins/vuetify.js. В нём добавлена возможность использовать иконки Font Awesome на страницах нашего приложения.

import Vue from 'vue'import Vuetify from 'vuetify/lib/framework'import 'vuetify/dist/vuetify.min.css'import '@fortawesome/fontawesome-free/css/all.css'Vue.use(Vuetify);const opts = {    icons: {        iconfont: 'fa'    }}export default new Vuetify(opts)
Немного мыслей об обработке ошибок

Функции Keycloak JS Adapter init() и updateToken() возвращают объект KeycloakPromise, у которого есть возможность вызывать catch() и в нём обрабатывать ошибки. Но лично я не понял что именно в данном случае будет считаться ошибками и когда мы попадём в этот блок, т.к., например, если Keycloak не доступен, то в этот блок мы не попадаем. Поэтому в приведённом здесь приложении, я возможные ошибки от этих двух функций не обрабатываю. Возможно, если Keycloak не работает, то в продакшене стоит делать так, чтоб и наше приложение тоже становилось недоступным и не пытаться это как-то обработать. Ну или если всё-таки нужно такие ошибки понимать именно в Vue.js приложении, то, возможно, нужно как-то доработать keycloak-js.

Исходники проекта можно посмотреть тут.

Login Flows

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

  • Авторизация и регистрация пользователей

  • Локализация страниц

  • Подтверждение email

  • Вход через социальные сети

Локализация страниц в Keycloak

Запустим наши Spring Boot и Vue.js приложения. При переходе в клиентское Vue.js приложение нас перенаправит на страницу логина Keycloak.

В первую очередь давайте добавим поддержку русского языка. Для этого в админке Keycloak, на вкладке Theams, в настройки realm включаем флаг Internationalization Enabled . В Supported Locales убираем все локали кроме ru, пусть наше приложение на Vue.js поддерживает только один язык. В Default Locale выставляем ru.

Нажимаем Save и возвращаемся в наше клиентское приложение.

Как видим, русский язык у нас появился, правда, не все текстовки были локализованы. Это можно исправить, добавив собственные варианты перевода. Сделать это можно на вкладке Localization, в настройках realm.

Здесь имеется возможность добавить текстовки вручную по одной, либо загрузить их из json файла. Давайте сделаем это вручную. Для начала требуется добавить локаль. Вводим ru и нажимаем Create. После чего попадаем на страницу Add localization text. На этой странице нам необходимо заполнить поля Key и Value. Если с value всё ясно, это будет просто значение нашей текстовки, то вот где взять Key не совсем понятно. В документации допустимые ключи нигде не описаны (либо я просто плохо искал), поэтому остаётся лишь найти их в исходниках Keycloak. Находим в ресурсах нужную нам базовую тему base и страницу login, а затем файл с текстовками в локали en - messages_en.properties. В этом файле по значению определяем нужный нам ключ текстовки, добавляем его в Key на странице Add localization text, а так же добавляем нужное нам Value и нажимаем Save.

После этого на вкладке Localization в настройках realm, при выборе локали ru, появляется таблица, в которой можно посмотреть, отредактировать или удалить нашу добавленную текстовку.

Вернёмся в наше клиентское приложение. Теперь все текстовки на странице логина локализованы.

Регистрация пользователей

Поддержку регистрации пользователей можно добавить, включив флаг User registration на вкладке Login в настройках realm.

После этого на странице логина появится кнопка Регистрация.

Нажимаем на кнопку Регистрация и попадаем на соответствующую страницу.

Давайте немного подкрутим эту страницу. Для начала добавим отсутствующий перевод текстовки, аналогично тому, как мы делали это ранее для страницы логина. Так же давайте уберём поле Имя пользователя. На самом деле совсем его убрать нельзя, т.к. это поля обязательно для заполнения у пользователя Keycloak, но можно сделать так, чтобы в качестве имени пользователя использовался email, при этом поле Имя пользователя исчезнет с формы регистрации. Сделать это можно, включив флаг Email as username на вкладке Login в настройках realm. После этого возвращаемся на страницу регистрации и видим что поле исчезло.

Кроме этого на странице логина поле, которое ранее называлось Имя пользователя или E-mail, теперь называется просто E-mail. Правда, пользователи, которые, например, были созданы до выставления этого флага, и у которых email отличается от имени пользователя, могут продолжать в качестве логина использовать имя пользователя и всё будет корректно работать.

Подтверждение email

Давайте включим подтверждение email у пользователей, чтобы после регистрации они не могли зайти в наше приложение, пока не подтвердят свой email. Сделать это можно, включив флаг Verify email на вкладке Login в настройках realm. И нет, после этого волшебным образом всё не заработает, нужно ещё где-то добавить конфигурацию SMTP-сервера, с которого мы будем осуществлять рассылку. Сделать это можно на вкладке Email, в настройках realm. Ниже приведён пример настроек SMTP-сервера Gmail.

Нажимаем Test connection и получаем ошибку.

Ошибка возникает из-за того, что при нажатии на Test connection должно отправиться письмо на адрес пользователя, под которым мы сейчас залогинены в Keycloak, но этот адрес не задан. Соответственно, если вы заранее задали этот email, ошибки не будет.

Давайте зададим email нашему пользователю Keycloak. Для этого перейдём в realm master на страницу Users и нажмём View all users, чтобы отобразить всех пользователей.

Перейдём на страницу редактирования нашего пользователя и зададим ему email.

Возвращаемся на страницу конфигурации SMTP-сервера, снова пробуем Test connection и видим что всё рабо... Нет, мы снова видим ошибку. Правда, уже другую.

Если все параметры подключения к SMTP-серверу заданы корректно, и вы тоже используете SMTP-сервер Gmail, то, возможно, вам поможет разрешение доступа к вашему аккаунту "ненадежных приложений" в настройках безопасности вашего аккаунта, с которого вы пытаетесь отправлять письма. Если не поможет, то да прибудет с вами Google. Если вы используете SMTP-сервер не от Gmail, то, возможно, у вас не будет подобной ошибки, а если будет, может быть, в настройках вашей почты тоже можно задать подобную конфигурацию для "ненадежных приложений".

Снова жмём Test connection и, наконец-то, получаем Success.

Содержимое письма, которое будет ждать нас на почте, представлено ниже.

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

После нажатия на кнопку Регистрация. Мы попадём на страницу с предупреждением о том, что нужно подтвердить email. На эту страницу мы будем попадать каждый раз при логине в нашем приложении, до тех пор пока не подтвердим email.

На почту нам придёт письмо с ссылкой, по которой можно подтвердить email.

После перехода по ссылке мы попадём на нашу тестовую страницу /pages/Home.vue, на которой просто выводится имя пользователя. Т.к. в настройках нашего realm мы указали Email as username, то на данной странице мы увидим email нашего пользователя.

Social Login

Теперь добавим вход через социальные сети. В качестве примера давайте рассмотрим вход с помощью Google. Для того чтобы добавить данный функционал нужно в нашем realm создать соответствующий Identity Provider. Для этого нужно перейти на страницу Identity Providers и в списке Add provider... выбрать Google.

После этого мы попадём на страницу создания Identity Provider.

Здесь нам требуется задать два обязательных параметра - Client ID и Client Secret. Взять их можно из Google Cloud Platform.

Сказ о получении ключей из Google Cloud Platform

В первую очередь нам нужно создать в Google Cloud Platform проект.

Жмём CREATE PROJECT и попадаем на страницу создания проекта.

Задаём имя, жмём CREATE, ждём некоторое время, пока не будет создан наш проект, и после этого попадаем на DASHBOARD проекта.

Выбираем в меню APIs & Services -> Credentials. И попадаем на страницу на которой мы можем создавать различные ключи для нашего приложения.

Жмём Create credentials -> OAuth client ID и попадаем на очередную страницу.

Видим, что нам так просто не хотят давать возможность создавать ключи, а сначала просят создать некий OAuth consent screen. Что ж, хорошо, жмём CONFIGURE CONSENT SCREEN и снова новая страница.

Здесь давайте выберем External. Ну как выберем, выбора, на самом деле, у нас нет, т.к. Internal доступно только пользователямGoogle Workspace и эта штука платная и нужна, в общем-то, только организациям. Нажимаем Create и попадаем на страницу OAuth consent screen. Здесь заполняем название приложения и почты и жмём SAVE AND CONTINUE.

На следующей странице можно задать так называемые области действия OAuth 2.0 для API Google. Ничего задавать не будем, жмём SAVE AND CONTINUE.

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

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

Жмём BACK TO DASHBOARD, чтобы всё это уже закончить, и попадаем на страницу, на которой мы можем редактировать все те данные, которые мы вводили на предыдущих страницах.

Жмём Credentials, затем снова Create credentials -> OAuth client ID и попадаем на страницу создания OAuth client ID. И снова нужно что-то вводить. Google, ну сколько можно?! Ниже приведены поля, которые необходимо заполнить на этой странице.

  • Application type - выбираем Web application

  • Name - пишем имя нашего приложения

  • Authorized redirect URIs - сюда пишем значение из поля Redirect URI со страницы создания Identity Provider, чтобы Google редиректил пользователей на корректный адрес Keycloak после авторизации

Жмём CREATE и, наконец-то, получаем требуемые нам Client ID и Client Secret, которые нам нужно указать на странице создания Identity Provider в Keycloak.

Заполняем поля Client ID и Client Secret и жмём Save, чтобы создать Identity Provider. Теперь вернёмся на страницу логина нашего клиентского приложения. На этой странице появится нелокализованная текстовка, добавить её можно аналогично тому, как это было сделано ранее. Ниже на скрине ниже эта проблема уже устранена.

Итак, это всё что требовалось сделать, теперь мы можем входить в наше приложение с помощью Google.

Импорт и экспорт в Keycloak

В Keycloak есть возможность импортировать и экспортировать конфигурации ваших realm'ов. Это можно использовать, например, для переноса конфигураций между различными инстансами Keycloak. Или, что более вероятно, для того чтобы можно было запускать Keycloak локально с уже готовой конфигурацией и использовать его для разработки. Это может быть полезно в тех ситуациях, когда нет возможности запустить Keycloak глобально на каком-нибудь сервере либо когда до этого инстанса Keycloak по какой-либо причине нет доступа.

Для того чтобы экспортировать конфигурацию из Keycloak, нужно перейти на страницу Export, выбрать данные, которые нужно экспортировать и нажать Export.

После этого выгрузится файл realm-export.json с конфигурацией того realm в котором мы сейчас находимся. При этом различные пароли и секреты в этом файле будут в виде **********, поэтому, прежде чем куда-то импортировать этот файл, нужно заменить все такие значения на корректные. Либо сделать это после импорта через адиминку.

Импортировать данные можно на странице Import. Либо в yml-файле Docker Compose, если вы его используете. Для этого нужно указать в переменной окружения KEYCLOAK_IMPORT путь до ранее экспортированного файла и примонтировать этот файл в контейнер с помощью volumes. Итоговый файл приведен ниже.

# For developmentversion: "3.8"services:  keycloak:    image: jboss/keycloak:12.0.2    environment:      KEYCLOAK_USER: admin      KEYCLOAK_PASSWORD: admin      KEYCLOAK_IMPORT: "/tmp/realm-export.json"    volumes:      - "./keycloak/realm-export.json:/tmp/realm-export.json"    ports:      - 8080:8080
Импорт файлов локализации

Как уже упоминалось ранее, файлы локализации можно импортировать через админку. Помимо этого у Keycloak есть Admin REST API, а именно метод POST /{realm}/localization/{locale}, с помощью которого можно это сделать. В теории это можно использовать в Docker Compose, чтобы при запуске сразу загружать все текстовки в автоматическом режиме. На практике для этого можно написать bash-скрипт и вызвать его после того как в контейнере запустится Keycloak. Пример такого скрипта приведен ниже.

#!/bin/bashDIRECT_GRANT_RESPONSE=$(curl -i --request POST http://localhost:8080/auth/realms/master/protocol/openid-connect/token --header "Accept: application/json" --header "Content-Type: application/x-www-form-urlencoded" --data "grant_type=password&username=admin&password=admin&client_id=admin-cli");export DIRECT_GRANT_RESPONSEACCESS_TOKEN=$(echo $DIRECT_GRANT_RESPONSE | grep "access_token" | sed 's/.*\"access_token\":\"\([^\"]*\)\".*/\1/g');export ACCESS_TOKENcurl -i --request POST http://localhost:8080/auth/admin/realms/list-keep/localization/ru -F "file=@ru.json" --header "Content-Type: multipart/form-data" --header "Authorization: Bearer $ACCESS_TOKEN";

И в докер образе jboss/keycloak даже есть возможность запускать скрипты при старте (см. раздел Running custom scripts on startup на странице докер образа). Но запускаются они до фактического старта Keycloak. Поэтому пока я оставил данный вопрос не решенным. Если у кого-то есть идеи как это можно красиво сделать - оставляйте их в комментариях.

Заключение

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

Подробнее..

Из Vue 2 на Vue 3 Migration Helper

09.06.2021 20:16:20 | Автор: admin

Предистория

Была у меня курсовая по веб-разработке, делать очередной интернет-магазин как-то не хотелось, и решил я написать помощник миграции из Vue 2 (options-api) в Vue 3 (composition-api) с авторазделением на композиции с помощью алгоритма Косарайю по поиску областей сильной связности

Для тех, кто не в теме, поясню, так выглядит код с options-api:

export default {  data () {    return {      foo: 0,      bar: 'hello',    }  },  watch: {    ...  },  methods: {    log(v) {      console.log(v);    },  },  mounted () {    this.log('Hello');  }}

и примерно так с composition-api:

export default {  setup (props) {    const foo = reactive(0);    const bar = reactive('hello');    watch(...);    const log = (v) => { console.log(v); };    onMounted(() => { log('hello'); });    return {      foo,      bar,      log,    };  }}

Автоматическое разделение на композиции

Дабы не отходить от самой идеи композиций, помимо трансляции кода под новый синтаксис composition-api, было принято решение добавить и возможность разделения монолитного компонента на самостоятельные композиции, и их последующее переиспользование в главном компоненте. Как же это сделать?

Сначала зададимся вопросом, что же такое композиции? Для себя я ответил так:

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

Блоками кода в нашем случае будем считать: свойства data, методы, вотчеры, хуки, и все то, из чего строится компонент Vue.

Теперь определимся на счёт зависимостей блоков кода между собой. С этим во Vue достаточно просто:

  • Если computed, method, hook, provide свойство внутри себя использует другие свойства, то оно от них и зависит

  • Если на свойство навешен вотчер, то вотчер зависит от наблюдаемого им свойства

  • и так далее :)

data: () => ({  array: ['Hello', 'World'], // block 1}),watch: {  array() { // block 2 (watch handler) depends on block 1    console.log('array changed');  },},computed: {  arrayCount() { // block 3    return this.array.length; // block 3 depends on block 1  },},methods: {  arrayToString() { // block 4    return this.array.join(' '); // block 4 depends on block 1  }},

Допустим, мы смогли пройтись по коду и выделить все-все зависимости свойств между собой. Как всё это делить на композиции?

А теперь абстрагируемся от Vue, проблемы миграции, синтаксиса и т.д. Оставим только сами свойства и их зависимости друг с другом.

Выделим из этого ориентированный граф, где вершинами будут свойства, а ребрами - зависимости между свойствами. А теперь самое интересное!

Алгоритм Косарайю

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

Никогда бы не подумал, что простое переписывание реализации из C на TS может быть таким проблемным :)

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

Поиск зависимостей

Примечание: во всех функциях компонента в options-api свойства доступны через this

Здесь немного грусти, поскольку искать зависимости в .js приходится так:

const splitter = /this.[0-9a-zA-Z]{0,}/const splitterThis = 'this.'export const findDepsByString = (  vueExpression: string,  instanceDeps: InstanceDeps): ConnectionsType | undefined => {  return vueExpression    .match(splitter)    ?.map((match) => match.split(splitterThis)[1])    .filter((value) => instanceDeps[value])    .map((value) => value)

Да, просто проходясь регуляркой по строкому представлению функции в поисках всего, что идет после this. :(

Более продвинутый вариант, но такой же костыльный:

export const findDeps = (  vueExpression: Noop,  instanceDeps: InstanceDeps): ConnectionsType | undefined => {  const target = {}  const proxy = new Proxy(target, {  // прокси, который записывает в объект вызываемые им свойства    get(target: any, name) {      target[name] = 'get'      return true    },    set(target: any, name) {      target[name] = 'set'      return true    }  })  try {    vueExpression.bind(proxy)() // вызываем функцию в скоупе прокси    return Object.keys(target) || [] // все свойства которые вызвались при this.  } catch (e) { // при ошибке возвращаемся к первому способу    return findDepsByString(vueExpression.toString(), instanceDeps) || []  }}

При использовании прокси вышло несколько проблем:

  • не работает с анонимными функциями

  • при использовании вызывается сама функция а если вы там пентагон взламываете?

Создание файлов и кода

Вспомним зачем мы тут собрались: миграция.

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

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

const toString = (item: any): string => {  if (Array.isArray(item)) {    // array    const builder: string[] = []    item.forEach((_) => {      builder.push(toString(_)) // wow, it's recursion!    })    return `[${builder.join(',')}]`  }  if (typeof item === 'object' && item !== null) {    // object    const builder: string[] = []    Object.keys(item).forEach((name) => {      builder.push(`${name}: ${toString(item[name])}`) // wow, it's recursion!    })    return `{${builder.join(',')}}`  }  if (typeof item === 'string') {    // string    return `'${item}'`  }  return item // number, float, boolean}// Exampleconsole.log(toString([{ foo: { bar: 'hello', baz: 'hello', }}, 1]);// [{foo:{bar: 'hello',baz: 'hello'}},1]  т.е. то же самое, что и в коде

Про остальной говнокод я тактично промолчу :)

Итоговые строки мы записываем в новые файлы через простой fs.writeFile() в ноде и получаем результат

Пример работы

Собрав всё это в пакет, протестировав и опубликовав, можно наконец увидеть результат работы.

Ставим пакет vue2-to-3 глобально (иначе не будет работать через консоль) и проверяем!

Пример HelloWorld.js:

export default {  name: 'HelloWorld',  data: () => ({    some: 0,    another: 0,    foo: ['potato'],  }),  methods: {    somePlus() {      this.some++;    },    anotherPlus() {      this.another++;    },  },};

Пишем в консоли: migrate ./HelloWorld.js и получаем на выход 3 файла:

// CompositionSome.jsimport { reactive } from 'vue';export const CompositionSome = () => {  const some = reactive(0);  const somePlus = () => { some++ };  return {    some,    somePlus,  };};// CompositionAnother.jsimport { reactive } from 'vue';export const CompositionAnother = () => {  const another = reactive(0);  const anotherPlus = () => { another++ };  return {    another,    anotherPlus,  };};// HelloWorld.jsimport { reactive } from 'vue';import { CompositionSome } from './CompositionSome.js'import { CompositionAnother } from './CompositionAnother.js'export default {  name: 'HelloWorld',  setup() {    const _CompositionSome = CompositionSome();    const _CompositionAnother = CompositionAnother();    const foo = reactive(['potato']);    return {      foo,      some: _CompositionSome.some,      somePlus: _CompositionSome.somePlus,      another: _CompositionAnother.another,      anotherPlus: _CompositionAnother.anotherPlus,    };  },};

Итого

На данный момент все это доступно и работает, но ещё есть некоторые баги со строковым представлением не анонимных функций и путями (в некоторых случаях фатально для linux систем)

В планах запилить миграцию для single-file-components и .ts файлов (сейчас работает только для .js)

Спасибо за внимание!

npm, git

Подробнее..

Идеальное Vue приложение на Typescript

04.02.2021 02:20:23 | Автор: admin

Пока Vue 3 официально еще не вышел, а продакшене в основном 2 версия - я хочу поговорить о типизации и о том, что она все еще не идеальна во Vue. И сегодня мы попробуем создать идеальное приложение с типизацией на typescript сделав упор на code style, пропагандируя vue style guide и прочие обычно не значащие вещи, которые были придуманы умными людьми!

Ремарка

Стоит учитывать, что автор пишет первый свой пост и это пагубно скажется на его качестве и возможно на качестве всего контента на Хабре!

Почему "идеальное"?

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

Какие проблемы с типизацией у Vue?

Версия 2 принесла и популизировала typescript в компонентах и в хранилище (store), но оставила бреши в связи их между собой, store все еще не подсказывает типизацию в компонентах, и это плохо! Следить в ручную можно за чем угодно, но зачем, если можно это исправить и автоматизировать? Чтобы улучшить типизацию мы будем использовать vue-property-decorator для компонентов и vuex-smart-module для прокидывания типов из стора.

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

Тут все просто, сначала вызываем vue cli и выбираем галочки!

vue create habratest

Наш выбор - vue2, все features, Vue Router History Mode, Vue Class Components, и другие настройки как на скриншоте ниже. Тесты настраиваем по вкусу, в этой статье их касаться не будем.

Vue CLI настройки создания проектаVue CLI настройки создания проекта

Также я рекомендую в графе "Разнести конфиги всего и вся в разные файлы" ставить галочку.

Стили и красота, пока вся тяжесть человеческого прогресса грузится вам в node_modules

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

Настройка eslint

Первым делом идем менять настройки! Меняем extends на код ниже - это позволит eslint проверять ваш код более строго - в соответствии с Vue style guide - в соответствии с низшим приоритетом ошибок - recommended.

  extends: [    'plugin:vue/recommended',    '@vue/typescript/recommended'  ],

Это половина победы! Изменение eslint таким образом поможет вам строже соблюдать style guide и в существующем проекте, но будьте осторожны, нередко что-то может пойти не так.

Да, это даже во Vue cli шаблоне исправит пару ошибокДа, это даже во Vue cli шаблоне исправит пару ошибокЧто в eslint можно отлючить

Ничего. Это все поможет вам в написании кода и менять я ничего не рекомендую. Единственное правило, которое можно исключить из проверки - это директива v-html, которая нередко встречается в старой кодовой базе. Но подобное подчеркивание позволит собрать проект и будет лишним напоминанием для вас, так что лучше оставить.

Заодно давайте включим правила для отслеживания одинарных кавычек и удаления лишних точек с запятой - добавим в массив rules в этом же файле следующие строку:

"semi": [2, "never"],"quotes": [2, "single", { "avoidEscape": true }]

Установка и настройка "правильного" Vuex

npm install vuex-smart-module

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

  1. От автора vue-property-decorator, который ставится по умолчанию для class-style components;

  2. Максимально близок к обычной версии стора.

Идем src/main.ts и убираем store оттуда. Для типизации нам не нужно определение this.$store на прототипе, мы будем его импортировать.

Создаем в папке store директорию modules и в ней папку habrModule, в ней файлы: index.ts, actions.ts, getters.ts, mutations.ts, state.ts.

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

(!) Также я не использую в корне стора никаких стейтов и геттеров, если это понадобится - попробуйте придумать модуль для этого, например appSettings

Файл src/store/modules/habrModule/state.ts:

export default class HabrState {  value = 'hello';}

Файл src/store/modules/habrModule/getters.ts:

import { Getters } from 'vuex-smart-module'import HabrState from './state'export default class HabrGetters extends Getters<HabrState> {  /**   * Параметризированный greeting, не кэшируется Vuex   * @param name    * @example module.getters.greeting("Habr!")   */  greeting(name: string): string {    return this.state.value + ', ' + name  }  /**   * Не параметризированный greeting, кэшируется Vuex   * @example module.getters.greetingDefault   */  get greetingDefault(): string {    return this.getters.greeting('Habr!')  }}

Файл src/store/modules/habrModule/index.ts:

import { Module } from 'vuex-smart-module'import getters from './getters'import state from './state'const habr = new Module({  state: state,  getters: getters,})export default habr

Мы получили маленький, но гордый модуль! Осталось зарегистрировать его в store

Файл src/store/index.ts

import Vue from 'vue'import Vuex from 'vuex'import { Module, createStore } from 'vuex-smart-module'import habr from './modules/habrModule'Vue.use(Vuex)const root = new Module({  modules: {    habr,  },})const store = createStore(root)export default storeexport const habrModule = habr.context(store)

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

Чтобы использовать модуль - нужно импортировать его из стора. Далее он будет типизированным.

Пример компонента
<template>  <div class="home">    <img      alt="Vue logo"      src="../assets/logo.png"    >    <HelloComponent msg="Welcome to Your Vue.js + TypeScript App" />    {{ computedTest }}  </div></template><script lang="ts">import { Component, Vue } from 'vue-property-decorator'import { habrModule } from '@/store'@Component({  name: 'HomeView',  components: {    HelloComponent: () => import('@/components/HelloComponent.vue'),  },})export default class HomeView extends Vue {  get computedTest() {    return habrModule.getters.greetingDefault  }}</script>

И подсказочки будут работать!

И подсказочки работают!И подсказочки работают!

И вроде бы почти закончили! Осталось только...

Рефакторинг

  1. Идем в папке src/views и переименовываем там файлы в HomeView.vue и AboutView.vue, затем в src/components и переименуем там файл в HelloComponent.vue. Также дописываем им name в аннотацию компонента.

  2. Идем в src/router/index.ts и делаем импорт HomeView.vue динамическим - чтобы было одинаково все и везде, это рекомендация vue style guide.

  3. Поправляем поломавшиеся импорты.

  4. Запускаем автоматический линтер через npm run lint.

  5. Довольные идем пить кофе и отдыхать.

О чем в статье ни слова

Я постарался добить качество кода до хорошего уровня, следовать большинству заповедей Vue, а также добавил немного от себя, но о некоторых вещах я не рассказал и не использовал:

  1. Component-naming - Vue имеет некоторые правила касающиеся названия компонентов, которые не отслеживаются eslint.

    1. Например компоненты, которые используются лишь раз - следует называть c префиксом The, к примеру TheNavigationComponent.vue - так бы я назвал панель навигации, которая лежала бы в корне и больше никуда не импортировалась.

    2. Мой принцип названия и разделения между views/components: если компонент импортируется только в роутер - он View, иначе - Component (с опциональным The), суффиксы позволяют выполнять другую рекомендацию - названия компонентов не должны быть из одного слова (Navigation.vue - ALERT!), потому что могут совпадать с названиями html тегов по умолчанию (текущими и будущими).

    3. Названия компонентов в темплейтах: <MyComponent /> vs <my-component /> Vue разрешает и то и другое, но я рекомендую CamelCase для удобства (например - подсвечивается большинством ide).

  2. Разбиение хранилища на модули - опционально по стайл гайдам, но я приверженец делать его всегда.

  3. Динамические импорты компонентов - это по умолчанию, всегда так делайте (это Components: () => import(path) ), webpack умный и такие импорты в 90% случаях уменьшат вам время полной загрузки страницы, а в оставшихся 10% - ничего не поменяют, так что оно того стоит. Это просто, одинаково и удобно.

  4. Всегда в компонентах прописывайте имя (даже с учетом того, что оно опционально).

  5. Старайтесь названия файлов компонентов согласовывать с названием классов (и именами внутри).

  6. Вызовы к api - ТОЛЬКО из store, об этом вам скажет любой Vue разработчик.

От всего остальное вас должен спасти eslint и голова, но style guide почитайте! :)

Конец!

Я надеюсь вам понравилось, качество кода и стайл гайды - это интересно, а также порождает продуктивные дискуссии в командах, обсуждайте такие вещи, это принесет удовлетворение и иногда поднимет самооценку! Но без негатива!

Гитхаб получившегося приложения: github

Также все это получится с небольшими доработками запустить и на Vue 3, у меня получилось, но так как я в нем не до конца еще уверен и разобрался - статья про уходящий Vue 2.

P. S.

Буду рад любому развернутому фидбеку!

Было бы вам интересно почитать нечто подобное для "идеального" тестирования Vue приложений?

Подробнее..

Из песочницы vuex typescript vuexok. Велосипед, который поехал и обогнал всех

07.11.2020 22:21:21 | Автор: admin
Доброго времени суток.

Как и многие разработчики, я в свободное от работы время пишу свойотносительнонебольшой проект. Раньше писал на react, а на работе используется vue. Ну и что бы прокачаться во vue начал пилить свой проект на нем. Сначала всё было хорошо, прямо-таки радужно, пока я не решил, что надо бы еще прокачаться и в typescript. Так в моем проекте появился typescript. И если с компонентами всё былонеплохо, то с vuex всё оказалось печально. Так мне пришлось пройти все 5 стадий принятия проблемы, ну почти все.

Отрицание


Основные требования для стора:

  1. В модулях должны работать типы typescript
  2. Модули должно быть легко использовать в компонентах, должны работать типы для стейта, экшенов, мутаций и геттеров
  3. Не придумывать новое api для vuex, надо сделать так,чтобы как-то типы typescript заработали с модулями vuex, чтобы не приходилось разом переписывать всё приложение
  4. Вызов мутаций и экшенов должен быть максимально простым и понятным
  5. Пакет должен быть как можно меньше
  6. Не хочу хранить константы с именами мутаций и экшенов
  7. Оно должно работать (А как же без этого)

Не может быть что у такого уже зрелого проекта как vuex не было нормальной поддержки typescript. Ну-с, открываемGoogleYandex и погнали. Я был уверен на 100500% что с typescript всё должно быть отлично (как же я ошибался). Есть куча разных попыток подружить vuex и typescript. Приведу несколько примеров, которые запомнились, без кода чтобы не раздувать статью. Всё есть в документации по ссылкам ниже.

vuex-smart-module

github.com/ktsn/vuex-smart-module
Добротно, даже очень. Всё при себе, но лично мне не понравилось то, что для экшенов, мутаций, стейта, геттеров надо создавать отдельные классы. Это, конечно, вкусовщина, но это я и мой проект) И в целом вопрос типизации решен не до конца (ветка комментариев с объяснением почему).

Vuex Typescript Support

Хорошая попытка, но что-то много переписывать, да и вообще не принялось сообществом.

vuex-module-decorators

Казалось, что это идеальный способ подружить vuex и typescript. Похоже наvue-property-decorator, который я использую в разработке, работать с модулем можно как с классом, в общем супер, но

Но наследования нет. Классы модулей не корректно наследуются и issue на эту проблему висят уже очень давно! А без наследования будет очень много дублирования кода. Блин

Гнев


Дальше было совсем уже не очень, ну или так же идеального решения нет. Это тот самый момент, когда говоришь себе: Ну зачем я начал писать проект на vue? Ну ты же знаешь react, ну писал бы на react, там бы таких проблем не было! На основной работе проект на vue и тебе надо в нем прокачаться зашибись аргумент. А оно стоит потраченных нервов и бессонных ночей? Сиди как все, пиши компонентики, нет, тебе больше всех надо! Бросай этот vue! Пиши на react, прокачивайся в нем, за него и платят больше!

В тот момент я был готов хейтить vue как никто другой, но это были эмоции, и интеллект всё же был выше этого. Vue имеет (на мой субъективный взгляд) много преимуществ над react, но совершенства не бывает, как и победителей на поле сражений. И vue, и react по-своему хороши, а так как уже значительная часть проекта написана на vue, то было бы максимально глупо сейчас переходить на react. Надо было решить, что же делать с vuex.

Торг


Ну что же, дела обстоят не очень хорошо. Может тогда vuex-smart-module? Этот пакет вроде хорош, да, надо создавать много классов, но работает отлично же. Или может попробовать прописывать типы для мутаций и экшенов руками в компонентах и использовать чистый vuex? Там и vue3 c vuex4 на подходе, может у них дела с typescript обстоят лучше. Так что давай попробуем чистый vuex. И вообще на работу проекта это не влияет, всё же работает, типов нет, но вы держитесь. И держимся же)

Сначала так и начал делать, но код получается монструозный

Депрессия


Надо было двигаться дальше. Но куда неизвестно. Это был совсем отчаянный шаг. Решил сделатьконтейнер состояния с нуля. Код был набросан за пару часов. И получилось даже хорошо. Типы работают, состояние реактивно и даже наследование есть. Но вскоре агония отчаяния стала отступать, а здравый смысл возвращаться. В общем, эта идея отправилась на свалку. По большому счету это был паттерн глобальной шины событий. А он хорош только для не больших приложений. И вообще писать свой vuex всё же совсем перебор (по крайней мерев моей ситуации). Тут я уже догадывался, что совсем загнался. Но отступать было уже поздно.

Но если кому интересно, то код тут: (Наверное зря добавил этот фрагмент, но путь будет)

Слабонервным не смотреть
const getModule = <T>(name:string, module:T) => {  const $$state = {}  const computed: Record<string, () => any> = {}  Object.keys(module).forEach(key => {    const descriptor = Object.getOwnPropertyDescriptor(      module,      key,    );    if (!descriptor) {      return    }    if (descriptor.get) {      const get = descriptor.get      computed[key] = () => {        return get.call(module)      }    } else if (typeof descriptor.value === 'function') {      // @ts-ignore      module[key] = module[key].bind(module)    } else {      // @ts-ignore      $$state[key] = module[key]    }  })  const _vm = new Vue({    data: {      $$state,    },    computed  })  Object.keys(computed).forEach((computedName) => {    var propDescription = Object.getOwnPropertyDescriptor(_vm, computedName);    if (!propDescription) {      throw new Error()    }    propDescription.enumerable = true    Object.defineProperty(module, computedName, {      get() { return _vm[computedName as keyof typeof _vm]},      // @ts-ignore      set(val) { _vm[computedName] = val}    })  })  Object.keys($$state).forEach(name => {    var propDescription = Object.getOwnPropertyDescriptor($$state,name);    if (!propDescription) {      throw new Error()    }    Object.defineProperty(module, name, propDescription)  })  return module}function createModule<  S extends {[key:string]: any},  M,  P extends Chain<M, S>>(state:S, name:string, payload:P) {  Object.getOwnPropertyNames(payload).forEach(function(prop) {    const descriptor = Object.getOwnPropertyDescriptor(payload, prop)    if (!descriptor) {      throw new Error()    }    Object.defineProperty(      state,      prop,      descriptor,    );  });  const module = state as S & P  return {    module,    getModule() {      return getModule(name, module)    },    extends<E>(payload:Chain<E, typeof module>) {      return createModule(module, name, payload)    }  }}export default function SimpleStore<T>(name:string, payload:T) {  return createModule({}, name, payload)}type NonUndefined<A> = A extends undefined ? never : A;type Chain<T extends {[key:string]: any}, THIS extends {[key:string]: any}> = {  [K in keyof T]: (    NonUndefined<T[K]> extends Function       ? (this:THIS & T, ...p:Parameters<T[K]>) => ReturnType<T[K]>      : T[K]  )}


ПринятиеРождение велосипеда который обогнал всех. vuexok


Для нетерпеливых код тут, краткая документация тут.

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

Простейший модуль с vuexok выглядит так:

import { createModule } from 'vuexok'import store from '@/store'export const counterModule = createModule(store, 'counterModule', {  state: {    count: 0,  },  actions: {    async increment() {      counterModule.mutations.plus(1)    },  },  mutations: {    plus(state, payload:number) {      state.count += payload    },    setNumber(state, payload:number) {      state.count = payload    },  },  getters: {    x2(state) {      return state.count * 2    },  },})

Ну вроде почти как vuex, хотя что там на 10й строке?

counterModule.mutations.plus(1)

Воу! А это легально? Ну с vuexok да, легально) Метод createModule возвращает объект, который в точности повторяет структуру объекта модуля vuex, только без свойства namespaced, и мы можем использовать его для вызова мутаций и экшенов или для получения стейта и геттеров, причем все типы сохраняются. Причем из любого места, где его можно импортировать.

А что там с компонентами?

А с ними все отлично, так как фактически это vuex, то в принципе ничего не поменялось, commit, dispatch, mapState и т.д. работают как и раньше.

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

import Vue from 'vue'import { counterModule } from '@/store/modules/counterModule'import Component from 'vue-class-component'@Component({  template: '<div>{{ count }}</div>'})export default class MyComponent extends Vue {  private get count() {    return counterModule.state.count // type number  }}

Свойство state в модуле реактивно, как и в store.state, так что чтобы использовать состояние модуля в компонентах Vue достаточно просто вернуть часть состояния модуля в вычисляемом свойстве. Есть только одна оговорка. Я намеренно сделал стейт Readonly типом, не хорошо так стейт vuex изменять.

Вызов экшенов и мутаций прост до безобразия и тоже сохраняются типы входных параметров

 private async doSomething() {   counterModule.mutations.setNumber(10)   // Аналогично вызову this.$store.commit('counterModule/setNumber', 10)   await counterModule.actions.increment()   // Аналогично вызову await this.$store.dispatch('counterModule/increment') }

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

const unwatch = jwtModule.watch(  (state) => state.jwt,  (jwt) => console.log(`New token: ${jwt}`),  { immediate: true },)

Итак, что мы имеем:

  1. типизированный стор есть
  2. типы работают в компонентах есть
  3. апи как у vuex и всё что было до этого на чистом vuex не ломается есть
  4. декларативная работа со стором есть
  5. маленький размер пакета (~400 байт gzip) есть
  6. не иметь необходимости хранить в константах названия экшенов и мутаций есть
  7. оно должно работать есть

Вообще странно что такой прекрасной фичи нет во vuex из коробки, это же офигеть как удобно!
Что касается поддержки vuex4 и vue3 не проверял, но судя по докам должно быть совместимо.

Так же решены проблемы представленные в этих статьях:

Vuex решаем старый спор новыми методами
Vuex нарушает инкапсуляцию

Влажные мечты:


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

Как это сделать в контексте типов typescript хер его знает. Но если бы можно было делать так:

{  actions: {    one(injectee) {       injectee.actions.two()    },    two() {      console.log('tada!')    }}

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

Вот такое приключение с vuex и typescript. Ну, вроде выговорился. Спасибо за внимание.
Подробнее..

Пишем мессенджер на Vue в облаке Amazon

16.01.2021 20:09:22 | Автор: admin

Разберем, как использовать облачный сервис Amazon для созданиямессенджера Chattyмногопользовательского чат-приложения в реальном времени с одной комнатой с помощью фреймворка Vue иAWS Amplify. Настроим регистрацию пользователей и хранение данных.

Темы, которые мы рассмотрим:

  • Аутентификация

  • GraphQL API с AWS AppSync

  • Настроить Amplify DataStore

  • Развертывание через консоль Amplify

  • Удаление сервисов

  • Приложение и устранение неисправностей

Результат можно увидеть тут: https://master.d58xs5f4j0v44.amplifyapp.com/

Предварительные условия:

  • Регистрация за $1 в сервисах Amazon AWS уже произведена

  • Бесплатный редактор Visual Studio Code установлен, локализован и установлен npm:

    • Node:14.7.0. VisitNode

    • npm:6.14.7.

npm install -g npm

Начало работы - Создание приложения

Чтобы начать, нам сначала нужно создать новый проект Vue используя:

npm install -g @vue/clivue create amplify-datastore
  • ?Выберитепредустановку: поумолчанию [Vue 2] (babel, eslint)

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

cd amplify-datastore npm run serve

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

Откроем папку проекта C:\Users\Admin\amplify-datastore> в редакторе Visual Studio Code:

Обратите внимание на окончание строк LFОбратите внимание на окончание строк LF

Мы видим шаблон приложения Vue, и оно работает локально.

Остановим его CTRL+C что бы продолжить.

Давайте теперь установим API AWS Amplify и библиотеку AWS Amplify Vue:

npm install --save aws-amplify @aws-amplify/ui-vue moment

Установка интерфейса командной строки AWS Amplify

Далее мы установим интерфейс командной строки AWS Amplify:

npm install -g @aws-amplify/cli

Теперь нам нужно настроить CLI с нашими учетными данными:

amplify configure

Если вы хотите посмотреть видео-пошаговое руководство по процессу настройки, щелкнитездесь.

Здесь мы рассмотримamplify configureнастройку.После входа в консоль AWS продолжайте:

  • Укажите регион AWS:eu-central-1 (Франкфурт)

  • Укажите имя пользователя нового пользователя IAM:ampify-datastore

В консоли AWS нажмитеДалее: Разрешения,Далее: Теги,Далее: ОбзориСоздать пользователя,чтобы создать нового пользователя IAM.Затем вернитесь в командную строку и нажмите Enter.

  • Введите ключ доступа вновь созданного пользователя:

    accessKeyId:(<YOURACCESSKEYID>)secretAccessKey:(<YOURSECRETACCESSKEY>)

  • Имя профиля: поумолчанию

Чтобы просмотреть нового созданного пользователя IAM, перейдите на панель управления по адресуhttps://console.aws.amazon.com/iam/home?region=eu-central-1#/users.Также убедитесь, что ваш регион соответствует вашему выбору.

Инициализация нового проекта

amplify init
  • Введите название проекта:ampifydatastore

  • Введите имя для среды:dev

  • Выберите редактор по умолчанию:Visual Studio Code

  • Выберите тип приложения, которое вы создаетеjavascript

  • Какую среду JavaScript вы используетеvue

  • Путь к исходному каталогу:src

  • Путь ккаталогураспространения:dist

  • Команда сборки:npm run-script build

  • Команда запуска:npm run-script serve

  • Вы хотите использовать профиль AWS? да

  • Пожалуйста, выберите профиль, который вы хотите использовать по умолчанию

Теперь интерфейс командной строки AWS Amplify инициировал новый проект, и вы увидите новую папку:ampify.Файлы в этой папке содержат конфигурацию вашего проекта.

<amplify-app>    |_ amplify      |_ .config      |_ #current-cloud-backend      |_ backend      team-provider-info.json

Добавление аутентификации

Чтобы добавить аутентификацию в наш проект Amplify, используем следующую команду:

amplify add auth

При появлении запроса выберите

  • Вы хотите использовать конфигурацию аутентификации и безопасности поумолчанию?:Конфигурация по умолчанию

  • Как вы хотите, чтобы пользователи могли входить в систему при использовании вашего пула пользователей Cognito ?:Имя пользователя

  • Вы хотите настроить дополнительные параметры?Да, я хочу внести дополнительные изменения.

  • Какие атрибуты необходимы для регистрации?(Нажмите <пробел>, чтобы выбрать, <a>, чтобы переключить все, <i>, чтобы инвертировать выделение):Электронная почта

  • Вы хотите включить какие-либо из следующих возможностей?(Нажмите <пробел>, чтобы выбрать, <a>, чтобы переключить все, <i>, чтобы инвертировать выделение):Нет

Чтобы ничего не выбрать, просто нажмитеEnterпоследнюю опцию.

Теперь мы запустим команду push, и облачные ресурсы будут созданы в нашей учетной записи AWS.

amplify push Current Environment: dev| Category | Resource name      | Operation | Provider plugin   || -------- | ------------------ | --------- | ----------------- || Auth     | amplifyappuuid     | Create    | awscloudformation |? Are you sure you want to continue? Yes

Чтобы быстро проверить созданныйпул пользователей Cognito,запустим:

amplify status

Чтобы получить доступ кконсоли AWS Cognitoв любое время, перейдите на панель управления по адресуhttps://console.aws.amazon.com/cognito/.

Настраиваем приложение Vue

Теперь наши ресурсы созданы, и мы можем начать их использовать!

Первое, что нам нужно сделать, это настроить наше приложение Vue, чтобы оно было осведомлено о нашем новом проекте AWS Amplify.Мы можем сделать это, обратившись к автоматически сгенерированномуaws-exports.jsфайлу, который теперь находится в srcпапке.

Чтобы настроить приложение, откройтеmain.jsи добавьте следующий код:

import Vue from 'vue'import App from './App.vue'import Amplify from 'aws-amplify';import '@aws-amplify/ui-vue';import aws_exports from './aws-exports';Amplify.configure(aws_exports);Vue.config.productionTip = falsenew Vue({  render: h => h(App),}).$mount('#app')

Теперь наше приложение готово к использованию сервисов AWS.

Использование компонента аутентификации

AWS Amplify предоставляет компоненты пользовательского интерфейса, которые можно использовать в своем приложении.Добавим эти компоненты в проект

Чтобы использовать компонент Authenticator, добавьте его вsrc / App.vue:

<template>  <div id="app">    <amplify-authenticator>      <div>        <h1>Hey, {{user.username}}!</h1>        <amplify-sign-out></amplify-sign-out>      </div>    </amplify-authenticator>  </div></template>

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

Чтобы просмотреть всех созданных пользователей, вернитесь напанель управленияCognito поадресуhttps://console.aws.amazon.com/cognito/.

В качестве альтернативы мы также можем использовать

amplify console auth

Доступ к данным пользователя

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

<script>import { AuthState, onAuthUIStateChange } from '@aws-amplify/ui-components'export default {  name: 'app',  data() {    return {      user: { },    }  },  created() {    // authentication state managament    onAuthUIStateChange((state, user) => {      // set current user and load data after login      if (state === AuthState.SignedIn) {        this.user = user;      }    })  }}</script>

Добавление GraphQL API

Чтобы добавить GraphQL API, мы можем использовать следующую команду:

amplify add api

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

  • Пожалуйста, выберите одну из нижеуказанных службGraphQL

  • Укажите имя API:ChattyAPI

  • Выберите тип авторизации по умолчанию дляключаAPIAPI

  • Введите описание ключа API:(пусто)

  • Через сколько дней истечет срок действия ключа API (1-365):7

  • Вы хотите настроить дополнительные параметры для GraphQL API?Да, я хочу внести дополнительные изменения.

  • Настроить дополнительные типы авторизации?N

  • Настроить обнаружение конфликтов?Y

  • Выберите стратегию разрешения по умолчаниюAuto Merge

  • Вы хотите изменить настройки по умолчанию для каждой модели? N

  • У вас есть аннотированная схема GraphQL? N

  • Вам нужно управляемое создание схемы? Y

  • Что лучше всего описывает ваш проект: один объект с полями (например, Todo с идентификатором, именем, описанием)

  • Вы хотите отредактировать схему сейчас? Y

Чтобы ничего не выбрать, просто нажмитеEnter.

При появлении запроса обновите схему до следующего:

type Chatty @model {  id: ID!  user: String!  message: String!  createdAt: AWSDateTime}

Это позволит нам отображать сообщения каждого пользователя вместе с датой и временем создания.

Примечание: не забудьте сохранить изменения в файле схемы!

Затем давайте перенесем конфигурацию в нашу учетную запись:

amplify push
  • Вы уверены что хотите продолжить?да

  • Вы хотите сгенерировать код для недавно созданного GraphQL API?Да

  • Выберите целевой язык генерации кодаjavascript

  • Введите шаблон имени файла для запросов, мутаций и подписокgraphql src / graphql / ** / *. Js

  • Вы хотите сгенерировать / обновить все возможные операции GraphQL - запросы, изменения и подписки?Да

  • Введите максимальную глубину инструкции [увеличьте значение по умолчанию, если ваша схема глубоко вложена]2

Обратите внимание наконечную точку GraphQLиКЛЮЧ API.

На этом шаге был создан новый API AWS AppSync.Используйте команду ниже, чтобы получить доступ к панели управления AWS AppSync.Убедитесь, что ваш регион правильный.

amplify console api
  • Пожалуйста, выберите одну из нижеуказанных службGraphQL

Настроить Amplify DataStore

Установка Amplify DataStore

Далее мы установим необходимые зависимости:

npm install --save @aws-amplify/core @aws-amplify/datastore

Генерация модели данных

Затем мы сгенерируем модели для доступа к нашим сообщениям из нашегоChattyAPI.

amplify codegen models

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

Теперь интерфейс командной строки AWS Amplify сгенерировал необходимые модели данных, и вы увидите новую папку в своем источнике:models.Файлы в этой папке содержат классы и схему вашей модели данных.

<amplify-app>    |_ src      |_ models

Самые нетерпеливые могут заглянуть в репозиторий проекта https://github.com/lazy-fox-code/amplify-datastore и уточнить куда какой код добавлять. Основная логика приложения описывается в App.vue и следующие разделы будут разбирать именно этот файл.

Структура App.vue содержит шаблон <template> в котором присутствуют блоки <divid="app"> с формами, кнопками, авторизацией и прочими деталями приложения. Затем идет раздел <script> в котором импортируются и применяются модели и методы приложения. Затем следуют стили оформления приложения.

Создание сообщения

Теперь, когда GraphQL API и модели данных созданы, мы можем начать с ними взаимодействовать!

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

import { DataStore } from "@aws-amplify/datastore";import { Chatty } from "./models";await DataStore.save(new Chatty({  user: "amplify-user",  message: "Hi everyone!",  createdAt: new Date().toISOString()}))

Это создаст запись локально в вашем браузере и синхронизирует ее в фоновом режиме с помощью базового API GraphQL.

Запрос данных

Давайте теперь посмотрим, как мы можем запрашивать данные с помощью Amplify DataStore.Чтобы запросить нашу модель данных, мы будем использовать запрос и предикат, чтобы указать, что нам нужны все записи.

import { DataStore, Predicates } from "@aws-amplify/datastore";import { Chatty } from "./models";const messages = await DataStore.query(Chatty, Predicates.ALL);

Это вернет массив сообщений, которые мы можем отобразить в нашем пользовательском интерфейсе.

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

Найдите все поддерживаемые фильтры вразделе "Запрос с предикатами"

Создание UI

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

<template>  <div v-for="message of sorted" :key="message.id">    <div>{{ message.user }} - {{ moment(message.createdAt).format('YYYY-MM-DD HH:mm:ss')}})</div>    <div>{{ message.message }}</div>  </div></template><script>import { Auth } from "aws-amplify";import { DataStore, Predicates } from "@aws-amplify/datastore";import { Chatty } from "./models";import moment from "moment";export default {  name: 'app',  data() {    return {      user: {},      messages: [],    }  },  computed: {    sorted() {      return [...this.messages].sort((a, b) => -a.createdAt.localeCompare(b.createdAt));    }  },  created() {    this.currentUser();  },  methods: {    moment: () => moment(),    currentUser() {      Auth.currentAuthenticatedUser().then(user => {        this.user = user;        this.loadMessages();      });    },    loadMessages() {      DataStore.query(Chatty, Predicates.ALL).then(messages => {        this.messages = messages;      });    },  }}</script>

Создание сообщения

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

<template>  <form v-on:submit.prevent>    <input v-model="form.message" placeholder="Enter your message..." />    <button @click="sendMessage">Send</button>  </form></template><script>export default {  data() {    return {      form: {},    };  },   methods: {    sendMessage() {      const { message } = this.form      if (!message) return;      DataStore.save(new Chatty({        user: this.user.username,        message: message,        createdAt: new Date().toISOString()      })).then(() => {        this.form = { message: '' };        this.loadMessages();      }).catch(e => {        console.log('error creating message...', e);      });    },  }}</script>

Удаление всех сообщений

Одним из основных преимуществ работы с Amplify DataStore является возможность запускать пакетные мутации без использования серии отдельных операций.

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

DataStore.delete(Chatty, Predicates.ALL).then(() => {  console.log('messages deleted!');});

Подписки GraphQL

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

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

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

<script>export default {  data() {    return {      subscription: undefined;    };  },  created() {    //Subscribe to changes    this.subscription = DataStore.observe(Chatty).subscribe(msg => {      console.log(msg.model, msg.opType, msg.element);      this.loadMessages();    });  },   destroyed() {    if (!this.subscription) return;    this.subscription.unsubscribe();  },}</script>

Развертывание через консоль Amplify

Мы рассмотрели развертывание через категорию хостинга Amplify CLI, но что, если нам нужно непрерывное развертывание CI/CD?Для этого мы можем использоватьконсоль Amplifyдля развертывания приложения.

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

git initgit remote add origin git@github.com:username/project-name.gitgit add .git commit -m 'initial commit'git push origin master

Затем мы посетим консоль Amplify в нашей учетной записи AWS по адресуhttps://eu-central-1.console.aws.amazon.com/amplify/home.

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

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

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

Наконец, мы можем нажать Сохранить иразвернуть, чтобы развернуть наше приложение!

Теперь мы можем отправлять обновления в Master, чтобы обновить наше приложение.

Удаление сервисов

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

amplify remove authamplify push

Если вы не знаете, какие службы вы в любой момент включили, вы можете запуститьamplify statusкоманду:

amplify status

amplify statusпредоставит вам список ресурсов, которые в настоящее время включены в вашем приложении.

Приложение

Настройка учетной записи AWS

Чтобы пройти этот урок, необходимо создать и активировать учетную запись Amazon Web Services.

Следуйте инструкциямздесь

Устранение неполадок

Сообщение: для идентификатора ключа доступа AWS требуется подписка на сервис

Решение: убедитесь, что вы подписаны на бесплатный план.Подписывайся

Сообщение: TypeError: fsevents не является конструктором

Решение:npm audit fix --force

Поведение: данные не синхронизируются с облаком и / или наоборот

Решение:

amplify update apiamplify push

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

  • Настроить обнаружение конфликтов?Y

  • Выберите стратегию разрешения по умолчаниюAuto Merge

  • Вы хотите изменить настройки по умолчанию для каждой модели?N

Послесловие

Надеюсь данный пост поможет легче пройти квест создания приложения Vue в облаке AWS.

Результат работы двух пользователейРезультат работы двух пользователей

Что дальше?

Конечно у разработчика возникнет ряд вопросов по эффективности использования приложения в среде Amplify. Вот некоторые из них:

5 шагов администрирования приложения5 шагов администрирования приложения

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

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

Подробнее..

Перевод Создание трекера пульсоксиметрии с использованием AWS Amplify и AWS serverless

08.02.2021 22:18:49 | Автор: admin

В этом руководстве демонстрируется пример решения для сбора, отслеживания и обмена данными пульсовой оксиметрии для нескольких пользователей.Он построен с использованием бессерверныхтехнологийAWS, что обеспечивает надежную масштабируемость и безопасность. Внешнееприложение написано наVueJSи используетAmplify Framework.Измерения сатурации кислородавыполняютсявручную или спомощью пульсоксиметра BerryMed,подключенного к браузеру черезИнтернет через Bluetooth.

Снимок экрана внешнего приложения, запущенного в браузере настольного компьютера.Снимок экрана внешнего приложения, запущенного в браузере настольного компьютера.

Бессерверный бэкэнд, который обрабатывает пользовательские данные и управление общим доступом, развертывается с использованием моделибессерверных приложенийAWS (AWS SAM).Бэкэнд-приложение состоит изREST APIAmazon API Gateway, который вызывает функции AWS Lambda.Код написан на Python для обработки бизнес-логики взаимодействия сбазой данныхAmazon DynamoDB.Аутентификацией управляетAmazon Cognito.

Предпосылки

Для реализации проекта потребуется:

Развернем приложение

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

Решение состоит из двух частей: внешнего интерфейса и бессерверного внутреннего интерфейса.Amplify CLIразвертывает все аутентификации Amazon Cognito и ресурсы хостинга для веб -интерфейсе.Серверной части требуетсяидентификаторпула пользователейAmazon Cognitoдля настройкиавторизаторав API.Это включает рабочий процесс авторизации, как показано на следующем изображении.

Схема, показывающая, как работает рабочий процесс авторизации Amazon CognitoСхема, показывающая, как работает рабочий процесс авторизации Amazon Cognito

Сначала настройте интерфейс.Выполните следующие шаги с помощью терминала, запущенного на компьютере, или с помощьюAWS Cloud9IDE.Если вы используете AWS Cloud9, создайте экземпляр, используя параметры по умолчанию.

Из терминала:

Установите Amplify CLI, выполнив эту команду.

  1. npm install -g @aws-amplify/cli
    
  2. Настройте Amplify CLI с помощью этой команды.Следуйте инструкциям до завершения.

    amplify configure
    
  3. Клонируйтепроект с GitHub.

    git clone https://github.com/aws-samples/aws-serverless-oxygen-monitor-web-bluetooth.git
    
  4. Перейдите в каталог ampify-frontend и инициализируйте проект с помощью команды Amplify CLI.Следуйте инструкциям до завершения.

    cd aws-serverless-oxygen-monitor-web-bluetooth/amplify-frontendamplify init
    
  5. Разверните все внешние ресурсы в облаке AWS с помощью команды Amplify CLI.

    amplify push
    
  6. После завершения развертывания ресурсов обратите внимание насвойствоaws_userpools_idвфайлеsrc / aws-exports.js.Это требуется при развертывании бессерверного бэкэнда.

    aws_user_pools_id в файле src / aws-exports.jsaws_user_pools_id в файле src / aws-exports.js

Затем разверните бессерверный бэкэнд.Хотя его можно развернуть с помощьюAWS SAM CLI, вы также можете развернуть его изКонсоли управления AWS:

  1. Перейдите ксерверномуприложениюOxygen-Monitor в репозитории бессерверных приложений AWS.

  2. Внастройках приложенияназовите приложение иукажите aws_userpools_id из внешнего приложения дляпараметраUserPoolID.

  3. ВыберитеDeploy (Развернуть).

  4. По завершении скопируйтеAPI endpoint (конечную точку API),чтобы ее можно было настроить во внешнем приложении на следующем шаге.

    Endpiont APIEndpiont API

Настроить и запустить интерфейсное приложение

  1. Создайте файлampify-frontend / src / api-config.jsв приложениивнешнегоинтерфейса со следующим содержимым.Включитеконечную точку APIиз предыдущего шага.

    const apiConfig = {  endpoint: <API ENDPOINT>};export default apiConfig;
    
  2. В терминале перейдите в корневой каталог внешнего приложения и запустите его локально для тестирования.

    cd aws-serverless-oxygen-monitor-web-bluetooth/amplify-frontendnpm installnpm run serve
    

    Вы должны увидеть такой вывод:

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

    amplify publish
    

    После завершения предоставляется URL-адрес размещенного приложения.

Использование внешнего интерфейса

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

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

Чтобы подключить пульсоксиметр BerryMed и начать считывание измерений, включите устройство. НажмитекнопкуПодключить пульсоксиметр, а затем выберите его из списка.Для использования функции Bluetooth в Интернете требуется браузер Chrome на настольном компьютере или мобильном устройстве Android.

Если у вас нет совместимого пульсоксиметра Bluetooth или доступа к Интернету Bluetooth, отметкафлажка"Enter Manually" (Ввести вручную)представляет поля прямого ввода.

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

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

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

Понимание бессерверного бэкэнда

В проектеGitHubпапкаserverless-backend /содержитфайл шаблонаAWS SAMифункции Lambda.Он создает конечную точку шлюза API, шесть лямбда-функций и две таблицы DynamoDB.Шаблон также определяет авторизатор Amazon Cognito для API, используя UserPoolID, переданный в качестве параметра:

Это позволяет только аутентифицированным пользователям внешнего интерфейса делать запросы с помощью токена JWT, содержащего их имя пользователя и адрес электронной почты.Бэкэнд использует эту информацию для извлечения и хранения данных в DynamoDB, соответствующих пользователю, отправившему запрос.

Первые три конечные точки обрабатывают обновление и получение уровней кислорода и частоты пульса.Когда пользователь публикует новое измерение,вызывается функция AddLevels, которая создает новый элемент "Уровни" втаблицеDynamoDB.

ФункцияFetchLevelsизвлекает личную историю пользователя. ФункцияFetchSharedUserLevels проверяет Access Table,чтобы узнать, есть ли у запрашивающего пользователя права общего доступа.

Остальные конечные точки обрабатывают управление доступом.Когда вы добавляете общего пользователя, это вызываетфункциюManageAccessс именем пользователя и действием, таким как совместное использование или отзыв.При совместном использовании элемент добавляется вAccess Table,которая разрешает связь.В случае отзыва элемент удаляется из таблицы.

ФункцияGetSharedUsersизвлекает список общих с пользователем, делающим запрос.Это заполняет раскрывающийся список доступных пользователей.FetchUsersWithAccessвыбирает всех пользователей, у которых есть доступ к пользователю, выполняющему запрос, это заполняет список пользователей в параметрах общего доступа.

Таблицы DynamoDB создаются шаблоном AWS SAM с ключом раздела и ключом диапазона, определенным для каждой таблицы.Они используются лямбда-функциями для запроса и сортировки элементов.См. Документацию,чтобы узнать больше о схеме ключей таблицы DynamoDB.

LevelsTable:    Type: AWS::DynamoDB::Table    Properties:       AttributeDefinitions:         -           AttributeName: "username"          AttributeType: "S"        -           AttributeName: "timestamp"          AttributeType: "N"      KeySchema:         - AttributeName: username          KeyType: HASH        - AttributeName: timestamp          KeyType: RANGE      ProvisionedThroughput:         ReadCapacityUnits: "5"        WriteCapacityUnits: "5"  SharedAccessTable:    Type: AWS::DynamoDB::Table    Properties:       AttributeDefinitions:         -           AttributeName: "username"          AttributeType: "S"        -           AttributeName: "shared_user"          AttributeType: "S"      KeySchema:         - AttributeName: username          KeyType: HASH        - AttributeName: shared_user          KeyType: RANGE      ProvisionedThroughput:         ReadCapacityUnits: "5"        WriteCapacityUnits: "5"

Понимание интерфейса

В проектеGitHubпапкаampify-frontend / src /содержит весь код для внешнего приложения. Вmain.jsмодули Amplify VueJS настроены на использование ресурсов, определенных вaws-exports.js.Он также настраивает конечную точку бессерверной серверной части, определенную вapi-config.js.

В файлеcomponents/OxygenMonitor.vueимпортируется модуль API и определяется желаемый API.

Вызовы API определяются как методы Vue, которые могут вызываться различными другими компонентами и элементами приложения.

Вкомпонентах /ConnectDevice.vue, метод подключенияинициализирует соединение Bluetooth Web -к пульсоксиметру.Он ищетUUID службы Bluetoothи имя устройства, характерное для пульсоксиметров BerryMed.При успешном соединении он создает прослушиватель событий нахарактеристике Bluetooth,который уведомляет об изменениях в измерениях.

МетодhandleDataанализирует события уведомления.Он отмечает любые изменения сатурации кислорода или частоты пульса.

КомпонентOxygenMonitorопределяет компонентConnectDeviceв своем шаблоне.Он связывает обработчики с отправленными событиями.

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

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

Заключение

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

Подробнее..

Типизация Vuex 4на Typescript для Vue 3

18.02.2021 12:05:10 | Автор: admin

Так получилось что изучать Vue я начал месяц назад с версии Vue 3. Предварительно было заявлено что Vue 3 переписана заново на typescript. Признаться того же самого я ожидал и от новой версии Vuex 4 для Vue 3.

Но почему то все оказалось не так как ожидалось. Посмотрев репозитарий Vuex 4
https://github.com/vuejs/vuex/tree/4.0

Мы неожиданно увидим что он написан на js и в самом конце написаны типы под уже готовый код.

С одной стороны нам как пользователям по идее все равно как написан код - главное чтобы было удобно им пользоваться. И вот тут начинающий пользователь сразу попадает в странную ситуацию при попытке использовать typescript для контроля типов для создаваемых объектов store. Дело в том что типизация создаваемого store в Vuex 4 отсутствует от слова совсем.

Ну да ладно, подумал я и начал искать решение.

Для VUE 2 предлагаются изящные решения на базе декораторов, например, это. Если мы заглянем в исходный код проекта, то увидим что оно разрабатывалось для "vue": ">=2" и "vuex": "3"

Использование декораторов в Vue 3 основано на библиотеке vue-class-component, на которую в наcтоящий момент даже не выпущена документация. Таким образом использование декораторов для типизации Vuex 4 для Vue 3 по моему мнению в настоящий момент выглядит сложной затеей и я решил отказаться от использования декораторов для типизации.

Предлагаемое мной решение основывается на проекте и статье.

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

Основные моменты

Я решил сделать store "одним целым", но с разделением на файлы по признакам общности функций. Соответственно в разных файлах- модулях я предлагаю добавлять к сущностям специальные префиксы. Например для части стора, реализующего авторизацию - auth.ts можно добавлять ко всем функциям префикс auth. Согласен что в таком подходе есть некоторое неудобство - а именно на разработчика ложится контроль за уникальностью имен (но если Вы ошибетесь - то компилятор typescript Вам все скажет).

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

Основная идея заключается в создании новых типов из типов Vuex с помощью Omit и переписывания имеющихся типов Vuex для мутаций, actions и getters.

В репозитарии имеется основной файл store - index.ts и подключаемые файлы (не модули Vuex в стандартном понимании) в которых разрабатывается некая своя логика (в примере это counter и auth).

Для mutatations я несколько упростил задачу и все mutations должны возвращать void. Для Actions я поддержал типизацию payload и значения, возвращаемого action. Типизация выполнена на основе возможности typescript 4 - named tupples. В файлах -примерах я комментариями пометил неизменяемую часть кода, остальной код Вы пишите сами, аналогично неизменяемую часть шаблона выделил в index.

Зависимости

Мой проект компилируется только в среде typescript 4 - обратите внимание на свой package.json.
При создании проекта с помощью vue-cli по умолчанию устанавливается typescript 3 версии версии.

Вам необходимо либо воспользоваться утилитой ncu либо самостоятельно заменить версию в package.json на > 4.0 (на момент написания статьи последняя версия typescript была 4.1.3 ). Ну и конечно же не забудьте удалить файл package-lock.json и запустить команду npm install.

Как пользоваться

Скачайте проект. В директории src Вы найдете папку store - скопируйте ее в свой проект. Проверьте что в Вашем проекте установлен typescript версии >= 4

В файл initialState.ts занесите начальные значение - они же типы для state. Для нормальной типизации state следует учесть, что используется возможность typescript по infer типов из значений. Например, если Вы хотите использовать массив и начальное значение для него планируется пустым - то запишите так - на примере users:

export const initialState = {  counter: {    counter: 0,  },  auth: {    name: "Ivan",    idUser: "89annsdj77",    email: "ivan@ivan.ru",    users:[] as Array<string>  },};

В основном файле - index.ts подключите Ваши модули согласно приведенному примеру - после строки "no change code " идет не изменяемая часть кода.

В папке modules создайте необходимые Вам файлы - модули с Вашей логикой (это не модули Vuex - это просто разбиение Вашей логики по группам).

Внутри каждого модуля есть неизменная часть кода (шаблон), который должен присутствовать там всегда. Этот шаблон начинается со строки Actions no change code и продолжается до конца модуля.

Далее в каждом модуле Вам необходимо описать типы Getters, ActionsPayload, MutationPayload согласно шаблонам, приведенным в файле counter.ts

Также необходимо реализовать и экспортировать сам функционал mutations, getters, actionsactions

Пример использования стора привел в компоненте HelloWorld.

P.S. мне будет приятно - если Вы поставите моему проекту звездочку на github.

Подробнее..
Категории: Vuejs , Vue , Vuex

Категории

Последние комментарии

  • Имя: Макс
    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